From a0501f2e2a50c88b2cb3ddfec64db37e7931dc6d Mon Sep 17 00:00:00 2001 From: Abdelaziz Mahdy Date: Mon, 26 Aug 2024 13:22:14 -0300 Subject: [PATCH] [video_player] Updates minimum supported SDK (#7498) fixes https://github.com/flutter/flutter/issues/154050 and https://github.com/flutter/flutter/issues/154054 by setting the min sdk version --- .../video_player_android/CHANGELOG.md | 8 ++ .../plugins/videoplayer/ExoPlayerState.java | 69 +++++++++++ .../plugins/videoplayer/VideoPlayer.java | 113 +++++++++++------- .../videoplayer/VideoPlayerPlugin.java | 5 +- .../plugins/videoplayer/VideoPlayerTest.java | 48 ++++++-- .../video_player_android/pubspec.yaml | 6 +- 6 files changed, 192 insertions(+), 57 deletions(-) create mode 100644 packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerState.java diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index e88f3385c874..0bd45e401acb 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -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). diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerState.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerState.java new file mode 100644 index 000000000000..cd55b54c1247 --- /dev/null +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerState.java @@ -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. + * + *

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 resume, 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. + * + *

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. + * + *

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); + } +} diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index ab20b30f42d0..8b12c8a2a0b8 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -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; @@ -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() { @@ -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) { @@ -118,12 +156,7 @@ long getPosition() { } void dispose() { - textureEntry.release(); - if (surface != null) { - surface.release(); - } - if (exoPlayer != null) { - exoPlayer.release(); - } + surfaceProducer.release(); + exoPlayer.release(); } } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java index af13c5395512..2a9f5cc38579 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java @@ -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. */ @@ -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()); @@ -113,7 +111,6 @@ public void initialize() { } else if (arg.getUri().startsWith("rtsp://")) { videoAsset = VideoAsset.fromRtspUrl(arg.getUri()); } else { - Map httpHeaders = arg.getHttpHeaders(); VideoAsset.StreamingFormat streamingFormat = VideoAsset.StreamingFormat.UNKNOWN; String formatHint = arg.getFormatHint(); if (formatHint != null) { diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java index a30166c0aa51..1b7ff4071196 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java @@ -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; @@ -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 attributesCaptor; + @Captor private ArgumentCaptor 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() { @@ -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 @@ -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)); @@ -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(); } @@ -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(); } } diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index 488e5e32e47a..c23731808265 100644 --- a/packages/video_player/video_player_android/pubspec.yaml +++ b/packages/video_player/video_player_android/pubspec.yaml @@ -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: