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

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

## 2.6.0

* Adds RTSP support.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2013 The Flutter 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.videoplayer;

import androidx.media3.common.PlaybackParameters;
import androidx.media3.exoplayer.ExoPlayer;

/**
* Internal state representing an {@link ExoPlayer} instance at a snapshot in time.
*
* <p>During the Android application lifecycle, the underlying {@link android.view.Surface} being
* rendered to by the player can be destroyed when the application is in the background and memory
* is reclaimed. Upon <em>resume</em>, the player will need to be recreated, but start again at the
* previous point (and settings).
*/
final class ExoPlayerState {
/**
* Saves a representation of the current state of the player at the current point in time.
*
* <p>The inverse of this operation is {@link #restore(ExoPlayer)}.
*
* @param exoPlayer the active player instance.
* @return an opaque object representing the state.
*/
static ExoPlayerState save(ExoPlayer exoPlayer) {
return new ExoPlayerState(
/*position=*/ exoPlayer.getCurrentPosition(),
/*repeatMode=*/ exoPlayer.getRepeatMode(),
/*volume=*/ exoPlayer.getVolume(),
/*playbackParameters=*/ exoPlayer.getPlaybackParameters());
}

private ExoPlayerState(
long position, int repeatMode, float volume, PlaybackParameters playbackParameters) {
this.position = position;
this.repeatMode = repeatMode;
this.volume = volume;
this.playbackParameters = playbackParameters;
}

/** Previous value of {@link ExoPlayer#getCurrentPosition()}. */
private final long position;

/** Previous value of {@link ExoPlayer#getRepeatMode()}. */
private final int repeatMode;

/** Previous value of {@link ExoPlayer#getVolume()}. */
private final float volume;

/** Previous value of {@link ExoPlayer#getPlaybackParameters()}. */
private final PlaybackParameters playbackParameters;

/**
* Restores the captured state onto the provided player.
*
* <p>This will typically be done after creating a new player, setting up a media source, and
* listening to events.
*
* @param exoPlayer the new player instance to reflect the state back to.
*/
void restore(ExoPlayer exoPlayer) {
exoPlayer.seekTo(position);
exoPlayer.setRepeatMode(repeatMode);
exoPlayer.setVolume(volume);
exoPlayer.setPlaybackParameters(playbackParameters);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
import static androidx.media3.common.Player.REPEAT_MODE_OFF;

import android.content.Context;
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
Expand All @@ -18,60 +19,97 @@
import androidx.media3.exoplayer.ExoPlayer;
import io.flutter.view.TextureRegistry;

final class VideoPlayer {
private ExoPlayer exoPlayer;
private Surface surface;
private final TextureRegistry.SurfaceTextureEntry textureEntry;
private final VideoPlayerCallbacks videoPlayerEvents;
private final VideoPlayerOptions options;
final class VideoPlayer implements TextureRegistry.SurfaceProducer.Callback {
@NonNull private final ExoPlayerProvider exoPlayerProvider;
@NonNull private final MediaItem mediaItem;
@NonNull private final TextureRegistry.SurfaceProducer surfaceProducer;
@NonNull private final VideoPlayerCallbacks videoPlayerEvents;
@NonNull private final VideoPlayerOptions options;
@NonNull private ExoPlayer exoPlayer;
@Nullable private ExoPlayerState savedStateDuring;

/**
* Creates a video player.
*
* @param context application context.
* @param events event callbacks.
* @param textureEntry texture to render to.
* @param surfaceProducer produces a texture to render to.
* @param asset asset to play.
* @param options options for playback.
* @return a video player instance.
*/
@NonNull
static VideoPlayer create(
Context context,
VideoPlayerCallbacks events,
TextureRegistry.SurfaceTextureEntry textureEntry,
VideoAsset asset,
VideoPlayerOptions options) {
ExoPlayer.Builder builder =
new ExoPlayer.Builder(context).setMediaSourceFactory(asset.getMediaSourceFactory(context));
return new VideoPlayer(builder, events, textureEntry, asset.getMediaItem(), options);
@NonNull Context context,
@NonNull VideoPlayerCallbacks events,
@NonNull TextureRegistry.SurfaceProducer surfaceProducer,
@NonNull VideoAsset asset,
@NonNull VideoPlayerOptions options) {
return new VideoPlayer(
() -> {
ExoPlayer.Builder builder =
new ExoPlayer.Builder(context)
.setMediaSourceFactory(asset.getMediaSourceFactory(context));
return builder.build();
},
events,
surfaceProducer,
asset.getMediaItem(),
options);
}

/** A closure-compatible signature since {@link java.util.function.Supplier} is API level 24. */
interface ExoPlayerProvider {
/**
* Returns a new {@link ExoPlayer}.
*
* @return new instance.
*/
ExoPlayer get();
}

@VisibleForTesting
VideoPlayer(
ExoPlayer.Builder builder,
VideoPlayerCallbacks events,
TextureRegistry.SurfaceTextureEntry textureEntry,
MediaItem mediaItem,
VideoPlayerOptions options) {
@NonNull ExoPlayerProvider exoPlayerProvider,
@NonNull VideoPlayerCallbacks events,
@NonNull TextureRegistry.SurfaceProducer surfaceProducer,
@NonNull MediaItem mediaItem,
@NonNull VideoPlayerOptions options) {
this.exoPlayerProvider = exoPlayerProvider;
this.videoPlayerEvents = events;
this.textureEntry = textureEntry;
this.surfaceProducer = surfaceProducer;
this.mediaItem = mediaItem;
this.options = options;
this.exoPlayer = createVideoPlayer();
surfaceProducer.setCallback(this);
}

ExoPlayer exoPlayer = builder.build();
exoPlayer.setMediaItem(mediaItem);
exoPlayer.prepare();
@RestrictTo(RestrictTo.Scope.LIBRARY)
public void onSurfaceCreated() {
exoPlayer = createVideoPlayer();
if (savedStateDuring != null) {
savedStateDuring.restore(exoPlayer);
savedStateDuring = null;
}
}

setUpVideoPlayer(exoPlayer);
@RestrictTo(RestrictTo.Scope.LIBRARY)
public void onSurfaceDestroyed() {
exoPlayer.stop();
savedStateDuring = ExoPlayerState.save(exoPlayer);
exoPlayer.release();
}

private void setUpVideoPlayer(ExoPlayer exoPlayer) {
this.exoPlayer = exoPlayer;
private ExoPlayer createVideoPlayer() {
ExoPlayer exoPlayer = exoPlayerProvider.get();
exoPlayer.setMediaItem(mediaItem);
exoPlayer.prepare();

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

return exoPlayer;
}

void sendBufferingUpdate() {
Expand All @@ -85,11 +123,11 @@ private static void setAudioAttributes(ExoPlayer exoPlayer, boolean isMixMode) {
}

void play() {
exoPlayer.setPlayWhenReady(true);
exoPlayer.play();
}

void pause() {
exoPlayer.setPlayWhenReady(false);
exoPlayer.pause();
}

void setLooping(boolean value) {
Expand Down Expand Up @@ -118,12 +156,7 @@ long getPosition() {
}

void dispose() {
textureEntry.release();
if (surface != null) {
surface.release();
}
if (exoPlayer != null) {
exoPlayer.release();
}
surfaceProducer.release();
exoPlayer.release();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import io.flutter.view.TextureRegistry;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;

/** Android platform implementation of the VideoPlayerPlugin. */
Expand Down Expand Up @@ -94,8 +93,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 All @@ -113,7 +111,6 @@ public void initialize() {
} else if (arg.getUri().startsWith("rtsp://")) {
videoAsset = VideoAsset.fromRtspUrl(arg.getUri());
} else {
Map<String, String> httpHeaders = arg.getHttpHeaders();
VideoAsset.StreamingFormat streamingFormat = VideoAsset.StreamingFormat.UNKNOWN;
String formatHint = arg.getFormatHint();
if (formatHint != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

import android.graphics.SurfaceTexture;
import android.view.Surface;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.PlaybackParameters;
Expand Down Expand Up @@ -44,18 +44,17 @@ public final class VideoPlayerTest {
private FakeVideoAsset fakeVideoAsset;

@Mock private VideoPlayerCallbacks mockEvents;
@Mock private TextureRegistry.SurfaceTextureEntry mockTexture;
@Mock private ExoPlayer.Builder mockBuilder;
@Mock private TextureRegistry.SurfaceProducer mockProducer;
@Mock private ExoPlayer mockExoPlayer;
@Captor private ArgumentCaptor<AudioAttributes> attributesCaptor;
@Captor private ArgumentCaptor<TextureRegistry.SurfaceProducer.Callback> callbackCaptor;

@Rule public MockitoRule initRule = MockitoJUnit.rule();

@Before
public void setUp() {
fakeVideoAsset = new FakeVideoAsset(FAKE_ASSET_URL);
when(mockBuilder.build()).thenReturn(mockExoPlayer);
when(mockTexture.surfaceTexture()).thenReturn(mock(SurfaceTexture.class));
when(mockProducer.getSurface()).thenReturn(mock(Surface.class));
}

private VideoPlayer createVideoPlayer() {
Expand All @@ -64,7 +63,7 @@ private VideoPlayer createVideoPlayer() {

private VideoPlayer createVideoPlayer(VideoPlayerOptions options) {
return new VideoPlayer(
mockBuilder, mockEvents, mockTexture, fakeVideoAsset.getMediaItem(), options);
() -> mockExoPlayer, mockEvents, mockProducer, fakeVideoAsset.getMediaItem(), options);
}

@Test
Expand All @@ -73,7 +72,7 @@ public void loadsAndPreparesProvidedMediaEnablesAudioFocusByDefault() {

verify(mockExoPlayer).setMediaItem(fakeVideoAsset.getMediaItem());
verify(mockExoPlayer).prepare();
verify(mockTexture).surfaceTexture();
verify(mockProducer).getSurface();
verify(mockExoPlayer).setVideoSurface(any());

verify(mockExoPlayer).setAudioAttributes(attributesCaptor.capture(), eq(true));
Expand All @@ -100,10 +99,10 @@ public void playsAndPausesProvidedMedia() {
VideoPlayer videoPlayer = createVideoPlayer();

videoPlayer.play();
verify(mockExoPlayer).setPlayWhenReady(true);
verify(mockExoPlayer).play();

videoPlayer.pause();
verify(mockExoPlayer).setPlayWhenReady(false);
verify(mockExoPlayer).pause();

videoPlayer.dispose();
}
Expand Down Expand Up @@ -169,12 +168,41 @@ public void seekAndGetPosition() {
assertEquals(20L, videoPlayer.getPosition());
}

@Test
public void onSurfaceProducerDestroyedAndRecreatedReleasesAndThenRecreatesAndResumesPlayer() {
VideoPlayer videoPlayer = createVideoPlayer();

verify(mockProducer).setCallback(callbackCaptor.capture());
verify(mockExoPlayer, never()).release();

when(mockExoPlayer.getCurrentPosition()).thenReturn(10L);
when(mockExoPlayer.getRepeatMode()).thenReturn(Player.REPEAT_MODE_ALL);
when(mockExoPlayer.getVolume()).thenReturn(0.5f);
when(mockExoPlayer.getPlaybackParameters()).thenReturn(new PlaybackParameters(2.5f));

TextureRegistry.SurfaceProducer.Callback producerLifecycle = callbackCaptor.getValue();
producerLifecycle.onSurfaceDestroyed();

verify(mockExoPlayer).release();

// Create a new mock exo player so that we get a new instance.
mockExoPlayer = mock(ExoPlayer.class);
producerLifecycle.onSurfaceCreated();

verify(mockExoPlayer).seekTo(10L);
verify(mockExoPlayer).setRepeatMode(Player.REPEAT_MODE_ALL);
verify(mockExoPlayer).setVolume(0.5f);
verify(mockExoPlayer).setPlaybackParameters(new PlaybackParameters(2.5f));

videoPlayer.dispose();
}

@Test
public void disposeReleasesTextureAndPlayer() {
VideoPlayer videoPlayer = createVideoPlayer();
videoPlayer.dispose();

verify(mockTexture).release();
verify(mockProducer).release();
verify(mockExoPlayer).release();
}
}
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.6.0
version: 2.7.0

environment:
sdk: ^3.4.0
Expand Down