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);