Skip to content

Commit

Permalink
Migrate video_player/android from SurfaceTexture->`SurfaceProduce…
Browse files Browse the repository at this point in the history
…r`. (#6456)

_**WIP**: We do not plan to land this PR until the next stable release (>= April 3rd 2024)_.

Work towards flutter/flutter#145930.

## Details

Migrates uses of `createSurfaceTexture` to `createSurfaceProducer`, which is intended to have no change in behavior, but _does_ change the backend rendering path, so it will require more testing (and we're also open to minor API renames or changes before it becomes stable).

## Background 

Android plugins previously requested a `SurfaceTexture` from the Android embedder, and used that to produce a `Surface` to render external textures on (i.e. `video_player`).  This worked because 100% of Flutter applications on Android used OpenGLES (via our Skia backend), and `SurfaceTexture` is actually an (opaque) OpenGLES-texture.

Starting soon (roughly ~Q3, this is not a guarantee and just an estimate), Flutter on Android will start to use our new Impeller graphics backend, which on newer devices (`>= API_VERSION_28`), will default to the Vulkan, _not_ OpenGLES. In other words, `SurfaceTexture` will cease to work (it is possible, but non-trivial, to map an OpenGLES texture over to Vulkan).

After consultation with the Android team, they helped us understand that vending `SurfaceTexture` (the _consumer-side_ API) was never the right abstraction, and we should have been vending the _producer-side_ API, or `Surface` directly. The new `SurfaceProducer` API is exactly that - it generates a `Surface`, and similar to our platform view strategy, picks the "right" _consumer-side_ implementation details _for_ the user/plugin packages.

The new `SurfaceProducer` API has 2 possible rendering types (as an implementation detail):

- `SurfaceTexture`, for older OpenGLES devices, which works exactly as it does today.
- `ImageReader`, for newer OpenGLES _or_ Vulkan devices.

These are some subtle nuances in how these two APIs work differently (one example: flutter/flutter#144407), but our theory at this point is we don't expect these changes to be observed by any users, and we have other ideas if necessary. 

> [!NOTE]
> These invariants are [tested on CI in `flutter/engine`](https://github.com/flutter/engine/tree/main/testing/scenario_app/android#ci-configuration).

Points of contact: 
- @matanlurey or @jonahwilliams  (Flutter Engine)
- @johnmccutchan or @reidbaker  (Flutter on Android)
  • Loading branch information
matanlurey authored May 29, 2024
1 parent 28e8afd commit a5dd314
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 38 deletions.
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;

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);
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

0 comments on commit a5dd314

Please sign in to comment.