Skip to content
Open
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.9.6

* Fixes a [bug](https://github.com/flutter/flutter/issues/184241) where the video freezes after returning from a full-screen transition on Android.

## 2.9.5

* Updates build files from Groovy to Kotlin.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import android.content.Context;
import android.os.Build;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
Expand All @@ -30,23 +31,14 @@ public final class PlatformVideoView implements PlatformView {
*/
@OptIn(markerClass = UnstableApi.class)
public PlatformVideoView(@NonNull Context context, @NonNull ExoPlayer exoPlayer) {
surfaceView = new SurfaceView(context);

if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
// Workaround for rendering issues on Android 9 (API 28).
// On Android 9, using setVideoSurfaceView seems to lead to issues where the first frame is
// not displayed if the video is paused initially.
// To ensure the first frame is visible, the surface is directly set using holder.getSurface()
// when the surface is created, and ExoPlayer seeks to a position to force rendering of the
// first frame.
setupSurfaceWithCallback(exoPlayer);
} else {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) {
// Avoid blank space instead of a video on Android versions below 8 by adjusting video's
// z-layer within the Android view hierarchy:
surfaceView.setZOrderMediaOverlay(true);
}
exoPlayer.setVideoSurfaceView(surfaceView);
this.surfaceView = new VideoSurfaceView(context, exoPlayer);

setupSurfaceWithCallback(exoPlayer);

if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) {
// Avoid blank space instead of a video on Android versions below 8 by adjusting video's
// z-layer within the Android view hierarchy:
surfaceView.setZOrderMediaOverlay(true);
}
}

Expand All @@ -57,24 +49,65 @@ private void setupSurfaceWithCallback(@NonNull ExoPlayer exoPlayer) {
new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
exoPlayer.setVideoSurface(holder.getSurface());
// Force first frame rendering:
exoPlayer.seekTo(1);
bindPlayerToSurface(exoPlayer, holder.getSurface());
}

@Override
public void surfaceChanged(
@NonNull SurfaceHolder holder, int format, int width, int height) {
// No implementation needed.
}
@NonNull SurfaceHolder holder, int format, int width, int height) {}

@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
exoPlayer.setVideoSurface(null);
// Use clearVideoSurface to ensure we only unbind if this surface is currently active.
exoPlayer.clearVideoSurface(holder.getSurface());
}
});
}

/**
* Binds the ExoPlayer to the provided surface and performs a seek operation to ensure the video
* frame is rendered. Includes special handling for Android 9.
*/
private static void bindPlayerToSurface(@NonNull ExoPlayer exoPlayer, @NonNull Surface surface) {
if (surface.isValid()) {
exoPlayer.setVideoSurface(surface);

// Workaround for a rendering bug on Android 9 (API 28) where the decoder does not
// flush its output buffer when a new surface is attached while the player is paused,
// resulting in a black frame. A seek forces the codec to produce and display a frame.
if (!exoPlayer.getPlayWhenReady()) {
long current = exoPlayer.getCurrentPosition();
if (current == 0 && Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
exoPlayer.seekTo(1);
} else {
exoPlayer.seekTo(current);
}
}
}
}

/**
* A custom SurfaceView that handles visibility changes to ensure video rendering is restored
* during route transitions (e.g., returning from a full-screen view).
*/
private static class VideoSurfaceView extends SurfaceView {
private final ExoPlayer exoPlayer;

public VideoSurfaceView(Context context, ExoPlayer exoPlayer) {
super(context);
this.exoPlayer = exoPlayer;
}

@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
// When the view becomes visible again, ensure the ExoPlayer is re-attached to the surface.
if (visibility == View.VISIBLE && isShown()) {
bindPlayerToSurface(exoPlayer, getHolder().getSurface());
}
}
}

/**
* Returns the view associated with this PlatformView.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,91 @@
import static org.mockito.Mockito.*;

import android.content.Context;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.test.core.app.ApplicationProvider;
import io.flutter.plugins.videoplayer.platformview.PlatformVideoView;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Objects;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.Shadows;
import org.robolectric.shadows.ShadowSurfaceView;

/** Unit tests for {@link PlatformVideoViewTest}. */
/** Unit tests for {@link PlatformVideoView}. */
@RunWith(RobolectricTestRunner.class)
public class PlatformVideoViewTest {

@Test
public void createsSurfaceViewAndSetsItForExoPlayer() throws Exception {
final Context context = ApplicationProvider.getApplicationContext();
final ExoPlayer exoPlayer = spy(new ExoPlayer.Builder(context).build());

final PlatformVideoView view = new PlatformVideoView(context, exoPlayer);

// Get the internal SurfaceView via reflection for testing.
final Field field = PlatformVideoView.class.getDeclaredField("surfaceView");
field.setAccessible(true);
final SurfaceView surfaceView = (SurfaceView) field.get(view);

assertNotNull(surfaceView);
verify(exoPlayer).setVideoSurfaceView(surfaceView);
// Bypass FakeSurfaceHolder to get the callback registered by PlatformVideoView
ShadowSurfaceView shadowView = Shadows.shadowOf(surfaceView);
Iterable<SurfaceHolder.Callback> callbacks = shadowView.getFakeSurfaceHolder().getCallbacks();
assertNotNull("SurfaceCallbacks should not be null", callbacks);

SurfaceHolder.Callback callback = callbacks.iterator().next();
assertNotNull("Callback must exist", callback);

Surface mockSurface = mock(Surface.class);
when(mockSurface.isValid()).thenReturn(true);
SurfaceHolder mockHolder = mock(SurfaceHolder.class);
when(mockHolder.getSurface()).thenReturn(mockSurface);

// Trigger manually
callback.surfaceCreated(mockHolder);

// Verify it used the manual surface mechanism instead of setVideoSurfaceView()
verify(exoPlayer).setVideoSurface(mockSurface);

// For Android 9 bug workaround
verify(exoPlayer).seekTo(anyLong());

exoPlayer.release();
}

@Test
public void rebindsSurfaceWhenVisibilityChangesToVisible() throws Exception {
final Context context = ApplicationProvider.getApplicationContext();
final ExoPlayer exoPlayer = spy(new ExoPlayer.Builder(context).build());
final PlatformVideoView view = new PlatformVideoView(context, exoPlayer);

final Field field = PlatformVideoView.class.getDeclaredField("surfaceView");
field.setAccessible(true);
final SurfaceView surfaceView = spy((SurfaceView) Objects.requireNonNull(field.get(view)));
when(surfaceView.isShown()).thenReturn(true);
field.set(view, surfaceView); // Inject the spy back

Surface mockSurface = mock(Surface.class);
when(mockSurface.isValid()).thenReturn(true);
SurfaceHolder mockHolder = mock(SurfaceHolder.class);
when(mockHolder.getSurface()).thenReturn(mockSurface);
when(surfaceView.getHolder()).thenReturn(mockHolder);

// Trigger visibility changed
Method method = View.class.getDeclaredMethod("onVisibilityChanged", View.class, int.class);
method.setAccessible(true);

reset(exoPlayer);
method.invoke(surfaceView, surfaceView, View.VISIBLE);

verify(exoPlayer).setVideoSurface(mockSurface);
verify(exoPlayer).seekTo(anyLong());

exoPlayer.release();
}
Expand Down
2 changes: 1 addition & 1 deletion packages/video_player/video_player_android/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.9.5
version: 2.9.6

environment:
sdk: ^3.9.0
Expand Down