From fa57d78c8bdb176e9cd0090e3f91a8b0320edd26 Mon Sep 17 00:00:00 2001 From: Wolfgang Haupt Date: Fri, 14 Aug 2026 13:32:24 +0200 Subject: [PATCH] video_player_android: allow supplying a custom VideoAsset Adds VideoAssetProvider and VideoPlayerPlugin.setVideoAssetProvider, which is consulted before this plugin's own URI handling and may return null to fall through to it. This is the extension point for playback the plugin cannot express itself: reading from a download or HTTP cache, applying a custom DataSource.Factory, or resolving a scheme the plugin does not know about. VideoAsset is already public and already exposes MediaItem and MediaSource.Factory, so this adds no new type surface; it only makes the existing one reachable. Behavior is unchanged when no provider is registered, which the added tests cover alongside the override path. --- .../video_player_android/CHANGELOG.md | 8 +++ .../videoplayer/VideoAssetProvider.java | 37 ++++++++++++++ .../videoplayer/VideoPlayerPlugin.java | 24 +++++++++ .../videoplayer/VideoPlayerPluginTest.java | 51 +++++++++++++++++++ .../video_player_android/pubspec.yaml | 2 +- 5 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoAssetProvider.java diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index 26f60a5c2b71..8f5b9397e9e9 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.13.0 + +* Adds `VideoAssetProvider` and `VideoPlayerPlugin.setVideoAssetProvider`, letting another + component supply the `VideoAsset` used for a URI. This is the extension point for playback + this plugin cannot express itself, such as reading from a download or HTTP cache, applying a + custom `DataSource.Factory`, or resolving an unknown scheme. Behavior is unchanged when no + provider is registered. + ## 2.12.0 * Fixes a [bug](https://github.com/flutter/flutter/issues/176575) where some videos report an incorrect duration when initialized without a video duration. diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoAssetProvider.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoAssetProvider.java new file mode 100644 index 000000000000..6cf4fd572a61 --- /dev/null +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoAssetProvider.java @@ -0,0 +1,37 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.videoplayer; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Supplies a {@link VideoAsset} for a URI, overriding this plugin's default handling. + * + *

This is the extension point for playback that the plugin cannot express on its own, such as + * reading from a download or HTTP cache, applying a custom {@code DataSource.Factory}, or resolving + * a scheme this plugin does not know about. Because {@link VideoAsset} already exposes the media + * item and media source factory, an implementation has full control over how a URI is played + * without this plugin needing to know why. + * + *

Register with {@link VideoPlayerPlugin#setVideoAssetProvider}. At most one provider is active + * at a time; the last one registered wins. + */ +public interface VideoAssetProvider { + /** + * Returns the asset to play for {@code uri}, or null to use this plugin's default handling. + * + *

Called on the main thread each time a player is created, before the URI is inspected for a + * known scheme, so a provider may override any URI including {@code asset:} and {@code rtsp:} + * ones. + * + * @param context application context. + * @param uri the URI the player was created with. + * @return the asset to play, or null to fall through to the default. + */ + @Nullable + VideoAsset getAsset(@NonNull Context context, @NonNull String uri); +} diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java index 5d520ac1f827..71a19c824067 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java @@ -27,6 +27,21 @@ public class VideoPlayerPlugin implements FlutterPlugin, AndroidVideoPlayerApi { private final VideoPlayerOptions sharedOptions = new VideoPlayerOptions(); private long nextPlayerIdentifier = 1; + private static @Nullable VideoAssetProvider videoAssetProvider; + + /** + * Sets the provider consulted before this plugin's own URI handling, or null to clear it. + * + *

Lets another component take over how a URI is played; see {@link VideoAssetProvider}. This + * is process-wide rather than per-plugin-instance because it is typically registered once at + * startup, before any player exists. + * + * @param provider the provider to consult, or null for default handling only. + */ + public static void setVideoAssetProvider(@Nullable VideoAssetProvider provider) { + videoAssetProvider = provider; + } + /** Register this with the v2 embedding for the plugin to respond to lifecycle callbacks. */ public VideoPlayerPlugin() {} @@ -126,6 +141,15 @@ public long createForPlatformView(@NonNull CreationOptions options) { private @NonNull VideoAsset videoAssetWithOptions(@NonNull CreationOptions options) { final @NonNull String uri = options.getUri(); + + VideoAssetProvider provider = videoAssetProvider; + if (provider != null) { + VideoAsset providedAsset = provider.getAsset(flutterState.applicationContext, uri); + if (providedAsset != null) { + return providedAsset; + } + } + if (uri.startsWith("asset:")) { return VideoAsset.fromAssetUrl(uri); } else if (uri.startsWith("rtsp:")) { diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java index 9f5e42fa66d3..17cc60d5696d 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java @@ -113,4 +113,55 @@ public void createsTextureVideoPlayer() throws Exception { assertTrue(videoPlayers.get(ids.getPlayerId()) instanceof TextureVideoPlayer); } } + + @Test + public void videoAssetProviderOverridesDefaultAsset() throws Exception { + final VideoAsset providedAsset = mock(VideoAsset.class); + final String uri = "https://example.com/video.m3u8"; + final String[] requestedUri = new String[1]; + + VideoPlayerPlugin.setVideoAssetProvider( + (context, requested) -> { + requestedUri[0] = requested; + return providedAsset; + }); + + try (MockedStatic mockedTextureVideoPlayerStatic = + mockStatic(TextureVideoPlayer.class)) { + mockedTextureVideoPlayerStatic + .when(() -> TextureVideoPlayer.create(any(), any(), any(), any(), any())) + .thenReturn(mock(TextureVideoPlayer.class)); + + plugin.createForTextureView(new CreationOptions(uri, null, new HashMap<>(), null, null)); + + assertEquals(uri, requestedUri[0]); + mockedTextureVideoPlayerStatic.verify( + () -> TextureVideoPlayer.create(any(), any(), any(), eq(providedAsset), any())); + } finally { + VideoPlayerPlugin.setVideoAssetProvider(null); + } + } + + @Test + public void videoAssetProviderReturningNullFallsBackToDefault() throws Exception { + VideoPlayerPlugin.setVideoAssetProvider((context, requested) -> null); + + try (MockedStatic mockedTextureVideoPlayerStatic = + mockStatic(TextureVideoPlayer.class)) { + mockedTextureVideoPlayerStatic + .when(() -> TextureVideoPlayer.create(any(), any(), any(), any(), any())) + .thenReturn(mock(TextureVideoPlayer.class)); + + final TexturePlayerIds ids = + plugin.createForTextureView( + new CreationOptions( + "https://example.com/video.m3u8", null, new HashMap<>(), null, null)); + + // The default handling still produced a working player. + final LongSparseArray videoPlayers = getVideoPlayers(); + assertTrue(videoPlayers.get(ids.getPlayerId()) instanceof TextureVideoPlayer); + } finally { + VideoPlayerPlugin.setVideoAssetProvider(null); + } + } } diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index 896c612d45e5..d6f4ff2e6cb4 100644 --- a/packages/video_player/video_player_android/pubspec.yaml +++ b/packages/video_player/video_player_android/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_android description: Android implementation of the video_player plugin. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.12.0 +version: 2.13.0 environment: sdk: ^3.12.0