Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate video_player/android from SurfaceTexture->SurfaceProducer. #6456

Merged
merged 20 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/video_player/video_player_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.4.16

* [Supports Impeller](https://docs.flutter.dev/release/breaking-changes/android-surface-plugins).

## 2.4.15

* Updates minimum supported SDK version to Flutter 3.22/Dart 3.4.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

import android.content.Context;
import android.net.Uri;
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C;
Expand Down Expand Up @@ -48,32 +47,37 @@ final class VideoPlayer {

private ExoPlayer exoPlayer;

private Surface surface;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't need to cache surface.


private final TextureRegistry.SurfaceTextureEntry textureEntry;
private TextureRegistry.SurfaceProducer surfaceProducer;

private QueuingEventSink eventSink;

private final EventChannel eventChannel;

private static final String USER_AGENT = "User-Agent";

private MediaSource mediaSource;

@VisibleForTesting boolean isInitialized = false;

// State that must be reset when the surface is re-created.
private final VideoPlayerOptions options;
private long restoreVideoLocation = 0;
private int restoreRepeatMode = 0;
private float restoreVolume = 0;
private PlaybackParameters restorePlaybackParameters;

private DefaultHttpDataSource.Factory httpDataSourceFactory = new DefaultHttpDataSource.Factory();

VideoPlayer(
Context context,
EventChannel eventChannel,
TextureRegistry.SurfaceTextureEntry textureEntry,
TextureRegistry.SurfaceProducer surfaceProducer,
String dataSource,
String formatHint,
@NonNull Map<String, String> httpHeaders,
VideoPlayerOptions options) {
this.eventChannel = eventChannel;
this.textureEntry = textureEntry;
this.surfaceProducer = surfaceProducer;
this.options = options;

ExoPlayer exoPlayer = new ExoPlayer.Builder(context).build();
Expand All @@ -83,7 +87,7 @@ final class VideoPlayer {
DataSource.Factory dataSourceFactory =
new DefaultDataSource.Factory(context, httpDataSourceFactory);

MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint);
mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint);

exoPlayer.setMediaSource(mediaSource);
exoPlayer.prepare();
Expand All @@ -96,12 +100,12 @@ final class VideoPlayer {
VideoPlayer(
ExoPlayer exoPlayer,
EventChannel eventChannel,
TextureRegistry.SurfaceTextureEntry textureEntry,
TextureRegistry.SurfaceProducer surfaceProducer,
VideoPlayerOptions options,
QueuingEventSink eventSink,
DefaultHttpDataSource.Factory httpDataSourceFactory) {
this.eventChannel = eventChannel;
this.textureEntry = textureEntry;
this.surfaceProducer = surfaceProducer;
this.options = options;
this.httpDataSourceFactory = httpDataSourceFactory;

Expand Down Expand Up @@ -169,6 +173,40 @@ private MediaSource buildMediaSource(
}
}

public void recreateSurface(Context context) {
ExoPlayer exoPlayer = new ExoPlayer.Builder(context).build();

exoPlayer.setMediaSource(mediaSource);
exoPlayer.prepare();

setUpVideoPlayer(exoPlayer, new QueuingEventSink());
exoPlayer.setVideoSurface(surfaceProducer.getSurface());
exoPlayer.seekTo(restoreVideoLocation);
exoPlayer.setRepeatMode(restoreRepeatMode);
exoPlayer.setVolume(restoreVolume);
if (restorePlaybackParameters != null) {
exoPlayer.setPlaybackParameters(restorePlaybackParameters);
}
}

public void pauseSurface() {
if (!isInitialized) {
return;
}
restoreVideoLocation = exoPlayer.getCurrentPosition();
restoreRepeatMode = exoPlayer.getRepeatMode();
restoreVolume = exoPlayer.getVolume();
restorePlaybackParameters = exoPlayer.getPlaybackParameters();
eventChannel.setStreamHandler(null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried just calling exoPlayer.setSurface again with a new surface, however the old surface being disposed seems to break some internal state, so I have to recreate the exo instance. This might imply some loss of state.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree but I'm not sure because I'm not deeply familiar with ExoPlayer. Does the case where a video is playing and then the app is moved to the background and brought back get impacted by this, i.e. the video is no longer playing from the point it was stopped?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me investigate this .

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at this and it seems like the video state is reset. I'm going to see if there is a way to restore it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added logic to restore all of the visible exo state when re-creating exo player.

if (isInitialized) {
exoPlayer.stop();
}
if (exoPlayer != null) {
exoPlayer.release();
}
isInitialized = false;
}

private void setUpVideoPlayer(ExoPlayer exoPlayer, QueuingEventSink eventSink) {
this.exoPlayer = exoPlayer;
this.eventSink = eventSink;
Expand All @@ -186,8 +224,7 @@ public void onCancel(Object o) {
}
});

surface = new Surface(textureEntry.surfaceTexture());
exoPlayer.setVideoSurface(surface);
exoPlayer.setVideoSurface(surfaceProducer.getSurface());
setAudioAttributes(exoPlayer, options.mixWithOthers);

exoPlayer.addListener(
Expand Down Expand Up @@ -334,11 +371,8 @@ void dispose() {
if (isInitialized) {
exoPlayer.stop();
}
textureEntry.release();
surfaceProducer.release();
eventChannel.setStreamHandler(null);
if (surface != null) {
surface.release();
}
if (exoPlayer != null) {
exoPlayer.release();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@
import android.os.Build;
import android.util.LongSparseArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import io.flutter.FlutterInjector;
import io.flutter.Log;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugins.videoplayer.Messages.AndroidVideoPlayerApi;
Expand All @@ -29,11 +36,13 @@
import javax.net.ssl.HttpsURLConnection;

/** Android platform implementation of the VideoPlayerPlugin. */
public class VideoPlayerPlugin implements FlutterPlugin, AndroidVideoPlayerApi {
public class VideoPlayerPlugin
implements FlutterPlugin, AndroidVideoPlayerApi, DefaultLifecycleObserver, ActivityAware {
private static final String TAG = "VideoPlayerPlugin";
private final LongSparseArray<VideoPlayer> videoPlayers = new LongSparseArray<>();
private FlutterState flutterState;
private final VideoPlayerOptions options = new VideoPlayerOptions();
@Nullable Lifecycle lifecycle;

/** Register this with the v2 embedding for the plugin to respond to lifecycle callbacks. */
public VideoPlayerPlugin() {}
Expand Down Expand Up @@ -83,7 +92,7 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
}
flutterState.stopListening(binding.getBinaryMessenger());
flutterState = null;
onDestroy();
performDestroy();
}

private void disposeAllPlayers() {
Expand All @@ -93,7 +102,7 @@ private void disposeAllPlayers() {
videoPlayers.clear();
}

public void onDestroy() {
public void performDestroy() {
// The whole FlutterView is being destroyed. Here we release resources acquired for all
// instances
// of VideoPlayer. Once https://github.com/flutter/flutter/issues/19358 is resolved this may
Expand All @@ -107,8 +116,7 @@ public void initialize() {
}

public @NonNull TextureMessage create(@NonNull CreateMessage arg) {
TextureRegistry.SurfaceTextureEntry handle =
flutterState.textureRegistry.createSurfaceTexture();
TextureRegistry.SurfaceProducer handle = flutterState.textureRegistry.createSurfaceProducer();
EventChannel eventChannel =
new EventChannel(
flutterState.binaryMessenger, "flutter.io/videoPlayer/videoEvents" + handle.id());
Expand Down Expand Up @@ -144,7 +152,6 @@ public void initialize() {
options);
}
videoPlayers.put(handle.id(), player);

return new TextureMessage.Builder().setTextureId(handle.id()).build();
}

Expand Down Expand Up @@ -200,6 +207,62 @@ public void setMixWithOthers(@NonNull MixWithOthersMessage arg) {
options.mixWithOthers = arg.getMixWithOthers();
}

// Activity Aware

@Override
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding);
lifecycle.addObserver(this);
}

@Override
public void onDetachedFromActivity() {}

@Override
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
onAttachedToActivity(binding);
}

@Override
public void onDetachedFromActivityForConfigChanges() {
onDetachedFromActivity();
}

// DefaultLifecycleObserver
@Override
public void onResume(@NonNull LifecycleOwner owner) {
recreateAllSurfaces();
}

@Override
public void onPause(@NonNull LifecycleOwner owner) {
destroyAllSurfaces();
}

@Override
public void onStop(@NonNull LifecycleOwner owner) {
destroyAllSurfaces();
}

@Override
public void onDestroy(@NonNull LifecycleOwner owner) {
if (lifecycle != null) {
lifecycle.removeObserver(this);
}
}

private void destroyAllSurfaces() {
for (int i = 0; i < videoPlayers.size(); i++) {
videoPlayers.valueAt(i).pauseSurface();
}
}

private void recreateAllSurfaces() {
for (int i = 0; i < videoPlayers.size(); i++) {
videoPlayers.valueAt(i).recreateSurface(flutterState.applicationContext);
}
}

private interface KeyForAssetFn {
String get(String asset);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;

import android.graphics.SurfaceTexture;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackException;
Expand All @@ -38,8 +37,7 @@
public class VideoPlayerTest {
private ExoPlayer fakeExoPlayer;
private EventChannel fakeEventChannel;
private TextureRegistry.SurfaceTextureEntry fakeSurfaceTextureEntry;
private SurfaceTexture fakeSurfaceTexture;
private TextureRegistry.SurfaceProducer fakeSurfaceProducer;
private VideoPlayerOptions fakeVideoPlayerOptions;
private QueuingEventSink fakeEventSink;
private DefaultHttpDataSource.Factory httpDataSourceFactorySpy;
Expand All @@ -52,9 +50,7 @@ public void before() {

fakeExoPlayer = mock(ExoPlayer.class);
fakeEventChannel = mock(EventChannel.class);
fakeSurfaceTextureEntry = mock(TextureRegistry.SurfaceTextureEntry.class);
fakeSurfaceTexture = mock(SurfaceTexture.class);
when(fakeSurfaceTextureEntry.surfaceTexture()).thenReturn(fakeSurfaceTexture);
fakeSurfaceProducer = mock(TextureRegistry.SurfaceProducer.class);
fakeVideoPlayerOptions = mock(VideoPlayerOptions.class);
fakeEventSink = mock(QueuingEventSink.class);
httpDataSourceFactorySpy = spy(new DefaultHttpDataSource.Factory());
Expand All @@ -66,7 +62,7 @@ public void videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNull()
new VideoPlayer(
fakeExoPlayer,
fakeEventChannel,
fakeSurfaceTextureEntry,
fakeSurfaceProducer,
fakeVideoPlayerOptions,
fakeEventSink,
httpDataSourceFactorySpy);
Expand All @@ -85,7 +81,7 @@ public void videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNull()
new VideoPlayer(
fakeExoPlayer,
fakeEventChannel,
fakeSurfaceTextureEntry,
fakeSurfaceProducer,
fakeVideoPlayerOptions,
fakeEventSink,
httpDataSourceFactorySpy);
Expand All @@ -111,7 +107,7 @@ public void videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNull()
new VideoPlayer(
fakeExoPlayer,
fakeEventChannel,
fakeSurfaceTextureEntry,
fakeSurfaceProducer,
fakeVideoPlayerOptions,
fakeEventSink,
httpDataSourceFactorySpy);
Expand All @@ -135,7 +131,7 @@ public void sendInitializedSendsExpectedEvent_90RotationDegrees() {
new VideoPlayer(
fakeExoPlayer,
fakeEventChannel,
fakeSurfaceTextureEntry,
fakeSurfaceProducer,
fakeVideoPlayerOptions,
fakeEventSink,
httpDataSourceFactorySpy);
Expand Down Expand Up @@ -164,7 +160,7 @@ public void sendInitializedSendsExpectedEvent_270RotationDegrees() {
new VideoPlayer(
fakeExoPlayer,
fakeEventChannel,
fakeSurfaceTextureEntry,
fakeSurfaceProducer,
fakeVideoPlayerOptions,
fakeEventSink,
httpDataSourceFactorySpy);
Expand Down Expand Up @@ -193,7 +189,7 @@ public void sendInitializedSendsExpectedEvent_0RotationDegrees() {
new VideoPlayer(
fakeExoPlayer,
fakeEventChannel,
fakeSurfaceTextureEntry,
fakeSurfaceProducer,
fakeVideoPlayerOptions,
fakeEventSink,
httpDataSourceFactorySpy);
Expand Down Expand Up @@ -222,7 +218,7 @@ public void sendInitializedSendsExpectedEvent_180RotationDegrees() {
new VideoPlayer(
fakeExoPlayer,
fakeEventChannel,
fakeSurfaceTextureEntry,
fakeSurfaceProducer,
fakeVideoPlayerOptions,
fakeEventSink,
httpDataSourceFactorySpy);
Expand Down Expand Up @@ -251,7 +247,7 @@ public void onIsPlayingChangedSendsExpectedEvent() {
new VideoPlayer(
fakeExoPlayer,
fakeEventChannel,
fakeSurfaceTextureEntry,
fakeSurfaceProducer,
fakeVideoPlayerOptions,
fakeEventSink,
httpDataSourceFactorySpy);
Expand Down Expand Up @@ -296,7 +292,7 @@ public void behindLiveWindowErrorResetsPlayerToDefaultPosition() {
new VideoPlayer(
fakeExoPlayer,
fakeEventChannel,
fakeSurfaceTextureEntry,
fakeSurfaceProducer,
fakeVideoPlayerOptions,
fakeEventSink,
httpDataSourceFactorySpy);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,6 @@ public void disposeAllPlayers() {

engine.destroy();
verify(videoPlayerPlugin, times(1)).onDetachedFromEngine(pluginBindingCaptor.capture());
verify(videoPlayerPlugin, times(1)).onDestroy();
verify(videoPlayerPlugin, times(1)).performDestroy();
}
}
Loading