diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index 544dff2c94b..6b97bf3a2d6 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.8.0 +* Adds support for platform views as an optional way of displaying a video. * Suppresses deprecation and removal warnings for `TextureRegistry.SurfaceProducer.onSurfaceDestroyed`. diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java index df6115fd584..e96eca8b3a7 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java @@ -4,23 +4,18 @@ package io.flutter.plugins.videoplayer; -import android.os.Build; import androidx.annotation.NonNull; -import androidx.annotation.OptIn; -import androidx.media3.common.Format; import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; -import androidx.media3.common.VideoSize; import androidx.media3.exoplayer.ExoPlayer; -import java.util.Objects; -final class ExoPlayerEventListener implements Player.Listener { - private final ExoPlayer exoPlayer; - private final VideoPlayerCallbacks events; +public abstract class ExoPlayerEventListener implements Player.Listener { private boolean isBuffering = false; private boolean isInitialized; + protected final ExoPlayer exoPlayer; + protected final VideoPlayerCallbacks events; - private enum RotationDegrees { + protected enum RotationDegrees { ROTATE_0(0), ROTATE_90(90), ROTATE_180(180), @@ -46,11 +41,8 @@ public int getDegrees() { } } - ExoPlayerEventListener(ExoPlayer exoPlayer, VideoPlayerCallbacks events) { - this(exoPlayer, events, false); - } - - ExoPlayerEventListener(ExoPlayer exoPlayer, VideoPlayerCallbacks events, boolean initialized) { + public ExoPlayerEventListener( + @NonNull ExoPlayer exoPlayer, @NonNull VideoPlayerCallbacks events, boolean initialized) { this.exoPlayer = exoPlayer; this.events = events; this.isInitialized = initialized; @@ -68,90 +60,7 @@ private void setBuffering(boolean buffering) { } } - @SuppressWarnings("SuspiciousNameCombination") - private void sendInitialized() { - if (isInitialized) { - return; - } - isInitialized = true; - VideoSize videoSize = exoPlayer.getVideoSize(); - int rotationCorrection = 0; - int width = videoSize.width; - int height = videoSize.height; - if (width != 0 && height != 0) { - RotationDegrees reportedRotationCorrection = RotationDegrees.ROTATE_0; - - if (Build.VERSION.SDK_INT <= 21) { - // On API 21 and below, Exoplayer may not internally handle rotation correction - // and reports it through VideoSize.unappliedRotationDegrees. We may apply it to - // fix the case of upside-down playback. - try { - reportedRotationCorrection = - RotationDegrees.fromDegrees(videoSize.unappliedRotationDegrees); - rotationCorrection = - getRotationCorrectionFromUnappliedRotation(reportedRotationCorrection); - } catch (IllegalArgumentException e) { - // Unapplied rotation other than 0, 90, 180, 270 reported by VideoSize. Because this is unexpected, - // we apply no rotation correction. - reportedRotationCorrection = RotationDegrees.ROTATE_0; - rotationCorrection = 0; - } - } - // TODO(camsim99): Replace this with a call to `handlesCropAndRotation` when it is - // available in stable. https://github.com/flutter/flutter/issues/157198 - else if (Build.VERSION.SDK_INT < 29) { - // When the SurfaceTexture backend for Impeller is used, the preview should already - // be correctly rotated. - rotationCorrection = 0; - } else { - // The video's Format also provides a rotation correction that may be used to - // correct the rotation, so we try to use that to correct the video rotation - // when the ImageReader backend for Impeller is used. - rotationCorrection = getRotationCorrectionFromFormat(exoPlayer); - - try { - reportedRotationCorrection = RotationDegrees.fromDegrees(rotationCorrection); - } catch (IllegalArgumentException e) { - // Rotation correction other than 0, 90, 180, 270 reported by Format. Because this is unexpected, - // we apply no rotation correction. - reportedRotationCorrection = RotationDegrees.ROTATE_0; - rotationCorrection = 0; - } - } - - // Switch the width/height if video was taken in portrait mode and a rotation - // correction was detected. - if (reportedRotationCorrection == RotationDegrees.ROTATE_90 - || reportedRotationCorrection == RotationDegrees.ROTATE_270) { - width = videoSize.height; - height = videoSize.width; - } - } - events.onInitialized(width, height, exoPlayer.getDuration(), rotationCorrection); - } - - private int getRotationCorrectionFromUnappliedRotation(RotationDegrees unappliedRotationDegrees) { - int rotationCorrection = 0; - - // Rotating the video with ExoPlayer does not seem to be possible with a Surface, - // so inform the Flutter code that the widget needs to be rotated to prevent - // upside-down playback for videos with unappliedRotationDegrees of 180 (other orientations - // work correctly without correction). - if (unappliedRotationDegrees == RotationDegrees.ROTATE_180) { - rotationCorrection = unappliedRotationDegrees.getDegrees(); - } - - return rotationCorrection; - } - - @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) - // A video's Format and its rotation degrees are unstable because they are not guaranteed - // the same implementation across API versions. It is possible that this logic may need - // revisiting should the implementation change across versions of the Exoplayer API. - private int getRotationCorrectionFromFormat(ExoPlayer exoPlayer) { - Format videoFormat = Objects.requireNonNull(exoPlayer.getVideoFormat()); - return videoFormat.rotationDegrees; - } + protected abstract void sendInitialized(); @Override public void onPlaybackStateChanged(final int playbackState) { @@ -161,6 +70,10 @@ public void onPlaybackStateChanged(final int playbackState) { events.onBufferingUpdate(exoPlayer.getBufferedPosition()); break; case Player.STATE_READY: + if (isInitialized) { + return; + } + isInitialized = true; sendInitialized(); break; case Player.STATE_ENDED: @@ -178,7 +91,8 @@ public void onPlaybackStateChanged(final int playbackState) { public void onPlayerError(@NonNull final PlaybackException error) { setBuffering(false); if (error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) { - // See https://exoplayer.dev/live-streaming.html#behindlivewindowexception-and-error_code_behind_live_window + // See + // https://exoplayer.dev/live-streaming.html#behindlivewindowexception-and-error_code_behind_live_window exoPlayer.seekToDefaultPosition(); exoPlayer.prepare(); } else { 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 index cd55b54c124..e0e9babaa02 100644 --- 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 @@ -4,6 +4,7 @@ package io.flutter.plugins.videoplayer; +import androidx.annotation.NonNull; import androidx.media3.common.PlaybackParameters; import androidx.media3.exoplayer.ExoPlayer; @@ -15,7 +16,7 @@ * is reclaimed. Upon resume, the player will need to be recreated, but start again at the * previous point (and settings). */ -final class ExoPlayerState { +public final class ExoPlayerState { /** * Saves a representation of the current state of the player at the current point in time. * @@ -24,12 +25,13 @@ final class ExoPlayerState { * @param exoPlayer the active player instance. * @return an opaque object representing the state. */ - static ExoPlayerState save(ExoPlayer exoPlayer) { + @NonNull + public static ExoPlayerState save(@NonNull ExoPlayer exoPlayer) { return new ExoPlayerState( - /*position=*/ exoPlayer.getCurrentPosition(), - /*repeatMode=*/ exoPlayer.getRepeatMode(), - /*volume=*/ exoPlayer.getVolume(), - /*playbackParameters=*/ exoPlayer.getPlaybackParameters()); + /* position= */ exoPlayer.getCurrentPosition(), + /* repeatMode= */ exoPlayer.getRepeatMode(), + /* volume= */ exoPlayer.getVolume(), + /* playbackParameters= */ exoPlayer.getPlaybackParameters()); } private ExoPlayerState( @@ -60,7 +62,7 @@ private ExoPlayerState( * * @param exoPlayer the new player instance to reflect the state back to. */ - void restore(ExoPlayer exoPlayer) { + public void restore(@NonNull ExoPlayer exoPlayer) { exoPlayer.seekTo(position); exoPlayer.setRepeatMode(repeatMode); exoPlayer.setVolume(volume); diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/HttpVideoAsset.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/HttpVideoAsset.java index f29efc3cab6..b13714de111 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/HttpVideoAsset.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/HttpVideoAsset.java @@ -37,7 +37,7 @@ final class HttpVideoAsset extends VideoAsset { @NonNull @Override - MediaItem getMediaItem() { + public MediaItem getMediaItem() { MediaItem.Builder builder = new MediaItem.Builder().setUri(assetUrl); String mimeType = null; switch (streamingFormat) { @@ -57,8 +57,9 @@ MediaItem getMediaItem() { return builder.build(); } + @NonNull @Override - MediaSource.Factory getMediaSourceFactory(Context context) { + public MediaSource.Factory getMediaSourceFactory(@NonNull Context context) { return getMediaSourceFactory(context, new DefaultHttpDataSource.Factory()); } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/LocalVideoAsset.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/LocalVideoAsset.java index 3d1b3d850d2..bf55b9ba4ee 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/LocalVideoAsset.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/LocalVideoAsset.java @@ -17,12 +17,13 @@ final class LocalVideoAsset extends VideoAsset { @NonNull @Override - MediaItem getMediaItem() { + public MediaItem getMediaItem() { return new MediaItem.Builder().setUri(assetUrl).build(); } + @NonNull @Override - MediaSource.Factory getMediaSourceFactory(Context context) { + public MediaSource.Factory getMediaSourceFactory(@NonNull Context context) { return new DefaultMediaSourceFactory(context); } } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java index 87004551885..581cb7b80f6 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v22.5.0), do not edit directly. +// Autogenerated from Pigeon (v22.6.1), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.videoplayer; @@ -65,6 +65,90 @@ protected static ArrayList wrapError(@NonNull Throwable exception) { @Retention(CLASS) @interface CanIgnoreReturnValue {} + /** Pigeon equivalent of VideoViewType. */ + public enum PlatformVideoViewType { + TEXTURE_VIEW(0), + PLATFORM_VIEW(1); + + final int index; + + PlatformVideoViewType(final int index) { + this.index = index; + } + } + + /** + * Information passed to the platform view creation. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class PlatformVideoViewCreationParams { + private @NonNull Long playerId; + + public @NonNull Long getPlayerId() { + return playerId; + } + + public void setPlayerId(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"playerId\" is null."); + } + this.playerId = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + PlatformVideoViewCreationParams() {} + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PlatformVideoViewCreationParams that = (PlatformVideoViewCreationParams) o; + return playerId.equals(that.playerId); + } + + @Override + public int hashCode() { + return Objects.hash(playerId); + } + + public static final class Builder { + + private @Nullable Long playerId; + + @CanIgnoreReturnValue + public @NonNull Builder setPlayerId(@NonNull Long setterArg) { + this.playerId = setterArg; + return this; + } + + public @NonNull PlatformVideoViewCreationParams build() { + PlatformVideoViewCreationParams pigeonReturn = new PlatformVideoViewCreationParams(); + pigeonReturn.setPlayerId(playerId); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(1); + toListResult.add(playerId); + return toListResult; + } + + static @NonNull PlatformVideoViewCreationParams fromList( + @NonNull ArrayList pigeonVar_list) { + PlatformVideoViewCreationParams pigeonResult = new PlatformVideoViewCreationParams(); + Object playerId = pigeonVar_list.get(0); + pigeonResult.setPlayerId((Long) playerId); + return pigeonResult; + } + } + /** Generated class from Pigeon that represents data sent in messages. */ public static final class CreateMessage { private @Nullable String asset; @@ -120,6 +204,16 @@ public void setHttpHeaders(@NonNull Map setterArg) { this.httpHeaders = setterArg; } + private @Nullable PlatformVideoViewType viewType; + + public @Nullable PlatformVideoViewType getViewType() { + return viewType; + } + + public void setViewType(@Nullable PlatformVideoViewType setterArg) { + this.viewType = setterArg; + } + /** Constructor is non-public to enforce null safety; use Builder. */ CreateMessage() {} @@ -136,12 +230,13 @@ public boolean equals(Object o) { && Objects.equals(uri, that.uri) && Objects.equals(packageName, that.packageName) && Objects.equals(formatHint, that.formatHint) - && httpHeaders.equals(that.httpHeaders); + && httpHeaders.equals(that.httpHeaders) + && Objects.equals(viewType, that.viewType); } @Override public int hashCode() { - return Objects.hash(asset, uri, packageName, formatHint, httpHeaders); + return Objects.hash(asset, uri, packageName, formatHint, httpHeaders, viewType); } public static final class Builder { @@ -186,6 +281,14 @@ public static final class Builder { return this; } + private @Nullable PlatformVideoViewType viewType; + + @CanIgnoreReturnValue + public @NonNull Builder setViewType(@Nullable PlatformVideoViewType setterArg) { + this.viewType = setterArg; + return this; + } + public @NonNull CreateMessage build() { CreateMessage pigeonReturn = new CreateMessage(); pigeonReturn.setAsset(asset); @@ -193,18 +296,20 @@ public static final class Builder { pigeonReturn.setPackageName(packageName); pigeonReturn.setFormatHint(formatHint); pigeonReturn.setHttpHeaders(httpHeaders); + pigeonReturn.setViewType(viewType); return pigeonReturn; } } @NonNull ArrayList toList() { - ArrayList toListResult = new ArrayList<>(5); + ArrayList toListResult = new ArrayList<>(6); toListResult.add(asset); toListResult.add(uri); toListResult.add(packageName); toListResult.add(formatHint); toListResult.add(httpHeaders); + toListResult.add(viewType); return toListResult; } @@ -220,6 +325,8 @@ ArrayList toList() { pigeonResult.setFormatHint((String) formatHint); Object httpHeaders = pigeonVar_list.get(4); pigeonResult.setHttpHeaders((Map) httpHeaders); + Object viewType = pigeonVar_list.get(5); + pigeonResult.setViewType((PlatformVideoViewType) viewType); return pigeonResult; } } @@ -233,6 +340,13 @@ private PigeonCodec() {} protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { case (byte) 129: + { + Object value = readValue(buffer); + return value == null ? null : PlatformVideoViewType.values()[((Long) value).intValue()]; + } + case (byte) 130: + return PlatformVideoViewCreationParams.fromList((ArrayList) readValue(buffer)); + case (byte) 131: return CreateMessage.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -241,8 +355,14 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { @Override protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { - if (value instanceof CreateMessage) { + if (value instanceof PlatformVideoViewType) { stream.write(129); + writeValue(stream, value == null ? null : ((PlatformVideoViewType) value).index); + } else if (value instanceof PlatformVideoViewCreationParams) { + stream.write(130); + writeValue(stream, ((PlatformVideoViewCreationParams) value).toList()); + } else if (value instanceof CreateMessage) { + stream.write(131); writeValue(stream, ((CreateMessage) value).toList()); } else { super.writeValue(stream, value); @@ -258,22 +378,22 @@ public interface AndroidVideoPlayerApi { @NonNull Long create(@NonNull CreateMessage msg); - void dispose(@NonNull Long textureId); + void dispose(@NonNull Long playerId); - void setLooping(@NonNull Long textureId, @NonNull Boolean looping); + void setLooping(@NonNull Long playerId, @NonNull Boolean looping); - void setVolume(@NonNull Long textureId, @NonNull Double volume); + void setVolume(@NonNull Long playerId, @NonNull Double volume); - void setPlaybackSpeed(@NonNull Long textureId, @NonNull Double speed); + void setPlaybackSpeed(@NonNull Long playerId, @NonNull Double speed); - void play(@NonNull Long textureId); + void play(@NonNull Long playerId); @NonNull - Long position(@NonNull Long textureId); + Long position(@NonNull Long playerId); - void seekTo(@NonNull Long textureId, @NonNull Long position); + void seekTo(@NonNull Long playerId, @NonNull Long position); - void pause(@NonNull Long textureId); + void pause(@NonNull Long playerId); void setMixWithOthers(@NonNull Boolean mixWithOthers); @@ -281,6 +401,7 @@ public interface AndroidVideoPlayerApi { static @NonNull MessageCodec getCodec() { return PigeonCodec.INSTANCE; } + /** * Sets up an instance of `AndroidVideoPlayerApi` to handle messages through the * `binaryMessenger`. @@ -355,9 +476,9 @@ static void setUp( (message, reply) -> { ArrayList wrapped = new ArrayList<>(); ArrayList args = (ArrayList) message; - Long textureIdArg = (Long) args.get(0); + Long playerIdArg = (Long) args.get(0); try { - api.dispose(textureIdArg); + api.dispose(playerIdArg); wrapped.add(0, null); } catch (Throwable exception) { wrapped = wrapError(exception); @@ -380,10 +501,10 @@ static void setUp( (message, reply) -> { ArrayList wrapped = new ArrayList<>(); ArrayList args = (ArrayList) message; - Long textureIdArg = (Long) args.get(0); + Long playerIdArg = (Long) args.get(0); Boolean loopingArg = (Boolean) args.get(1); try { - api.setLooping(textureIdArg, loopingArg); + api.setLooping(playerIdArg, loopingArg); wrapped.add(0, null); } catch (Throwable exception) { wrapped = wrapError(exception); @@ -406,10 +527,10 @@ static void setUp( (message, reply) -> { ArrayList wrapped = new ArrayList<>(); ArrayList args = (ArrayList) message; - Long textureIdArg = (Long) args.get(0); + Long playerIdArg = (Long) args.get(0); Double volumeArg = (Double) args.get(1); try { - api.setVolume(textureIdArg, volumeArg); + api.setVolume(playerIdArg, volumeArg); wrapped.add(0, null); } catch (Throwable exception) { wrapped = wrapError(exception); @@ -432,10 +553,10 @@ static void setUp( (message, reply) -> { ArrayList wrapped = new ArrayList<>(); ArrayList args = (ArrayList) message; - Long textureIdArg = (Long) args.get(0); + Long playerIdArg = (Long) args.get(0); Double speedArg = (Double) args.get(1); try { - api.setPlaybackSpeed(textureIdArg, speedArg); + api.setPlaybackSpeed(playerIdArg, speedArg); wrapped.add(0, null); } catch (Throwable exception) { wrapped = wrapError(exception); @@ -458,9 +579,9 @@ static void setUp( (message, reply) -> { ArrayList wrapped = new ArrayList<>(); ArrayList args = (ArrayList) message; - Long textureIdArg = (Long) args.get(0); + Long playerIdArg = (Long) args.get(0); try { - api.play(textureIdArg); + api.play(playerIdArg); wrapped.add(0, null); } catch (Throwable exception) { wrapped = wrapError(exception); @@ -483,9 +604,9 @@ static void setUp( (message, reply) -> { ArrayList wrapped = new ArrayList<>(); ArrayList args = (ArrayList) message; - Long textureIdArg = (Long) args.get(0); + Long playerIdArg = (Long) args.get(0); try { - Long output = api.position(textureIdArg); + Long output = api.position(playerIdArg); wrapped.add(0, output); } catch (Throwable exception) { wrapped = wrapError(exception); @@ -508,10 +629,10 @@ static void setUp( (message, reply) -> { ArrayList wrapped = new ArrayList<>(); ArrayList args = (ArrayList) message; - Long textureIdArg = (Long) args.get(0); + Long playerIdArg = (Long) args.get(0); Long positionArg = (Long) args.get(1); try { - api.seekTo(textureIdArg, positionArg); + api.seekTo(playerIdArg, positionArg); wrapped.add(0, null); } catch (Throwable exception) { wrapped = wrapError(exception); @@ -534,9 +655,9 @@ static void setUp( (message, reply) -> { ArrayList wrapped = new ArrayList<>(); ArrayList args = (ArrayList) message; - Long textureIdArg = (Long) args.get(0); + Long playerIdArg = (Long) args.get(0); try { - api.pause(textureIdArg); + api.pause(playerIdArg); wrapped.add(0, null); } catch (Throwable exception) { wrapped = wrapError(exception); diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/RtspVideoAsset.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/RtspVideoAsset.java index 1eb87c8bac0..6b0edb4adf4 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/RtspVideoAsset.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/RtspVideoAsset.java @@ -19,14 +19,14 @@ final class RtspVideoAsset extends VideoAsset { @NonNull @Override - MediaItem getMediaItem() { + public MediaItem getMediaItem() { return new MediaItem.Builder().setUri(assetUrl).build(); } // TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039. @OptIn(markerClass = UnstableApi.class) @Override - MediaSource.Factory getMediaSourceFactory(Context context) { + public MediaSource.Factory getMediaSourceFactory(Context context) { return new RtspMediaSource.Factory(); } } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoAsset.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoAsset.java index 3fab758e52f..ba8dd55e93f 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoAsset.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoAsset.java @@ -13,7 +13,7 @@ import java.util.Map; /** A video to be played by {@link VideoPlayer}. */ -abstract class VideoAsset { +public abstract class VideoAsset { /** * Returns an asset from a local {@code asset:///} URL, i.e. an on-device asset. * @@ -70,7 +70,7 @@ protected VideoAsset(@Nullable String assetUrl) { * @return media item. */ @NonNull - abstract MediaItem getMediaItem(); + public abstract MediaItem getMediaItem(); /** * Returns the configured media source factory, if needed for this asset type. @@ -78,7 +78,8 @@ protected VideoAsset(@Nullable String assetUrl) { * @param context application context. * @return configured factory, or {@code null} if not needed for this asset type. */ - abstract MediaSource.Factory getMediaSourceFactory(Context context); + @NonNull + public abstract MediaSource.Factory getMediaSourceFactory(@NonNull Context context); /** Streaming formats that can be provided to the video player as a hint. */ enum StreamingFormat { 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 3e855e53b7c..8040e86419c 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 @@ -7,117 +7,64 @@ import static androidx.media3.common.Player.REPEAT_MODE_ALL; import static androidx.media3.common.Player.REPEAT_MODE_OFF; -import android.content.Context; 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; import androidx.media3.common.MediaItem; import androidx.media3.common.PlaybackParameters; import androidx.media3.exoplayer.ExoPlayer; -import io.flutter.view.TextureRegistry; -final class VideoPlayer implements TextureRegistry.SurfaceProducer.Callback { +/** + * A class responsible for managing video playback using {@link ExoPlayer}. + * + *

It provides methods to control playback, adjust volume, and handle seeking. + */ +public abstract class VideoPlayer { @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 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( - @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); - } + @NonNull protected final VideoPlayerCallbacks videoPlayerEvents; + @NonNull protected ExoPlayer exoPlayer; /** A closure-compatible signature since {@link java.util.function.Supplier} is API level 24. */ - interface ExoPlayerProvider { + public interface ExoPlayerProvider { /** * Returns a new {@link ExoPlayer}. * * @return new instance. */ + @NonNull ExoPlayer get(); } - @VisibleForTesting - VideoPlayer( - @NonNull ExoPlayerProvider exoPlayerProvider, + public VideoPlayer( @NonNull VideoPlayerCallbacks events, - @NonNull TextureRegistry.SurfaceProducer surfaceProducer, @NonNull MediaItem mediaItem, - @NonNull VideoPlayerOptions options) { - this.exoPlayerProvider = exoPlayerProvider; + @NonNull VideoPlayerOptions options, + @NonNull ExoPlayerProvider exoPlayerProvider) { this.videoPlayerEvents = events; - this.surfaceProducer = surfaceProducer; this.mediaItem = mediaItem; this.options = options; + this.exoPlayerProvider = exoPlayerProvider; this.exoPlayer = createVideoPlayer(); - surfaceProducer.setCallback(this); - } - - @RestrictTo(RestrictTo.Scope.LIBRARY) - public void onSurfaceAvailable() { - if (savedStateDuring != null) { - exoPlayer = createVideoPlayer(); - savedStateDuring.restore(exoPlayer); - savedStateDuring = null; - } - } - - @RestrictTo(RestrictTo.Scope.LIBRARY) - // TODO(bparrishMines): Replace with onSurfaceCleanup once available on stable. See - // https://github.com/flutter/flutter/issues/161256. - @SuppressWarnings({"deprecation", "removal"}) - public void onSurfaceDestroyed() { - // Intentionally do not call pause/stop here, because the surface has already been released - // at this point (see https://github.com/flutter/flutter/issues/156451). - savedStateDuring = ExoPlayerState.save(exoPlayer); - exoPlayer.release(); } - private ExoPlayer createVideoPlayer() { + @NonNull + protected ExoPlayer createVideoPlayer() { ExoPlayer exoPlayer = exoPlayerProvider.get(); exoPlayer.setMediaItem(mediaItem); exoPlayer.prepare(); - exoPlayer.setVideoSurface(surfaceProducer.getSurface()); - - boolean wasInitialized = savedStateDuring != null; - exoPlayer.addListener(new ExoPlayerEventListener(exoPlayer, videoPlayerEvents, wasInitialized)); + exoPlayer.addListener(createExoPlayerEventListener(exoPlayer)); setAudioAttributes(exoPlayer, options.mixWithOthers); return exoPlayer; } + @NonNull + protected abstract ExoPlayerEventListener createExoPlayerEventListener( + @NonNull ExoPlayer exoPlayer); + void sendBufferingUpdate() { videoPlayerEvents.onBufferingUpdate(exoPlayer.getBufferedPosition()); } @@ -161,12 +108,12 @@ long getPosition() { return exoPlayer.getCurrentPosition(); } - void dispose() { - exoPlayer.release(); - surfaceProducer.release(); + @NonNull + public ExoPlayer getExoPlayer() { + return exoPlayer; + } - // TODO(matanlurey): Remove when embedder no longer calls-back once released. - // https://github.com/flutter/flutter/issues/156434. - surfaceProducer.setCallback(null); + public void dispose() { + exoPlayer.release(); } } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java index b3a1a3967d8..24ecf8c6f3e 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java @@ -16,7 +16,7 @@ * *

See {@link androidx.media3.common.Player.Listener} for details. */ -interface VideoPlayerCallbacks { +public interface VideoPlayerCallbacks { void onInitialized(int width, int height, long durationInMs, int rotationCorrectionInDegrees); void onBufferingStart(); diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java index 85ad892f9e1..7c8a6aab726 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java @@ -4,6 +4,6 @@ package io.flutter.plugins.videoplayer; -class VideoPlayerOptions { +public class VideoPlayerOptions { public boolean mixWithOthers; } 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 d248ad2f0be..3db5fd42a26 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 @@ -14,6 +14,9 @@ import io.flutter.plugin.common.EventChannel; import io.flutter.plugins.videoplayer.Messages.AndroidVideoPlayerApi; import io.flutter.plugins.videoplayer.Messages.CreateMessage; +import io.flutter.plugins.videoplayer.platformview.PlatformVideoViewFactory; +import io.flutter.plugins.videoplayer.platformview.PlatformViewVideoPlayer; +import io.flutter.plugins.videoplayer.texture.TextureVideoPlayer; import io.flutter.view.TextureRegistry; /** Android platform implementation of the VideoPlayerPlugin. */ @@ -23,6 +26,13 @@ public class VideoPlayerPlugin implements FlutterPlugin, AndroidVideoPlayerApi { private FlutterState flutterState; private final VideoPlayerOptions options = new VideoPlayerOptions(); + // TODO(stuartmorgan): Decouple identifiers for platform views and texture views. + /** + * The next non-texture player ID, initialized to a high number to avoid collisions with texture + * IDs (which are generated separately). + */ + private Long nextPlatformViewPlayerId = Long.MAX_VALUE; + /** Register this with the v2 embedding for the plugin to respond to lifecycle callbacks. */ public VideoPlayerPlugin() {} @@ -37,6 +47,12 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { injector.flutterLoader()::getLookupKeyForAsset, binding.getTextureRegistry()); flutterState.startListening(this, binding.getBinaryMessenger()); + + binding + .getPlatformViewRegistry() + .registerViewFactory( + "plugins.flutter.dev/video_player_android", + new PlatformVideoViewFactory(videoPlayers::get)); } @Override @@ -72,11 +88,6 @@ public void initialize() { @Override public @NonNull Long create(@NonNull CreateMessage arg) { - TextureRegistry.SurfaceProducer handle = flutterState.textureRegistry.createSurfaceProducer(); - EventChannel eventChannel = - new EventChannel( - flutterState.binaryMessenger, "flutter.io/videoPlayer/videoEvents" + handle.id()); - final VideoAsset videoAsset; if (arg.getAsset() != null) { String assetLookupKey; @@ -107,25 +118,46 @@ public void initialize() { } videoAsset = VideoAsset.fromRemoteUrl(arg.getUri(), streamingFormat, arg.getHttpHeaders()); } - videoPlayers.put( - handle.id(), - VideoPlayer.create( - flutterState.applicationContext, - VideoPlayerEventCallbacks.bindTo(eventChannel), - handle, - videoAsset, - options)); - return handle.id(); + long id; + VideoPlayer videoPlayer; + if (arg.getViewType() == Messages.PlatformVideoViewType.PLATFORM_VIEW) { + id = nextPlatformViewPlayerId--; + videoPlayer = + PlatformViewVideoPlayer.create( + flutterState.applicationContext, + VideoPlayerEventCallbacks.bindTo(createEventChannel(id)), + videoAsset, + options); + } else { + TextureRegistry.SurfaceProducer handle = flutterState.textureRegistry.createSurfaceProducer(); + id = handle.id(); + videoPlayer = + TextureVideoPlayer.create( + flutterState.applicationContext, + VideoPlayerEventCallbacks.bindTo(createEventChannel(id)), + handle, + videoAsset, + options); + } + + videoPlayers.put(id, videoPlayer); + return id; + } + + @NonNull + private EventChannel createEventChannel(long id) { + return new EventChannel( + flutterState.binaryMessenger, "flutter.io/videoPlayer/videoEvents" + id); } @NonNull - private VideoPlayer getPlayer(long textureId) { - VideoPlayer player = videoPlayers.get(textureId); + private VideoPlayer getPlayer(long playerId) { + VideoPlayer player = videoPlayers.get(playerId); // Avoid a very ugly un-debuggable NPE that results in returning a null player. if (player == null) { - String message = "No player found with textureId <" + textureId + ">"; + String message = "No player found with playerId <" + playerId + ">"; if (videoPlayers.size() == 0) { message += " and no active players created by the plugin."; } @@ -136,53 +168,53 @@ private VideoPlayer getPlayer(long textureId) { } @Override - public void dispose(@NonNull Long textureId) { - VideoPlayer player = getPlayer(textureId); + public void dispose(@NonNull Long playerId) { + VideoPlayer player = getPlayer(playerId); player.dispose(); - videoPlayers.remove(textureId); + videoPlayers.remove(playerId); } @Override - public void setLooping(@NonNull Long textureId, @NonNull Boolean looping) { - VideoPlayer player = getPlayer(textureId); + public void setLooping(@NonNull Long playerId, @NonNull Boolean looping) { + VideoPlayer player = getPlayer(playerId); player.setLooping(looping); } @Override - public void setVolume(@NonNull Long textureId, @NonNull Double volume) { - VideoPlayer player = getPlayer(textureId); + public void setVolume(@NonNull Long playerId, @NonNull Double volume) { + VideoPlayer player = getPlayer(playerId); player.setVolume(volume); } @Override - public void setPlaybackSpeed(@NonNull Long textureId, @NonNull Double speed) { - VideoPlayer player = getPlayer(textureId); + public void setPlaybackSpeed(@NonNull Long playerId, @NonNull Double speed) { + VideoPlayer player = getPlayer(playerId); player.setPlaybackSpeed(speed); } @Override - public void play(@NonNull Long textureId) { - VideoPlayer player = getPlayer(textureId); + public void play(@NonNull Long playerId) { + VideoPlayer player = getPlayer(playerId); player.play(); } @Override - public @NonNull Long position(@NonNull Long textureId) { - VideoPlayer player = getPlayer(textureId); + public @NonNull Long position(@NonNull Long playerId) { + VideoPlayer player = getPlayer(playerId); long position = player.getPosition(); player.sendBufferingUpdate(); return position; } @Override - public void seekTo(@NonNull Long textureId, @NonNull Long position) { - VideoPlayer player = getPlayer(textureId); + public void seekTo(@NonNull Long playerId, @NonNull Long position) { + VideoPlayer player = getPlayer(playerId); player.seekTo(position.intValue()); } @Override - public void pause(@NonNull Long textureId) { - VideoPlayer player = getPlayer(textureId); + public void pause(@NonNull Long playerId) { + VideoPlayer player = getPlayer(playerId); player.pause(); } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformVideoView.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformVideoView.java new file mode 100644 index 00000000000..02ac659c086 --- /dev/null +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformVideoView.java @@ -0,0 +1,94 @@ +// 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.platformview; + +import android.content.Context; +import android.os.Build; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.ExoPlayer; +import io.flutter.plugin.platform.PlatformView; + +/** + * A class used to create a native video view that can be embedded in a Flutter app. It wraps an + * {@link ExoPlayer} instance and displays its video content. + */ +public final class PlatformVideoView implements PlatformView { + @NonNull private final SurfaceView surfaceView; + + /** + * Constructs a new PlatformVideoView. + * + * @param context The context in which the view is running. + * @param exoPlayer The ExoPlayer instance used to play the video. + */ + @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); + } + } + + private void setupSurfaceWithCallback(@NonNull ExoPlayer exoPlayer) { + surfaceView + .getHolder() + .addCallback( + new SurfaceHolder.Callback() { + @Override + public void surfaceCreated(@NonNull SurfaceHolder holder) { + exoPlayer.setVideoSurface(holder.getSurface()); + // Force first frame rendering: + exoPlayer.seekTo(1); + } + + @Override + public void surfaceChanged( + @NonNull SurfaceHolder holder, int format, int width, int height) { + // No implementation needed. + } + + @Override + public void surfaceDestroyed(@NonNull SurfaceHolder holder) { + exoPlayer.setVideoSurface(null); + } + }); + } + + /** + * Returns the view associated with this PlatformView. + * + * @return The SurfaceView used to display the video. + */ + @NonNull + @Override + public View getView() { + return surfaceView; + } + + /** Disposes of the resources used by this PlatformView. */ + @Override + public void dispose() { + surfaceView.getHolder().getSurface().release(); + } +} diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformVideoViewFactory.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformVideoViewFactory.java new file mode 100644 index 00000000000..8bb8516267a --- /dev/null +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformVideoViewFactory.java @@ -0,0 +1,68 @@ +// 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.platformview; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.media3.exoplayer.ExoPlayer; +import io.flutter.plugin.platform.PlatformView; +import io.flutter.plugin.platform.PlatformViewFactory; +import io.flutter.plugins.videoplayer.Messages; +import io.flutter.plugins.videoplayer.VideoPlayer; +import java.util.Objects; + +/** + * A factory class responsible for creating platform video views that can be embedded in a Flutter + * app. + */ +public class PlatformVideoViewFactory extends PlatformViewFactory { + private final VideoPlayerProvider videoPlayerProvider; + + /** Functional interface for providing a VideoPlayer instance based on the player ID. */ + @FunctionalInterface + public interface VideoPlayerProvider { + /** + * Retrieves a VideoPlayer instance based on the provided player ID. + * + * @param playerId The unique identifier for the video player. + * @return A VideoPlayer instance associated with the given player ID. + */ + @NonNull + VideoPlayer getVideoPlayer(@NonNull Long playerId); + } + + /** + * Constructs a new PlatformVideoViewFactory. + * + * @param videoPlayerProvider The provider used to retrieve the video player associated with the + * view. + */ + public PlatformVideoViewFactory(@NonNull VideoPlayerProvider videoPlayerProvider) { + super(Messages.AndroidVideoPlayerApi.getCodec()); + this.videoPlayerProvider = videoPlayerProvider; + } + + /** + * Creates a new instance of platform view. + * + * @param context The context in which the view is running. + * @param id The unique identifier for the view. + * @param args The arguments for creating the view. + * @return A new instance of PlatformVideoView. + */ + @NonNull + @Override + public PlatformView create(@NonNull Context context, int id, @Nullable Object args) { + final Messages.PlatformVideoViewCreationParams params = + Objects.requireNonNull((Messages.PlatformVideoViewCreationParams) args); + final Long playerId = params.getPlayerId(); + + final VideoPlayer player = videoPlayerProvider.getVideoPlayer(playerId); + final ExoPlayer exoPlayer = player.getExoPlayer(); + + return new PlatformVideoView(context, exoPlayer); + } +} diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewExoPlayerEventListener.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewExoPlayerEventListener.java new file mode 100644 index 00000000000..82343d796f2 --- /dev/null +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewExoPlayerEventListener.java @@ -0,0 +1,52 @@ +// 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.platformview; + +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.annotation.VisibleForTesting; +import androidx.media3.common.Format; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.ExoPlayer; +import io.flutter.plugins.videoplayer.ExoPlayerEventListener; +import io.flutter.plugins.videoplayer.VideoPlayerCallbacks; +import java.util.Objects; + +public final class PlatformViewExoPlayerEventListener extends ExoPlayerEventListener { + @VisibleForTesting + public PlatformViewExoPlayerEventListener( + @NonNull ExoPlayer exoPlayer, @NonNull VideoPlayerCallbacks events) { + this(exoPlayer, events, false); + } + + public PlatformViewExoPlayerEventListener( + @NonNull ExoPlayer exoPlayer, @NonNull VideoPlayerCallbacks events, boolean initialized) { + super(exoPlayer, events, initialized); + } + + @OptIn(markerClass = UnstableApi.class) + @Override + protected void sendInitialized() { + // We can't rely on VideoSize here, because at this point it is not available - the platform + // view was not created yet. We use the video format instead. + Format videoFormat = exoPlayer.getVideoFormat(); + RotationDegrees rotationCorrection = + RotationDegrees.fromDegrees(Objects.requireNonNull(videoFormat).rotationDegrees); + int width = videoFormat.width; + int height = videoFormat.height; + + // Switch the width/height if video was taken in portrait mode and a rotation + // correction was detected. + if (rotationCorrection == RotationDegrees.ROTATE_90 + || rotationCorrection == RotationDegrees.ROTATE_270) { + width = videoFormat.height; + height = videoFormat.width; + + rotationCorrection = RotationDegrees.fromDegrees(0); + } + + events.onInitialized(width, height, exoPlayer.getDuration(), rotationCorrection.getDegrees()); + } +} diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java new file mode 100644 index 00000000000..144c942676c --- /dev/null +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java @@ -0,0 +1,66 @@ +// 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.platformview; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.media3.common.MediaItem; +import androidx.media3.exoplayer.ExoPlayer; +import io.flutter.plugins.videoplayer.ExoPlayerEventListener; +import io.flutter.plugins.videoplayer.VideoAsset; +import io.flutter.plugins.videoplayer.VideoPlayer; +import io.flutter.plugins.videoplayer.VideoPlayerCallbacks; +import io.flutter.plugins.videoplayer.VideoPlayerOptions; + +/** + * A subclass of {@link VideoPlayer} that adds functionality related to platform view as a way of + * displaying the video in the app. + */ +public class PlatformViewVideoPlayer extends VideoPlayer { + @VisibleForTesting + public PlatformViewVideoPlayer( + @NonNull VideoPlayerCallbacks events, + @NonNull MediaItem mediaItem, + @NonNull VideoPlayerOptions options, + @NonNull ExoPlayerProvider exoPlayerProvider) { + super(events, mediaItem, options, exoPlayerProvider); + } + + /** + * Creates a platform view video player. + * + * @param context application context. + * @param events event callbacks. + * @param asset asset to play. + * @param options options for playback. + * @return a video player instance. + */ + @NonNull + public static PlatformViewVideoPlayer create( + @NonNull Context context, + @NonNull VideoPlayerCallbacks events, + @NonNull VideoAsset asset, + @NonNull VideoPlayerOptions options) { + return new PlatformViewVideoPlayer( + events, + asset.getMediaItem(), + options, + () -> { + ExoPlayer.Builder builder = + new ExoPlayer.Builder(context) + .setMediaSourceFactory(asset.getMediaSourceFactory(context)); + return builder.build(); + }); + } + + @NonNull + @Override + protected ExoPlayerEventListener createExoPlayerEventListener(@NonNull ExoPlayer exoPlayer) { + // Platform view video player does not suspend and re-create the exoPlayer, hence initialized + // is always false. + return new PlatformViewExoPlayerEventListener(exoPlayer, videoPlayerEvents, false); + } +} diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureExoPlayerEventListener.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureExoPlayerEventListener.java new file mode 100644 index 00000000000..3722ddebeec --- /dev/null +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureExoPlayerEventListener.java @@ -0,0 +1,110 @@ +// 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.texture; + +import android.os.Build; +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.annotation.VisibleForTesting; +import androidx.media3.common.Format; +import androidx.media3.common.VideoSize; +import androidx.media3.exoplayer.ExoPlayer; +import io.flutter.plugins.videoplayer.ExoPlayerEventListener; +import io.flutter.plugins.videoplayer.VideoPlayerCallbacks; +import java.util.Objects; + +public final class TextureExoPlayerEventListener extends ExoPlayerEventListener { + @VisibleForTesting + public TextureExoPlayerEventListener( + @NonNull ExoPlayer exoPlayer, @NonNull VideoPlayerCallbacks events) { + this(exoPlayer, events, false); + } + + public TextureExoPlayerEventListener( + @NonNull ExoPlayer exoPlayer, @NonNull VideoPlayerCallbacks events, boolean initialized) { + super(exoPlayer, events, initialized); + } + + @Override + protected void sendInitialized() { + VideoSize videoSize = exoPlayer.getVideoSize(); + int rotationCorrection = 0; + int width = videoSize.width; + int height = videoSize.height; + if (width != 0 && height != 0) { + RotationDegrees reportedRotationCorrection = RotationDegrees.ROTATE_0; + + if (Build.VERSION.SDK_INT <= 21) { + // On API 21 and below, Exoplayer may not internally handle rotation correction + // and reports it through VideoSize.unappliedRotationDegrees. We may apply it to + // fix the case of upside-down playback. + try { + reportedRotationCorrection = + RotationDegrees.fromDegrees(videoSize.unappliedRotationDegrees); + rotationCorrection = + getRotationCorrectionFromUnappliedRotation(reportedRotationCorrection); + } catch (IllegalArgumentException e) { + // Unapplied rotation other than 0, 90, 180, 270 reported by VideoSize. Because this is + // unexpected, we apply no rotation correction. + reportedRotationCorrection = RotationDegrees.ROTATE_0; + rotationCorrection = 0; + } + } + // TODO(camsim99): Replace this with a call to `handlesCropAndRotation` when it is + // available in stable. https://github.com/flutter/flutter/issues/157198 + else if (Build.VERSION.SDK_INT < 29) { + // When the SurfaceTexture backend for Impeller is used, the preview should already + // be correctly rotated. + rotationCorrection = 0; + } else { + // The video's Format also provides a rotation correction that may be used to + // correct the rotation, so we try to use that to correct the video rotation + // when the ImageReader backend for Impeller is used. + rotationCorrection = getRotationCorrectionFromFormat(exoPlayer); + + try { + reportedRotationCorrection = RotationDegrees.fromDegrees(rotationCorrection); + } catch (IllegalArgumentException e) { + // Rotation correction other than 0, 90, 180, 270 reported by Format. Because this is + // unexpected we apply no rotation correction. + reportedRotationCorrection = RotationDegrees.ROTATE_0; + rotationCorrection = 0; + } + } + + // Switch the width/height if video was taken in portrait mode and a rotation + // correction was detected. + if (reportedRotationCorrection == RotationDegrees.ROTATE_90 + || reportedRotationCorrection == RotationDegrees.ROTATE_270) { + width = videoSize.height; + height = videoSize.width; + } + } + events.onInitialized(width, height, exoPlayer.getDuration(), rotationCorrection); + } + + private int getRotationCorrectionFromUnappliedRotation(RotationDegrees unappliedRotationDegrees) { + int rotationCorrection = 0; + + // Rotating the video with ExoPlayer does not seem to be possible with a Surface, + // so inform the Flutter code that the widget needs to be rotated to prevent + // upside-down playback for videos with unappliedRotationDegrees of 180 (other orientations + // work correctly without correction). + if (unappliedRotationDegrees == RotationDegrees.ROTATE_180) { + rotationCorrection = unappliedRotationDegrees.getDegrees(); + } + + return rotationCorrection; + } + + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) + // A video's Format and its rotation degrees are unstable because they are not guaranteed + // the same implementation across API versions. It is possible that this logic may need + // revisiting should the implementation change across versions of the Exoplayer API. + private int getRotationCorrectionFromFormat(ExoPlayer exoPlayer) { + Format videoFormat = Objects.requireNonNull(exoPlayer.getVideoFormat()); + return videoFormat.rotationDegrees; + } +} diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java new file mode 100644 index 00000000000..5bf8c6fd86b --- /dev/null +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java @@ -0,0 +1,119 @@ +// 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.texture; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.annotation.VisibleForTesting; +import androidx.media3.common.MediaItem; +import androidx.media3.exoplayer.ExoPlayer; +import io.flutter.plugins.videoplayer.ExoPlayerEventListener; +import io.flutter.plugins.videoplayer.ExoPlayerState; +import io.flutter.plugins.videoplayer.VideoAsset; +import io.flutter.plugins.videoplayer.VideoPlayer; +import io.flutter.plugins.videoplayer.VideoPlayerCallbacks; +import io.flutter.plugins.videoplayer.VideoPlayerOptions; +import io.flutter.view.TextureRegistry; + +/** + * A subclass of {@link VideoPlayer} that adds functionality related to texture view as a way of + * displaying the video in the app. + * + *

It manages the lifecycle of the texture and ensures that the video is properly displayed on + * the texture. + */ +public final class TextureVideoPlayer extends VideoPlayer + implements TextureRegistry.SurfaceProducer.Callback { + @NonNull private final TextureRegistry.SurfaceProducer surfaceProducer; + @Nullable private ExoPlayerState savedStateDuring; + + /** + * Creates a texture video player. + * + * @param context application context. + * @param events event callbacks. + * @param surfaceProducer produces a texture to render to. + * @param asset asset to play. + * @param options options for playback. + * @return a video player instance. + */ + @NonNull + public static TextureVideoPlayer create( + @NonNull Context context, + @NonNull VideoPlayerCallbacks events, + @NonNull TextureRegistry.SurfaceProducer surfaceProducer, + @NonNull VideoAsset asset, + @NonNull VideoPlayerOptions options) { + return new TextureVideoPlayer( + events, + surfaceProducer, + asset.getMediaItem(), + options, + () -> { + ExoPlayer.Builder builder = + new ExoPlayer.Builder(context) + .setMediaSourceFactory(asset.getMediaSourceFactory(context)); + return builder.build(); + }); + } + + @VisibleForTesting + public TextureVideoPlayer( + @NonNull VideoPlayerCallbacks events, + @NonNull TextureRegistry.SurfaceProducer surfaceProducer, + @NonNull MediaItem mediaItem, + @NonNull VideoPlayerOptions options, + @NonNull ExoPlayerProvider exoPlayerProvider) { + super(events, mediaItem, options, exoPlayerProvider); + + this.surfaceProducer = surfaceProducer; + surfaceProducer.setCallback(this); + + this.exoPlayer.setVideoSurface(surfaceProducer.getSurface()); + } + + @NonNull + @Override + protected ExoPlayerEventListener createExoPlayerEventListener(@NonNull ExoPlayer exoPlayer) { + return new TextureExoPlayerEventListener( + exoPlayer, videoPlayerEvents, playerHasBeenSuspended()); + } + + @RestrictTo(RestrictTo.Scope.LIBRARY) + public void onSurfaceAvailable() { + if (savedStateDuring != null) { + exoPlayer = createVideoPlayer(); + savedStateDuring.restore(exoPlayer); + savedStateDuring = null; + } + } + + @RestrictTo(RestrictTo.Scope.LIBRARY) + // TODO(bparrishMines): Replace with onSurfaceCleanup once available on stable. See + // https://github.com/flutter/flutter/issues/161256. + @SuppressWarnings({"deprecation", "removal"}) + public void onSurfaceDestroyed() { + // Intentionally do not call pause/stop here, because the surface has already been released + // at this point (see https://github.com/flutter/flutter/issues/156451). + savedStateDuring = ExoPlayerState.save(exoPlayer); + exoPlayer.release(); + } + + private boolean playerHasBeenSuspended() { + return savedStateDuring != null; + } + + public void dispose() { + // Super must be called first to ensure the player is released before the surface. + super.dispose(); + + surfaceProducer.release(); + // TODO(matanlurey): Remove when embedder no longer calls-back once released. + // https://github.com/flutter/flutter/issues/156434. + surfaceProducer.setCallback(null); + } +} diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/ExoPlayerEventListenerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/ExoPlayerEventListenerTest.java index 65dfb311c31..ac40efbcf3b 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/ExoPlayerEventListenerTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/ExoPlayerEventListenerTest.java @@ -12,10 +12,8 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import androidx.media3.common.Format; import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; -import androidx.media3.common.VideoSize; import androidx.media3.exoplayer.ExoPlayer; import org.junit.Before; import org.junit.Rule; @@ -25,14 +23,13 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit tests for {@link ExoPlayerEventListener}. * *

This test suite narrowly verifies that the events emitted by the underlying {@link * androidx.media3.exoplayer.ExoPlayer} instance are translated to the callback interface we expect - * ({@link VideoPlayerCallbacks} and/or interface with the player instance as expected. + * ({@link VideoPlayerCallbacks} and/or interface with the player instance as expected). */ @RunWith(RobolectricTestRunner.class) public final class ExoPlayerEventListenerTest { @@ -42,128 +39,23 @@ public final class ExoPlayerEventListenerTest { @Rule public MockitoRule initRule = MockitoJUnit.rule(); - @Before - public void setUp() { - eventListener = new ExoPlayerEventListener(mockExoPlayer, mockCallbacks); - } + /** + * A test subclass of {@link ExoPlayerEventListener} that exposes the abstract class for testing. + */ + private static final class TestExoPlayerEventListener extends ExoPlayerEventListener { + public TestExoPlayerEventListener(ExoPlayer exoPlayer, VideoPlayerCallbacks callbacks) { + super(exoPlayer, callbacks, false); + } - @Test - @Config(maxSdk = 28) - public void onPlaybackStateChangedReadySendInitialized_belowAndroid29() { - VideoSize size = new VideoSize(800, 400, 0, 0); - when(mockExoPlayer.getVideoSize()).thenReturn(size); - when(mockExoPlayer.getDuration()).thenReturn(10L); - - eventListener.onPlaybackStateChanged(Player.STATE_READY); - verify(mockCallbacks).onInitialized(800, 400, 10L, 0); + @Override + protected void sendInitialized() { + // No implementation needed. + } } - @Test - @Config(minSdk = 29) - public void - onPlaybackStateChangedReadySendInitializedWithRotationCorrectionAndWidthAndHeightSwap_aboveAndroid29() { - VideoSize size = new VideoSize(800, 400, 0, 0); - int rotationCorrection = 90; - Format videoFormat = new Format.Builder().setRotationDegrees(rotationCorrection).build(); - - when(mockExoPlayer.getVideoSize()).thenReturn(size); - when(mockExoPlayer.getDuration()).thenReturn(10L); - when(mockExoPlayer.getVideoFormat()).thenReturn(videoFormat); - - eventListener.onPlaybackStateChanged(Player.STATE_READY); - verify(mockCallbacks).onInitialized(400, 800, 10L, rotationCorrection); - } - - @Test - @Config(maxSdk = 21) - public void - onPlaybackStateChangedReadyInPortraitMode90DegreesSwapWidthAndHeight_belowAndroid21() { - VideoSize size = new VideoSize(800, 400, 90, 0); - when(mockExoPlayer.getVideoSize()).thenReturn(size); - when(mockExoPlayer.getDuration()).thenReturn(10L); - - eventListener.onPlaybackStateChanged(Player.STATE_READY); - verify(mockCallbacks).onInitialized(400, 800, 10L, 0); - } - - @Test - @Config(minSdk = 22, maxSdk = 28) - public void - onPlaybackStateChangedReadyInPortraitMode90DegreesDoesNotSwapWidthAndHeight_aboveAndroid21belowAndroid29() { - VideoSize size = new VideoSize(800, 400, 90, 0); - - when(mockExoPlayer.getVideoSize()).thenReturn(size); - when(mockExoPlayer.getDuration()).thenReturn(10L); - - eventListener.onPlaybackStateChanged(Player.STATE_READY); - verify(mockCallbacks).onInitialized(800, 400, 10L, 0); - } - - @Test - @Config(minSdk = 29) - public void - onPlaybackStateChangedReadyInPortraitMode90DegreesSwapWidthAndHeight_aboveAndroid29() { - VideoSize size = new VideoSize(800, 400, 0, 0); - int rotationCorrection = 90; - Format videoFormat = new Format.Builder().setRotationDegrees(rotationCorrection).build(); - - when(mockExoPlayer.getVideoSize()).thenReturn(size); - when(mockExoPlayer.getDuration()).thenReturn(10L); - when(mockExoPlayer.getVideoFormat()).thenReturn(videoFormat); - - eventListener.onPlaybackStateChanged(Player.STATE_READY); - verify(mockCallbacks).onInitialized(400, 800, 10L, 90); - } - - @Test - @Config(maxSdk = 21) - public void - onPlaybackStateChangedReadyInPortraitMode270DegreesSwapWidthAndHeight_belowAndroid21() { - VideoSize size = new VideoSize(800, 400, 270, 0); - when(mockExoPlayer.getVideoSize()).thenReturn(size); - when(mockExoPlayer.getDuration()).thenReturn(10L); - - eventListener.onPlaybackStateChanged(Player.STATE_READY); - verify(mockCallbacks).onInitialized(400, 800, 10L, 0); - } - - @Test - @Config(minSdk = 22, maxSdk = 28) - public void - onPlaybackStateChangedReadyInPortraitMode270DegreesDoesNotSwapWidthAndHeight_aboveAndroid21belowAndroid29() { - VideoSize size = new VideoSize(800, 400, 270, 0); - when(mockExoPlayer.getVideoSize()).thenReturn(size); - when(mockExoPlayer.getDuration()).thenReturn(10L); - - eventListener.onPlaybackStateChanged(Player.STATE_READY); - verify(mockCallbacks).onInitialized(800, 400, 10L, 0); - } - - @Test - @Config(minSdk = 29) - public void - onPlaybackStateChangedReadyInPortraitMode270DegreesSwapWidthAndHeight_aboveAndroid29() { - VideoSize size = new VideoSize(800, 400, 0, 0); - int rotationCorrection = 270; - Format videoFormat = new Format.Builder().setRotationDegrees(rotationCorrection).build(); - - when(mockExoPlayer.getVideoSize()).thenReturn(size); - when(mockExoPlayer.getDuration()).thenReturn(10L); - when(mockExoPlayer.getVideoFormat()).thenReturn(videoFormat); - - eventListener.onPlaybackStateChanged(Player.STATE_READY); - verify(mockCallbacks).onInitialized(400, 800, 10L, 270); - } - - @Test - @Config(maxSdk = 21) - public void onPlaybackStateChangedReadyFlipped180DegreesInformEventHandler_belowAndroid21() { - VideoSize size = new VideoSize(800, 400, 180, 0); - when(mockExoPlayer.getVideoSize()).thenReturn(size); - when(mockExoPlayer.getDuration()).thenReturn(10L); - - eventListener.onPlaybackStateChanged(Player.STATE_READY); - verify(mockCallbacks).onInitialized(800, 400, 10L, 180); + @Before + public void setUp() { + eventListener = new TestExoPlayerEventListener(mockExoPlayer, mockCallbacks); } @Test diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/FakeVideoAsset.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/FakeVideoAsset.java index 1e3b856a648..8b5c4418db8 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/FakeVideoAsset.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/FakeVideoAsset.java @@ -25,12 +25,13 @@ final class FakeVideoAsset extends VideoAsset { @NonNull @Override - MediaItem getMediaItem() { + public MediaItem getMediaItem() { return new MediaItem.Builder().setUri(assetUrl).build(); } + @NonNull @Override - MediaSource.Factory getMediaSourceFactory(Context context) { + public MediaSource.Factory getMediaSourceFactory(@NonNull Context context) { return mediaSourceFactory; } } diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformVideoViewFactoryTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformVideoViewFactoryTest.java new file mode 100644 index 00000000000..a6cfa93ea58 --- /dev/null +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformVideoViewFactoryTest.java @@ -0,0 +1,40 @@ +// 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 static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.*; + +import android.content.Context; +import androidx.media3.exoplayer.ExoPlayer; +import io.flutter.plugin.platform.PlatformView; +import io.flutter.plugins.videoplayer.platformview.PlatformVideoView; +import io.flutter.plugins.videoplayer.platformview.PlatformVideoViewFactory; +import org.junit.Test; + +public class PlatformVideoViewFactoryTest { + @Test + public void createsPlatformVideoViewBasedOnSuppliedArguments() { + final PlatformVideoViewFactory.VideoPlayerProvider videoPlayerProvider = + mock(PlatformVideoViewFactory.VideoPlayerProvider.class); + final VideoPlayer videoPlayer = mock(VideoPlayer.class); + final ExoPlayer exoPlayer = mock(ExoPlayer.class); + final Context context = mock(Context.class); + final long playerId = 1L; + + when(videoPlayerProvider.getVideoPlayer(playerId)).thenReturn(videoPlayer); + when(videoPlayer.getExoPlayer()).thenReturn(exoPlayer); + + final PlatformVideoViewFactory factory = new PlatformVideoViewFactory(videoPlayerProvider); + final Messages.PlatformVideoViewCreationParams args = + new Messages.PlatformVideoViewCreationParams.Builder().setPlayerId(playerId).build(); + + final PlatformView view = factory.create(context, 0, args); + + assertTrue(view instanceof PlatformVideoView); + verify(videoPlayerProvider).getVideoPlayer(playerId); + verify(videoPlayer).getExoPlayer(); + } +} diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformVideoViewTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformVideoViewTest.java new file mode 100644 index 00000000000..274e086a640 --- /dev/null +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformVideoViewTest.java @@ -0,0 +1,33 @@ +// 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 static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.*; + +import android.content.Context; +import android.view.SurfaceView; +import androidx.media3.exoplayer.ExoPlayer; +import io.flutter.plugins.videoplayer.platformview.PlatformVideoView; +import java.lang.reflect.Field; +import org.junit.Test; + +/** Unit tests for {@link PlatformVideoViewTest}. */ +public class PlatformVideoViewTest { + @Test + public void createsSurfaceViewAndSetsItForExoPlayer() throws Exception { + final Context mockContext = mock(Context.class); + final ExoPlayer mockExoPlayer = mock(ExoPlayer.class); + + final PlatformVideoView view = new PlatformVideoView(mockContext, mockExoPlayer); + + final Field field = PlatformVideoView.class.getDeclaredField("surfaceView"); + field.setAccessible(true); + final SurfaceView surfaceView = (SurfaceView) field.get(view); + + assertNotNull(surfaceView); + verify(mockExoPlayer).setVideoSurfaceView(surfaceView); + } +} diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformViewExoPlayerEventListenerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformViewExoPlayerEventListenerTest.java new file mode 100644 index 00000000000..8aecf667131 --- /dev/null +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformViewExoPlayerEventListenerTest.java @@ -0,0 +1,80 @@ +// 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 static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import androidx.media3.common.Format; +import androidx.media3.common.Player; +import androidx.media3.exoplayer.ExoPlayer; +import io.flutter.plugins.videoplayer.platformview.PlatformViewExoPlayerEventListener; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +/** + * Unit tests for {@link PlatformViewExoPlayerEventListener}. + * + *

This test suite narrowly verifies that the events emitted by the underlying {@link + * androidx.media3.exoplayer.ExoPlayer} instance are translated to the callback interface we expect + * ({@link VideoPlayerCallbacks} and/or interface with the player instance as expected). + */ +@RunWith(RobolectricTestRunner.class) +public final class PlatformViewExoPlayerEventListenerTest { + @Mock private ExoPlayer mockExoPlayer; + @Mock private VideoPlayerCallbacks mockCallbacks; + private ExoPlayerEventListener eventListener; + + @Rule public MockitoRule initRule = MockitoJUnit.rule(); + + @Before + public void setUp() { + eventListener = new PlatformViewExoPlayerEventListener(mockExoPlayer, mockCallbacks); + } + + @Test + public void onPlaybackStateChangedReadySendInitialized() { + eventListener = new PlatformViewExoPlayerEventListener(mockExoPlayer, mockCallbacks); + + Format format = new Format.Builder().setWidth(800).setHeight(400).build(); + when(mockExoPlayer.getVideoFormat()).thenReturn(format); + when(mockExoPlayer.getDuration()).thenReturn(10L); + + eventListener.onPlaybackStateChanged(Player.STATE_READY); + verify(mockCallbacks).onInitialized(800, 400, 10L, 0); + } + + @Test + public void onPlaybackStateChangedReadyInPortraitMode90DegreesSwapsWidthAndHeight() { + eventListener = new PlatformViewExoPlayerEventListener(mockExoPlayer, mockCallbacks); + + Format format = + new Format.Builder().setWidth(800).setHeight(400).setRotationDegrees(90).build(); + when(mockExoPlayer.getVideoFormat()).thenReturn(format); + when(mockExoPlayer.getDuration()).thenReturn(10L); + + eventListener.onPlaybackStateChanged(Player.STATE_READY); + verify(mockCallbacks).onInitialized(400, 800, 10L, 0); + } + + @Test + public void onPlaybackStateChangedReadyInPortraitMode270DegreesSwapsWidthAndHeight() { + eventListener = new PlatformViewExoPlayerEventListener(mockExoPlayer, mockCallbacks); + + Format format = + new Format.Builder().setWidth(800).setHeight(400).setRotationDegrees(270).build(); + when(mockExoPlayer.getVideoFormat()).thenReturn(format); + when(mockExoPlayer.getDuration()).thenReturn(10L); + + eventListener.onPlaybackStateChanged(Player.STATE_READY); + verify(mockCallbacks).onInitialized(400, 800, 10L, 0); + } +} diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureExoPlayerEventListenerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureExoPlayerEventListenerTest.java new file mode 100644 index 00000000000..931023b7a7b --- /dev/null +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureExoPlayerEventListenerTest.java @@ -0,0 +1,163 @@ +// 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 static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import androidx.media3.common.Format; +import androidx.media3.common.Player; +import androidx.media3.common.VideoSize; +import androidx.media3.exoplayer.ExoPlayer; +import io.flutter.plugins.videoplayer.texture.TextureExoPlayerEventListener; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit tests for {@link TextureExoPlayerEventListener}. + * + *

This test suite narrowly verifies that the events emitted by the underlying {@link + * androidx.media3.exoplayer.ExoPlayer} instance are translated to the callback interface we expect + * ({@link VideoPlayerCallbacks} and/or interface with the player instance as expected). + */ +@RunWith(RobolectricTestRunner.class) +public class TextureExoPlayerEventListenerTest { + @Mock private ExoPlayer mockExoPlayer; + @Mock private VideoPlayerCallbacks mockCallbacks; + private TextureExoPlayerEventListener eventListener; + + @Rule public MockitoRule initRule = MockitoJUnit.rule(); + + @Before + public void setUp() { + eventListener = new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks); + } + + @Test + @Config(maxSdk = 28) + public void onPlaybackStateChangedReadySendInitialized_belowAndroid29() { + VideoSize size = new VideoSize(800, 400, 0, 0); + when(mockExoPlayer.getVideoSize()).thenReturn(size); + when(mockExoPlayer.getDuration()).thenReturn(10L); + + eventListener.onPlaybackStateChanged(Player.STATE_READY); + verify(mockCallbacks).onInitialized(800, 400, 10L, 0); + } + + @Test + @Config(minSdk = 29) + public void + onPlaybackStateChangedReadySendInitializedWithRotationCorrectionAndWidthAndHeightSwap_aboveAndroid29() { + VideoSize size = new VideoSize(800, 400, 0, 0); + int rotationCorrection = 90; + Format videoFormat = new Format.Builder().setRotationDegrees(rotationCorrection).build(); + + when(mockExoPlayer.getVideoSize()).thenReturn(size); + when(mockExoPlayer.getDuration()).thenReturn(10L); + when(mockExoPlayer.getVideoFormat()).thenReturn(videoFormat); + + eventListener.onPlaybackStateChanged(Player.STATE_READY); + verify(mockCallbacks).onInitialized(400, 800, 10L, rotationCorrection); + } + + @Test + @Config(maxSdk = 21) + public void + onPlaybackStateChangedReadyInPortraitMode90DegreesSwapWidthAndHeight_belowAndroid21() { + VideoSize size = new VideoSize(800, 400, 90, 0); + when(mockExoPlayer.getVideoSize()).thenReturn(size); + when(mockExoPlayer.getDuration()).thenReturn(10L); + + eventListener.onPlaybackStateChanged(Player.STATE_READY); + verify(mockCallbacks).onInitialized(400, 800, 10L, 0); + } + + @Test + @Config(minSdk = 22, maxSdk = 28) + public void + onPlaybackStateChangedReadyInPortraitMode90DegreesDoesNotSwapWidthAndHeight_aboveAndroid21belowAndroid29() { + VideoSize size = new VideoSize(800, 400, 90, 0); + + when(mockExoPlayer.getVideoSize()).thenReturn(size); + when(mockExoPlayer.getDuration()).thenReturn(10L); + + eventListener.onPlaybackStateChanged(Player.STATE_READY); + verify(mockCallbacks).onInitialized(800, 400, 10L, 0); + } + + @Test + @Config(minSdk = 29) + public void + onPlaybackStateChangedReadyInPortraitMode90DegreesSwapWidthAndHeight_aboveAndroid29() { + VideoSize size = new VideoSize(800, 400, 0, 0); + int rotationCorrection = 90; + Format videoFormat = new Format.Builder().setRotationDegrees(rotationCorrection).build(); + + when(mockExoPlayer.getVideoSize()).thenReturn(size); + when(mockExoPlayer.getDuration()).thenReturn(10L); + when(mockExoPlayer.getVideoFormat()).thenReturn(videoFormat); + + eventListener.onPlaybackStateChanged(Player.STATE_READY); + verify(mockCallbacks).onInitialized(400, 800, 10L, 90); + } + + @Test + @Config(maxSdk = 21) + public void + onPlaybackStateChangedReadyInPortraitMode270DegreesSwapWidthAndHeight_belowAndroid21() { + VideoSize size = new VideoSize(800, 400, 270, 0); + when(mockExoPlayer.getVideoSize()).thenReturn(size); + when(mockExoPlayer.getDuration()).thenReturn(10L); + + eventListener.onPlaybackStateChanged(Player.STATE_READY); + verify(mockCallbacks).onInitialized(400, 800, 10L, 0); + } + + @Test + @Config(minSdk = 22, maxSdk = 28) + public void + onPlaybackStateChangedReadyInPortraitMode270DegreesDoesNotSwapWidthAndHeight_aboveAndroid21belowAndroid29() { + VideoSize size = new VideoSize(800, 400, 270, 0); + when(mockExoPlayer.getVideoSize()).thenReturn(size); + when(mockExoPlayer.getDuration()).thenReturn(10L); + + eventListener.onPlaybackStateChanged(Player.STATE_READY); + verify(mockCallbacks).onInitialized(800, 400, 10L, 0); + } + + @Test + @Config(minSdk = 29) + public void + onPlaybackStateChangedReadyInPortraitMode270DegreesSwapWidthAndHeight_aboveAndroid29() { + VideoSize size = new VideoSize(800, 400, 0, 0); + int rotationCorrection = 270; + Format videoFormat = new Format.Builder().setRotationDegrees(rotationCorrection).build(); + + when(mockExoPlayer.getVideoSize()).thenReturn(size); + when(mockExoPlayer.getDuration()).thenReturn(10L); + when(mockExoPlayer.getVideoFormat()).thenReturn(videoFormat); + + eventListener.onPlaybackStateChanged(Player.STATE_READY); + verify(mockCallbacks).onInitialized(400, 800, 10L, 270); + } + + @Test + @Config(maxSdk = 21) + public void onPlaybackStateChangedReadyFlipped180DegreesInformEventHandler_belowAndroid21() { + VideoSize size = new VideoSize(800, 400, 180, 0); + when(mockExoPlayer.getVideoSize()).thenReturn(size); + when(mockExoPlayer.getDuration()).thenReturn(10L); + + eventListener.onPlaybackStateChanged(Player.STATE_READY); + verify(mockCallbacks).onInitialized(800, 400, 10L, 180); + } +} diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureVideoPlayerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureVideoPlayerTest.java new file mode 100644 index 00000000000..26953687954 --- /dev/null +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureVideoPlayerTest.java @@ -0,0 +1,230 @@ +// 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 static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import android.view.Surface; +import androidx.media3.common.AudioAttributes; +import androidx.media3.common.C; +import androidx.media3.common.PlaybackParameters; +import androidx.media3.common.Player; +import androidx.media3.common.VideoSize; +import androidx.media3.exoplayer.ExoPlayer; +import io.flutter.plugins.videoplayer.texture.TextureVideoPlayer; +import io.flutter.view.TextureRegistry; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +/** + * Unit tests for {@link TextureVideoPlayer}. + * + *

This test suite narrowly verifies that {@link TextureVideoPlayer} interfaces with the + * {@link ExoPlayer} interface exactly as it did when the test suite was created. That is, + * if the behavior changes, this test will need to change. However, this suite should catch bugs + * related to "this is a safe refactor with no behavior changes". + */ +@RunWith(RobolectricTestRunner.class) +public final class TextureVideoPlayerTest { + private static final String FAKE_ASSET_URL = "https://flutter.dev/movie.mp4"; + private FakeVideoAsset fakeVideoAsset; + + @Mock private VideoPlayerCallbacks mockEvents; + @Mock private TextureRegistry.SurfaceProducer mockProducer; + @Mock private ExoPlayer mockExoPlayer; + @Captor private ArgumentCaptor attributesCaptor; + @Captor private ArgumentCaptor callbackCaptor; + @Captor private ArgumentCaptor listenerCaptor; + + @Rule public MockitoRule initRule = MockitoJUnit.rule(); + + @Before + public void setUp() { + fakeVideoAsset = new FakeVideoAsset(FAKE_ASSET_URL); + when(mockProducer.getSurface()).thenReturn(mock(Surface.class)); + } + + private VideoPlayer createVideoPlayer() { + return createVideoPlayer(new VideoPlayerOptions()); + } + + private TextureVideoPlayer createVideoPlayer(VideoPlayerOptions options) { + return new TextureVideoPlayer( + mockEvents, mockProducer, fakeVideoAsset.getMediaItem(), options, () -> mockExoPlayer); + } + + @Test + public void loadsAndPreparesProvidedMediaEnablesAudioFocusByDefault() { + VideoPlayer videoPlayer = createVideoPlayer(); + + verify(mockExoPlayer).setMediaItem(fakeVideoAsset.getMediaItem()); + verify(mockExoPlayer).prepare(); + verify(mockProducer).getSurface(); + verify(mockExoPlayer).setVideoSurface(any()); + + verify(mockExoPlayer).setAudioAttributes(attributesCaptor.capture(), eq(true)); + assertEquals(attributesCaptor.getValue().contentType, C.AUDIO_CONTENT_TYPE_MOVIE); + + videoPlayer.dispose(); + } + + @Test + public void onSurfaceProducerDestroyedAndAvailableReleasesAndThenRecreatesAndResumesPlayer() { + 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(); + simulateSurfaceDestruction(producerLifecycle); + + verify(mockExoPlayer).release(); + + // Create a new mock exo player so that we get a new instance. + mockExoPlayer = mock(ExoPlayer.class); + producerLifecycle.onSurfaceAvailable(); + + 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 onSurfaceProducerDestroyedDoesNotStopOrPauseVideo() { + VideoPlayer videoPlayer = createVideoPlayer(); + + verify(mockProducer).setCallback(callbackCaptor.capture()); + TextureRegistry.SurfaceProducer.Callback producerLifecycle = callbackCaptor.getValue(); + simulateSurfaceDestruction(producerLifecycle); + + verify(mockExoPlayer, never()).stop(); + verify(mockExoPlayer, never()).pause(); + verify(mockExoPlayer, never()).setPlayWhenReady(anyBoolean()); + + videoPlayer.dispose(); + } + + @Test + public void onDisposeSurfaceProducerCallbackIsDisconnected() { + // Regression test for https://github.com/flutter/flutter/issues/156158. + VideoPlayer videoPlayer = createVideoPlayer(); + verify(mockProducer).setCallback(any()); + + videoPlayer.dispose(); + verify(mockProducer).setCallback(null); + } + + @Test + public void onInitializedCalledWhenVideoPlayerInitiallyCreated() { + VideoPlayer videoPlayer = createVideoPlayer(); + + // Pretend we have a video, and capture the registered event listener. + when(mockExoPlayer.getVideoSize()).thenReturn(new VideoSize(300, 200)); + verify(mockExoPlayer).addListener(listenerCaptor.capture()); + Player.Listener listener = listenerCaptor.getValue(); + + // Trigger an event that would trigger onInitialized. + listener.onPlaybackStateChanged(Player.STATE_READY); + verify(mockEvents).onInitialized(anyInt(), anyInt(), anyLong(), anyInt()); + + videoPlayer.dispose(); + } + + @Test + public void onSurfaceAvailableDoesNotSendInitializeEventAgain() { + // The VideoPlayer contract assumes that the event "initialized" is sent exactly once + // (duplicate events cause an error to be thrown at the shared Dart layer). This test verifies + // that the onInitialized event is sent exactly once per player. + // + // Regression test for https://github.com/flutter/flutter/issues/154602. + VideoPlayer videoPlayer = createVideoPlayer(); + when(mockExoPlayer.getVideoSize()).thenReturn(new VideoSize(300, 200)); + + // Capture the lifecycle events so we can simulate onSurfaceAvailableDestroyed. + verify(mockProducer).setCallback(callbackCaptor.capture()); + TextureRegistry.SurfaceProducer.Callback producerLifecycle = callbackCaptor.getValue(); + + // Trigger destroyed/available. + simulateSurfaceDestruction(producerLifecycle); + producerLifecycle.onSurfaceAvailable(); + + // Initial listener, and the new one from the resume. + verify(mockExoPlayer, times(2)).addListener(listenerCaptor.capture()); + Player.Listener listener = listenerCaptor.getValue(); + + // Now trigger that same event, which would happen in the case of a background/resume. + listener.onPlaybackStateChanged(Player.STATE_READY); + + // Was not called because it was a result of a background/resume. + verify(mockEvents, never()).onInitialized(anyInt(), anyInt(), anyLong(), anyInt()); + + videoPlayer.dispose(); + } + + @Test + public void onSurfaceAvailableWithoutDestroyDoesNotRecreate() { + // Initially create the video player, which creates the initial surface. + VideoPlayer videoPlayer = createVideoPlayer(); + verify(mockProducer).getSurface(); + + // Capture the lifecycle events so we can simulate onSurfaceAvailable/Destroyed. + verify(mockProducer).setCallback(callbackCaptor.capture()); + TextureRegistry.SurfaceProducer.Callback producerLifecycle = callbackCaptor.getValue(); + + // Calling onSurfaceAvailable does not do anything, since the surface was never destroyed. + producerLifecycle.onSurfaceAvailable(); + verifyNoMoreInteractions(mockProducer); + + videoPlayer.dispose(); + } + + @Test + public void disposeReleasesExoPlayerBeforeTexture() { + VideoPlayer videoPlayer = createVideoPlayer(); + + videoPlayer.dispose(); + + // Regression test for https://github.com/flutter/flutter/issues/156158. + // The player must be destroyed before the surface it is writing to. + InOrder inOrder = inOrder(mockExoPlayer, mockProducer); + inOrder.verify(mockExoPlayer).release(); + inOrder.verify(mockProducer).release(); + } + + // TODO(matanlurey): Replace with inline calls to onSurfaceAvailable once + // available on stable; see https://github.com/flutter/flutter/issues/155131. + // This separate method only exists to scope the suppression. + @SuppressWarnings({"deprecation", "removal"}) + void simulateSurfaceCreation(TextureRegistry.SurfaceProducer.Callback producerLifecycle) { + producerLifecycle.onSurfaceCreated(); + } + + // TODO(bparrishMines): Replace with inline calls to onSurfaceCleanup once available on stable; + // see https://github.com/flutter/flutter/issues/16125. This separate method only exists to scope + // the suppression. + @SuppressWarnings({"deprecation", "removal"}) + void simulateSurfaceDestruction(TextureRegistry.SurfaceProducer.Callback producerLifecycle) { + producerLifecycle.onSurfaceDestroyed(); + } +} diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java index 2ed11653a4b..4a5e9135ca3 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java @@ -4,12 +4,113 @@ package io.flutter.plugins.videoplayer; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import android.content.Context; +import android.util.LongSparseArray; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.platform.PlatformViewRegistry; +import io.flutter.plugins.videoplayer.Messages.CreateMessage; +import io.flutter.plugins.videoplayer.Messages.PlatformVideoViewType; +import io.flutter.plugins.videoplayer.platformview.PlatformVideoViewFactory; +import io.flutter.plugins.videoplayer.platformview.PlatformViewVideoPlayer; +import io.flutter.plugins.videoplayer.texture.TextureVideoPlayer; +import io.flutter.view.TextureRegistry; +import java.lang.reflect.Field; +import java.util.HashMap; +import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +@RunWith(RobolectricTestRunner.class) public class VideoPlayerPluginTest { + @Mock private TextureRegistry mockTextureRegistry; + @Mock private TextureRegistry.SurfaceProducer mockSurfaceProducer; + @Mock private PlatformViewRegistry mockPlatformViewRegistry; + private VideoPlayerPlugin plugin; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + when(mockTextureRegistry.createSurfaceProducer()).thenReturn(mockSurfaceProducer); + + FlutterPlugin.FlutterPluginBinding binding = mock(FlutterPlugin.FlutterPluginBinding.class); + when(binding.getApplicationContext()).thenReturn(mock(Context.class)); + when(binding.getTextureRegistry()).thenReturn(mockTextureRegistry); + when(binding.getBinaryMessenger()) + .thenReturn(mock(io.flutter.plugin.common.BinaryMessenger.class)); + when(binding.getPlatformViewRegistry()).thenReturn(mockPlatformViewRegistry); + + plugin = new VideoPlayerPlugin(); + plugin.onAttachedToEngine(binding); + } + + @SuppressWarnings("unchecked") + private LongSparseArray getVideoPlayers() throws Exception { + final Field field = VideoPlayerPlugin.class.getDeclaredField("videoPlayers"); + field.setAccessible(true); + return (LongSparseArray) field.get(plugin); + } + // This is only a placeholder test and doesn't actually initialize the plugin. @Test public void initPluginDoesNotThrow() { final VideoPlayerPlugin plugin = new VideoPlayerPlugin(); } + + @Test + public void registersPlatformVideoViewFactory() { + verify(mockPlatformViewRegistry) + .registerViewFactory( + eq("plugins.flutter.dev/video_player_android"), any(PlatformVideoViewFactory.class)); + } + + @Test + public void createsPlatformViewVideoPlayer() throws Exception { + try (MockedStatic mockedPlatformViewVideoPlayerStatic = + mockStatic(PlatformViewVideoPlayer.class)) { + mockedPlatformViewVideoPlayerStatic + .when(() -> PlatformViewVideoPlayer.create(any(), any(), any(), any())) + .thenReturn(mock(PlatformViewVideoPlayer.class)); + + final CreateMessage createMessage = + new CreateMessage.Builder() + .setViewType(PlatformVideoViewType.PLATFORM_VIEW) + .setUri("https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4") + .setHttpHeaders(new HashMap<>()) + .build(); + + final long playerId = plugin.create(createMessage); + + final LongSparseArray videoPlayers = getVideoPlayers(); + assertNotNull(videoPlayers.get(playerId)); + } + } + + @Test + public void createsTextureVideoPlayer() throws Exception { + try (MockedStatic mockedTextureVideoPlayerStatic = + mockStatic(TextureVideoPlayer.class)) { + mockedTextureVideoPlayerStatic + .when(() -> TextureVideoPlayer.create(any(), any(), any(), any(), any())) + .thenReturn(mock(TextureVideoPlayer.class)); + + final CreateMessage createMessage = + new CreateMessage.Builder() + .setViewType(PlatformVideoViewType.TEXTURE_VIEW) + .setUri("https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4") + .setHttpHeaders(new HashMap<>()) + .build(); + + final long playerId = plugin.create(createMessage); + + final LongSparseArray videoPlayers = getVideoPlayers(); + assertTrue(videoPlayers.get(playerId) instanceof TextureVideoPlayer); + } + } } 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 99a29aaacb3..b3f354b6619 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 @@ -5,24 +5,23 @@ package io.flutter.plugins.videoplayer; import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; -import android.view.Surface; +import androidx.annotation.NonNull; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.MediaItem; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; -import androidx.media3.common.VideoSize; import androidx.media3.exoplayer.ExoPlayer; -import io.flutter.view.TextureRegistry; +import io.flutter.plugins.videoplayer.platformview.PlatformViewExoPlayerEventListener; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; -import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -46,18 +45,33 @@ public final class VideoPlayerTest { private FakeVideoAsset fakeVideoAsset; @Mock private VideoPlayerCallbacks mockEvents; - @Mock private TextureRegistry.SurfaceProducer mockProducer; @Mock private ExoPlayer mockExoPlayer; @Captor private ArgumentCaptor attributesCaptor; - @Captor private ArgumentCaptor callbackCaptor; @Captor private ArgumentCaptor listenerCaptor; @Rule public MockitoRule initRule = MockitoJUnit.rule(); + /** A test subclass of {@link VideoPlayer} that exposes the abstract class for testing. */ + private final class TestVideoPlayer extends VideoPlayer { + private TestVideoPlayer( + @NonNull VideoPlayerCallbacks events, + @NonNull MediaItem mediaItem, + @NonNull VideoPlayerOptions options, + @NonNull ExoPlayerProvider exoPlayerProvider) { + super(events, mediaItem, options, exoPlayerProvider); + } + + @NonNull + @Override + protected ExoPlayerEventListener createExoPlayerEventListener(@NonNull ExoPlayer exoPlayer) { + // Use platform view implementation for testing. + return new PlatformViewExoPlayerEventListener(exoPlayer, mockEvents); + } + } + @Before public void setUp() { fakeVideoAsset = new FakeVideoAsset(FAKE_ASSET_URL); - when(mockProducer.getSurface()).thenReturn(mock(Surface.class)); } private VideoPlayer createVideoPlayer() { @@ -65,8 +79,8 @@ private VideoPlayer createVideoPlayer() { } private VideoPlayer createVideoPlayer(VideoPlayerOptions options) { - return new VideoPlayer( - () -> mockExoPlayer, mockEvents, mockProducer, fakeVideoAsset.getMediaItem(), options); + return new TestVideoPlayer( + mockEvents, fakeVideoAsset.getMediaItem(), options, () -> mockExoPlayer); } @Test @@ -75,8 +89,6 @@ public void loadsAndPreparesProvidedMediaEnablesAudioFocusByDefault() { verify(mockExoPlayer).setMediaItem(fakeVideoAsset.getMediaItem()); verify(mockExoPlayer).prepare(); - verify(mockProducer).getSurface(); - verify(mockExoPlayer).setVideoSurface(any()); verify(mockExoPlayer).setAudioAttributes(attributesCaptor.capture(), eq(true)); assertEquals(attributesCaptor.getValue().contentType, C.AUDIO_CONTENT_TYPE_MOVIE); @@ -171,66 +183,14 @@ public void seekAndGetPosition() { assertEquals(20L, videoPlayer.getPosition()); } - @Test - public void onSurfaceProducerDestroyedAndAvailableReleasesAndThenRecreatesAndResumesPlayer() { - 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(); - simulateSurfaceDestruction(producerLifecycle); - - verify(mockExoPlayer).release(); - - // Create a new mock exo player so that we get a new instance. - mockExoPlayer = mock(ExoPlayer.class); - producerLifecycle.onSurfaceAvailable(); - - 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 onSurfaceProducerDestroyedDoesNotStopOrPauseVideo() { - VideoPlayer videoPlayer = createVideoPlayer(); - - verify(mockProducer).setCallback(callbackCaptor.capture()); - TextureRegistry.SurfaceProducer.Callback producerLifecycle = callbackCaptor.getValue(); - simulateSurfaceDestruction(producerLifecycle); - - verify(mockExoPlayer, never()).stop(); - verify(mockExoPlayer, never()).pause(); - verify(mockExoPlayer, never()).setPlayWhenReady(anyBoolean()); - - videoPlayer.dispose(); - } - - @Test - public void onDisposeSurfaceProducerCallbackIsDisconnected() { - // Regression test for https://github.com/flutter/flutter/issues/156158. - VideoPlayer videoPlayer = createVideoPlayer(); - verify(mockProducer).setCallback(any()); - - videoPlayer.dispose(); - verify(mockProducer).setCallback(null); - } - @Test public void onInitializedCalledWhenVideoPlayerInitiallyAvailable() { VideoPlayer videoPlayer = createVideoPlayer(); // Pretend we have a video, and capture the registered event listener. - when(mockExoPlayer.getVideoSize()).thenReturn(new VideoSize(300, 200)); + when(mockExoPlayer.getVideoFormat()) + .thenReturn( + new Format.Builder().setWidth(300).setHeight(200).setRotationDegrees(0).build()); verify(mockExoPlayer).addListener(listenerCaptor.capture()); Player.Listener listener = listenerCaptor.getValue(); @@ -242,71 +202,11 @@ public void onInitializedCalledWhenVideoPlayerInitiallyAvailable() { } @Test - public void onSurfaceAvailableDoesNotSendInitializeEventAgain() { - // The VideoPlayer contract assumes that the event "initialized" is sent exactly once - // (duplicate events cause an error to be thrown at the shared Dart layer). This test verifies - // that the onInitialized event is sent exactly once per player. - // - // Regression test for https://github.com/flutter/flutter/issues/154602. - VideoPlayer videoPlayer = createVideoPlayer(); - when(mockExoPlayer.getVideoSize()).thenReturn(new VideoSize(300, 200)); - - // Capture the lifecycle events so we can simulate onSurfaceAvailableDestroyed. - verify(mockProducer).setCallback(callbackCaptor.capture()); - TextureRegistry.SurfaceProducer.Callback producerLifecycle = callbackCaptor.getValue(); - - // Trigger destroyed/available. - simulateSurfaceDestruction(producerLifecycle); - producerLifecycle.onSurfaceAvailable(); - - // Initial listener, and the new one from the resume. - verify(mockExoPlayer, times(2)).addListener(listenerCaptor.capture()); - Player.Listener listener = listenerCaptor.getValue(); - - // Now trigger that same event, which would happen in the case of a background/resume. - listener.onPlaybackStateChanged(Player.STATE_READY); - - // Was not called because it was a result of a background/resume. - verify(mockEvents, never()).onInitialized(anyInt(), anyInt(), anyLong(), anyInt()); - - videoPlayer.dispose(); - } - - @Test - public void onSurfaceAvailableWithoutDestroyDoesNotRecreate() { - // Initially create the video player, which creates the initial surface. - VideoPlayer videoPlayer = createVideoPlayer(); - verify(mockProducer).getSurface(); - - // Capture the lifecycle events so we can simulate onSurfaceAvailable/Destroyed. - verify(mockProducer).setCallback(callbackCaptor.capture()); - TextureRegistry.SurfaceProducer.Callback producerLifecycle = callbackCaptor.getValue(); - - // Calling onSurfaceAvailable does not do anything, since the surface was never destroyed. - producerLifecycle.onSurfaceAvailable(); - verifyNoMoreInteractions(mockProducer); - - videoPlayer.dispose(); - } - - @Test - public void disposeReleasesExoPlayerBeforeTexture() { + public void disposeReleasesExoPlayer() { VideoPlayer videoPlayer = createVideoPlayer(); videoPlayer.dispose(); - // Regression test for https://github.com/flutter/flutter/issues/156158. - // The player must be destroyed before the surface it is writing to. - InOrder inOrder = inOrder(mockExoPlayer, mockProducer); - inOrder.verify(mockExoPlayer).release(); - inOrder.verify(mockProducer).release(); - } - - // TODO(bparrishMines): Replace with inline calls to onSurfaceCleanup once available on stable; - // see https://github.com/flutter/flutter/issues/16125. This separate method only exists to scope - // the suppression. - @SuppressWarnings({"deprecation", "removal"}) - void simulateSurfaceDestruction(TextureRegistry.SurfaceProducer.Callback producerLifecycle) { - producerLifecycle.onSurfaceDestroyed(); + verify(mockExoPlayer).release(); } } diff --git a/packages/video_player/video_player_android/example/android/app/build.gradle b/packages/video_player/video_player_android/example/android/app/build.gradle index 708438d8747..73242b32cff 100644 --- a/packages/video_player/video_player_android/example/android/app/build.gradle +++ b/packages/video_player/video_player_android/example/android/app/build.gradle @@ -57,9 +57,13 @@ flutter { } dependencies { + testImplementation 'androidx.test.ext:junit:1.2.1' + testImplementation "com.google.truth:truth:1.1.3" testImplementation 'junit:junit:4.13' testImplementation 'org.robolectric:robolectric:4.10.3' testImplementation 'org.mockito:mockito-core:5.0.0' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + implementation project(':espresso') + api 'androidx.test:core:1.2.0' } diff --git a/packages/video_player/video_player_android/example/android/app/src/androidTest/java/io/flutter/plugins/videoplayerexample/VideoPlayerUITest.java b/packages/video_player/video_player_android/example/android/app/src/androidTest/java/io/flutter/plugins/videoplayerexample/VideoPlayerUITest.java new file mode 100644 index 00000000000..367d3dc9761 --- /dev/null +++ b/packages/video_player/video_player_android/example/android/app/src/androidTest/java/io/flutter/plugins/videoplayerexample/VideoPlayerUITest.java @@ -0,0 +1,73 @@ +// 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.videoplayerexample; + +import static androidx.test.espresso.flutter.EspressoFlutter.WidgetInteraction; +import static androidx.test.espresso.flutter.EspressoFlutter.onFlutterWidget; +import static androidx.test.espresso.flutter.action.FlutterActions.click; +import static androidx.test.espresso.flutter.assertion.FlutterAssertions.matches; +import static androidx.test.espresso.flutter.matcher.FlutterMatchers.isExisting; +import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withText; +import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withValueKey; + +import androidx.test.ext.junit.rules.ActivityScenarioRule; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class VideoPlayerUITest { + @Rule + public ActivityScenarioRule activityRule = + new ActivityScenarioRule<>(DriverExtensionActivity.class); + + @Test + @Ignore("Doesn't run in Firebase Test Lab: https://github.com/flutter/flutter/issues/94748") + public void playVideo() { + WidgetInteraction remoteTab = onFlutterWidget(withText("Remote")); + remoteTab.check(matches(isExisting())); + + for (String tabName : new String[] {"Platform view", "Texture view"}) { + WidgetInteraction viewTypeTab = onFlutterWidget(withText(tabName)); + viewTypeTab.check(matches(isExisting())); + viewTypeTab.perform(click()); + + WidgetInteraction playButton = onFlutterWidget(withValueKey("Play")); + playButton.check(matches(isExisting())); + playButton.perform(click()); + + WidgetInteraction playbackSpeed1x = onFlutterWidget(withText("1.0x")); + playbackSpeed1x.check(matches(isExisting())); + playbackSpeed1x.perform(click()); + + WidgetInteraction playbackSpeed5xButton = onFlutterWidget(withText("5.0x")); + playbackSpeed5xButton.check(matches(isExisting())); + playbackSpeed5xButton.perform(click()); + + WidgetInteraction playbackSpeed5x = onFlutterWidget(withText("5.0x")); + playbackSpeed5x.check(matches(isExisting())); + } + + for (String[] tabData : + new String[][] {{"Asset", "With assets mp4"}, {"Remote", "With remote mp4"}}) { + String tabName = tabData[0]; + String videoDescription = tabData[1]; + WidgetInteraction tab = onFlutterWidget(withText(tabName)); + WidgetInteraction tabDescription = onFlutterWidget(withText(videoDescription)); + tab.check(matches(isExisting())); + + // TODO(FirentisTFW): Assert that testDescription is not visible before we tap on tab. + // This should be done once the Espresso API allows us to perform such an assertion. See + // https://github.com/flutter/flutter/issues/160599 + + tab.perform(click()); + + tab.check(matches(isExisting())); + tabDescription.check(matches(isExisting())); + } + } +} diff --git a/packages/video_player/video_player_android/example/android/app/src/debug/AndroidManifest.xml b/packages/video_player/video_player_android/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000000..03eb1a7bf7a --- /dev/null +++ b/packages/video_player/video_player_android/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/packages/video_player/video_player_android/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/DriverExtensionActivity.java b/packages/video_player/video_player_android/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/DriverExtensionActivity.java new file mode 100644 index 00000000000..98fadc7f4e8 --- /dev/null +++ b/packages/video_player/video_player_android/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/DriverExtensionActivity.java @@ -0,0 +1,10 @@ +// 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.videoplayerexample; + +import io.flutter.embedding.android.FlutterActivity; + +/** Test Activity that sets the name of the Dart method entrypoint in the manifest. */ +public class DriverExtensionActivity extends FlutterActivity {} diff --git a/packages/video_player/video_player_android/example/android/app/src/main/res/xml/network_security_config.xml b/packages/video_player/video_player_android/example/android/app/src/main/res/xml/network_security_config.xml index 043e5ce55a2..37ad2d39041 100644 --- a/packages/video_player/video_player_android/example/android/app/src/main/res/xml/network_security_config.xml +++ b/packages/video_player/video_player_android/example/android/app/src/main/res/xml/network_security_config.xml @@ -3,5 +3,7 @@ www.sample-videos.com 184.72.239.149 + + 127.0.0.1 \ No newline at end of file diff --git a/packages/video_player/video_player_android/example/integration_test/video_player_android_test.dart b/packages/video_player/video_player_android/example/integration_test/video_player_android_test.dart new file mode 100644 index 00000000000..3a83d1c17c4 --- /dev/null +++ b/packages/video_player/video_player_android/example/integration_test/video_player_android_test.dart @@ -0,0 +1,24 @@ +// 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. + +import 'package:flutter_driver/driver_extension.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:video_player_example/main.dart' as app; + +/// Entry point for integration tests that require espresso. +@pragma('vm:entry-point') +void integrationTestMain() { + enableFlutterDriverExtension(); + + app.main(); +} + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // Since this file is lacking integration tests, this test ensures the example + // app can be launched on an emulator/device. + testWidgets('Launch Test', (WidgetTester tester) async {}); +} diff --git a/packages/video_player/video_player_android/example/integration_test/video_player_test.dart b/packages/video_player/video_player_android/example/integration_test/video_player_test.dart index f347214b9e7..9e8e6893fc1 100644 --- a/packages/video_player/video_player_android/example/integration_test/video_player_test.dart +++ b/packages/video_player/video_player_android/example/integration_test/video_player_test.dart @@ -43,64 +43,64 @@ void main() { }); testWidgets('initializes at the start', (_) async { - final int textureId = (await player.create(DataSource( + final int playerId = (await player.create(DataSource( sourceType: DataSourceType.asset, asset: _videoAssetKey, )))!; expect( - await _getDuration(player, textureId), + await _getDuration(player, playerId), const Duration(seconds: 7, milliseconds: 540), ); - await player.dispose(textureId); + await player.dispose(playerId); }); testWidgets('can be played', (WidgetTester tester) async { - final int textureId = (await player.create(DataSource( + final int playerId = (await player.create(DataSource( sourceType: DataSourceType.asset, asset: _videoAssetKey, )))!; - await player.play(textureId); + await player.play(playerId); await tester.pumpAndSettle(_playDuration); - expect(await player.getPosition(textureId), greaterThan(Duration.zero)); - await player.dispose(textureId); + expect(await player.getPosition(playerId), greaterThan(Duration.zero)); + await player.dispose(playerId); }); testWidgets('can seek', (WidgetTester tester) async { - final int textureId = (await player.create(DataSource( + final int playerId = (await player.create(DataSource( sourceType: DataSourceType.asset, asset: _videoAssetKey, )))!; - await player.seekTo(textureId, const Duration(seconds: 3)); + await player.seekTo(playerId, const Duration(seconds: 3)); await tester.pumpAndSettle(_playDuration); expect( - await player.getPosition(textureId), + await player.getPosition(playerId), greaterThanOrEqualTo(const Duration(seconds: 3)), ); - await player.dispose(textureId); + await player.dispose(playerId); }); testWidgets('can pause', (WidgetTester tester) async { - final int textureId = (await player.create(DataSource( + final int playerId = (await player.create(DataSource( sourceType: DataSourceType.asset, asset: _videoAssetKey, )))!; - await player.play(textureId); + await player.play(playerId); await tester.pumpAndSettle(_playDuration); - await player.pause(textureId); + await player.pause(playerId); await tester.pumpAndSettle(_playDuration); - final Duration pausedDuration = await player.getPosition(textureId); + final Duration pausedDuration = await player.getPosition(playerId); await tester.pumpAndSettle(_playDuration); - expect(await player.getPosition(textureId), pausedDuration); - await player.dispose(textureId); + expect(await player.getPosition(playerId), pausedDuration); + await player.dispose(playerId); }); testWidgets('can play a video from a file', (WidgetTester tester) async { @@ -112,45 +112,45 @@ void main() { ), ); - final int textureId = (await player.create(DataSource( + final int playerId = (await player.create(DataSource( sourceType: DataSourceType.file, uri: file.path, )))!; - await player.play(textureId); + await player.play(playerId); await tester.pumpAndSettle(_playDuration); - expect(await player.getPosition(textureId), greaterThan(Duration.zero)); + expect(await player.getPosition(playerId), greaterThan(Duration.zero)); await directory.delete(recursive: true); - await player.dispose(textureId); + await player.dispose(playerId); }); testWidgets('can play a video from network', (WidgetTester tester) async { - final int textureId = (await player.create(DataSource( + final int playerId = (await player.create(DataSource( sourceType: DataSourceType.network, uri: getUrlForAssetAsNetworkSource(_videoAssetKey), )))!; - await player.play(textureId); - await player.seekTo(textureId, const Duration(seconds: 5)); + await player.play(playerId); + await player.seekTo(playerId, const Duration(seconds: 5)); await tester.pumpAndSettle(_playDuration); - await player.pause(textureId); + await player.pause(playerId); - expect(await player.getPosition(textureId), greaterThan(Duration.zero)); + expect(await player.getPosition(playerId), greaterThan(Duration.zero)); - final DurationRange range = await _getBufferingRange(player, textureId); + final DurationRange range = await _getBufferingRange(player, playerId); expect(range.start, Duration.zero); expect(range.end, greaterThan(Duration.zero)); - await player.dispose(textureId); + await player.dispose(playerId); }); } Future _getDuration( AndroidVideoPlayer player, - int textureId, + int playerId, ) { - return player.videoEventsFor(textureId).firstWhere((VideoEvent event) { + return player.videoEventsFor(playerId).firstWhere((VideoEvent event) { return event.eventType == VideoEventType.initialized; }).then((VideoEvent event) { return event.duration!; @@ -159,9 +159,9 @@ Future _getDuration( Future _getBufferingRange( AndroidVideoPlayer player, - int textureId, + int playerId, ) { - return player.videoEventsFor(textureId).firstWhere((VideoEvent event) { + return player.videoEventsFor(playerId).firstWhere((VideoEvent event) { return event.eventType == VideoEventType.bufferingUpdate; }).then((VideoEvent event) { return event.buffered!.first; diff --git a/packages/video_player/video_player_android/example/lib/main.dart b/packages/video_player/video_player_android/example/lib/main.dart index 79f4963bbfa..2e638b2fb0d 100644 --- a/packages/video_player/video_player_android/example/lib/main.dart +++ b/packages/video_player/video_player_android/example/lib/main.dart @@ -5,6 +5,7 @@ // ignore_for_file: public_member_api_docs import 'package:flutter/material.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; import 'mini_controller.dart'; @@ -36,9 +37,17 @@ class _App extends StatelessWidget { ), body: TabBarView( children: [ - _BumbleBeeRemoteVideo(), - _RtspRemoteVideo(), - _ButterFlyAssetVideo(), + _ViewTypeTabBar( + builder: (VideoViewType viewType) => + _BumbleBeeRemoteVideo(viewType), + ), + _ViewTypeTabBar( + builder: (VideoViewType viewType) => _RtspRemoteVideo(viewType), + ), + _ViewTypeTabBar( + builder: (VideoViewType viewType) => + _ButterFlyAssetVideo(viewType), + ), ], ), ), @@ -46,7 +55,70 @@ class _App extends StatelessWidget { } } +class _ViewTypeTabBar extends StatefulWidget { + const _ViewTypeTabBar({ + required this.builder, + }); + + final Widget Function(VideoViewType) builder; + + @override + State<_ViewTypeTabBar> createState() => _ViewTypeTabBarState(); +} + +class _ViewTypeTabBarState extends State<_ViewTypeTabBar> + with SingleTickerProviderStateMixin { + late final TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + TabBar( + controller: _tabController, + isScrollable: true, + tabs: const [ + Tab( + icon: Icon(Icons.texture), + text: 'Texture view', + ), + Tab( + icon: Icon(Icons.construction), + text: 'Platform view', + ), + ], + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + widget.builder(VideoViewType.textureView), + widget.builder(VideoViewType.platformView), + ], + ), + ), + ], + ); + } +} + class _ButterFlyAssetVideo extends StatefulWidget { + const _ButterFlyAssetVideo(this.viewType); + + final VideoViewType viewType; + @override _ButterFlyAssetVideoState createState() => _ButterFlyAssetVideoState(); } @@ -57,7 +129,10 @@ class _ButterFlyAssetVideoState extends State<_ButterFlyAssetVideo> { @override void initState() { super.initState(); - _controller = MiniController.asset('assets/Butterfly-209.mp4'); + _controller = MiniController.asset( + 'assets/Butterfly-209.mp4', + viewType: widget.viewType, + ); _controller.addListener(() { setState(() {}); @@ -101,6 +176,10 @@ class _ButterFlyAssetVideoState extends State<_ButterFlyAssetVideo> { } class _BumbleBeeRemoteVideo extends StatefulWidget { + const _BumbleBeeRemoteVideo(this.viewType); + + final VideoViewType viewType; + @override _BumbleBeeRemoteVideoState createState() => _BumbleBeeRemoteVideoState(); } @@ -113,6 +192,7 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { super.initState(); _controller = MiniController.network( 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', + viewType: widget.viewType, ); _controller.addListener(() { @@ -155,6 +235,10 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { } class _RtspRemoteVideo extends StatefulWidget { + const _RtspRemoteVideo(this.viewType); + + final VideoViewType viewType; + @override _RtspRemoteVideoState createState() => _RtspRemoteVideoState(); } @@ -174,7 +258,10 @@ class _RtspRemoteVideoState extends State<_RtspRemoteVideo> { } setState(() { - _controller = MiniController.network(url); + _controller = MiniController.network( + url, + viewType: widget.viewType, + ); }); _controller!.addListener(() { @@ -267,6 +354,7 @@ class _ControlsOverlay extends StatelessWidget { color: Colors.black26, child: Center( child: Icon( + key: ValueKey('Play'), Icons.play_arrow, color: Colors.white, size: 100.0, diff --git a/packages/video_player/video_player_android/example/lib/mini_controller.dart b/packages/video_player/video_player_android/example/lib/mini_controller.dart index a38013533d6..db8692a4a91 100644 --- a/packages/video_player/video_player_android/example/lib/mini_controller.dart +++ b/packages/video_player/video_player_android/example/lib/mini_controller.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -39,6 +40,7 @@ class VideoPlayerValue { this.isBuffering = false, this.playbackSpeed = 1.0, this.errorDescription, + this.rotationCorrection = 0, }); /// Returns an instance for a video that hasn't been loaded. @@ -83,6 +85,9 @@ class VideoPlayerValue { /// Indicates whether or not the video has been loaded and is ready to play. final bool isInitialized; + /// Degrees to rotate the video (clockwise) so it is displayed correctly. + final int rotationCorrection; + /// Indicates whether or not the video is in an error state. If this is true /// [errorDescription] should have information about the problem. bool get hasError => errorDescription != null; @@ -116,6 +121,7 @@ class VideoPlayerValue { bool? isBuffering, double? playbackSpeed, String? errorDescription, + int? rotationCorrection, }) { return VideoPlayerValue( duration: duration ?? this.duration, @@ -127,6 +133,7 @@ class VideoPlayerValue { isBuffering: isBuffering ?? this.isBuffering, playbackSpeed: playbackSpeed ?? this.playbackSpeed, errorDescription: errorDescription ?? this.errorDescription, + rotationCorrection: rotationCorrection ?? this.rotationCorrection, ); } @@ -143,7 +150,8 @@ class VideoPlayerValue { playbackSpeed == other.playbackSpeed && errorDescription == other.errorDescription && size == other.size && - isInitialized == other.isInitialized; + isInitialized == other.isInitialized && + rotationCorrection == other.rotationCorrection; @override int get hashCode => Object.hash( @@ -156,6 +164,7 @@ class VideoPlayerValue { errorDescription, size, isInitialized, + rotationCorrection, ); } @@ -167,20 +176,27 @@ class MiniController extends ValueNotifier { /// The name of the asset is given by the [dataSource] argument and must not be /// null. The [package] argument must be non-null when the asset comes from a /// package and null otherwise. - MiniController.asset(this.dataSource, {this.package}) - : dataSourceType = DataSourceType.asset, + MiniController.asset( + this.dataSource, { + this.package, + this.viewType = VideoViewType.textureView, + }) : dataSourceType = DataSourceType.asset, super(const VideoPlayerValue(duration: Duration.zero)); /// Constructs a [MiniController] playing a video from obtained from /// the network. - MiniController.network(this.dataSource) - : dataSourceType = DataSourceType.network, + MiniController.network( + this.dataSource, { + this.viewType = VideoViewType.textureView, + }) : dataSourceType = DataSourceType.network, package = null, super(const VideoPlayerValue(duration: Duration.zero)); /// Constructs a [MiniController] playing a video from obtained from a file. - MiniController.file(File file) - : dataSource = Uri.file(file.absolute.path).toString(), + MiniController.file( + File file, { + this.viewType = VideoViewType.textureView, + }) : dataSource = Uri.file(file.absolute.path).toString(), dataSourceType = DataSourceType.file, package = null, super(const VideoPlayerValue(duration: Duration.zero)); @@ -196,19 +212,22 @@ class MiniController extends ValueNotifier { /// Only set for [asset] videos. The package that the asset was loaded from. final String? package; + /// The type of view used to display the video. + final VideoViewType viewType; + Timer? _timer; Completer? _creatingCompleter; StreamSubscription? _eventSubscription; - /// The id of a texture that hasn't been initialized. + /// The id of a player that hasn't been initialized. @visibleForTesting - static const int kUninitializedTextureId = -1; - int _textureId = kUninitializedTextureId; + static const int kUninitializedPlayerId = -1; + int _playerId = kUninitializedPlayerId; /// This is just exposed for testing. It shouldn't be used by anyone depending /// on the plugin. @visibleForTesting - int get textureId => _textureId; + int get playerId => _playerId; /// Attempts to open the given [dataSource] and load metadata about the video. Future initialize() async { @@ -239,8 +258,13 @@ class MiniController extends ValueNotifier { ); } - _textureId = (await _platform.create(dataSourceDescription)) ?? - kUninitializedTextureId; + final VideoCreationOptions creationOptions = VideoCreationOptions( + dataSource: dataSourceDescription, + viewType: viewType, + ); + + _playerId = (await _platform.createWithOptions(creationOptions)) ?? + kUninitializedPlayerId; _creatingCompleter!.complete(null); final Completer initializingCompleter = Completer(); @@ -249,12 +273,13 @@ class MiniController extends ValueNotifier { case VideoEventType.initialized: value = value.copyWith( duration: event.duration, + rotationCorrection: event.rotationCorrection, size: event.size, isInitialized: event.duration != null, ); initializingCompleter.complete(null); - _platform.setVolume(_textureId, 1.0); - _platform.setLooping(_textureId, true); + _platform.setVolume(_playerId, 1.0); + _platform.setLooping(_playerId, true); _applyPlayPause(); case VideoEventType.completed: pause().then((void pauseResult) => seekTo(value.duration)); @@ -281,7 +306,7 @@ class MiniController extends ValueNotifier { } _eventSubscription = _platform - .videoEventsFor(_textureId) + .videoEventsFor(_playerId) .listen(eventListener, onError: errorListener); return initializingCompleter.future; } @@ -292,7 +317,7 @@ class MiniController extends ValueNotifier { await _creatingCompleter!.future; _timer?.cancel(); await _eventSubscription?.cancel(); - await _platform.dispose(_textureId); + await _platform.dispose(_playerId); } super.dispose(); } @@ -312,7 +337,7 @@ class MiniController extends ValueNotifier { Future _applyPlayPause() async { _timer?.cancel(); if (value.isPlaying) { - await _platform.play(_textureId); + await _platform.play(_playerId); _timer = Timer.periodic( const Duration(milliseconds: 500), @@ -326,14 +351,14 @@ class MiniController extends ValueNotifier { ); await _applyPlaybackSpeed(); } else { - await _platform.pause(_textureId); + await _platform.pause(_playerId); } } Future _applyPlaybackSpeed() async { if (value.isPlaying) { await _platform.setPlaybackSpeed( - _textureId, + _playerId, value.playbackSpeed, ); } @@ -341,7 +366,7 @@ class MiniController extends ValueNotifier { /// The position in the current video. Future get position async { - return _platform.getPosition(_textureId); + return _platform.getPosition(_playerId); } /// Sets the video's current timestamp to be at [position]. @@ -351,7 +376,7 @@ class MiniController extends ValueNotifier { } else if (position < Duration.zero) { position = Duration.zero; } - await _platform.seekTo(_textureId, position); + await _platform.seekTo(_playerId, position); _updatePosition(position); } @@ -382,10 +407,10 @@ class VideoPlayer extends StatefulWidget { class _VideoPlayerState extends State { _VideoPlayerState() { _listener = () { - final int newTextureId = widget.controller.textureId; - if (newTextureId != _textureId) { + final int newPlayerId = widget.controller.playerId; + if (newPlayerId != _playerId) { setState(() { - _textureId = newTextureId; + _playerId = newPlayerId; }); } }; @@ -393,13 +418,13 @@ class _VideoPlayerState extends State { late VoidCallback _listener; - late int _textureId; + late int _playerId; @override void initState() { super.initState(); - _textureId = widget.controller.textureId; - // Need to listen for initialization events since the actual texture ID + _playerId = widget.controller.playerId; + // Need to listen for initialization events since the actual player ID // becomes available after asynchronous initialization finishes. widget.controller.addListener(_listener); } @@ -408,7 +433,7 @@ class _VideoPlayerState extends State { void didUpdateWidget(VideoPlayer oldWidget) { super.didUpdateWidget(oldWidget); oldWidget.controller.removeListener(_listener); - _textureId = widget.controller.textureId; + _playerId = widget.controller.playerId; widget.controller.addListener(_listener); } @@ -420,9 +445,34 @@ class _VideoPlayerState extends State { @override Widget build(BuildContext context) { - return _textureId == MiniController.kUninitializedTextureId + return _playerId == MiniController.kUninitializedPlayerId ? Container() - : _platform.buildView(_textureId); + : _VideoPlayerWithRotation( + rotation: widget.controller.value.rotationCorrection, + child: _platform.buildViewWithOptions( + VideoViewOptions(playerId: _playerId), + ), + ); + } +} + +class _VideoPlayerWithRotation extends StatelessWidget { + const _VideoPlayerWithRotation({ + required this.rotation, + required this.child, + }); + + final int rotation; + final Widget child; + + @override + Widget build(BuildContext context) { + return rotation == 0 + ? child + : Transform.rotate( + angle: rotation * pi / 180, + child: child, + ); } } diff --git a/packages/video_player/video_player_android/example/pubspec.yaml b/packages/video_player/video_player_android/example/pubspec.yaml index 01ce43d2c53..a08ae689747 100644 --- a/packages/video_player/video_player_android/example/pubspec.yaml +++ b/packages/video_player/video_player_android/example/pubspec.yaml @@ -9,6 +9,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_driver: + sdk: flutter video_player_android: # When depending on this package from a real application you should use: # video_player_android: ^x.y.z @@ -16,9 +18,10 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - video_player_platform_interface: ">=6.1.0 <7.0.0" + video_player_platform_interface: ^6.3.0 dev_dependencies: + espresso: ^0.4.0 flutter_test: sdk: flutter integration_test: diff --git a/packages/video_player/video_player_android/lib/src/android_video_player.dart b/packages/video_player/video_player_android/lib/src/android_video_player.dart index 42b509729f9..2df0a0ebc36 100644 --- a/packages/video_player/video_player_android/lib/src/android_video_player.dart +++ b/packages/video_player/video_player_android/lib/src/android_video_player.dart @@ -9,15 +9,18 @@ import 'package:flutter/widgets.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; import 'messages.g.dart'; - -// TODO(FirentisTFW): Remove the ignore and rename parameters when adding support for platform views. -// ignore_for_file: avoid_renaming_method_parameters +import 'platform_view_player.dart'; /// An Android implementation of [VideoPlayerPlatform] that uses the /// Pigeon-generated [VideoPlayerApi]. class AndroidVideoPlayer extends VideoPlayerPlatform { final AndroidVideoPlayerApi _api = AndroidVideoPlayerApi(); + /// A map that associates player ID with a view state. + /// This is used to determine which view type to use when building a view. + final Map _playerViewStates = + {}; + /// Registers this class as the default instance of [PathProviderPlatform]. static void registerWith() { VideoPlayerPlatform.instance = AndroidVideoPlayer(); @@ -29,12 +32,27 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { } @override - Future dispose(int textureId) { - return _api.dispose(textureId); + Future dispose(int playerId) async { + await _api.dispose(playerId); + _playerViewStates.remove(playerId); + } + + @override + Future create(DataSource dataSource) { + return createWithOptions( + VideoCreationOptions( + dataSource: dataSource, + // Compatibility; "create" is always a textureView (createWithOptions + // allows selecting). + viewType: VideoViewType.textureView, + ), + ); } @override - Future create(DataSource dataSource) async { + Future createWithOptions(VideoCreationOptions options) async { + final DataSource dataSource = options.dataSource; + String? asset; String? packageName; String? uri; @@ -60,52 +78,61 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { uri: uri, httpHeaders: httpHeaders, formatHint: formatHint, + viewType: _platformVideoViewTypeFromVideoViewType(options.viewType), ); - return _api.create(message); + final int playerId = await _api.create(message); + _playerViewStates[playerId] = switch (options.viewType) { + // playerId is also the textureId when using texture view. + VideoViewType.textureView => + _VideoPlayerTextureViewState(textureId: playerId), + VideoViewType.platformView => const _VideoPlayerPlatformViewState(), + }; + + return playerId; } @override - Future setLooping(int textureId, bool looping) { - return _api.setLooping(textureId, looping); + Future setLooping(int playerId, bool looping) { + return _api.setLooping(playerId, looping); } @override - Future play(int textureId) { - return _api.play(textureId); + Future play(int playerId) { + return _api.play(playerId); } @override - Future pause(int textureId) { - return _api.pause(textureId); + Future pause(int playerId) { + return _api.pause(playerId); } @override - Future setVolume(int textureId, double volume) { - return _api.setVolume(textureId, volume); + Future setVolume(int playerId, double volume) { + return _api.setVolume(playerId, volume); } @override - Future setPlaybackSpeed(int textureId, double speed) { + Future setPlaybackSpeed(int playerId, double speed) { assert(speed > 0); - return _api.setPlaybackSpeed(textureId, speed); + return _api.setPlaybackSpeed(playerId, speed); } @override - Future seekTo(int textureId, Duration position) { - return _api.seekTo(textureId, position.inMilliseconds); + Future seekTo(int playerId, Duration position) { + return _api.seekTo(playerId, position.inMilliseconds); } @override - Future getPosition(int textureId) async { - final int position = await _api.position(textureId); + Future getPosition(int playerId) async { + final int position = await _api.position(playerId); return Duration(milliseconds: position); } @override - Stream videoEventsFor(int textureId) { - return _eventChannelFor(textureId) + Stream videoEventsFor(int playerId) { + return _eventChannelFor(playerId) .receiveBroadcastStream() .map((dynamic event) { final Map map = event as Map; @@ -145,8 +172,25 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { } @override - Widget buildView(int textureId) { - return Texture(textureId: textureId); + Widget buildView(int playerId) { + return buildViewWithOptions( + VideoViewOptions(playerId: playerId), + ); + } + + @override + Widget buildViewWithOptions(VideoViewOptions options) { + final int playerId = options.playerId; + final _VideoPlayerViewState? viewState = _playerViewStates[playerId]; + + return switch (viewState) { + _VideoPlayerTextureViewState(:final int textureId) => + Texture(textureId: textureId), + _VideoPlayerPlatformViewState() => PlatformViewPlayer(playerId: playerId), + null => throw Exception( + 'Could not find corresponding view type for playerId: $playerId', + ), + }; } @override @@ -154,8 +198,8 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { return _api.setMixWithOthers(mixWithOthers); } - EventChannel _eventChannelFor(int textureId) { - return EventChannel('flutter.io/videoPlayer/videoEvents$textureId'); + EventChannel _eventChannelFor(int playerId) { + return EventChannel('flutter.io/videoPlayer/videoEvents$playerId'); } static const Map _videoFormatStringMap = @@ -174,3 +218,35 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { ); } } + +PlatformVideoViewType _platformVideoViewTypeFromVideoViewType( + VideoViewType viewType, +) { + return switch (viewType) { + VideoViewType.textureView => PlatformVideoViewType.textureView, + VideoViewType.platformView => PlatformVideoViewType.platformView, + }; +} + +/// Base class representing the state of a video player view. +@immutable +sealed class _VideoPlayerViewState { + const _VideoPlayerViewState(); +} + +/// Represents the state of a video player view that uses a texture. +final class _VideoPlayerTextureViewState extends _VideoPlayerViewState { + /// Creates a new instance of [_VideoPlayerTextureViewState]. + const _VideoPlayerTextureViewState({ + required this.textureId, + }); + + /// The ID of the texture used by the video player. + final int textureId; +} + +/// Represents the state of a video player view that uses a platform view. +final class _VideoPlayerPlatformViewState extends _VideoPlayerViewState { + /// Creates a new instance of [_VideoPlayerPlatformViewState]. + const _VideoPlayerPlatformViewState(); +} diff --git a/packages/video_player/video_player_android/lib/src/messages.g.dart b/packages/video_player/video_player_android/lib/src/messages.g.dart index e3190b389e4..717e8e020d0 100644 --- a/packages/video_player/video_player_android/lib/src/messages.g.dart +++ b/packages/video_player/video_player_android/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v22.5.0), do not edit directly. +// Autogenerated from Pigeon (v22.6.1), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -29,6 +29,34 @@ List wrapResponse( return [error.code, error.message, error.details]; } +/// Pigeon equivalent of VideoViewType. +enum PlatformVideoViewType { + textureView, + platformView, +} + +/// Information passed to the platform view creation. +class PlatformVideoViewCreationParams { + PlatformVideoViewCreationParams({ + required this.playerId, + }); + + int playerId; + + Object encode() { + return [ + playerId, + ]; + } + + static PlatformVideoViewCreationParams decode(Object result) { + result as List; + return PlatformVideoViewCreationParams( + playerId: result[0]! as int, + ); + } +} + class CreateMessage { CreateMessage({ this.asset, @@ -36,6 +64,7 @@ class CreateMessage { this.packageName, this.formatHint, required this.httpHeaders, + this.viewType, }); String? asset; @@ -48,6 +77,8 @@ class CreateMessage { Map httpHeaders; + PlatformVideoViewType? viewType; + Object encode() { return [ asset, @@ -55,6 +86,7 @@ class CreateMessage { packageName, formatHint, httpHeaders, + viewType, ]; } @@ -67,6 +99,7 @@ class CreateMessage { formatHint: result[3] as String?, httpHeaders: (result[4] as Map?)!.cast(), + viewType: result[5] as PlatformVideoViewType?, ); } } @@ -78,8 +111,14 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is CreateMessage) { + } else if (value is PlatformVideoViewType) { buffer.putUint8(129); + writeValue(buffer, value.index); + } else if (value is PlatformVideoViewCreationParams) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is CreateMessage) { + buffer.putUint8(131); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); @@ -90,6 +129,11 @@ class _PigeonCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 129: + final int? value = readValue(buffer) as int?; + return value == null ? null : PlatformVideoViewType.values[value]; + case 130: + return PlatformVideoViewCreationParams.decode(readValue(buffer)!); + case 131: return CreateMessage.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -165,7 +209,7 @@ class AndroidVideoPlayerApi { } } - Future dispose(int textureId) async { + Future dispose(int playerId) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -175,7 +219,7 @@ class AndroidVideoPlayerApi { binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = - await pigeonVar_channel.send([textureId]) as List?; + await pigeonVar_channel.send([playerId]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -189,7 +233,7 @@ class AndroidVideoPlayerApi { } } - Future setLooping(int textureId, bool looping) async { + Future setLooping(int playerId, bool looping) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setLooping$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -199,7 +243,7 @@ class AndroidVideoPlayerApi { binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = await pigeonVar_channel - .send([textureId, looping]) as List?; + .send([playerId, looping]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -213,7 +257,7 @@ class AndroidVideoPlayerApi { } } - Future setVolume(int textureId, double volume) async { + Future setVolume(int playerId, double volume) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setVolume$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -223,7 +267,7 @@ class AndroidVideoPlayerApi { binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = await pigeonVar_channel - .send([textureId, volume]) as List?; + .send([playerId, volume]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -237,7 +281,7 @@ class AndroidVideoPlayerApi { } } - Future setPlaybackSpeed(int textureId, double speed) async { + Future setPlaybackSpeed(int playerId, double speed) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -247,7 +291,7 @@ class AndroidVideoPlayerApi { binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = await pigeonVar_channel - .send([textureId, speed]) as List?; + .send([playerId, speed]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -261,7 +305,7 @@ class AndroidVideoPlayerApi { } } - Future play(int textureId) async { + Future play(int playerId) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.play$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -271,7 +315,7 @@ class AndroidVideoPlayerApi { binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = - await pigeonVar_channel.send([textureId]) as List?; + await pigeonVar_channel.send([playerId]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -285,7 +329,7 @@ class AndroidVideoPlayerApi { } } - Future position(int textureId) async { + Future position(int playerId) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.position$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -295,7 +339,7 @@ class AndroidVideoPlayerApi { binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = - await pigeonVar_channel.send([textureId]) as List?; + await pigeonVar_channel.send([playerId]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -314,7 +358,7 @@ class AndroidVideoPlayerApi { } } - Future seekTo(int textureId, int position) async { + Future seekTo(int playerId, int position) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.seekTo$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -324,7 +368,7 @@ class AndroidVideoPlayerApi { binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = await pigeonVar_channel - .send([textureId, position]) as List?; + .send([playerId, position]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -338,7 +382,7 @@ class AndroidVideoPlayerApi { } } - Future pause(int textureId) async { + Future pause(int playerId) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.pause$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -348,7 +392,7 @@ class AndroidVideoPlayerApi { binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = - await pigeonVar_channel.send([textureId]) as List?; + await pigeonVar_channel.send([playerId]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { diff --git a/packages/video_player/video_player_android/lib/src/platform_view_player.dart b/packages/video_player/video_player_android/lib/src/platform_view_player.dart new file mode 100644 index 00000000000..0411b104426 --- /dev/null +++ b/packages/video_player/video_player_android/lib/src/platform_view_player.dart @@ -0,0 +1,60 @@ +// 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. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +import 'messages.g.dart'; + +/// A widget that displays a video player using a platform view. +class PlatformViewPlayer extends StatelessWidget { + /// Creates a new instance of [PlatformViewPlayer]. + const PlatformViewPlayer({ + super.key, + required this.playerId, + }); + + /// The ID of the player. + final int playerId; + + @override + Widget build(BuildContext context) { + const String viewType = 'plugins.flutter.dev/video_player_android'; + final PlatformVideoViewCreationParams creationParams = + PlatformVideoViewCreationParams(playerId: playerId); + + // IgnorePointer so that GestureDetector can be used above the platform view. + return IgnorePointer( + child: PlatformViewLink( + viewType: viewType, + surfaceFactory: ( + BuildContext context, + PlatformViewController controller, + ) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + return PlatformViewsService.initSurfaceAndroidView( + id: params.id, + viewType: viewType, + layoutDirection: + Directionality.maybeOf(context) ?? TextDirection.ltr, + creationParams: creationParams, + creationParamsCodec: AndroidVideoPlayerApi.pigeonChannelCodec, + onFocus: () => params.onFocusChanged(true), + ) + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) + ..create(); + }, + ), + ); + } +} diff --git a/packages/video_player/video_player_android/pigeons/messages.dart b/packages/video_player/video_player_android/pigeons/messages.dart index 37bde3dcda5..a1f1211e533 100644 --- a/packages/video_player/video_player_android/pigeons/messages.dart +++ b/packages/video_player/video_player_android/pigeons/messages.dart @@ -13,6 +13,22 @@ import 'package:pigeon/pigeon.dart'; ), copyrightHeader: 'pigeons/copyright.txt', )) + +/// Pigeon equivalent of VideoViewType. +enum PlatformVideoViewType { + textureView, + platformView, +} + +/// Information passed to the platform view creation. +class PlatformVideoViewCreationParams { + const PlatformVideoViewCreationParams({ + required this.playerId, + }); + + final int playerId; +} + class CreateMessage { CreateMessage({required this.httpHeaders}); String? asset; @@ -20,19 +36,20 @@ class CreateMessage { String? packageName; String? formatHint; Map httpHeaders; + PlatformVideoViewType? viewType; } @HostApi(dartHostTestHandler: 'TestHostVideoPlayerApi') abstract class AndroidVideoPlayerApi { void initialize(); int create(CreateMessage msg); - void dispose(int textureId); - void setLooping(int textureId, bool looping); - void setVolume(int textureId, double volume); - void setPlaybackSpeed(int textureId, double speed); - void play(int textureId); - int position(int textureId); - void seekTo(int textureId, int position); - void pause(int textureId); + void dispose(int playerId); + void setLooping(int playerId, bool looping); + void setVolume(int playerId, double volume); + void setPlaybackSpeed(int playerId, double speed); + void play(int playerId); + int position(int playerId); + void seekTo(int playerId, int position); + void pause(int playerId); void setMixWithOthers(bool mixWithOthers); } diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index c32c49579d1..9e7cdae69cf 100644 --- a/packages/video_player/video_player_android/pubspec.yaml +++ b/packages/video_player/video_player_android/pubspec.yaml @@ -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.7.17 +version: 2.8.0 environment: sdk: ^3.6.0 @@ -20,7 +20,7 @@ flutter: dependencies: flutter: sdk: flutter - video_player_platform_interface: ">=6.1.0 <7.0.0" + video_player_platform_interface: ^6.3.0 dev_dependencies: flutter_test: diff --git a/packages/video_player/video_player_android/test/android_video_player_test.dart b/packages/video_player/video_player_android/test/android_video_player_test.dart index 2c99cb934d0..ddc6617fc4e 100644 --- a/packages/video_player/video_player_android/test/android_video_player_test.dart +++ b/packages/video_player/video_player_android/test/android_video_player_test.dart @@ -3,8 +3,10 @@ // found in the LICENSE file. import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:video_player_android/src/messages.g.dart'; +import 'package:video_player_android/src/platform_view_player.dart'; import 'package:video_player_android/video_player_android.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; @@ -12,7 +14,7 @@ import 'test_api.g.dart'; class _ApiLogger implements TestHostVideoPlayerApi { final List log = []; - int? passedTextureId; + int? passedPlayerId; CreateMessage? passedCreateMessage; int? passedPosition; bool? passedLooping; @@ -28,9 +30,9 @@ class _ApiLogger implements TestHostVideoPlayerApi { } @override - void dispose(int textureId) { + void dispose(int playerId) { log.add('dispose'); - passedTextureId = textureId; + passedPlayerId = playerId; } @override @@ -39,15 +41,15 @@ class _ApiLogger implements TestHostVideoPlayerApi { } @override - void pause(int textureId) { + void pause(int playerId) { log.add('pause'); - passedTextureId = textureId; + passedPlayerId = playerId; } @override - void play(int textureId) { + void play(int playerId) { log.add('play'); - passedTextureId = textureId; + passedPlayerId = playerId; } @override @@ -57,37 +59,37 @@ class _ApiLogger implements TestHostVideoPlayerApi { } @override - int position(int textureId) { + int position(int playerId) { log.add('position'); - passedTextureId = textureId; + passedPlayerId = playerId; return 234; } @override - void seekTo(int textureId, int position) { + void seekTo(int playerId, int position) { log.add('seekTo'); - passedTextureId = textureId; + passedPlayerId = playerId; passedPosition = position; } @override - void setLooping(int textureId, bool looping) { + void setLooping(int playerId, bool looping) { log.add('setLooping'); - passedTextureId = textureId; + passedPlayerId = playerId; passedLooping = looping; } @override - void setVolume(int textureId, double volume) { + void setVolume(int playerId, double volume) { log.add('setVolume'); - passedTextureId = textureId; + passedPlayerId = playerId; passedVolume = volume; } @override - void setPlaybackSpeed(int textureId, double speed) { + void setPlaybackSpeed(int playerId, double speed) { log.add('setPlaybackSpeed'); - passedTextureId = textureId; + passedPlayerId = playerId; passedPlaybackSpeed = speed; } } @@ -120,11 +122,11 @@ void main() { test('dispose', () async { await player.dispose(1); expect(log.log.last, 'dispose'); - expect(log.passedTextureId, 1); + expect(log.passedPlayerId, 1); }); test('create with asset', () async { - final int? textureId = await player.create(DataSource( + final int? playerId = await player.create(DataSource( sourceType: DataSourceType.asset, asset: 'someAsset', package: 'somePackage', @@ -132,11 +134,13 @@ void main() { expect(log.log.last, 'create'); expect(log.passedCreateMessage?.asset, 'someAsset'); expect(log.passedCreateMessage?.packageName, 'somePackage'); - expect(textureId, 3); + expect(playerId, 3); + expect(player.buildViewWithOptions(VideoViewOptions(playerId: playerId!)), + isA()); }); test('create with network', () async { - final int? textureId = await player.create(DataSource( + final int? playerId = await player.create(DataSource( sourceType: DataSourceType.network, uri: 'someUri', formatHint: VideoFormat.dash, @@ -147,11 +151,13 @@ void main() { expect(log.passedCreateMessage?.packageName, null); expect(log.passedCreateMessage?.formatHint, 'dash'); expect(log.passedCreateMessage?.httpHeaders, {}); - expect(textureId, 3); + expect(playerId, 3); + expect(player.buildViewWithOptions(VideoViewOptions(playerId: playerId!)), + isA()); }); test('create with network (some headers)', () async { - final int? textureId = await player.create(DataSource( + final int? playerId = await player.create(DataSource( sourceType: DataSourceType.network, uri: 'someUri', httpHeaders: {'Authorization': 'Bearer token'}, @@ -163,21 +169,25 @@ void main() { expect(log.passedCreateMessage?.formatHint, null); expect(log.passedCreateMessage?.httpHeaders, {'Authorization': 'Bearer token'}); - expect(textureId, 3); + expect(playerId, 3); + expect(player.buildViewWithOptions(VideoViewOptions(playerId: playerId!)), + isA()); }); test('create with file', () async { - final int? textureId = await player.create(DataSource( + final int? playerId = await player.create(DataSource( sourceType: DataSourceType.file, uri: 'someUri', )); expect(log.log.last, 'create'); expect(log.passedCreateMessage?.uri, 'someUri'); - expect(textureId, 3); + expect(playerId, 3); + expect(player.buildViewWithOptions(VideoViewOptions(playerId: playerId!)), + isA()); }); test('create with file (some headers)', () async { - final int? textureId = await player.create(DataSource( + final int? playerId = await player.create(DataSource( sourceType: DataSourceType.file, uri: 'someUri', httpHeaders: {'Authorization': 'Bearer token'}, @@ -186,25 +196,147 @@ void main() { expect(log.passedCreateMessage?.uri, 'someUri'); expect(log.passedCreateMessage?.httpHeaders, {'Authorization': 'Bearer token'}); - expect(textureId, 3); + expect(playerId, 3); + expect(player.buildViewWithOptions(VideoViewOptions(playerId: playerId!)), + isA()); }); + + test('createWithOptions with asset', () async { + final int? playerId = await player.createWithOptions( + VideoCreationOptions( + dataSource: DataSource( + sourceType: DataSourceType.asset, + asset: 'someAsset', + package: 'somePackage', + ), + viewType: VideoViewType.textureView, + ), + ); + expect(log.log.last, 'create'); + expect(log.passedCreateMessage?.asset, 'someAsset'); + expect(log.passedCreateMessage?.packageName, 'somePackage'); + expect(playerId, 3); + expect(player.buildViewWithOptions(const VideoViewOptions(playerId: 3)), + isA()); + }); + + test('createWithOptions with network', () async { + final int? playerId = await player.createWithOptions( + VideoCreationOptions( + dataSource: DataSource( + sourceType: DataSourceType.network, + uri: 'someUri', + formatHint: VideoFormat.dash, + ), + viewType: VideoViewType.textureView, + ), + ); + expect(log.log.last, 'create'); + expect(log.passedCreateMessage?.asset, null); + expect(log.passedCreateMessage?.uri, 'someUri'); + expect(log.passedCreateMessage?.packageName, null); + expect(log.passedCreateMessage?.formatHint, 'dash'); + expect(log.passedCreateMessage?.httpHeaders, {}); + expect(playerId, 3); + expect(player.buildViewWithOptions(VideoViewOptions(playerId: playerId!)), + isA()); + }); + + test('createWithOptions with network (some headers)', () async { + final int? playerId = await player.createWithOptions( + VideoCreationOptions( + dataSource: DataSource( + sourceType: DataSourceType.network, + uri: 'someUri', + httpHeaders: {'Authorization': 'Bearer token'}, + ), + viewType: VideoViewType.textureView, + ), + ); + expect(log.log.last, 'create'); + expect(log.passedCreateMessage?.asset, null); + expect(log.passedCreateMessage?.uri, 'someUri'); + expect(log.passedCreateMessage?.packageName, null); + expect(log.passedCreateMessage?.formatHint, null); + expect(log.passedCreateMessage?.httpHeaders, + {'Authorization': 'Bearer token'}); + expect(playerId, 3); + expect(player.buildViewWithOptions(VideoViewOptions(playerId: playerId!)), + isA()); + }); + + test('createWithOptions with file', () async { + final int? playerId = await player.createWithOptions( + VideoCreationOptions( + dataSource: DataSource( + sourceType: DataSourceType.file, + uri: 'someUri', + ), + viewType: VideoViewType.textureView, + ), + ); + expect(log.log.last, 'create'); + expect(log.passedCreateMessage?.uri, 'someUri'); + expect(playerId, 3); + expect(player.buildViewWithOptions(VideoViewOptions(playerId: playerId!)), + isA()); + }); + + test('createWithOptions with file (some headers)', () async { + final int? playerId = await player.createWithOptions( + VideoCreationOptions( + dataSource: DataSource( + sourceType: DataSourceType.file, + uri: 'someUri', + httpHeaders: {'Authorization': 'Bearer token'}, + ), + viewType: VideoViewType.textureView, + ), + ); + expect(log.log.last, 'create'); + expect(log.passedCreateMessage?.uri, 'someUri'); + expect(log.passedCreateMessage?.httpHeaders, + {'Authorization': 'Bearer token'}); + expect(playerId, 3); + expect(player.buildViewWithOptions(VideoViewOptions(playerId: playerId!)), + isA()); + }); + + test('createWithOptions with platform view', () async { + final int? playerId = await player.createWithOptions( + VideoCreationOptions( + dataSource: DataSource( + sourceType: DataSourceType.file, + uri: 'someUri', + ), + viewType: VideoViewType.platformView, + ), + ); + expect(log.log.last, 'create'); + expect(log.passedCreateMessage?.viewType, + PlatformVideoViewType.platformView); + expect(playerId, 3); + expect(player.buildViewWithOptions(VideoViewOptions(playerId: playerId!)), + isA()); + }); + test('setLooping', () async { await player.setLooping(1, true); expect(log.log.last, 'setLooping'); - expect(log.passedTextureId, 1); + expect(log.passedPlayerId, 1); expect(log.passedLooping, true); }); test('play', () async { await player.play(1); expect(log.log.last, 'play'); - expect(log.passedTextureId, 1); + expect(log.passedPlayerId, 1); }); test('pause', () async { await player.pause(1); expect(log.log.last, 'pause'); - expect(log.passedTextureId, 1); + expect(log.passedPlayerId, 1); }); test('setMixWithOthers', () async { @@ -220,28 +352,28 @@ void main() { test('setVolume', () async { await player.setVolume(1, 0.7); expect(log.log.last, 'setVolume'); - expect(log.passedTextureId, 1); + expect(log.passedPlayerId, 1); expect(log.passedVolume, 0.7); }); test('setPlaybackSpeed', () async { await player.setPlaybackSpeed(1, 1.5); expect(log.log.last, 'setPlaybackSpeed'); - expect(log.passedTextureId, 1); + expect(log.passedPlayerId, 1); expect(log.passedPlaybackSpeed, 1.5); }); test('seekTo', () async { await player.seekTo(1, const Duration(milliseconds: 12345)); expect(log.log.last, 'seekTo'); - expect(log.passedTextureId, 1); + expect(log.passedPlayerId, 1); expect(log.passedPosition, 12345); }); test('getPosition', () async { final Duration position = await player.getPosition(1); expect(log.log.last, 'position'); - expect(log.passedTextureId, 1); + expect(log.passedPlayerId, 1); expect(position, const Duration(milliseconds: 234)); }); diff --git a/packages/video_player/video_player_android/test/test_api.g.dart b/packages/video_player/video_player_android/test/test_api.g.dart index 838dcfbc5a2..f3ac1b7fb73 100644 --- a/packages/video_player/video_player_android/test/test_api.g.dart +++ b/packages/video_player/video_player_android/test/test_api.g.dart @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v22.5.0), do not edit directly. +// Autogenerated from Pigeon (v22.6.1), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import, no_leading_underscores_for_local_identifiers // ignore_for_file: avoid_relative_lib_imports @@ -20,8 +20,14 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is CreateMessage) { + } else if (value is PlatformVideoViewType) { buffer.putUint8(129); + writeValue(buffer, value.index); + } else if (value is PlatformVideoViewCreationParams) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is CreateMessage) { + buffer.putUint8(131); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); @@ -32,6 +38,11 @@ class _PigeonCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 129: + final int? value = readValue(buffer) as int?; + return value == null ? null : PlatformVideoViewType.values[value]; + case 130: + return PlatformVideoViewCreationParams.decode(readValue(buffer)!); + case 131: return CreateMessage.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -48,21 +59,21 @@ abstract class TestHostVideoPlayerApi { int create(CreateMessage msg); - void dispose(int textureId); + void dispose(int playerId); - void setLooping(int textureId, bool looping); + void setLooping(int playerId, bool looping); - void setVolume(int textureId, double volume); + void setVolume(int playerId, double volume); - void setPlaybackSpeed(int textureId, double speed); + void setPlaybackSpeed(int playerId, double speed); - void play(int textureId); + void play(int playerId); - int position(int textureId); + int position(int playerId); - void seekTo(int textureId, int position); + void seekTo(int playerId, int position); - void pause(int textureId); + void pause(int playerId); void setMixWithOthers(bool mixWithOthers); @@ -148,11 +159,11 @@ abstract class TestHostVideoPlayerApi { assert(message != null, 'Argument for dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose was null.'); final List args = (message as List?)!; - final int? arg_textureId = (args[0] as int?); - assert(arg_textureId != null, + final int? arg_playerId = (args[0] as int?); + assert(arg_playerId != null, 'Argument for dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose was null, expected non-null int.'); try { - api.dispose(arg_textureId!); + api.dispose(arg_playerId!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); @@ -180,14 +191,14 @@ abstract class TestHostVideoPlayerApi { assert(message != null, 'Argument for dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setLooping was null.'); final List args = (message as List?)!; - final int? arg_textureId = (args[0] as int?); - assert(arg_textureId != null, + final int? arg_playerId = (args[0] as int?); + assert(arg_playerId != null, 'Argument for dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setLooping was null, expected non-null int.'); final bool? arg_looping = (args[1] as bool?); assert(arg_looping != null, 'Argument for dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setLooping was null, expected non-null bool.'); try { - api.setLooping(arg_textureId!, arg_looping!); + api.setLooping(arg_playerId!, arg_looping!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); @@ -215,14 +226,14 @@ abstract class TestHostVideoPlayerApi { assert(message != null, 'Argument for dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setVolume was null.'); final List args = (message as List?)!; - final int? arg_textureId = (args[0] as int?); - assert(arg_textureId != null, + final int? arg_playerId = (args[0] as int?); + assert(arg_playerId != null, 'Argument for dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setVolume was null, expected non-null int.'); final double? arg_volume = (args[1] as double?); assert(arg_volume != null, 'Argument for dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setVolume was null, expected non-null double.'); try { - api.setVolume(arg_textureId!, arg_volume!); + api.setVolume(arg_playerId!, arg_volume!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); @@ -250,14 +261,14 @@ abstract class TestHostVideoPlayerApi { assert(message != null, 'Argument for dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setPlaybackSpeed was null.'); final List args = (message as List?)!; - final int? arg_textureId = (args[0] as int?); - assert(arg_textureId != null, + final int? arg_playerId = (args[0] as int?); + assert(arg_playerId != null, 'Argument for dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setPlaybackSpeed was null, expected non-null int.'); final double? arg_speed = (args[1] as double?); assert(arg_speed != null, 'Argument for dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setPlaybackSpeed was null, expected non-null double.'); try { - api.setPlaybackSpeed(arg_textureId!, arg_speed!); + api.setPlaybackSpeed(arg_playerId!, arg_speed!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); @@ -285,11 +296,11 @@ abstract class TestHostVideoPlayerApi { assert(message != null, 'Argument for dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.play was null.'); final List args = (message as List?)!; - final int? arg_textureId = (args[0] as int?); - assert(arg_textureId != null, + final int? arg_playerId = (args[0] as int?); + assert(arg_playerId != null, 'Argument for dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.play was null, expected non-null int.'); try { - api.play(arg_textureId!); + api.play(arg_playerId!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); @@ -317,11 +328,11 @@ abstract class TestHostVideoPlayerApi { assert(message != null, 'Argument for dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.position was null.'); final List args = (message as List?)!; - final int? arg_textureId = (args[0] as int?); - assert(arg_textureId != null, + final int? arg_playerId = (args[0] as int?); + assert(arg_playerId != null, 'Argument for dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.position was null, expected non-null int.'); try { - final int output = api.position(arg_textureId!); + final int output = api.position(arg_playerId!); return [output]; } on PlatformException catch (e) { return wrapResponse(error: e); @@ -349,14 +360,14 @@ abstract class TestHostVideoPlayerApi { assert(message != null, 'Argument for dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.seekTo was null.'); final List args = (message as List?)!; - final int? arg_textureId = (args[0] as int?); - assert(arg_textureId != null, + final int? arg_playerId = (args[0] as int?); + assert(arg_playerId != null, 'Argument for dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.seekTo was null, expected non-null int.'); final int? arg_position = (args[1] as int?); assert(arg_position != null, 'Argument for dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.seekTo was null, expected non-null int.'); try { - api.seekTo(arg_textureId!, arg_position!); + api.seekTo(arg_playerId!, arg_position!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); @@ -384,11 +395,11 @@ abstract class TestHostVideoPlayerApi { assert(message != null, 'Argument for dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.pause was null.'); final List args = (message as List?)!; - final int? arg_textureId = (args[0] as int?); - assert(arg_textureId != null, + final int? arg_playerId = (args[0] as int?); + assert(arg_playerId != null, 'Argument for dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.pause was null, expected non-null int.'); try { - api.pause(arg_textureId!); + api.pause(arg_playerId!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e);