Skip to content

Commit

Permalink
[video_player] Updates minimum supported SDK (flutter#7498)
Browse files Browse the repository at this point in the history
fixes flutter/flutter#154050 and flutter/flutter#154054 by setting the min sdk version
  • Loading branch information
abdelaziz-mahdy authored Aug 26, 2024
1 parent 6c12c88 commit a0501f2
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 57 deletions.
8 changes: 8 additions & 0 deletions packages/video_player/video_player_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
## 2.7.2

* Updates minimum supported SDK version to Flutter 3.24/Dart 3.5.

* Re-adds Impeller support.


## 2.7.1

* Revert Impeller support.


## 2.7.0

* Re-adds [support for Impeller](https://docs.flutter.dev/release/breaking-changes/android-surface-plugins).
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();
}
}
6 changes: 3 additions & 3 deletions packages/video_player/video_player_android/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ 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.7.1
version: 2.7.2

environment:
sdk: ^3.4.0
flutter: ">=3.22.0"
sdk: ^3.5.0
flutter: ">=3.24.0"

flutter:
plugin:
Expand Down

0 comments on commit a0501f2

Please sign in to comment.