Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[video_player] Updates minimum supported SDK #7498

Merged
merged 8 commits into from
Aug 26, 2024
Merged
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.

* Re-adds Impeller support.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future please make sure that the list under a given version is one list with one or more entries, not multiple separate lists of one item each, which renders differently on pub.dev.

Copy link
Contributor Author

@abdelaziz-mahdy abdelaziz-mahdy Aug 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I am sorry for that want me to issue a pr fixing that?

If yes, what should be the updated version number

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, CHANGELOG formatting isn't something that we would do another release for unless it were extremely broken.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok thank you for letting me know I will make sure it's correct next time.



## 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();
}
}
4 changes: 2 additions & 2 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"
flutter: ">=3.24.0"

flutter:
plugin:
Expand Down