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