From 208060ebf270346a321bb1753f6d74c53f61135b Mon Sep 17 00:00:00 2001 From: Michael Klimushyn Date: Tue, 15 Oct 2019 16:13:31 -0700 Subject: [PATCH 1/7] Move VideoPlayer into its own top level class --- .../plugins/videoplayer/VideoPlayer.java | 283 ++++++++++++++++ .../videoplayer/VideoPlayerPlugin.java | 302 +----------------- .../example/android/gradle.properties | 3 + 3 files changed, 294 insertions(+), 294 deletions(-) create mode 100644 packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java diff --git a/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java new file mode 100644 index 000000000000..43123ef09238 --- /dev/null +++ b/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -0,0 +1,283 @@ +package io.flutter.plugins.videoplayer; + +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; + +import android.content.Context; +import android.net.Uri; +import android.os.Build; +import android.view.Surface; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.EventListener; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.audio.AudioAttributes; +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.util.Util; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.view.TextureRegistry; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +final class VideoPlayer { + private static final String FORMAT_SS = "ss"; + private static final String FORMAT_DASH = "dash"; + private static final String FORMAT_HLS = "hls"; + private static final String FORMAT_OTHER = "other"; + + private SimpleExoPlayer exoPlayer; + + private Surface surface; + + private final TextureRegistry.SurfaceTextureEntry textureEntry; + + private QueuingEventSink eventSink = new QueuingEventSink(); + + private final EventChannel eventChannel; + + private boolean isInitialized = false; + + VideoPlayer( + Context context, + EventChannel eventChannel, + TextureRegistry.SurfaceTextureEntry textureEntry, + String dataSource, + Result result, + String formatHint) { + this.eventChannel = eventChannel; + this.textureEntry = textureEntry; + + TrackSelector trackSelector = new DefaultTrackSelector(); + exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector); + + Uri uri = Uri.parse(dataSource); + + DataSource.Factory dataSourceFactory; + if (isHTTP(uri)) { + dataSourceFactory = + new DefaultHttpDataSourceFactory( + "ExoPlayer", + null, + DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, + DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, + true); + } else { + dataSourceFactory = new DefaultDataSourceFactory(context, "ExoPlayer"); + } + + MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint, context); + exoPlayer.prepare(mediaSource); + + setupVideoPlayer(eventChannel, textureEntry, result); + } + + private static boolean isHTTP(Uri uri) { + if (uri == null || uri.getScheme() == null) { + return false; + } + String scheme = uri.getScheme(); + return scheme.equals("http") || scheme.equals("https"); + } + + private MediaSource buildMediaSource( + Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint, Context context) { + int type; + if (formatHint == null) { + type = Util.inferContentType(uri.getLastPathSegment()); + } else { + switch (formatHint) { + case FORMAT_SS: + type = C.TYPE_SS; + break; + case FORMAT_DASH: + type = C.TYPE_DASH; + break; + case FORMAT_HLS: + type = C.TYPE_HLS; + break; + case FORMAT_OTHER: + type = C.TYPE_OTHER; + break; + default: + type = -1; + break; + } + } + switch (type) { + case C.TYPE_SS: + return new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(mediaDataSourceFactory), + new DefaultDataSourceFactory(context, null, mediaDataSourceFactory)) + .createMediaSource(uri); + case C.TYPE_DASH: + return new DashMediaSource.Factory( + new DefaultDashChunkSource.Factory(mediaDataSourceFactory), + new DefaultDataSourceFactory(context, null, mediaDataSourceFactory)) + .createMediaSource(uri); + case C.TYPE_HLS: + return new HlsMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri); + case C.TYPE_OTHER: + return new ExtractorMediaSource.Factory(mediaDataSourceFactory) + .setExtractorsFactory(new DefaultExtractorsFactory()) + .createMediaSource(uri); + default: + { + throw new IllegalStateException("Unsupported type: " + type); + } + } + } + + private void setupVideoPlayer( + EventChannel eventChannel, TextureRegistry.SurfaceTextureEntry textureEntry, Result result) { + + eventChannel.setStreamHandler( + new EventChannel.StreamHandler() { + @Override + public void onListen(Object o, EventChannel.EventSink sink) { + eventSink.setDelegate(sink); + } + + @Override + public void onCancel(Object o) { + eventSink.setDelegate(null); + } + }); + + surface = new Surface(textureEntry.surfaceTexture()); + exoPlayer.setVideoSurface(surface); + setAudioAttributes(exoPlayer); + + exoPlayer.addListener( + new EventListener() { + + @Override + public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) { + if (playbackState == Player.STATE_BUFFERING) { + sendBufferingUpdate(); + } else if (playbackState == Player.STATE_READY) { + if (!isInitialized) { + isInitialized = true; + sendInitialized(); + } + } else if (playbackState == Player.STATE_ENDED) { + Map event = new HashMap<>(); + event.put("event", "completed"); + eventSink.success(event); + } + } + + @Override + public void onPlayerError(final ExoPlaybackException error) { + if (eventSink != null) { + eventSink.error("VideoError", "Video player had error " + error, null); + } + } + }); + + Map reply = new HashMap<>(); + reply.put("textureId", textureEntry.id()); + result.success(reply); + } + + void sendBufferingUpdate() { + Map event = new HashMap<>(); + event.put("event", "bufferingUpdate"); + List range = Arrays.asList(0, exoPlayer.getBufferedPosition()); + // iOS supports a list of buffered ranges, so here is a list with a single range. + event.put("values", Collections.singletonList(range)); + eventSink.success(event); + } + + @SuppressWarnings("deprecation") + private static void setAudioAttributes(SimpleExoPlayer exoPlayer) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + exoPlayer.setAudioAttributes( + new AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MOVIE).build()); + } else { + exoPlayer.setAudioStreamType(C.STREAM_TYPE_MUSIC); + } + } + + void play() { + exoPlayer.setPlayWhenReady(true); + } + + void pause() { + exoPlayer.setPlayWhenReady(false); + } + + void setLooping(boolean value) { + exoPlayer.setRepeatMode(value ? REPEAT_MODE_ALL : REPEAT_MODE_OFF); + } + + void setVolume(double value) { + float bracketedValue = (float) Math.max(0.0, Math.min(1.0, value)); + exoPlayer.setVolume(bracketedValue); + } + + void seekTo(int location) { + exoPlayer.seekTo(location); + } + + long getPosition() { + return exoPlayer.getCurrentPosition(); + } + + @SuppressWarnings("SuspiciousNameCombination") + private void sendInitialized() { + if (isInitialized) { + Map event = new HashMap<>(); + event.put("event", "initialized"); + event.put("duration", exoPlayer.getDuration()); + + if (exoPlayer.getVideoFormat() != null) { + Format videoFormat = exoPlayer.getVideoFormat(); + int width = videoFormat.width; + int height = videoFormat.height; + int rotationDegrees = videoFormat.rotationDegrees; + // Switch the width/height if video was taken in portrait mode + if (rotationDegrees == 90 || rotationDegrees == 270) { + width = exoPlayer.getVideoFormat().height; + height = exoPlayer.getVideoFormat().width; + } + event.put("width", width); + event.put("height", height); + } + eventSink.success(event); + } + } + + void dispose() { + if (isInitialized) { + exoPlayer.stop(); + } + textureEntry.release(); + eventChannel.setStreamHandler(null); + if (surface != null) { + surface.release(); + } + if (exoPlayer != null) { + exoPlayer.release(); + } + } +} diff --git a/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java b/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java index 5b1f55fe14d6..0fb1bacb2fb2 100644 --- a/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java +++ b/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java @@ -4,297 +4,23 @@ package io.flutter.plugins.videoplayer; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; - -import android.content.Context; -import android.net.Uri; -import android.os.Build; import android.util.LongSparseArray; -import android.view.Surface; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerFactory; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Player.EventListener; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.audio.AudioAttributes; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; -import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelector; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.exoplayer2.util.Util; import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.common.PluginRegistry; import io.flutter.plugin.common.PluginRegistry.Registrar; -import io.flutter.view.FlutterNativeView; import io.flutter.view.TextureRegistry; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; public class VideoPlayerPlugin implements MethodCallHandler { - private static class VideoPlayer { - private static final String FORMAT_SS = "ss"; - private static final String FORMAT_DASH = "dash"; - private static final String FORMAT_HLS = "hls"; - private static final String FORMAT_OTHER = "other"; - - private SimpleExoPlayer exoPlayer; - - private Surface surface; - - private final TextureRegistry.SurfaceTextureEntry textureEntry; - - private QueuingEventSink eventSink = new QueuingEventSink(); - - private final EventChannel eventChannel; - - private boolean isInitialized = false; - - VideoPlayer( - Context context, - EventChannel eventChannel, - TextureRegistry.SurfaceTextureEntry textureEntry, - String dataSource, - Result result, - String formatHint) { - this.eventChannel = eventChannel; - this.textureEntry = textureEntry; - - TrackSelector trackSelector = new DefaultTrackSelector(); - exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector); - - Uri uri = Uri.parse(dataSource); - - DataSource.Factory dataSourceFactory; - if (isHTTP(uri)) { - dataSourceFactory = - new DefaultHttpDataSourceFactory( - "ExoPlayer", - null, - DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, - DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, - true); - } else { - dataSourceFactory = new DefaultDataSourceFactory(context, "ExoPlayer"); - } - - MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint, context); - exoPlayer.prepare(mediaSource); - - setupVideoPlayer(eventChannel, textureEntry, result); - } - - private static boolean isHTTP(Uri uri) { - if (uri == null || uri.getScheme() == null) { - return false; - } - String scheme = uri.getScheme(); - return scheme.equals("http") || scheme.equals("https"); - } - - private MediaSource buildMediaSource( - Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint, Context context) { - int type; - if (formatHint == null) { - type = Util.inferContentType(uri.getLastPathSegment()); - } else { - switch (formatHint) { - case FORMAT_SS: - type = C.TYPE_SS; - break; - case FORMAT_DASH: - type = C.TYPE_DASH; - break; - case FORMAT_HLS: - type = C.TYPE_HLS; - break; - case FORMAT_OTHER: - type = C.TYPE_OTHER; - break; - default: - type = -1; - break; - } - } - switch (type) { - case C.TYPE_SS: - return new SsMediaSource.Factory( - new DefaultSsChunkSource.Factory(mediaDataSourceFactory), - new DefaultDataSourceFactory(context, null, mediaDataSourceFactory)) - .createMediaSource(uri); - case C.TYPE_DASH: - return new DashMediaSource.Factory( - new DefaultDashChunkSource.Factory(mediaDataSourceFactory), - new DefaultDataSourceFactory(context, null, mediaDataSourceFactory)) - .createMediaSource(uri); - case C.TYPE_HLS: - return new HlsMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri); - case C.TYPE_OTHER: - return new ExtractorMediaSource.Factory(mediaDataSourceFactory) - .setExtractorsFactory(new DefaultExtractorsFactory()) - .createMediaSource(uri); - default: - { - throw new IllegalStateException("Unsupported type: " + type); - } - } - } - - private void setupVideoPlayer( - EventChannel eventChannel, - TextureRegistry.SurfaceTextureEntry textureEntry, - Result result) { - - eventChannel.setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object o, EventChannel.EventSink sink) { - eventSink.setDelegate(sink); - } - - @Override - public void onCancel(Object o) { - eventSink.setDelegate(null); - } - }); - - surface = new Surface(textureEntry.surfaceTexture()); - exoPlayer.setVideoSurface(surface); - setAudioAttributes(exoPlayer); - - exoPlayer.addListener( - new EventListener() { - - @Override - public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) { - if (playbackState == Player.STATE_BUFFERING) { - sendBufferingUpdate(); - } else if (playbackState == Player.STATE_READY) { - if (!isInitialized) { - isInitialized = true; - sendInitialized(); - } - } else if (playbackState == Player.STATE_ENDED) { - Map event = new HashMap<>(); - event.put("event", "completed"); - eventSink.success(event); - } - } - - @Override - public void onPlayerError(final ExoPlaybackException error) { - if (eventSink != null) { - eventSink.error("VideoError", "Video player had error " + error, null); - } - } - }); - - Map reply = new HashMap<>(); - reply.put("textureId", textureEntry.id()); - result.success(reply); - } - - private void sendBufferingUpdate() { - Map event = new HashMap<>(); - event.put("event", "bufferingUpdate"); - List range = Arrays.asList(0, exoPlayer.getBufferedPosition()); - // iOS supports a list of buffered ranges, so here is a list with a single range. - event.put("values", Collections.singletonList(range)); - eventSink.success(event); - } - - @SuppressWarnings("deprecation") - private static void setAudioAttributes(SimpleExoPlayer exoPlayer) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - exoPlayer.setAudioAttributes( - new AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MOVIE).build()); - } else { - exoPlayer.setAudioStreamType(C.STREAM_TYPE_MUSIC); - } - } - - void play() { - exoPlayer.setPlayWhenReady(true); - } - - void pause() { - exoPlayer.setPlayWhenReady(false); - } - - void setLooping(boolean value) { - exoPlayer.setRepeatMode(value ? REPEAT_MODE_ALL : REPEAT_MODE_OFF); - } - - void setVolume(double value) { - float bracketedValue = (float) Math.max(0.0, Math.min(1.0, value)); - exoPlayer.setVolume(bracketedValue); - } - - void seekTo(int location) { - exoPlayer.seekTo(location); - } - - long getPosition() { - return exoPlayer.getCurrentPosition(); - } - - @SuppressWarnings("SuspiciousNameCombination") - private void sendInitialized() { - if (isInitialized) { - Map event = new HashMap<>(); - event.put("event", "initialized"); - event.put("duration", exoPlayer.getDuration()); - - if (exoPlayer.getVideoFormat() != null) { - Format videoFormat = exoPlayer.getVideoFormat(); - int width = videoFormat.width; - int height = videoFormat.height; - int rotationDegrees = videoFormat.rotationDegrees; - // Switch the width/height if video was taken in portrait mode - if (rotationDegrees == 90 || rotationDegrees == 270) { - width = exoPlayer.getVideoFormat().height; - height = exoPlayer.getVideoFormat().width; - } - event.put("width", width); - event.put("height", height); - } - eventSink.success(event); - } - } + private final LongSparseArray videoPlayers; + private final Registrar registrar; - void dispose() { - if (isInitialized) { - exoPlayer.stop(); - } - textureEntry.release(); - eventChannel.setStreamHandler(null); - if (surface != null) { - surface.release(); - } - if (exoPlayer != null) { - exoPlayer.release(); - } - } + private VideoPlayerPlugin(Registrar registrar) { + this.registrar = registrar; + this.videoPlayers = new LongSparseArray<>(); } public static void registerWith(Registrar registrar) { @@ -303,24 +29,12 @@ public static void registerWith(Registrar registrar) { new MethodChannel(registrar.messenger(), "flutter.io/videoPlayer"); channel.setMethodCallHandler(plugin); registrar.addViewDestroyListener( - new PluginRegistry.ViewDestroyListener() { - @Override - public boolean onViewDestroy(FlutterNativeView view) { - plugin.onDestroy(); - return false; // We are not interested in assuming ownership of the NativeView. - } + view -> { + plugin.onDestroy(); + return false; // We are not interested in assuming ownership of the NativeView. }); } - private VideoPlayerPlugin(Registrar registrar) { - this.registrar = registrar; - this.videoPlayers = new LongSparseArray<>(); - } - - private final LongSparseArray videoPlayers; - - private final Registrar registrar; - private void disposeAllPlayers() { for (int i = 0; i < videoPlayers.size(); i++) { videoPlayers.valueAt(i).dispose(); diff --git a/packages/video_player/example/android/gradle.properties b/packages/video_player/example/android/gradle.properties index 8bd86f680510..a6738207fd15 100644 --- a/packages/video_player/example/android/gradle.properties +++ b/packages/video_player/example/android/gradle.properties @@ -1 +1,4 @@ org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true +android.enableR8=true From 0bfed5b177a187c763ea8a61c77ec4a393d9929b Mon Sep 17 00:00:00 2001 From: Michael Klimushyn Date: Thu, 17 Oct 2019 16:34:07 -0700 Subject: [PATCH 2/7] [video_player] Migrate to the v2 embedding - Migrates the plugin - Adds a v2 embedding to the example app - Fixes a broken remote example in the example app - Increments the Flutter SDK dependency - Increments the version - Adds e2e tests for some simple use cases of the plugin --- packages/video_player/CHANGELOG.md | 5 + .../videoplayer/VideoPlayerPlugin.java | 106 +++++++++++++++--- .../android/app/src/main/AndroidManifest.xml | 55 +++++---- .../EmbeddingV1Activity.java | 17 +++ .../videoplayerexample/MainActivity.java | 11 +- packages/video_player/example/lib/main.dart | 4 +- packages/video_player/example/pubspec.yaml | 4 +- .../example/test_driver/video_player_e2e.dart | 64 +++++++++++ .../test_driver/video_player_e2e_test.dart | 16 +++ packages/video_player/pubspec.yaml | 4 +- 10 files changed, 236 insertions(+), 50 deletions(-) create mode 100644 packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/EmbeddingV1Activity.java create mode 100644 packages/video_player/example/test_driver/video_player_e2e.dart create mode 100644 packages/video_player/example/test_driver/video_player_e2e_test.dart diff --git a/packages/video_player/CHANGELOG.md b/packages/video_player/CHANGELOG.md index d4882496320f..759c955e39df 100644 --- a/packages/video_player/CHANGELOG.md +++ b/packages/video_player/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.10.3 + +* Add support for the v2 Android embedding. This shouldn't impact existing + functionality. + ## 0.10.2+6 * Remove AndroidX warnings. diff --git a/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java b/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java index 0fb1bacb2fb2..fdc0511e217b 100644 --- a/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java +++ b/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java @@ -4,30 +4,43 @@ package io.flutter.plugins.videoplayer; +import android.content.Context; +import android.util.Log; import android.util.LongSparseArray; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugin.common.PluginRegistry.Registrar; +import io.flutter.view.FlutterMain; import io.flutter.view.TextureRegistry; -public class VideoPlayerPlugin implements MethodCallHandler { +/** Android platform implementation of the VideoPlayerPlugin. */ +public class VideoPlayerPlugin implements MethodCallHandler, FlutterPlugin { + private static final String TAG = "VideoPlayerPlugin"; + private final LongSparseArray videoPlayers = new LongSparseArray<>(); + private FlutterState flutterState; - private final LongSparseArray videoPlayers; - private final Registrar registrar; + /** Register this with the v2 embedding for the plugin to respond to lifecycle callbacks. */ + public VideoPlayerPlugin() {} private VideoPlayerPlugin(Registrar registrar) { - this.registrar = registrar; - this.videoPlayers = new LongSparseArray<>(); + this.flutterState = + new FlutterState( + registrar.context(), + registrar.messenger(), + registrar::lookupKeyForAsset, + registrar::lookupKeyForAsset, + registrar.textures()); + flutterState.startListening(this); } + /** Registers this with the stable v1 embedding. Will not respond to lifecycle events. */ public static void registerWith(Registrar registrar) { final VideoPlayerPlugin plugin = new VideoPlayerPlugin(registrar); - final MethodChannel channel = - new MethodChannel(registrar.messenger(), "flutter.io/videoPlayer"); - channel.setMethodCallHandler(plugin); registrar.addViewDestroyListener( view -> { plugin.onDestroy(); @@ -35,6 +48,27 @@ public static void registerWith(Registrar registrar) { }); } + @Override + public void onAttachedToEngine(FlutterPluginBinding binding) { + this.flutterState = + new FlutterState( + binding.getApplicationContext(), + binding.getFlutterEngine().getDartExecutor(), + FlutterMain::getLookupKeyForAsset, + FlutterMain::getLookupKeyForAsset, + binding.getFlutterEngine().getRenderer()); + flutterState.startListening(this); + } + + @Override + public void onDetachedFromEngine(FlutterPluginBinding binding) { + if (flutterState == null) { + Log.wtf(TAG, "Detached from the engine before registering to it."); + } + flutterState.stopListening(); + flutterState = null; + } + private void disposeAllPlayers() { for (int i = 0; i < videoPlayers.size(); i++) { videoPlayers.valueAt(i).dispose(); @@ -53,8 +87,7 @@ private void onDestroy() { @Override public void onMethodCall(MethodCall call, Result result) { - TextureRegistry textures = registrar.textures(); - if (textures == null) { + if (flutterState == null || flutterState.textureRegistry == null) { result.error("no_activity", "video_player plugin requires a foreground activity", null); return; } @@ -64,23 +97,25 @@ public void onMethodCall(MethodCall call, Result result) { break; case "create": { - TextureRegistry.SurfaceTextureEntry handle = textures.createSurfaceTexture(); + TextureRegistry.SurfaceTextureEntry handle = + flutterState.textureRegistry.createSurfaceTexture(); EventChannel eventChannel = new EventChannel( - registrar.messenger(), "flutter.io/videoPlayer/videoEvents" + handle.id()); + flutterState.binaryMessenger, "flutter.io/videoPlayer/videoEvents" + handle.id()); VideoPlayer player; if (call.argument("asset") != null) { String assetLookupKey; if (call.argument("package") != null) { assetLookupKey = - registrar.lookupKeyForAsset(call.argument("asset"), call.argument("package")); + flutterState.keyForAssetAndPackageName.get( + call.argument("asset"), call.argument("package")); } else { - assetLookupKey = registrar.lookupKeyForAsset(call.argument("asset")); + assetLookupKey = flutterState.keyForAsset.get(call.argument("asset")); } player = new VideoPlayer( - registrar.context(), + flutterState.applicationContext, eventChannel, handle, "asset:///" + assetLookupKey, @@ -90,7 +125,7 @@ public void onMethodCall(MethodCall call, Result result) { } else { player = new VideoPlayer( - registrar.context(), + flutterState.applicationContext, eventChannel, handle, call.argument("uri"), @@ -154,4 +189,43 @@ private void onMethodCall(MethodCall call, Result result, long textureId, VideoP break; } } + + private interface KeyForAssetFn { + String get(String asset); + } + + private interface KeyForAssetAndPackageName { + String get(String asset, String packageName); + } + + private static final class FlutterState { + private final Context applicationContext; + private final BinaryMessenger binaryMessenger; + private final KeyForAssetFn keyForAsset; + private final KeyForAssetAndPackageName keyForAssetAndPackageName; + private final TextureRegistry textureRegistry; + private final MethodChannel methodChannel; + + FlutterState( + Context applicationContext, + BinaryMessenger messenger, + KeyForAssetFn keyForAsset, + KeyForAssetAndPackageName keyForAssetAndPackageName, + TextureRegistry textureRegistry) { + this.applicationContext = applicationContext; + this.binaryMessenger = messenger; + this.keyForAsset = keyForAsset; + this.keyForAssetAndPackageName = keyForAssetAndPackageName; + this.textureRegistry = textureRegistry; + methodChannel = new MethodChannel(messenger, "flutter.io/videoPlayer"); + } + + void startListening(VideoPlayerPlugin methodCallHandler) { + methodChannel.setMethodCallHandler(methodCallHandler); + } + + void stopListening() { + methodChannel.setMethodCallHandler(null); + } + } } diff --git a/packages/video_player/example/android/app/src/main/AndroidManifest.xml b/packages/video_player/example/android/app/src/main/AndroidManifest.xml index 914e82b3c894..1f77f8fcd6bd 100644 --- a/packages/video_player/example/android/app/src/main/AndroidManifest.xml +++ b/packages/video_player/example/android/app/src/main/AndroidManifest.xml @@ -1,27 +1,36 @@ + package="io.flutter.plugins.videoplayerexample"> - + + + + + + + + + + + - - - - - - - - - + diff --git a/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/EmbeddingV1Activity.java b/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/EmbeddingV1Activity.java new file mode 100644 index 000000000000..f1af8ecd74e7 --- /dev/null +++ b/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/EmbeddingV1Activity.java @@ -0,0 +1,17 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.videoplayerexample; + +import android.os.Bundle; +import io.flutter.app.FlutterActivity; +import io.flutter.plugins.GeneratedPluginRegistrant; + +public class EmbeddingV1Activity extends FlutterActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + GeneratedPluginRegistrant.registerWith(this); + } +} diff --git a/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/MainActivity.java b/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/MainActivity.java index 133c3fa2c898..2a0ae15e5e2f 100644 --- a/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/MainActivity.java +++ b/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/MainActivity.java @@ -4,14 +4,13 @@ package io.flutter.plugins.videoplayerexample; -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugins.videoplayer.VideoPlayerPlugin; public class MainActivity extends FlutterActivity { @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); + public void configureFlutterEngine(FlutterEngine flutterEngine) { + flutterEngine.getPlugins().add(new VideoPlayerPlugin()); } } diff --git a/packages/video_player/example/lib/main.dart b/packages/video_player/example/lib/main.dart index dea1086593df..196b90846e16 100644 --- a/packages/video_player/example/lib/main.dart +++ b/packages/video_player/example/lib/main.dart @@ -388,11 +388,11 @@ void main() { Container( padding: const EdgeInsets.only(top: 20.0), ), - const Text('With remote m3u8'), + const Text('With remote mp4'), Container( padding: const EdgeInsets.all(20), child: NetworkPlayerLifeCycle( - 'http://184.72.239.149/vod/smil:BigBuckBunny.smil/playlist.m3u8', + 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', (BuildContext context, VideoPlayerController controller) => AspectRatioVideo(controller), diff --git a/packages/video_player/example/pubspec.yaml b/packages/video_player/example/pubspec.yaml index da48f06f0796..bcb559b35bb5 100644 --- a/packages/video_player/example/pubspec.yaml +++ b/packages/video_player/example/pubspec.yaml @@ -8,7 +8,9 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - + flutter_driver: + sdk: flutter + e2e: "^0.2.0" video_player: path: ../ diff --git a/packages/video_player/example/test_driver/video_player_e2e.dart b/packages/video_player/example/test_driver/video_player_e2e.dart new file mode 100644 index 000000000000..4289a2d0f7d3 --- /dev/null +++ b/packages/video_player/example/test_driver/video_player_e2e.dart @@ -0,0 +1,64 @@ +// Copyright 2019, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:e2e/e2e.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:video_player/video_player.dart'; + +void main() { + E2EWidgetsFlutterBinding.ensureInitialized(); + VideoPlayerController _controller; + + tearDown(() async => _controller.dispose()); + + group('asset videos', () { + setUp(() { + _controller = VideoPlayerController.asset('assets/Butterfly-209.mp4'); + }); + + testWidgets('can be initialized', (WidgetTester tester) async { + await _controller.initialize(); + + expect(_controller.value.initialized, true); + expect(_controller.value.position, const Duration(seconds: 0)); + expect(_controller.value.isPlaying, false); + expect(_controller.value.duration, + const Duration(seconds: 7, milliseconds: 540)); + }); + + testWidgets('can be played', (WidgetTester tester) async { + await _controller.initialize(); + + await _controller.play(); + await tester.pump(const Duration(milliseconds: 750)); + + expect(_controller.value.isPlaying, true); + expect(_controller.value.position, + (Duration position) => position > const Duration(seconds: 0)); + }); + + testWidgets('can seek', (WidgetTester tester) async { + await _controller.initialize(); + + await _controller.seekTo(const Duration(seconds: 3)); + + expect(_controller.value.position, const Duration(seconds: 3)); + }); + + testWidgets('can be paused', (WidgetTester tester) async { + await _controller.initialize(); + + // Play for a second, then pause, and then wait a second. + await _controller.play(); + await tester.pump(const Duration(milliseconds: 750)); + await _controller.pause(); + final Duration pausedPosition = _controller.value.position; + await tester.pump(const Duration(milliseconds: 750)); + + // Verify that we stopped playing after the pause. + expect(_controller.value.isPlaying, false); + expect(_controller.value.position, pausedPosition); + }); + }); +} diff --git a/packages/video_player/example/test_driver/video_player_e2e_test.dart b/packages/video_player/example/test_driver/video_player_e2e_test.dart new file mode 100644 index 000000000000..2e5c27fd402e --- /dev/null +++ b/packages/video_player/example/test_driver/video_player_e2e_test.dart @@ -0,0 +1,16 @@ +// Copyright 2019, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter_driver/flutter_driver.dart'; + +Future main() async { + final FlutterDriver driver = await FlutterDriver.connect(); + final String result = + await driver.requestData(null, timeout: const Duration(minutes: 1)); + driver.close(); + exit(result == 'pass' ? 0 : 1); +} diff --git a/packages/video_player/pubspec.yaml b/packages/video_player/pubspec.yaml index 1b0805be1764..656a81fa8643 100644 --- a/packages/video_player/pubspec.yaml +++ b/packages/video_player/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player description: Flutter plugin for displaying inline video with other Flutter widgets on Android and iOS. author: Flutter Team -version: 0.10.2+6 +version: 0.10.3 homepage: https://github.com/flutter/plugins/tree/master/packages/video_player flutter: @@ -22,4 +22,4 @@ dev_dependencies: environment: sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=1.5.0 <2.0.0" + flutter: ">=1.9.1+hotfix.5 <2.0.0" From b54566eb24ff3a98e4cc75873941fc6ed403b848 Mon Sep 17 00:00:00 2001 From: Michael Klimushyn Date: Tue, 29 Oct 2019 15:02:40 -0700 Subject: [PATCH 3/7] Review feedback and time updates --- packages/video_player/android/build.gradle | 26 +++++++++++++++++++ .../android/app/src/main/AndroidManifest.xml | 1 - 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/video_player/android/build.gradle b/packages/video_player/android/build.gradle index edbb4c7acce4..4a73ec52e0ad 100644 --- a/packages/video_player/android/build.gradle +++ b/packages/video_player/android/build.gradle @@ -45,3 +45,29 @@ android { implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.9.6' } } + +// TODO(mklim): Remove this hack once androidx.lifecycle is included on stable. https://github.com/flutter/flutter/issues/42348 +afterEvaluate { + def containsEmbeddingDependencies = false + for (def configuration : configurations.all) { + for (def dependency : configuration.dependencies) { + if (dependency.group == 'io.flutter' && + dependency.name.startsWith('flutter_embedding') && + dependency.isTransitive()) + { + containsEmbeddingDependencies = true + break + } + } + } + if (!containsEmbeddingDependencies) { + android { + dependencies { + def lifecycle_version = "1.1.1" + compileOnly "android.arch.lifecycle:runtime:$lifecycle_version" + compileOnly "android.arch.lifecycle:common:$lifecycle_version" + compileOnly "android.arch.lifecycle:common-java8:$lifecycle_version" + } + } + } +} diff --git a/packages/video_player/example/android/app/src/main/AndroidManifest.xml b/packages/video_player/example/android/app/src/main/AndroidManifest.xml index 1f77f8fcd6bd..deec4b6b5b08 100644 --- a/packages/video_player/example/android/app/src/main/AndroidManifest.xml +++ b/packages/video_player/example/android/app/src/main/AndroidManifest.xml @@ -21,7 +21,6 @@ From b351f137d23c01871a9a6c69bf08208fca70983e Mon Sep 17 00:00:00 2001 From: Michael Klimushyn Date: Tue, 29 Oct 2019 16:15:22 -0700 Subject: [PATCH 4/7] Improve test --- .../example/test_driver/video_player_e2e.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/video_player/example/test_driver/video_player_e2e.dart b/packages/video_player/example/test_driver/video_player_e2e.dart index 4289a2d0f7d3..7a2ec5ce3bf4 100644 --- a/packages/video_player/example/test_driver/video_player_e2e.dart +++ b/packages/video_player/example/test_driver/video_player_e2e.dart @@ -6,6 +6,8 @@ import 'package:e2e/e2e.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:video_player/video_player.dart'; +const Duration _playDuration = Duration(seconds: 1); + void main() { E2EWidgetsFlutterBinding.ensureInitialized(); VideoPlayerController _controller; @@ -31,7 +33,7 @@ void main() { await _controller.initialize(); await _controller.play(); - await tester.pump(const Duration(milliseconds: 750)); + await tester.pump(_playDuration); expect(_controller.value.isPlaying, true); expect(_controller.value.position, @@ -51,10 +53,10 @@ void main() { // Play for a second, then pause, and then wait a second. await _controller.play(); - await tester.pump(const Duration(milliseconds: 750)); + await tester.pump(_playDuration); await _controller.pause(); final Duration pausedPosition = _controller.value.position; - await tester.pump(const Duration(milliseconds: 750)); + await tester.pump(_playDuration); // Verify that we stopped playing after the pause. expect(_controller.value.isPlaying, false); From 529796858755aa1da28165f55b9d53902d6a984c Mon Sep 17 00:00:00 2001 From: Michael Klimushyn Date: Wed, 30 Oct 2019 14:30:10 -0700 Subject: [PATCH 5/7] Speculative test fix --- .../video_player/example/test_driver/video_player_e2e.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/video_player/example/test_driver/video_player_e2e.dart b/packages/video_player/example/test_driver/video_player_e2e.dart index 7a2ec5ce3bf4..87c5333cd5e6 100644 --- a/packages/video_player/example/test_driver/video_player_e2e.dart +++ b/packages/video_player/example/test_driver/video_player_e2e.dart @@ -33,7 +33,7 @@ void main() { await _controller.initialize(); await _controller.play(); - await tester.pump(_playDuration); + await tester.pumpAndSettle(_playDuration); expect(_controller.value.isPlaying, true); expect(_controller.value.position, @@ -53,10 +53,10 @@ void main() { // Play for a second, then pause, and then wait a second. await _controller.play(); - await tester.pump(_playDuration); + await tester.pumpAndSettle(_playDuration); await _controller.pause(); final Duration pausedPosition = _controller.value.position; - await tester.pump(_playDuration); + await tester.pumpAndSettle(_playDuration); // Verify that we stopped playing after the pause. expect(_controller.value.isPlaying, false); From 88c174f33bfa4cd728327e39815cb294a96819e7 Mon Sep 17 00:00:00 2001 From: Michael Klimushyn Date: Tue, 12 Nov 2019 12:24:39 -0800 Subject: [PATCH 6/7] Await driver.close() --- .../video_player/example/test_driver/video_player_e2e_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/video_player/example/test_driver/video_player_e2e_test.dart b/packages/video_player/example/test_driver/video_player_e2e_test.dart index 2e5c27fd402e..ccd716607d60 100644 --- a/packages/video_player/example/test_driver/video_player_e2e_test.dart +++ b/packages/video_player/example/test_driver/video_player_e2e_test.dart @@ -11,6 +11,6 @@ Future main() async { final FlutterDriver driver = await FlutterDriver.connect(); final String result = await driver.requestData(null, timeout: const Duration(minutes: 1)); - driver.close(); + await driver.close(); exit(result == 'pass' ? 0 : 1); } From 267812fec6d1e5a7b785e53f8c10bf2bc801b45f Mon Sep 17 00:00:00 2001 From: Michael Klimushyn Date: Wed, 13 Nov 2019 10:54:03 -0800 Subject: [PATCH 7/7] Skip tests that involve playing on iOS --- .../video_player/example/test_driver/video_player_e2e.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/video_player/example/test_driver/video_player_e2e.dart b/packages/video_player/example/test_driver/video_player_e2e.dart index 87c5333cd5e6..e726ab2f410d 100644 --- a/packages/video_player/example/test_driver/video_player_e2e.dart +++ b/packages/video_player/example/test_driver/video_player_e2e.dart @@ -2,6 +2,7 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:io'; import 'package:e2e/e2e.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:video_player/video_player.dart'; @@ -38,7 +39,7 @@ void main() { expect(_controller.value.isPlaying, true); expect(_controller.value.position, (Duration position) => position > const Duration(seconds: 0)); - }); + }, skip: Platform.isIOS); testWidgets('can seek', (WidgetTester tester) async { await _controller.initialize(); @@ -46,7 +47,7 @@ void main() { await _controller.seekTo(const Duration(seconds: 3)); expect(_controller.value.position, const Duration(seconds: 3)); - }); + }, skip: Platform.isIOS); testWidgets('can be paused', (WidgetTester tester) async { await _controller.initialize(); @@ -61,6 +62,6 @@ void main() { // Verify that we stopped playing after the pause. expect(_controller.value.isPlaying, false); expect(_controller.value.position, pausedPosition); - }); + }, skip: Platform.isIOS); }); }