diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index 787c115036d..a6decfda03b 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -1,3 +1,10 @@ +## 2.8.17 + +* Moves video event processing logic to Dart, and fixes an issue where buffer + range would not be updated for a paused video. +* Switches to Kotlin for Pigeon-generated code. +* Adopts type-safe event channels for internal communication. + ## 2.8.16 * Updates Java compatibility version to 17 and minimum supported SDK version to Flutter 3.35/Dart 3.9. diff --git a/packages/video_player/video_player_android/android/build.gradle b/packages/video_player/video_player_android/android/build.gradle index 4ce0516f0e6..e8b1c341d44 100644 --- a/packages/video_player/video_player_android/android/build.gradle +++ b/packages/video_player/video_player_android/android/build.gradle @@ -2,6 +2,7 @@ group = 'io.flutter.plugins.videoplayer' version = '1.0-SNAPSHOT' buildscript { + ext.kotlin_version = '2.2.10' repositories { google() mavenCentral() @@ -9,6 +10,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.12.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -20,6 +22,7 @@ rootProject.allprojects { } apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' android { namespace = "io.flutter.plugins.videoplayer" @@ -38,6 +41,13 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } dependencies { def exoplayer_version = "1.5.1" diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java index 7e9486f2df7..5b5203b39e7 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java @@ -10,7 +10,6 @@ import androidx.media3.exoplayer.ExoPlayer; public abstract class ExoPlayerEventListener implements Player.Listener { - private boolean isBuffering = false; private boolean isInitialized = false; protected final ExoPlayer exoPlayer; protected final VideoPlayerCallbacks events; @@ -47,47 +46,34 @@ public ExoPlayerEventListener( this.events = events; } - private void setBuffering(boolean buffering) { - if (isBuffering == buffering) { - return; - } - isBuffering = buffering; - if (buffering) { - events.onBufferingStart(); - } else { - events.onBufferingEnd(); - } - } - protected abstract void sendInitialized(); @Override public void onPlaybackStateChanged(final int playbackState) { + PlatformPlaybackState platformState = PlatformPlaybackState.UNKNOWN; switch (playbackState) { case Player.STATE_BUFFERING: - setBuffering(true); - events.onBufferingUpdate(exoPlayer.getBufferedPosition()); + platformState = PlatformPlaybackState.BUFFERING; break; case Player.STATE_READY: + platformState = PlatformPlaybackState.READY; if (!isInitialized) { isInitialized = true; sendInitialized(); } break; case Player.STATE_ENDED: - events.onCompleted(); + platformState = PlatformPlaybackState.ENDED; break; case Player.STATE_IDLE: + platformState = PlatformPlaybackState.IDLE; break; } - if (playbackState != Player.STATE_BUFFERING) { - setBuffering(false); - } + events.onPlaybackStateChanged(platformState); } @Override public void onPlayerError(@NonNull final PlaybackException error) { - setBuffering(false); if (error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) { // See // https://exoplayer.dev/live-streaming.html#behindlivewindowexception-and-error_code_behind_live_window diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java deleted file mode 100644 index 1bac72cf7bd..00000000000 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java +++ /dev/null @@ -1,926 +0,0 @@ -// Copyright 2013 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -// Autogenerated from Pigeon (v25.5.0), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -package io.flutter.plugins.videoplayer; - -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.RetentionPolicy.CLASS; - -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import io.flutter.plugin.common.BasicMessageChannel; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MessageCodec; -import io.flutter.plugin.common.StandardMessageCodec; -import java.io.ByteArrayOutputStream; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Map; -import java.util.Objects; - -/** Generated class from Pigeon. */ -@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"}) -public class Messages { - - /** Error class for passing custom error details to Flutter via a thrown PlatformException. */ - public static class FlutterError extends RuntimeException { - - /** The error code. */ - public final String code; - - /** The error details. Must be a datatype supported by the api codec. */ - public final Object details; - - public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) { - super(message); - this.code = code; - this.details = details; - } - } - - @NonNull - protected static ArrayList wrapError(@NonNull Throwable exception) { - ArrayList errorList = new ArrayList<>(3); - if (exception instanceof FlutterError) { - FlutterError error = (FlutterError) exception; - errorList.add(error.code); - errorList.add(error.getMessage()); - errorList.add(error.details); - } else { - errorList.add(exception.toString()); - errorList.add(exception.getClass().getSimpleName()); - errorList.add( - "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); - } - return errorList; - } - - @Target(METHOD) - @Retention(CLASS) - @interface CanIgnoreReturnValue {} - - /** Pigeon equivalent of video_platform_interface's VideoFormat. */ - public enum PlatformVideoFormat { - DASH(0), - HLS(1), - SS(2); - - final int index; - - PlatformVideoFormat(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 CreationOptions { - private @NonNull String uri; - - public @NonNull String getUri() { - return uri; - } - - public void setUri(@NonNull String setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"uri\" is null."); - } - this.uri = setterArg; - } - - private @Nullable PlatformVideoFormat formatHint; - - public @Nullable PlatformVideoFormat getFormatHint() { - return formatHint; - } - - public void setFormatHint(@Nullable PlatformVideoFormat setterArg) { - this.formatHint = setterArg; - } - - private @NonNull Map httpHeaders; - - public @NonNull Map getHttpHeaders() { - return httpHeaders; - } - - public void setHttpHeaders(@NonNull Map setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"httpHeaders\" is null."); - } - this.httpHeaders = setterArg; - } - - private @Nullable String userAgent; - - public @Nullable String getUserAgent() { - return userAgent; - } - - public void setUserAgent(@Nullable String setterArg) { - this.userAgent = setterArg; - } - - /** Constructor is non-public to enforce null safety; use Builder. */ - CreationOptions() {} - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - CreationOptions that = (CreationOptions) o; - return uri.equals(that.uri) - && Objects.equals(formatHint, that.formatHint) - && httpHeaders.equals(that.httpHeaders) - && Objects.equals(userAgent, that.userAgent); - } - - @Override - public int hashCode() { - return Objects.hash(uri, formatHint, httpHeaders, userAgent); - } - - public static final class Builder { - - private @Nullable String uri; - - @CanIgnoreReturnValue - public @NonNull Builder setUri(@NonNull String setterArg) { - this.uri = setterArg; - return this; - } - - private @Nullable PlatformVideoFormat formatHint; - - @CanIgnoreReturnValue - public @NonNull Builder setFormatHint(@Nullable PlatformVideoFormat setterArg) { - this.formatHint = setterArg; - return this; - } - - private @Nullable Map httpHeaders; - - @CanIgnoreReturnValue - public @NonNull Builder setHttpHeaders(@NonNull Map setterArg) { - this.httpHeaders = setterArg; - return this; - } - - private @Nullable String userAgent; - - @CanIgnoreReturnValue - public @NonNull Builder setUserAgent(@Nullable String setterArg) { - this.userAgent = setterArg; - return this; - } - - public @NonNull CreationOptions build() { - CreationOptions pigeonReturn = new CreationOptions(); - pigeonReturn.setUri(uri); - pigeonReturn.setFormatHint(formatHint); - pigeonReturn.setHttpHeaders(httpHeaders); - pigeonReturn.setUserAgent(userAgent); - return pigeonReturn; - } - } - - @NonNull - ArrayList toList() { - ArrayList toListResult = new ArrayList<>(4); - toListResult.add(uri); - toListResult.add(formatHint); - toListResult.add(httpHeaders); - toListResult.add(userAgent); - return toListResult; - } - - static @NonNull CreationOptions fromList(@NonNull ArrayList pigeonVar_list) { - CreationOptions pigeonResult = new CreationOptions(); - Object uri = pigeonVar_list.get(0); - pigeonResult.setUri((String) uri); - Object formatHint = pigeonVar_list.get(1); - pigeonResult.setFormatHint((PlatformVideoFormat) formatHint); - Object httpHeaders = pigeonVar_list.get(2); - pigeonResult.setHttpHeaders((Map) httpHeaders); - Object userAgent = pigeonVar_list.get(3); - pigeonResult.setUserAgent((String) userAgent); - return pigeonResult; - } - } - - /** Generated class from Pigeon that represents data sent in messages. */ - public static final class TexturePlayerIds { - 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; - } - - private @NonNull Long textureId; - - public @NonNull Long getTextureId() { - return textureId; - } - - public void setTextureId(@NonNull Long setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"textureId\" is null."); - } - this.textureId = setterArg; - } - - /** Constructor is non-public to enforce null safety; use Builder. */ - TexturePlayerIds() {} - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - TexturePlayerIds that = (TexturePlayerIds) o; - return playerId.equals(that.playerId) && textureId.equals(that.textureId); - } - - @Override - public int hashCode() { - return Objects.hash(playerId, textureId); - } - - public static final class Builder { - - private @Nullable Long playerId; - - @CanIgnoreReturnValue - public @NonNull Builder setPlayerId(@NonNull Long setterArg) { - this.playerId = setterArg; - return this; - } - - private @Nullable Long textureId; - - @CanIgnoreReturnValue - public @NonNull Builder setTextureId(@NonNull Long setterArg) { - this.textureId = setterArg; - return this; - } - - public @NonNull TexturePlayerIds build() { - TexturePlayerIds pigeonReturn = new TexturePlayerIds(); - pigeonReturn.setPlayerId(playerId); - pigeonReturn.setTextureId(textureId); - return pigeonReturn; - } - } - - @NonNull - ArrayList toList() { - ArrayList toListResult = new ArrayList<>(2); - toListResult.add(playerId); - toListResult.add(textureId); - return toListResult; - } - - static @NonNull TexturePlayerIds fromList(@NonNull ArrayList pigeonVar_list) { - TexturePlayerIds pigeonResult = new TexturePlayerIds(); - Object playerId = pigeonVar_list.get(0); - pigeonResult.setPlayerId((Long) playerId); - Object textureId = pigeonVar_list.get(1); - pigeonResult.setTextureId((Long) textureId); - return pigeonResult; - } - } - - /** Generated class from Pigeon that represents data sent in messages. */ - public static final class PlaybackState { - /** The current playback position, in milliseconds. */ - private @NonNull Long playPosition; - - public @NonNull Long getPlayPosition() { - return playPosition; - } - - public void setPlayPosition(@NonNull Long setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"playPosition\" is null."); - } - this.playPosition = setterArg; - } - - /** The current buffer position, in milliseconds. */ - private @NonNull Long bufferPosition; - - public @NonNull Long getBufferPosition() { - return bufferPosition; - } - - public void setBufferPosition(@NonNull Long setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"bufferPosition\" is null."); - } - this.bufferPosition = setterArg; - } - - /** Constructor is non-public to enforce null safety; use Builder. */ - PlaybackState() {} - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - PlaybackState that = (PlaybackState) o; - return playPosition.equals(that.playPosition) && bufferPosition.equals(that.bufferPosition); - } - - @Override - public int hashCode() { - return Objects.hash(playPosition, bufferPosition); - } - - public static final class Builder { - - private @Nullable Long playPosition; - - @CanIgnoreReturnValue - public @NonNull Builder setPlayPosition(@NonNull Long setterArg) { - this.playPosition = setterArg; - return this; - } - - private @Nullable Long bufferPosition; - - @CanIgnoreReturnValue - public @NonNull Builder setBufferPosition(@NonNull Long setterArg) { - this.bufferPosition = setterArg; - return this; - } - - public @NonNull PlaybackState build() { - PlaybackState pigeonReturn = new PlaybackState(); - pigeonReturn.setPlayPosition(playPosition); - pigeonReturn.setBufferPosition(bufferPosition); - return pigeonReturn; - } - } - - @NonNull - ArrayList toList() { - ArrayList toListResult = new ArrayList<>(2); - toListResult.add(playPosition); - toListResult.add(bufferPosition); - return toListResult; - } - - static @NonNull PlaybackState fromList(@NonNull ArrayList pigeonVar_list) { - PlaybackState pigeonResult = new PlaybackState(); - Object playPosition = pigeonVar_list.get(0); - pigeonResult.setPlayPosition((Long) playPosition); - Object bufferPosition = pigeonVar_list.get(1); - pigeonResult.setBufferPosition((Long) bufferPosition); - return pigeonResult; - } - } - - private static class PigeonCodec extends StandardMessageCodec { - public static final PigeonCodec INSTANCE = new PigeonCodec(); - - private PigeonCodec() {} - - @Override - protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { - switch (type) { - case (byte) 129: - { - Object value = readValue(buffer); - return value == null ? null : PlatformVideoFormat.values()[((Long) value).intValue()]; - } - case (byte) 130: - return PlatformVideoViewCreationParams.fromList((ArrayList) readValue(buffer)); - case (byte) 131: - return CreationOptions.fromList((ArrayList) readValue(buffer)); - case (byte) 132: - return TexturePlayerIds.fromList((ArrayList) readValue(buffer)); - case (byte) 133: - return PlaybackState.fromList((ArrayList) readValue(buffer)); - default: - return super.readValueOfType(type, buffer); - } - } - - @Override - protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { - if (value instanceof PlatformVideoFormat) { - stream.write(129); - writeValue(stream, value == null ? null : ((PlatformVideoFormat) value).index); - } else if (value instanceof PlatformVideoViewCreationParams) { - stream.write(130); - writeValue(stream, ((PlatformVideoViewCreationParams) value).toList()); - } else if (value instanceof CreationOptions) { - stream.write(131); - writeValue(stream, ((CreationOptions) value).toList()); - } else if (value instanceof TexturePlayerIds) { - stream.write(132); - writeValue(stream, ((TexturePlayerIds) value).toList()); - } else if (value instanceof PlaybackState) { - stream.write(133); - writeValue(stream, ((PlaybackState) value).toList()); - } else { - super.writeValue(stream, value); - } - } - } - - /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ - public interface AndroidVideoPlayerApi { - - void initialize(); - - @NonNull - Long createForPlatformView(@NonNull CreationOptions options); - - @NonNull - TexturePlayerIds createForTextureView(@NonNull CreationOptions options); - - void dispose(@NonNull Long playerId); - - void setMixWithOthers(@NonNull Boolean mixWithOthers); - - @NonNull - String getLookupKeyForAsset(@NonNull String asset, @Nullable String packageName); - - /** The codec used by AndroidVideoPlayerApi. */ - static @NonNull MessageCodec getCodec() { - return PigeonCodec.INSTANCE; - } - /** - * Sets up an instance of `AndroidVideoPlayerApi` to handle messages through the - * `binaryMessenger`. - */ - static void setUp( - @NonNull BinaryMessenger binaryMessenger, @Nullable AndroidVideoPlayerApi api) { - setUp(binaryMessenger, "", api); - } - - static void setUp( - @NonNull BinaryMessenger binaryMessenger, - @NonNull String messageChannelSuffix, - @Nullable AndroidVideoPlayerApi api) { - messageChannelSuffix = messageChannelSuffix.isEmpty() ? "" : "." + messageChannelSuffix; - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.initialize" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - try { - api.initialize(); - wrapped.add(0, null); - } catch (Throwable exception) { - wrapped = wrapError(exception); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForPlatformView" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - ArrayList args = (ArrayList) message; - CreationOptions optionsArg = (CreationOptions) args.get(0); - try { - Long output = api.createForPlatformView(optionsArg); - wrapped.add(0, output); - } catch (Throwable exception) { - wrapped = wrapError(exception); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForTextureView" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - ArrayList args = (ArrayList) message; - CreationOptions optionsArg = (CreationOptions) args.get(0); - try { - TexturePlayerIds output = api.createForTextureView(optionsArg); - wrapped.add(0, output); - } catch (Throwable exception) { - wrapped = wrapError(exception); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - ArrayList args = (ArrayList) message; - Long playerIdArg = (Long) args.get(0); - try { - api.dispose(playerIdArg); - wrapped.add(0, null); - } catch (Throwable exception) { - wrapped = wrapError(exception); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setMixWithOthers" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - ArrayList args = (ArrayList) message; - Boolean mixWithOthersArg = (Boolean) args.get(0); - try { - api.setMixWithOthers(mixWithOthersArg); - wrapped.add(0, null); - } catch (Throwable exception) { - wrapped = wrapError(exception); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.getLookupKeyForAsset" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - ArrayList args = (ArrayList) message; - String assetArg = (String) args.get(0); - String packageNameArg = (String) args.get(1); - try { - String output = api.getLookupKeyForAsset(assetArg, packageNameArg); - wrapped.add(0, output); - } catch (Throwable exception) { - wrapped = wrapError(exception); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - } - } - /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ - public interface VideoPlayerInstanceApi { - /** Sets whether to automatically loop playback of the video. */ - void setLooping(@NonNull Boolean looping); - /** Sets the volume, with 0.0 being muted and 1.0 being full volume. */ - void setVolume(@NonNull Double volume); - /** Sets the playback speed as a multiple of normal speed. */ - void setPlaybackSpeed(@NonNull Double speed); - /** Begins playback if the video is not currently playing. */ - void play(); - /** Pauses playback if the video is currently playing. */ - void pause(); - /** Seeks to the given playback position, in milliseconds. */ - void seekTo(@NonNull Long position); - /** - * Returns the current playback state. - * - *

This is combined into a single call to minimize platform channel calls for state that - * needs to be polled frequently. - */ - @NonNull - PlaybackState getPlaybackState(); - - /** The codec used by VideoPlayerInstanceApi. */ - static @NonNull MessageCodec getCodec() { - return PigeonCodec.INSTANCE; - } - /** - * Sets up an instance of `VideoPlayerInstanceApi` to handle messages through the - * `binaryMessenger`. - */ - static void setUp( - @NonNull BinaryMessenger binaryMessenger, @Nullable VideoPlayerInstanceApi api) { - setUp(binaryMessenger, "", api); - } - - static void setUp( - @NonNull BinaryMessenger binaryMessenger, - @NonNull String messageChannelSuffix, - @Nullable VideoPlayerInstanceApi api) { - messageChannelSuffix = messageChannelSuffix.isEmpty() ? "" : "." + messageChannelSuffix; - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setLooping" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - ArrayList args = (ArrayList) message; - Boolean loopingArg = (Boolean) args.get(0); - try { - api.setLooping(loopingArg); - wrapped.add(0, null); - } catch (Throwable exception) { - wrapped = wrapError(exception); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setVolume" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - ArrayList args = (ArrayList) message; - Double volumeArg = (Double) args.get(0); - try { - api.setVolume(volumeArg); - wrapped.add(0, null); - } catch (Throwable exception) { - wrapped = wrapError(exception); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setPlaybackSpeed" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - ArrayList args = (ArrayList) message; - Double speedArg = (Double) args.get(0); - try { - api.setPlaybackSpeed(speedArg); - wrapped.add(0, null); - } catch (Throwable exception) { - wrapped = wrapError(exception); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.play" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - try { - api.play(); - wrapped.add(0, null); - } catch (Throwable exception) { - wrapped = wrapError(exception); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.pause" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - try { - api.pause(); - wrapped.add(0, null); - } catch (Throwable exception) { - wrapped = wrapError(exception); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.seekTo" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - ArrayList args = (ArrayList) message; - Long positionArg = (Long) args.get(0); - try { - api.seekTo(positionArg); - wrapped.add(0, null); - } catch (Throwable exception) { - wrapped = wrapError(exception); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getPlaybackState" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - try { - PlaybackState output = api.getPlaybackState(); - wrapped.add(0, output); - } catch (Throwable exception) { - wrapped = wrapError(exception); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - } - } -} diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java index c0150cdea3e..6e7db82d71f 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java @@ -4,11 +4,10 @@ package io.flutter.plugins.videoplayer; -import io.flutter.plugin.common.EventChannel; import java.util.ArrayList; /** - * And implementation of {@link EventChannel.EventSink} which can wrap an underlying sink. + * A wrapper for {@link PigeonEventSink} which can queue messages. * *

It delivers messages immediately when downstream is available, but it queues messages before * the delegate event sink is set with setDelegate. @@ -16,31 +15,28 @@ *

This class is not thread-safe. All calls must be done on the same thread or synchronized * externally. */ -final class QueuingEventSink implements EventChannel.EventSink { - private EventChannel.EventSink delegate; +final class QueuingEventSink { + private PigeonEventSink delegate; private final ArrayList eventQueue = new ArrayList<>(); private boolean done = false; - public void setDelegate(EventChannel.EventSink delegate) { + public void setDelegate(PigeonEventSink delegate) { this.delegate = delegate; maybeFlush(); } - @Override public void endOfStream() { enqueue(new EndOfStreamEvent()); maybeFlush(); done = true; } - @Override public void error(String code, String message, Object details) { enqueue(new ErrorEvent(code, message, details)); maybeFlush(); } - @Override - public void success(Object event) { + public void success(PlatformVideoEvent event) { enqueue(event); maybeFlush(); } @@ -63,7 +59,7 @@ private void maybeFlush() { ErrorEvent errorEvent = (ErrorEvent) event; delegate.error(errorEvent.code, errorEvent.message, errorEvent.details); } else { - delegate.success(event); + delegate.success((PlatformVideoEvent) event); } } eventQueue.clear(); 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 d7861f8a527..d297dad31cc 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 @@ -21,7 +21,7 @@ * *

It provides methods to control playback, adjust volume, and handle seeking. */ -public abstract class VideoPlayer implements Messages.VideoPlayerInstanceApi { +public abstract class VideoPlayer implements VideoPlayerInstanceApi { @NonNull protected final VideoPlayerCallbacks videoPlayerEvents; @Nullable protected final SurfaceProducer surfaceProducer; @Nullable private DisposeHandler disposeHandler; @@ -83,35 +83,37 @@ public void pause() { } @Override - public void setLooping(@NonNull Boolean looping) { + public void setLooping(boolean looping) { exoPlayer.setRepeatMode(looping ? REPEAT_MODE_ALL : REPEAT_MODE_OFF); } @Override - public void setVolume(@NonNull Double volume) { + public void setVolume(double volume) { float bracketedValue = (float) Math.max(0.0, Math.min(1.0, volume)); exoPlayer.setVolume(bracketedValue); } @Override - public void setPlaybackSpeed(@NonNull Double speed) { + public void setPlaybackSpeed(double speed) { // We do not need to consider pitch and skipSilence for now as we do not handle them and // therefore never diverge from the default values. - final PlaybackParameters playbackParameters = new PlaybackParameters(speed.floatValue()); + final PlaybackParameters playbackParameters = new PlaybackParameters((float) speed); exoPlayer.setPlaybackParameters(playbackParameters); } @Override - public @NonNull Messages.PlaybackState getPlaybackState() { - return new Messages.PlaybackState.Builder() - .setPlayPosition(exoPlayer.getCurrentPosition()) - .setBufferPosition(exoPlayer.getBufferedPosition()) - .build(); + public long getCurrentPosition() { + return exoPlayer.getCurrentPosition(); } @Override - public void seekTo(@NonNull Long position) { + public long getBufferedPosition() { + return exoPlayer.getBufferedPosition(); + } + + @Override + public void seekTo(long position) { exoPlayer.seekTo(position); } 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 13e1ffeb875..379f73e2091 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 @@ -19,13 +19,7 @@ public interface VideoPlayerCallbacks { void onInitialized(int width, int height, long durationInMs, int rotationCorrectionInDegrees); - void onBufferingStart(); - - void onBufferingUpdate(long bufferedPosition); - - void onBufferingEnd(); - - void onCompleted(); + void onPlaybackStateChanged(@NonNull PlatformPlaybackState state); void onError(@NonNull String code, @Nullable String message, @Nullable Object details); diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java index b0f144f6b81..782f1cc2ce8 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java @@ -7,19 +7,20 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import io.flutter.plugin.common.EventChannel; -import java.util.HashMap; -import java.util.Map; +import io.flutter.plugin.common.BinaryMessenger; final class VideoPlayerEventCallbacks implements VideoPlayerCallbacks { - private final EventChannel.EventSink eventSink; + private final QueuingEventSink eventSink; - static VideoPlayerEventCallbacks bindTo(EventChannel eventChannel) { + static VideoPlayerEventCallbacks bindTo( + @NonNull BinaryMessenger binaryMessenger, @NonNull String identifier) { QueuingEventSink eventSink = new QueuingEventSink(); - eventChannel.setStreamHandler( - new EventChannel.StreamHandler() { + VideoEventsStreamHandler.Companion.register( + binaryMessenger, + new VideoEventsStreamHandler() { @Override - public void onListen(Object arguments, EventChannel.EventSink events) { + public void onListen( + Object arguments, @NonNull PigeonEventSink events) { eventSink.setDelegate(events); } @@ -27,60 +28,30 @@ public void onListen(Object arguments, EventChannel.EventSink events) { public void onCancel(Object arguments) { eventSink.setDelegate(null); } - }); + }, + identifier); return VideoPlayerEventCallbacks.withSink(eventSink); } @VisibleForTesting - static VideoPlayerEventCallbacks withSink(EventChannel.EventSink eventSink) { + static VideoPlayerEventCallbacks withSink(QueuingEventSink eventSink) { return new VideoPlayerEventCallbacks(eventSink); } - private VideoPlayerEventCallbacks(EventChannel.EventSink eventSink) { + private VideoPlayerEventCallbacks(QueuingEventSink eventSink) { this.eventSink = eventSink; } @Override public void onInitialized( int width, int height, long durationInMs, int rotationCorrectionInDegrees) { - Map event = new HashMap<>(); - event.put("event", "initialized"); - event.put("width", width); - event.put("height", height); - event.put("duration", durationInMs); - if (rotationCorrectionInDegrees != 0) { - event.put("rotationCorrection", rotationCorrectionInDegrees); - } - eventSink.success(event); + eventSink.success( + new InitializationEvent(durationInMs, width, height, rotationCorrectionInDegrees)); } @Override - public void onBufferingStart() { - Map event = new HashMap<>(); - event.put("event", "bufferingStart"); - eventSink.success(event); - } - - @Override - public void onBufferingUpdate(long bufferedPosition) { - Map event = new HashMap<>(); - event.put("event", "bufferingUpdate"); - event.put("position", bufferedPosition); - eventSink.success(event); - } - - @Override - public void onBufferingEnd() { - Map event = new HashMap<>(); - event.put("event", "bufferingEnd"); - eventSink.success(event); - } - - @Override - public void onCompleted() { - Map event = new HashMap<>(); - event.put("event", "completed"); - eventSink.success(event); + public void onPlaybackStateChanged(@NonNull PlatformPlaybackState state) { + eventSink.success(new PlaybackStateChangeEvent(state)); } @Override @@ -90,9 +61,6 @@ public void onError(@NonNull String code, @Nullable String message, @Nullable Ob @Override public void onIsPlayingStateUpdate(boolean isPlaying) { - Map event = new HashMap<>(); - event.put("event", "isPlayingStateUpdate"); - event.put("isPlaying", isPlaying); - eventSink.success(event); + eventSink.success(new IsPlayingStateEvent(isPlaying)); } } 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 db0e811b55b..e3a8b4dc1d6 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 @@ -12,12 +12,6 @@ import io.flutter.Log; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.EventChannel; -import io.flutter.plugins.videoplayer.Messages.AndroidVideoPlayerApi; -import io.flutter.plugins.videoplayer.Messages.CreationOptions; -import io.flutter.plugins.videoplayer.Messages.PlatformVideoFormat; -import io.flutter.plugins.videoplayer.Messages.TexturePlayerIds; -import io.flutter.plugins.videoplayer.Messages.VideoPlayerInstanceApi; import io.flutter.plugins.videoplayer.platformview.PlatformVideoViewFactory; import io.flutter.plugins.videoplayer.platformview.PlatformViewVideoPlayer; import io.flutter.plugins.videoplayer.texture.TextureVideoPlayer; @@ -85,14 +79,15 @@ public void initialize() { } @Override - public @NonNull Long createForPlatformView(@NonNull CreationOptions options) { + public long createForPlatformView(@NonNull CreationOptions options) { final VideoAsset videoAsset = videoAssetWithOptions(options); long id = nextPlayerIdentifier++; + final String streamInstance = Long.toString(id); VideoPlayer videoPlayer = PlatformViewVideoPlayer.create( flutterState.applicationContext, - VideoPlayerEventCallbacks.bindTo(createEventChannel(id)), + VideoPlayerEventCallbacks.bindTo(flutterState.binaryMessenger, streamInstance), videoAsset, sharedOptions); @@ -105,17 +100,18 @@ public void initialize() { final VideoAsset videoAsset = videoAssetWithOptions(options); long id = nextPlayerIdentifier++; + final String streamInstance = Long.toString(id); TextureRegistry.SurfaceProducer handle = flutterState.textureRegistry.createSurfaceProducer(); VideoPlayer videoPlayer = TextureVideoPlayer.create( flutterState.applicationContext, - VideoPlayerEventCallbacks.bindTo(createEventChannel(id)), + VideoPlayerEventCallbacks.bindTo(flutterState.binaryMessenger, streamInstance), handle, videoAsset, sharedOptions); registerPlayerInstance(videoPlayer, id); - return new TexturePlayerIds.Builder().setPlayerId(id).setTextureId(handle.id()).build(); + return new TexturePlayerIds(id, handle.id()); } private @NonNull VideoAsset videoAssetWithOptions(@NonNull CreationOptions options) { @@ -150,18 +146,13 @@ private void registerPlayerInstance(VideoPlayer player, long id) { // disposed. BinaryMessenger messenger = flutterState.binaryMessenger; final String channelSuffix = Long.toString(id); - VideoPlayerInstanceApi.setUp(messenger, channelSuffix, player); - player.setDisposeHandler(() -> VideoPlayerInstanceApi.setUp(messenger, channelSuffix, null)); + VideoPlayerInstanceApi.Companion.setUp(messenger, player, channelSuffix); + player.setDisposeHandler( + () -> VideoPlayerInstanceApi.Companion.setUp(messenger, null, channelSuffix)); videoPlayers.put(id, player); } - @NonNull - private EventChannel createEventChannel(long id) { - return new EventChannel( - flutterState.binaryMessenger, "flutter.io/videoPlayer/videoEvents" + id); - } - @NonNull private VideoPlayer getPlayer(long playerId) { VideoPlayer player = videoPlayers.get(playerId); @@ -179,14 +170,14 @@ private VideoPlayer getPlayer(long playerId) { } @Override - public void dispose(@NonNull Long playerId) { + public void dispose(long playerId) { VideoPlayer player = getPlayer(playerId); player.dispose(); videoPlayers.remove(playerId); } @Override - public void setMixWithOthers(@NonNull Boolean mixWithOthers) { + public void setMixWithOthers(boolean mixWithOthers) { sharedOptions.mixWithOthers = mixWithOthers; } @@ -226,11 +217,11 @@ private static final class FlutterState { } void startListening(VideoPlayerPlugin methodCallHandler, BinaryMessenger messenger) { - AndroidVideoPlayerApi.setUp(messenger, methodCallHandler); + AndroidVideoPlayerApi.Companion.setUp(messenger, methodCallHandler); } void stopListening(BinaryMessenger messenger) { - AndroidVideoPlayerApi.setUp(messenger, null); + AndroidVideoPlayerApi.Companion.setUp(messenger, null); } } } 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 index 4c2b65054c0..19434db53a7 100644 --- 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 @@ -10,7 +10,8 @@ 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.AndroidVideoPlayerApi; +import io.flutter.plugins.videoplayer.PlatformVideoViewCreationParams; import io.flutter.plugins.videoplayer.VideoPlayer; import java.util.Objects; @@ -41,7 +42,7 @@ public interface VideoPlayerProvider { * view. */ public PlatformVideoViewFactory(@NonNull VideoPlayerProvider videoPlayerProvider) { - super(Messages.AndroidVideoPlayerApi.getCodec()); + super(AndroidVideoPlayerApi.Companion.getCodec()); this.videoPlayerProvider = videoPlayerProvider; } @@ -56,8 +57,8 @@ public PlatformVideoViewFactory(@NonNull VideoPlayerProvider videoPlayerProvider @NonNull @Override public PlatformView create(@NonNull Context context, int id, @Nullable Object args) { - final Messages.PlatformVideoViewCreationParams params = - Objects.requireNonNull((Messages.PlatformVideoViewCreationParams) args); + final PlatformVideoViewCreationParams params = + Objects.requireNonNull((PlatformVideoViewCreationParams) args); final Long playerId = params.getPlayerId(); final VideoPlayer player = videoPlayerProvider.getVideoPlayer(playerId); 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 index 0483eaa3e8a..bcc901b7218 100644 --- 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 @@ -14,7 +14,7 @@ import java.util.Objects; public final class TextureExoPlayerEventListener extends ExoPlayerEventListener { - private boolean surfaceProducerHandlesCropAndRotation; + private final boolean surfaceProducerHandlesCropAndRotation; public TextureExoPlayerEventListener( @NonNull ExoPlayer exoPlayer, @@ -31,11 +31,9 @@ protected void sendInitialized() { int width = videoSize.width; int height = videoSize.height; if (width != 0 && height != 0) { - if (surfaceProducerHandlesCropAndRotation) { - // When the SurfaceTexture backend for Impeller is used, the preview should already - // be correctly rotated. - rotationCorrection = RotationDegrees.ROTATE_0; - } else { + // When the SurfaceTexture backend for Impeller is used, the preview should already + // be correctly rotated. + if (!surfaceProducerHandlesCropAndRotation) { // 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. @@ -53,21 +51,6 @@ protected void sendInitialized() { events.onInitialized(width, height, exoPlayer.getDuration(), rotationCorrection.getDegrees()); } - private RotationDegrees getRotationCorrectionFromUnappliedRotation( - RotationDegrees unappliedRotationDegrees) { - RotationDegrees rotationCorrection = RotationDegrees.ROTATE_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; - } - - 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 diff --git a/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt b/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt new file mode 100644 index 00000000000..800026ab4d8 --- /dev/null +++ b/packages/video_player/video_player_android/android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt @@ -0,0 +1,833 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package io.flutter.plugins.videoplayer + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMessageCodec +import io.flutter.plugin.common.StandardMethodCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer + +private object MessagesPigeonUtils { + + fun wrapResult(result: Any?): List { + return listOf(result) + } + + fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf(exception.code, exception.message, exception.details) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)) + } + } + + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a is ByteArray && b is ByteArray) { + return a.contentEquals(b) + } + if (a is IntArray && b is IntArray) { + return a.contentEquals(b) + } + if (a is LongArray && b is LongArray) { + return a.contentEquals(b) + } + if (a is DoubleArray && b is DoubleArray) { + return a.contentEquals(b) + } + if (a is Array<*> && b is Array<*>) { + return a.size == b.size && a.indices.all { deepEquals(a[it], b[it]) } + } + if (a is List<*> && b is List<*>) { + return a.size == b.size && a.indices.all { deepEquals(a[it], b[it]) } + } + if (a is Map<*, *> && b is Map<*, *>) { + return a.size == b.size && + a.all { (b as Map).contains(it.key) && deepEquals(it.value, b[it.key]) } + } + return a == b + } +} + +/** + * Error class for passing custom error details to Flutter via a thrown PlatformException. + * + * @property code The error code. + * @property message The error message. + * @property details The error details. Must be a datatype supported by the api codec. + */ +class FlutterError( + val code: String, + override val message: String? = null, + val details: Any? = null +) : Throwable() + +/** Pigeon equivalent of video_platform_interface's VideoFormat. */ +enum class PlatformVideoFormat(val raw: Int) { + DASH(0), + HLS(1), + SS(2); + + companion object { + fun ofRaw(raw: Int): PlatformVideoFormat? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** + * Pigeon equivalent of Player's playback state. + * https://developer.android.com/media/media3/exoplayer/listening-to-player-events#playback-state + */ +enum class PlatformPlaybackState(val raw: Int) { + IDLE(0), + BUFFERING(1), + READY(2), + ENDED(3), + UNKNOWN(4); + + companion object { + fun ofRaw(raw: Int): PlatformPlaybackState? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** + * Generated class from Pigeon that represents data sent in messages. This class should not be + * extended by any user class outside of the generated file. + */ +sealed class PlatformVideoEvent +/** + * Sent when the video is initialized and ready to play. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class InitializationEvent( + /** The video duration in milliseconds. */ + val duration: Long, + /** The width of the video in pixels. */ + val width: Long, + /** The height of the video in pixels. */ + val height: Long, + /** The rotation that should be applied during playback. */ + val rotationCorrection: Long +) : PlatformVideoEvent() { + companion object { + fun fromList(pigeonVar_list: List): InitializationEvent { + val duration = pigeonVar_list[0] as Long + val width = pigeonVar_list[1] as Long + val height = pigeonVar_list[2] as Long + val rotationCorrection = pigeonVar_list[3] as Long + return InitializationEvent(duration, width, height, rotationCorrection) + } + } + + fun toList(): List { + return listOf( + duration, + width, + height, + rotationCorrection, + ) + } + + override fun equals(other: Any?): Boolean { + if (other !is InitializationEvent) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } + + override fun hashCode(): Int = toList().hashCode() +} + +/** + * Sent when the video state changes. + * + * Corresponds to ExoPlayer's onPlaybackStateChanged. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class PlaybackStateChangeEvent(val state: PlatformPlaybackState) : PlatformVideoEvent() { + companion object { + fun fromList(pigeonVar_list: List): PlaybackStateChangeEvent { + val state = pigeonVar_list[0] as PlatformPlaybackState + return PlaybackStateChangeEvent(state) + } + } + + fun toList(): List { + return listOf( + state, + ) + } + + override fun equals(other: Any?): Boolean { + if (other !is PlaybackStateChangeEvent) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } + + override fun hashCode(): Int = toList().hashCode() +} + +/** + * Sent when the video starts or stops playing. + * + * Corresponds to ExoPlayer's onIsPlayingChanged. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class IsPlayingStateEvent(val isPlaying: Boolean) : PlatformVideoEvent() { + companion object { + fun fromList(pigeonVar_list: List): IsPlayingStateEvent { + val isPlaying = pigeonVar_list[0] as Boolean + return IsPlayingStateEvent(isPlaying) + } + } + + fun toList(): List { + return listOf( + isPlaying, + ) + } + + override fun equals(other: Any?): Boolean { + if (other !is IsPlayingStateEvent) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } + + override fun hashCode(): Int = toList().hashCode() +} + +/** + * Information passed to the platform view creation. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class PlatformVideoViewCreationParams(val playerId: Long) { + companion object { + fun fromList(pigeonVar_list: List): PlatformVideoViewCreationParams { + val playerId = pigeonVar_list[0] as Long + return PlatformVideoViewCreationParams(playerId) + } + } + + fun toList(): List { + return listOf( + playerId, + ) + } + + override fun equals(other: Any?): Boolean { + if (other !is PlatformVideoViewCreationParams) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } + + override fun hashCode(): Int = toList().hashCode() +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class CreationOptions( + val uri: String, + val formatHint: PlatformVideoFormat? = null, + val httpHeaders: Map, + val userAgent: String? = null +) { + companion object { + fun fromList(pigeonVar_list: List): CreationOptions { + val uri = pigeonVar_list[0] as String + val formatHint = pigeonVar_list[1] as PlatformVideoFormat? + val httpHeaders = pigeonVar_list[2] as Map + val userAgent = pigeonVar_list[3] as String? + return CreationOptions(uri, formatHint, httpHeaders, userAgent) + } + } + + fun toList(): List { + return listOf( + uri, + formatHint, + httpHeaders, + userAgent, + ) + } + + override fun equals(other: Any?): Boolean { + if (other !is CreationOptions) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } + + override fun hashCode(): Int = toList().hashCode() +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class TexturePlayerIds(val playerId: Long, val textureId: Long) { + companion object { + fun fromList(pigeonVar_list: List): TexturePlayerIds { + val playerId = pigeonVar_list[0] as Long + val textureId = pigeonVar_list[1] as Long + return TexturePlayerIds(playerId, textureId) + } + } + + fun toList(): List { + return listOf( + playerId, + textureId, + ) + } + + override fun equals(other: Any?): Boolean { + if (other !is TexturePlayerIds) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) + } + + override fun hashCode(): Int = toList().hashCode() +} + +private open class MessagesPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as Long?)?.let { PlatformVideoFormat.ofRaw(it.toInt()) } + } + 130.toByte() -> { + return (readValue(buffer) as Long?)?.let { PlatformPlaybackState.ofRaw(it.toInt()) } + } + 131.toByte() -> { + return (readValue(buffer) as? List)?.let { InitializationEvent.fromList(it) } + } + 132.toByte() -> { + return (readValue(buffer) as? List)?.let { PlaybackStateChangeEvent.fromList(it) } + } + 133.toByte() -> { + return (readValue(buffer) as? List)?.let { IsPlayingStateEvent.fromList(it) } + } + 134.toByte() -> { + return (readValue(buffer) as? List)?.let { + PlatformVideoViewCreationParams.fromList(it) + } + } + 135.toByte() -> { + return (readValue(buffer) as? List)?.let { CreationOptions.fromList(it) } + } + 136.toByte() -> { + return (readValue(buffer) as? List)?.let { TexturePlayerIds.fromList(it) } + } + else -> super.readValueOfType(type, buffer) + } + } + + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is PlatformVideoFormat -> { + stream.write(129) + writeValue(stream, value.raw) + } + is PlatformPlaybackState -> { + stream.write(130) + writeValue(stream, value.raw) + } + is InitializationEvent -> { + stream.write(131) + writeValue(stream, value.toList()) + } + is PlaybackStateChangeEvent -> { + stream.write(132) + writeValue(stream, value.toList()) + } + is IsPlayingStateEvent -> { + stream.write(133) + writeValue(stream, value.toList()) + } + is PlatformVideoViewCreationParams -> { + stream.write(134) + writeValue(stream, value.toList()) + } + is CreationOptions -> { + stream.write(135) + writeValue(stream, value.toList()) + } + is TexturePlayerIds -> { + stream.write(136) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + +val MessagesPigeonMethodCodec = StandardMethodCodec(MessagesPigeonCodec()) + +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface AndroidVideoPlayerApi { + fun initialize() + + fun createForPlatformView(options: CreationOptions): Long + + fun createForTextureView(options: CreationOptions): TexturePlayerIds + + fun dispose(playerId: Long) + + fun setMixWithOthers(mixWithOthers: Boolean) + + fun getLookupKeyForAsset(asset: String, packageName: String?): String + + companion object { + /** The codec used by AndroidVideoPlayerApi. */ + val codec: MessageCodec by lazy { MessagesPigeonCodec() } + /** + * Sets up an instance of `AndroidVideoPlayerApi` to handle messages through the + * `binaryMessenger`. + */ + @JvmOverloads + fun setUp( + binaryMessenger: BinaryMessenger, + api: AndroidVideoPlayerApi?, + messageChannelSuffix: String = "" + ) { + val separatedMessageChannelSuffix = + if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.initialize$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = + try { + api.initialize() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForPlatformView$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val optionsArg = args[0] as CreationOptions + val wrapped: List = + try { + listOf(api.createForPlatformView(optionsArg)) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.createForTextureView$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val optionsArg = args[0] as CreationOptions + val wrapped: List = + try { + listOf(api.createForTextureView(optionsArg)) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val playerIdArg = args[0] as Long + val wrapped: List = + try { + api.dispose(playerIdArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setMixWithOthers$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val mixWithOthersArg = args[0] as Boolean + val wrapped: List = + try { + api.setMixWithOthers(mixWithOthersArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.getLookupKeyForAsset$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val assetArg = args[0] as String + val packageNameArg = args[1] as String? + val wrapped: List = + try { + listOf(api.getLookupKeyForAsset(assetArg, packageNameArg)) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface VideoPlayerInstanceApi { + /** Sets whether to automatically loop playback of the video. */ + fun setLooping(looping: Boolean) + /** Sets the volume, with 0.0 being muted and 1.0 being full volume. */ + fun setVolume(volume: Double) + /** Sets the playback speed as a multiple of normal speed. */ + fun setPlaybackSpeed(speed: Double) + /** Begins playback if the video is not currently playing. */ + fun play() + /** Pauses playback if the video is currently playing. */ + fun pause() + /** Seeks to the given playback position, in milliseconds. */ + fun seekTo(position: Long) + /** Returns the current playback position, in milliseconds. */ + fun getCurrentPosition(): Long + /** Returns the current buffer position, in milliseconds. */ + fun getBufferedPosition(): Long + + companion object { + /** The codec used by VideoPlayerInstanceApi. */ + val codec: MessageCodec by lazy { MessagesPigeonCodec() } + /** + * Sets up an instance of `VideoPlayerInstanceApi` to handle messages through the + * `binaryMessenger`. + */ + @JvmOverloads + fun setUp( + binaryMessenger: BinaryMessenger, + api: VideoPlayerInstanceApi?, + messageChannelSuffix: String = "" + ) { + val separatedMessageChannelSuffix = + if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setLooping$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val loopingArg = args[0] as Boolean + val wrapped: List = + try { + api.setLooping(loopingArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setVolume$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val volumeArg = args[0] as Double + val wrapped: List = + try { + api.setVolume(volumeArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setPlaybackSpeed$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val speedArg = args[0] as Double + val wrapped: List = + try { + api.setPlaybackSpeed(speedArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.play$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = + try { + api.play() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.pause$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = + try { + api.pause() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.seekTo$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val positionArg = args[0] as Long + val wrapped: List = + try { + api.seekTo(positionArg) + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getCurrentPosition$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = + try { + listOf(api.getCurrentPosition()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getBufferedPosition$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = + try { + listOf(api.getBufferedPosition()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} + +private class MessagesPigeonStreamHandler(val wrapper: MessagesPigeonEventChannelWrapper) : + EventChannel.StreamHandler { + var pigeonSink: PigeonEventSink? = null + + override fun onListen(p0: Any?, sink: EventChannel.EventSink) { + pigeonSink = PigeonEventSink(sink) + wrapper.onListen(p0, pigeonSink!!) + } + + override fun onCancel(p0: Any?) { + pigeonSink = null + wrapper.onCancel(p0) + } +} + +interface MessagesPigeonEventChannelWrapper { + open fun onListen(p0: Any?, sink: PigeonEventSink) {} + + open fun onCancel(p0: Any?) {} +} + +class PigeonEventSink(private val sink: EventChannel.EventSink) { + fun success(value: T) { + sink.success(value) + } + + fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + sink.error(errorCode, errorMessage, errorDetails) + } + + fun endOfStream() { + sink.endOfStream() + } +} + +abstract class VideoEventsStreamHandler : MessagesPigeonEventChannelWrapper { + companion object { + fun register( + messenger: BinaryMessenger, + streamHandler: VideoEventsStreamHandler, + instanceName: String = "" + ) { + var channelName: String = + "dev.flutter.pigeon.video_player_android.VideoEventChannel.videoEvents" + if (instanceName.isNotEmpty()) { + channelName += ".$instanceName" + } + val internalStreamHandler = MessagesPigeonStreamHandler(streamHandler) + EventChannel(messenger, channelName, MessagesPigeonMethodCodec) + .setStreamHandler(internalStreamHandler) + } + } +} 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 c9cafb1bfe8..529e8d5a6af 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 @@ -4,13 +4,10 @@ package io.flutter.plugins.videoplayer; -import static org.mockito.ArgumentMatchers.contains; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; @@ -35,7 +32,7 @@ public final class ExoPlayerEventListenerTest { @Mock private ExoPlayer mockExoPlayer; @Mock private VideoPlayerCallbacks mockCallbacks; - private ExoPlayerEventListener eventListener; + private TestExoPlayerEventListener eventListener; @Rule public MockitoRule initRule = MockitoJUnit.rule(); @@ -43,13 +40,19 @@ public final class ExoPlayerEventListenerTest { * A test subclass of {@link ExoPlayerEventListener} that exposes the abstract class for testing. */ private static final class TestExoPlayerEventListener extends ExoPlayerEventListener { + private boolean calledSendInitialized = false; + public TestExoPlayerEventListener(ExoPlayer exoPlayer, VideoPlayerCallbacks callbacks) { super(exoPlayer, callbacks); } @Override protected void sendInitialized() { - // No implementation needed. + calledSendInitialized = true; + } + + boolean calledSendInitialized() { + return calledSendInitialized; } } @@ -59,85 +62,36 @@ public void setUp() { } @Test - public void onPlaybackStateChangedBufferingSendsBufferingStartAndUpdates() { - when(mockExoPlayer.getBufferedPosition()).thenReturn(10L); + public void onPlaybackStateChangedBufferingSendsBuffering() { eventListener.onPlaybackStateChanged(Player.STATE_BUFFERING); - verify(mockCallbacks).onBufferingStart(); - verify(mockCallbacks).onBufferingUpdate(10L); - verifyNoMoreInteractions(mockCallbacks); - - // If it's invoked again, only the update event is called. - verify(mockCallbacks).onBufferingUpdate(10L); + verify(mockCallbacks).onPlaybackStateChanged(PlatformPlaybackState.BUFFERING); verifyNoMoreInteractions(mockCallbacks); } @Test - public void onPlaybackStateChangedEndedSendsOnCompleted() { + public void onPlaybackStateChangedEndedSendsEnded() { eventListener.onPlaybackStateChanged(Player.STATE_ENDED); - verify(mockCallbacks).onCompleted(); + verify(mockCallbacks).onPlaybackStateChanged(PlatformPlaybackState.ENDED); verifyNoMoreInteractions(mockCallbacks); } @Test - public void onPlaybackStateChangedEndedAfterBufferingSendsBufferingEndAndOnCompleted() { - when(mockExoPlayer.getBufferedPosition()).thenReturn(10L); - eventListener.onPlaybackStateChanged(Player.STATE_BUFFERING); - verify(mockCallbacks).onBufferingStart(); - verify(mockCallbacks).onBufferingUpdate(10L); - - eventListener.onPlaybackStateChanged(Player.STATE_ENDED); - verify(mockCallbacks).onCompleted(); - verify(mockCallbacks).onBufferingEnd(); + public void onPlaybackStateChangedIdleSendsIdle() { + eventListener.onPlaybackStateChanged(Player.STATE_IDLE); + verify(mockCallbacks).onPlaybackStateChanged(PlatformPlaybackState.IDLE); verifyNoMoreInteractions(mockCallbacks); } @Test - public void onPlaybackStateChangedReadyAfterBufferingSendsBufferingEnd() { - when(mockExoPlayer.getBufferedPosition()).thenReturn(10L); - eventListener.onPlaybackStateChanged(Player.STATE_BUFFERING); - verify(mockCallbacks).onBufferingStart(); - verify(mockCallbacks).onBufferingUpdate(10L); - + public void onPlaybackStateChangedReadySendsInitializedAndReady() { eventListener.onPlaybackStateChanged(Player.STATE_READY); - verify(mockCallbacks).onBufferingEnd(); + verify(mockCallbacks).onPlaybackStateChanged(PlatformPlaybackState.READY); verifyNoMoreInteractions(mockCallbacks); - } - - @Test - public void onPlaybackStateChangedIdleDoNothing() { - eventListener.onPlaybackStateChanged(Player.STATE_IDLE); - - verifyNoInteractions(mockCallbacks); - } - - @Test - public void onPlaybackStateChangedIdleAfterBufferingSendsBufferingEnd() { - when(mockExoPlayer.getBufferedPosition()).thenReturn(10L); - eventListener.onPlaybackStateChanged(Player.STATE_BUFFERING); - verify(mockCallbacks).onBufferingStart(); - verify(mockCallbacks).onBufferingUpdate(10L); - - eventListener.onPlaybackStateChanged(Player.STATE_IDLE); - verify(mockCallbacks).onBufferingEnd(); - - verifyNoMoreInteractions(mockCallbacks); - } - - @Test - public void onErrorVideoErrorWhenBufferingInProgressAlsoEndBuffering() { - when(mockExoPlayer.getBufferedPosition()).thenReturn(10L); - eventListener.onPlaybackStateChanged(Player.STATE_BUFFERING); - verify(mockCallbacks).onBufferingStart(); - verify(mockCallbacks).onBufferingUpdate(10L); - - eventListener.onPlayerError( - new PlaybackException("BAD", null, PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED)); - verify(mockCallbacks).onBufferingEnd(); - verify(mockCallbacks).onError(eq("VideoError"), contains("BAD"), isNull()); + assertTrue(eventListener.calledSendInitialized()); } @Test 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 index c96dbd12d7c..7d36b65aaea 100644 --- 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 @@ -28,8 +28,7 @@ public void createsPlatformVideoViewBasedOnSuppliedArguments() { when(videoPlayer.getExoPlayer()).thenReturn(exoPlayer); final PlatformVideoViewFactory factory = new PlatformVideoViewFactory(videoPlayerProvider); - final Messages.PlatformVideoViewCreationParams args = - new Messages.PlatformVideoViewCreationParams.Builder().setPlayerId(playerId).build(); + final PlatformVideoViewCreationParams args = new PlatformVideoViewCreationParams(playerId); final PlatformView view = factory.create(context, 0, args); diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacksTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacksTest.java index ad941df53ab..acb3bfd2b40 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacksTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacksTest.java @@ -8,7 +8,6 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; -import java.util.HashMap; import java.util.Map; import org.junit.Before; import org.junit.Rule; @@ -25,8 +24,8 @@ * Unit tests {@link VideoPlayerEventCallbacks}. * *

This test suite narrowly verifies that calling the provided event callbacks, such as - * {@link VideoPlayerEventCallbacks#onBufferingUpdate(long)}, produces the expected data as an - * encoded {@link Map}. + * {@link VideoPlayerEventCallbacks#onPlaybackStateChanged(PlatformPlaybackState)}, produces the + * expected data as an encoded {@link Map}. * *

In other words, this tests that "the Java-side of the event channel works as expected". */ @@ -36,7 +35,7 @@ public final class VideoPlayerEventCallbacksTest { @Mock private QueuingEventSink mockEventSink; - @Captor private ArgumentCaptor> eventCaptor; + @Captor private ArgumentCaptor eventCaptor; @Rule public MockitoRule initRule = MockitoJUnit.rule(); @@ -46,84 +45,29 @@ public void setUp() { } @Test - public void onInitializedSendsWidthHeightAndDuration() { - eventCallbacks.onInitialized(800, 400, 10L, 0); + public void onInitializedSendsExpectedArguments() { + final int width = 800; + final int height = 600; + final long duration = 10L; + final int rotation = 180; + eventCallbacks.onInitialized(width, height, duration, rotation); verify(mockEventSink).success(eventCaptor.capture()); - Map actual = eventCaptor.getValue(); - Map expected = new HashMap<>(); - expected.put("event", "initialized"); - expected.put("duration", 10L); - expected.put("width", 800); - expected.put("height", 400); - - assertEquals(expected, actual); - } - - @Test - public void onInitializedIncludesRotationCorrectIfNonZero() { - eventCallbacks.onInitialized(800, 400, 10L, 180); - - verify(mockEventSink).success(eventCaptor.capture()); - - Map actual = eventCaptor.getValue(); - Map expected = new HashMap<>(); - expected.put("event", "initialized"); - expected.put("duration", 10L); - expected.put("width", 800); - expected.put("height", 400); - expected.put("rotationCorrection", 180); - - assertEquals(expected, actual); - } - - @Test - public void onBufferingStart() { - eventCallbacks.onBufferingStart(); - - verify(mockEventSink).success(eventCaptor.capture()); - - Map actual = eventCaptor.getValue(); - Map expected = new HashMap<>(); - expected.put("event", "bufferingStart"); - assertEquals(expected, actual); - } - - @Test - public void onBufferingUpdateProvidesPosition() { - eventCallbacks.onBufferingUpdate(10L); - - verify(mockEventSink).success(eventCaptor.capture()); - - Map actual = eventCaptor.getValue(); - Map expected = new HashMap<>(); - expected.put("event", "bufferingUpdate"); - expected.put("position", 10L); - assertEquals(expected, actual); - } - - @Test - public void onBufferingEnd() { - eventCallbacks.onBufferingEnd(); - - verify(mockEventSink).success(eventCaptor.capture()); - - Map actual = eventCaptor.getValue(); - Map expected = new HashMap<>(); - expected.put("event", "bufferingEnd"); + PlatformVideoEvent actual = eventCaptor.getValue(); + InitializationEvent expected = new InitializationEvent(duration, width, height, rotation); assertEquals(expected, actual); } @Test - public void onCompleted() { - eventCallbacks.onCompleted(); + public void onPlaybackStateChanged() { + PlatformPlaybackState state = PlatformPlaybackState.READY; + eventCallbacks.onPlaybackStateChanged(state); verify(mockEventSink).success(eventCaptor.capture()); - Map actual = eventCaptor.getValue(); - Map expected = new HashMap<>(); - expected.put("event", "completed"); + PlatformVideoEvent actual = eventCaptor.getValue(); + PlaybackStateChangeEvent expected = new PlaybackStateChangeEvent(state); assertEquals(expected, actual); } @@ -140,10 +84,8 @@ public void onIsPlayingStateUpdate() { verify(mockEventSink).success(eventCaptor.capture()); - Map actual = eventCaptor.getValue(); - Map expected = new HashMap<>(); - expected.put("event", "isPlayingStateUpdate"); - expected.put("isPlaying", true); + PlatformVideoEvent actual = eventCaptor.getValue(); + IsPlayingStateEvent expected = new IsPlayingStateEvent(true); assertEquals(expected, actual); } } 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 907d93b3cb1..6093dc86573 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 @@ -11,8 +11,6 @@ import android.util.LongSparseArray; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.platform.PlatformViewRegistry; -import io.flutter.plugins.videoplayer.Messages.CreationOptions; -import io.flutter.plugins.videoplayer.Messages.TexturePlayerIds; import io.flutter.plugins.videoplayer.platformview.PlatformVideoViewFactory; import io.flutter.plugins.videoplayer.platformview.PlatformViewVideoPlayer; import io.flutter.plugins.videoplayer.texture.TextureVideoPlayer; @@ -79,10 +77,11 @@ public void createsPlatformViewVideoPlayer() throws Exception { .thenReturn(mock(PlatformViewVideoPlayer.class)); final CreationOptions options = - new CreationOptions.Builder() - .setUri("https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4") - .setHttpHeaders(new HashMap<>()) - .build(); + new CreationOptions( + "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4", + null, + new HashMap<>(), + null); final long playerId = plugin.createForPlatformView(options); @@ -100,10 +99,11 @@ public void createsTextureVideoPlayer() throws Exception { .thenReturn(mock(TextureVideoPlayer.class)); final CreationOptions options = - new CreationOptions.Builder() - .setUri("https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4") - .setHttpHeaders(new HashMap<>()) - .build(); + new CreationOptions( + "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4", + null, + new HashMap<>(), + null); final TexturePlayerIds ids = plugin.createForTextureView(options); 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 4c1fd9f2134..9876f1245d6 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 @@ -176,17 +176,27 @@ public void seekTo() { } @Test - public void getPlaybackState() { + public void getCurrentPosition() { VideoPlayer videoPlayer = createVideoPlayer(); final long playbackPosition = 20L; - final long bufferedPosition = 10L; when(mockExoPlayer.getCurrentPosition()).thenReturn(playbackPosition); + + final Long position = videoPlayer.getCurrentPosition(); + assertEquals(playbackPosition, position.longValue()); + + videoPlayer.dispose(); + } + + @Test + public void getBufferedPosition() { + VideoPlayer videoPlayer = createVideoPlayer(); + + final long bufferedPosition = 10L; when(mockExoPlayer.getBufferedPosition()).thenReturn(bufferedPosition); - final Messages.PlaybackState state = videoPlayer.getPlaybackState(); - assertEquals(playbackPosition, state.getPlayPosition().longValue()); - assertEquals(bufferedPosition, state.getBufferPosition().longValue()); + final Long position = videoPlayer.getBufferedPosition(); + assertEquals(bufferedPosition, position.longValue()); videoPlayer.dispose(); } 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 1e4d27f9935..f65b83b8a84 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 @@ -4,24 +4,25 @@ import 'dart:async'; -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; -import 'messages.g.dart'; +import 'messages.g.dart' hide videoEvents; +import 'messages.g.dart' as pigeon show videoEvents; import 'platform_view_player.dart'; -/// The string to append a player ID to in order to construct the event channel -/// name for the event channel used to receive player state updates. -/// -/// Must match the string used to create the EventChannel on the Java side. -const String _videoEventChannelNameBase = 'flutter.io/videoPlayer/videoEvents'; - /// The non-test implementation of `_apiProvider`. VideoPlayerInstanceApi _productionApiProvider(int playerId) { return VideoPlayerInstanceApi(messageChannelSuffix: playerId.toString()); } +/// The non-test implementation of `_videoEventStreamProvider`. +Stream _productionVideoEventStreamProvider( + String streamIdentifier, +) { + return pigeon.videoEvents(instanceName: streamIdentifier); +} + /// An Android implementation of [VideoPlayerPlatform] that uses the /// Pigeon-generated [VideoPlayerApi]. class AndroidVideoPlayer extends VideoPlayerPlatform { @@ -30,13 +31,21 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { @visibleForTesting AndroidVideoPlayerApi? pluginApi, @visibleForTesting VideoPlayerInstanceApi Function(int playerId)? playerApiProvider, + Stream Function(String streamIdentifier)? + videoEventStreamProvider, }) : _api = pluginApi ?? AndroidVideoPlayerApi(), - _playerApiProvider = playerApiProvider ?? _productionApiProvider; + _playerApiProvider = playerApiProvider ?? _productionApiProvider, + _videoEventStreamProvider = + videoEventStreamProvider ?? _productionVideoEventStreamProvider; final AndroidVideoPlayerApi _api; // A method to create VideoPlayerInstanceApi instances, which can be - //overridden for testing. + // overridden for testing. final VideoPlayerInstanceApi Function(int playerId) _playerApiProvider; + // A method to create video event stream instances, which can be + // overridden for testing. + final Stream Function(String streamIdentifier) + _videoEventStreamProvider; final Map _players = {}; @@ -53,8 +62,8 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { @override Future dispose(int playerId) async { final _PlayerInstance? player = _players.remove(playerId); - await _api.dispose(playerId); await player?.dispose(); + await _api.dispose(playerId); } @override @@ -143,11 +152,10 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { @visibleForTesting void ensurePlayerInitialized(int playerId, VideoPlayerViewState viewState) { _players.putIfAbsent(playerId, () { - final String eventChannelName = '$_videoEventChannelNameBase$playerId'; return _PlayerInstance( _playerApiProvider(playerId), viewState, - eventChannelName: eventChannelName, + videoEventStream: _videoEventStreamProvider(playerId.toString()), ); }); } @@ -245,23 +253,25 @@ class _PlayerInstance { _PlayerInstance( this._api, this.viewState, { - required String eventChannelName, + required Stream videoEventStream, }) { - _eventChannel = EventChannel(eventChannelName); - _eventSubscription = _eventChannel.receiveBroadcastStream().listen( + _eventSubscription = videoEventStream.listen( _onStreamEvent, onError: (Object e) { + _setBuffering(false); _eventStreamController.addError(e); }, ); } final VideoPlayerInstanceApi _api; - late final EventChannel _eventChannel; final StreamController _eventStreamController = StreamController(); late final StreamSubscription _eventSubscription; + bool _isDisposed = false; + Timer? _bufferPollingTimer; int _lastBufferPosition = -1; + bool _isBuffering = false; final VideoPlayerViewState viewState; @@ -290,17 +300,7 @@ class _PlayerInstance { } Future getPosition() async { - final PlaybackState state = await _api.getPlaybackState(); - // TODO(stuartmorgan): Move this logic. This is a workaround for the fact - // that ExoPlayer doesn't have any way to observe buffer position - // changes, so polling is required. To minimize platform channel overhead, - // that's combined with getting the position, but this relies on the fact - // that the app-facing package polls getPosition frequently, which makes - // this fragile (for instance, as of writing, this won't be called while - // the video is paused). It should instead be called on its own timer, - // independent of higher-level package logic. - _updateBufferingState(state.bufferPosition); - return Duration(milliseconds: state.playPosition); + return Duration(milliseconds: await _api.getCurrentPosition()); } Stream videoEvents() { @@ -308,12 +308,35 @@ class _PlayerInstance { } Future dispose() async { + _isDisposed = true; + _bufferPollingTimer?.cancel(); await _eventSubscription.cancel(); } + void _setBuffering(bool buffering) { + if (buffering != _isBuffering) { + _isBuffering = buffering; + + _eventStreamController.add( + VideoEvent( + eventType: buffering + ? VideoEventType.bufferingStart + : VideoEventType.bufferingEnd, + ), + ); + // Trigger an extra buffer position check, so that clients have an + // accurate reporting of the current buffering state. + _api.getBufferedPosition().then((int position) { + if (!_isDisposed) { + _updateBufferPosition(position); + } + }); + } + } + /// Sends a buffering update if the buffer position has changed since the /// last check. - void _updateBufferingState(int bufferPosition) { + void _updateBufferPosition(int bufferPosition) { if (bufferPosition != _lastBufferPosition) { _lastBufferPosition = bufferPosition; _eventStreamController.add( @@ -325,31 +348,62 @@ class _PlayerInstance { } } - void _onStreamEvent(dynamic event) { - final Map map = event as Map; - _eventStreamController.add(switch (map['event']) { - 'initialized' => VideoEvent( - eventType: VideoEventType.initialized, - duration: Duration(milliseconds: map['duration'] as int), - size: Size( - (map['width'] as num?)?.toDouble() ?? 0.0, - (map['height'] as num?)?.toDouble() ?? 0.0, - ), - rotationCorrection: map['rotationCorrection'] as int? ?? 0, - ), - 'completed' => VideoEvent(eventType: VideoEventType.completed), - 'bufferingUpdate' => VideoEvent( - eventType: VideoEventType.bufferingUpdate, - buffered: _bufferRangeForPosition(map['position'] as int), - ), - 'bufferingStart' => VideoEvent(eventType: VideoEventType.bufferingStart), - 'bufferingEnd' => VideoEvent(eventType: VideoEventType.bufferingEnd), - 'isPlayingStateUpdate' => VideoEvent( - eventType: VideoEventType.isPlayingStateUpdate, - isPlaying: map['isPlaying'] as bool, - ), - _ => VideoEvent(eventType: VideoEventType.unknown), - }); + void _onStreamEvent(PlatformVideoEvent event) { + switch (event) { + case InitializationEvent _: + _eventStreamController.add( + VideoEvent( + eventType: VideoEventType.initialized, + duration: Duration(milliseconds: event.duration), + size: Size(event.width.toDouble(), event.height.toDouble()), + rotationCorrection: event.rotationCorrection, + ), + ); + + // Start polling for buffer position, since there is no buffer position + // event to listen to. + _bufferPollingTimer = Timer.periodic(const Duration(seconds: 1), ( + Timer timer, + ) async { + final int position = await _api.getBufferedPosition(); + if (!_isDisposed) { + _updateBufferPosition(position); + } + }); + case IsPlayingStateEvent _: + _eventStreamController.add( + VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: event.isPlaying, + ), + ); + case PlaybackStateChangeEvent _: + switch (event.state) { + case PlatformPlaybackState.idle: + // This is currently only used for buffering below. + break; + case PlatformPlaybackState.buffering: + _setBuffering(true); + case PlatformPlaybackState.ready: + // On the Dart side, this is only used for buffering below. On the + // native side it drives the 'initialized' event; that can't + // currently be moved here since gathering the initialization state + // should be synchronous with the state change. + break; + case PlatformPlaybackState.ended: + _eventStreamController.add( + VideoEvent(eventType: VideoEventType.completed), + ); + case PlatformPlaybackState.unknown: + // Ignore unknown states. This isn't an error since the media + // framework could add new states in the future. + break; + } + // Any state other than buffering should end the buffering state. + if (event.state != PlatformPlaybackState.buffering) { + _setBuffering(false); + } + } } // Turns a single buffer position, which is what ExoPlayer reports, into the 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 7701c6f4647..5674729aeb1 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 // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v25.5.0), do not edit directly. +// Autogenerated from Pigeon (v26.0.2), 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 @@ -39,6 +39,145 @@ bool _deepEquals(Object? a, Object? b) { /// Pigeon equivalent of video_platform_interface's VideoFormat. enum PlatformVideoFormat { dash, hls, ss } +/// Pigeon equivalent of Player's playback state. +/// https://developer.android.com/media/media3/exoplayer/listening-to-player-events#playback-state +enum PlatformPlaybackState { idle, buffering, ready, ended, unknown } + +sealed class PlatformVideoEvent {} + +/// Sent when the video is initialized and ready to play. +class InitializationEvent extends PlatformVideoEvent { + InitializationEvent({ + required this.duration, + required this.width, + required this.height, + required this.rotationCorrection, + }); + + /// The video duration in milliseconds. + int duration; + + /// The width of the video in pixels. + int width; + + /// The height of the video in pixels. + int height; + + /// The rotation that should be applied during playback. + int rotationCorrection; + + List _toList() { + return [duration, width, height, rotationCorrection]; + } + + Object encode() { + return _toList(); + } + + static InitializationEvent decode(Object result) { + result as List; + return InitializationEvent( + duration: result[0]! as int, + width: result[1]! as int, + height: result[2]! as int, + rotationCorrection: result[3]! as int, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! InitializationEvent || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +/// Sent when the video state changes. +/// +/// Corresponds to ExoPlayer's onPlaybackStateChanged. +class PlaybackStateChangeEvent extends PlatformVideoEvent { + PlaybackStateChangeEvent({required this.state}); + + PlatformPlaybackState state; + + List _toList() { + return [state]; + } + + Object encode() { + return _toList(); + } + + static PlaybackStateChangeEvent decode(Object result) { + result as List; + return PlaybackStateChangeEvent(state: result[0]! as PlatformPlaybackState); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PlaybackStateChangeEvent || + other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +/// Sent when the video starts or stops playing. +/// +/// Corresponds to ExoPlayer's onIsPlayingChanged. +class IsPlayingStateEvent extends PlatformVideoEvent { + IsPlayingStateEvent({required this.isPlaying}); + + bool isPlaying; + + List _toList() { + return [isPlaying]; + } + + Object encode() { + return _toList(); + } + + static IsPlayingStateEvent decode(Object result) { + result as List; + return IsPlayingStateEvent(isPlaying: result[0]! as bool); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! IsPlayingStateEvent || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + /// Information passed to the platform view creation. class PlatformVideoViewCreationParams { PlatformVideoViewCreationParams({required this.playerId}); @@ -168,48 +307,6 @@ class TexturePlayerIds { int get hashCode => Object.hashAll(_toList()); } -class PlaybackState { - PlaybackState({required this.playPosition, required this.bufferPosition}); - - /// The current playback position, in milliseconds. - int playPosition; - - /// The current buffer position, in milliseconds. - int bufferPosition; - - List _toList() { - return [playPosition, bufferPosition]; - } - - Object encode() { - return _toList(); - } - - static PlaybackState decode(Object result) { - result as List; - return PlaybackState( - playPosition: result[0]! as int, - bufferPosition: result[1]! as int, - ); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - bool operator ==(Object other) { - if (other is! PlaybackState || other.runtimeType != runtimeType) { - return false; - } - if (identical(this, other)) { - return true; - } - return _deepEquals(encode(), other.encode()); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); -} - class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -220,18 +317,27 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is PlatformVideoFormat) { buffer.putUint8(129); writeValue(buffer, value.index); - } else if (value is PlatformVideoViewCreationParams) { + } else if (value is PlatformPlaybackState) { buffer.putUint8(130); - writeValue(buffer, value.encode()); - } else if (value is CreationOptions) { + writeValue(buffer, value.index); + } else if (value is InitializationEvent) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is TexturePlayerIds) { + } else if (value is PlaybackStateChangeEvent) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else if (value is PlaybackState) { + } else if (value is IsPlayingStateEvent) { buffer.putUint8(133); writeValue(buffer, value.encode()); + } else if (value is PlatformVideoViewCreationParams) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else if (value is CreationOptions) { + buffer.putUint8(135); + writeValue(buffer, value.encode()); + } else if (value is TexturePlayerIds) { + buffer.putUint8(136); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -244,19 +350,30 @@ class _PigeonCodec extends StandardMessageCodec { final int? value = readValue(buffer) as int?; return value == null ? null : PlatformVideoFormat.values[value]; case 130: - return PlatformVideoViewCreationParams.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : PlatformPlaybackState.values[value]; case 131: - return CreationOptions.decode(readValue(buffer)!); + return InitializationEvent.decode(readValue(buffer)!); case 132: - return TexturePlayerIds.decode(readValue(buffer)!); + return PlaybackStateChangeEvent.decode(readValue(buffer)!); case 133: - return PlaybackState.decode(readValue(buffer)!); + return IsPlayingStateEvent.decode(readValue(buffer)!); + case 134: + return PlatformVideoViewCreationParams.decode(readValue(buffer)!); + case 135: + return CreationOptions.decode(readValue(buffer)!); + case 136: + return TexturePlayerIds.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } } } +const StandardMethodCodec pigeonMethodCodec = StandardMethodCodec( + _PigeonCodec(), +); + class AndroidVideoPlayerApi { /// Constructor for [AndroidVideoPlayerApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default @@ -631,13 +748,10 @@ class VideoPlayerInstanceApi { } } - /// Returns the current playback state. - /// - /// This is combined into a single call to minimize platform channel calls for - /// state that needs to be polled frequently. - Future getPlaybackState() async { + /// Returns the current playback position, in milliseconds. + Future getCurrentPosition() async { final String pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getPlaybackState$pigeonVar_messageChannelSuffix'; + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getCurrentPosition$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, @@ -661,7 +775,51 @@ class VideoPlayerInstanceApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (pigeonVar_replyList[0] as PlaybackState?)!; + return (pigeonVar_replyList[0] as int?)!; } } + + /// Returns the current buffer position, in milliseconds. + Future getBufferedPosition() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getBufferedPosition$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as int?)!; + } + } +} + +Stream videoEvents({String instanceName = ''}) { + if (instanceName.isNotEmpty) { + instanceName = '.$instanceName'; + } + final EventChannel videoEventsChannel = EventChannel( + 'dev.flutter.pigeon.video_player_android.VideoEventChannel.videoEvents$instanceName', + pigeonMethodCodec, + ); + return videoEventsChannel.receiveBroadcastStream().map((dynamic event) { + return event as PlatformVideoEvent; + }); } diff --git a/packages/video_player/video_player_android/pigeons/messages.dart b/packages/video_player/video_player_android/pigeons/messages.dart index 3762c0703e2..6fee5973760 100644 --- a/packages/video_player/video_player_android/pigeons/messages.dart +++ b/packages/video_player/video_player_android/pigeons/messages.dart @@ -7,15 +7,50 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon( PigeonOptions( dartOut: 'lib/src/messages.g.dart', - javaOut: - 'android/src/main/java/io/flutter/plugins/videoplayer/Messages.java', - javaOptions: JavaOptions(package: 'io.flutter.plugins.videoplayer'), + kotlinOut: + 'android/src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt', + kotlinOptions: KotlinOptions(package: 'io.flutter.plugins.videoplayer'), copyrightHeader: 'pigeons/copyright.txt', ), ) /// Pigeon equivalent of video_platform_interface's VideoFormat. enum PlatformVideoFormat { dash, hls, ss } +/// Pigeon equivalent of Player's playback state. +/// https://developer.android.com/media/media3/exoplayer/listening-to-player-events#playback-state +enum PlatformPlaybackState { idle, buffering, ready, ended, unknown } + +sealed class PlatformVideoEvent {} + +/// Sent when the video is initialized and ready to play. +class InitializationEvent extends PlatformVideoEvent { + /// The video duration in milliseconds. + late final int duration; + + /// The width of the video in pixels. + late final int width; + + /// The height of the video in pixels. + late final int height; + + /// The rotation that should be applied during playback. + late final int rotationCorrection; +} + +/// Sent when the video state changes. +/// +/// Corresponds to ExoPlayer's onPlaybackStateChanged. +class PlaybackStateChangeEvent extends PlatformVideoEvent { + late final PlatformPlaybackState state; +} + +/// Sent when the video starts or stops playing. +/// +/// Corresponds to ExoPlayer's onIsPlayingChanged. +class IsPlayingStateEvent extends PlatformVideoEvent { + late final bool isPlaying; +} + /// Information passed to the platform view creation. class PlatformVideoViewCreationParams { const PlatformVideoViewCreationParams({required this.playerId}); @@ -38,16 +73,6 @@ class TexturePlayerIds { final int textureId; } -class PlaybackState { - PlaybackState({required this.playPosition, required this.bufferPosition}); - - /// The current playback position, in milliseconds. - final int playPosition; - - /// The current buffer position, in milliseconds. - final int bufferPosition; -} - @HostApi() abstract class AndroidVideoPlayerApi { void initialize(); @@ -81,9 +106,14 @@ abstract class VideoPlayerInstanceApi { /// Seeks to the given playback position, in milliseconds. void seekTo(int position); - /// Returns the current playback state. - /// - /// This is combined into a single call to minimize platform channel calls for - /// state that needs to be polled frequently. - PlaybackState getPlaybackState(); + /// Returns the current playback position, in milliseconds. + int getCurrentPosition(); + + /// Returns the current buffer position, in milliseconds. + int getBufferedPosition(); +} + +@EventChannelApi() +abstract class VideoEventChannel { + PlatformVideoEvent videoEvents(); } diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index 641a1ff8927..8c996569854 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.8.16 +version: 2.8.17 environment: sdk: ^3.9.0 @@ -27,7 +27,7 @@ dev_dependencies: flutter_test: sdk: flutter mockito: ^5.4.4 - pigeon: ^25.5.0 + pigeon: ^26.0.2 topics: - video 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 efc2c2e7aec..8ce82b0fdfb 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 @@ -2,7 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/services.dart'; +import 'dart:async'; + import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; @@ -38,6 +39,32 @@ void main() { return (player, pluginApi, instanceApi); } + ( + AndroidVideoPlayer, + MockAndroidVideoPlayerApi, + MockVideoPlayerInstanceApi, + StreamController, + ) + setUpMockPlayerWithStream({required int playerId, int? textureId}) { + final MockAndroidVideoPlayerApi pluginApi = MockAndroidVideoPlayerApi(); + final MockVideoPlayerInstanceApi instanceApi = MockVideoPlayerInstanceApi(); + final StreamController streamController = + StreamController(); + final AndroidVideoPlayer player = AndroidVideoPlayer( + pluginApi: pluginApi, + playerApiProvider: (_) => instanceApi, + videoEventStreamProvider: (_) => + streamController.stream.asBroadcastStream(), + ); + player.ensurePlayerInitialized( + playerId, + textureId == null + ? const VideoPlayerPlatformViewState() + : VideoPlayerTextureViewState(textureId: textureId), + ); + return (player, pluginApi, instanceApi, streamController); + } + test('registration', () async { AndroidVideoPlayer.registerWith(); expect(VideoPlayerPlatform.instance, isA()); @@ -543,7 +570,7 @@ void main() { verify(playerApi.seekTo(positionMilliseconds)); }); - test('getPlaybackState', () async { + test('getPosition', () async { final ( AndroidVideoPlayer player, _, @@ -552,178 +579,240 @@ void main() { playerId: 1, ); const int positionMilliseconds = 12345; - when(playerApi.getPlaybackState()).thenAnswer( - (_) async => PlaybackState( - playPosition: positionMilliseconds, - bufferPosition: 0, - ), - ); + when( + playerApi.getCurrentPosition(), + ).thenAnswer((_) async => positionMilliseconds); final Duration position = await player.getPosition(1); expect(position, const Duration(milliseconds: positionMilliseconds)); }); - test('videoEventsFor', () async { - const int playerId = 1; - const String mockChannel = 'flutter.io/videoPlayer/videoEvents$playerId'; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMessageHandler(mockChannel, (ByteData? message) async { - final MethodCall methodCall = const StandardMethodCodec() - .decodeMethodCall(message); - if (methodCall.method == 'listen') { - await TestDefaultBinaryMessengerBinding - .instance - .defaultBinaryMessenger - .handlePlatformMessage( - mockChannel, - const StandardMethodCodec() - .encodeSuccessEnvelope({ - 'event': 'initialized', - 'duration': 98765, - 'width': 1920, - 'height': 1080, - }), - (ByteData? data) {}, - ); - - await TestDefaultBinaryMessengerBinding - .instance - .defaultBinaryMessenger - .handlePlatformMessage( - mockChannel, - const StandardMethodCodec() - .encodeSuccessEnvelope({ - 'event': 'initialized', - 'duration': 98765, - 'width': 1920, - 'height': 1080, - 'rotationCorrection': 180, - }), - (ByteData? data) {}, - ); - - await TestDefaultBinaryMessengerBinding - .instance - .defaultBinaryMessenger - .handlePlatformMessage( - mockChannel, - const StandardMethodCodec().encodeSuccessEnvelope( - {'event': 'completed'}, - ), - (ByteData? data) {}, - ); - - await TestDefaultBinaryMessengerBinding - .instance - .defaultBinaryMessenger - .handlePlatformMessage( - mockChannel, - const StandardMethodCodec().encodeSuccessEnvelope( - { - 'event': 'bufferingUpdate', - 'position': 1234, - }, - ), - (ByteData? data) {}, - ); - - await TestDefaultBinaryMessengerBinding - .instance - .defaultBinaryMessenger - .handlePlatformMessage( - mockChannel, - const StandardMethodCodec().encodeSuccessEnvelope( - {'event': 'bufferingStart'}, - ), - (ByteData? data) {}, - ); - - await TestDefaultBinaryMessengerBinding - .instance - .defaultBinaryMessenger - .handlePlatformMessage( - mockChannel, - const StandardMethodCodec().encodeSuccessEnvelope( - {'event': 'bufferingEnd'}, - ), - (ByteData? data) {}, - ); - - await TestDefaultBinaryMessengerBinding - .instance - .defaultBinaryMessenger - .handlePlatformMessage( - mockChannel, - const StandardMethodCodec().encodeSuccessEnvelope( - { - 'event': 'isPlayingStateUpdate', - 'isPlaying': true, - }, - ), - (ByteData? data) {}, - ); - - await TestDefaultBinaryMessengerBinding - .instance - .defaultBinaryMessenger - .handlePlatformMessage( - mockChannel, - const StandardMethodCodec().encodeSuccessEnvelope( - { - 'event': 'isPlayingStateUpdate', - 'isPlaying': false, - }, - ), - (ByteData? data) {}, - ); - - return const StandardMethodCodec().encodeSuccessEnvelope(null); - } else if (methodCall.method == 'cancel') { - return const StandardMethodCodec().encodeSuccessEnvelope(null); - } else { - fail('Expected listen or cancel'); - } - }); - - // Creating the player triggers the stream listener, so that must be done - // after setting up the mock native handler above. - final (AndroidVideoPlayer player, MockAndroidVideoPlayerApi api, _) = - setUpMockPlayer(playerId: playerId); + group('video events', () { + // Sets up a mock player that emits the given event structure as a success + // callback on the internal platform channel event stream, and returns + // the player's videoEventsFor(...) stream. + Stream mockPlayerEmitingEvents( + List events, + ) { + const int playerId = 1; + final ( + AndroidVideoPlayer player, + _, + _, + StreamController streamController, + ) = setUpMockPlayerWithStream( + playerId: playerId, + ); + + events.forEach(streamController.add); + + return player.videoEventsFor(playerId); + } + + test('initialize', () async { + final Stream eventStream = + mockPlayerEmitingEvents([ + InitializationEvent( + duration: 98765, + width: 1920, + height: 1080, + rotationCorrection: 90, + ), + ]); + + expect( + eventStream, + emitsInOrder([ + VideoEvent( + eventType: VideoEventType.initialized, + duration: const Duration(milliseconds: 98765), + size: const Size(1920, 1080), + rotationCorrection: 90, + ), + ]), + ); + }); - expect( - player.videoEventsFor(playerId), - emitsInOrder([ - VideoEvent( - eventType: VideoEventType.initialized, - duration: const Duration(milliseconds: 98765), - size: const Size(1920, 1080), - rotationCorrection: 0, - ), - VideoEvent( - eventType: VideoEventType.initialized, - duration: const Duration(milliseconds: 98765), - size: const Size(1920, 1080), - rotationCorrection: 180, - ), - VideoEvent(eventType: VideoEventType.completed), - VideoEvent( - eventType: VideoEventType.bufferingUpdate, - buffered: [ - DurationRange(Duration.zero, const Duration(milliseconds: 1234)), - ], - ), - VideoEvent(eventType: VideoEventType.bufferingStart), - VideoEvent(eventType: VideoEventType.bufferingEnd), - VideoEvent( - eventType: VideoEventType.isPlayingStateUpdate, - isPlaying: true, - ), - VideoEvent( - eventType: VideoEventType.isPlayingStateUpdate, - isPlaying: false, - ), - ]), - ); + test('initialization triggers buffer update polling', () async { + final Stream eventStream = + mockPlayerEmitingEvents([ + InitializationEvent( + duration: 98765, + width: 1920, + height: 1080, + rotationCorrection: 90, + ), + ]); + + expect( + eventStream, + emitsInOrder([ + VideoEvent( + eventType: VideoEventType.initialized, + duration: const Duration(milliseconds: 98765), + size: const Size(1920, 1080), + rotationCorrection: 90, + ), + VideoEvent( + eventType: VideoEventType.bufferingUpdate, + buffered: [ + DurationRange(Duration.zero, Duration.zero), + ], + ), + ]), + ); + }); + + test('completed', () async { + final Stream eventStream = mockPlayerEmitingEvents( + [ + PlaybackStateChangeEvent(state: PlatformPlaybackState.ended), + ], + ); + + expect( + eventStream, + emitsInOrder([ + VideoEvent(eventType: VideoEventType.completed), + ]), + ); + }); + + test('buffering start', () async { + final Stream eventStream = mockPlayerEmitingEvents( + [ + PlaybackStateChangeEvent(state: PlatformPlaybackState.buffering), + ], + ); + + expect( + eventStream, + emitsInOrder([ + VideoEvent(eventType: VideoEventType.bufferingStart), + // A buffer start should trigger a buffer update as well. + VideoEvent( + eventType: VideoEventType.bufferingUpdate, + buffered: [ + DurationRange(Duration.zero, Duration.zero), + ], + ), + ]), + ); + }); + + test('buffering end for ready', () async { + final Stream eventStream = mockPlayerEmitingEvents( + [ + // Trigger a start first, since end is only emitted if it's + // started. + PlaybackStateChangeEvent(state: PlatformPlaybackState.buffering), + PlaybackStateChangeEvent(state: PlatformPlaybackState.ready), + ], + ); + + expect( + eventStream, + emitsInOrder([ + // Emitted by buffering. + VideoEvent(eventType: VideoEventType.bufferingStart), + VideoEvent( + eventType: VideoEventType.bufferingUpdate, + buffered: [ + DurationRange(Duration.zero, Duration.zero), + ], + ), + // Emitted by ready. + VideoEvent(eventType: VideoEventType.bufferingEnd), + ]), + ); + }); + + test('buffering end for idle', () async { + final Stream eventStream = mockPlayerEmitingEvents( + [ + // Trigger a start first, since end is only emitted if it's + // started. + PlaybackStateChangeEvent(state: PlatformPlaybackState.buffering), + PlaybackStateChangeEvent(state: PlatformPlaybackState.idle), + ], + ); + + expect( + eventStream, + emitsInOrder([ + // Emitted by buffering. + VideoEvent(eventType: VideoEventType.bufferingStart), + VideoEvent( + eventType: VideoEventType.bufferingUpdate, + buffered: [ + DurationRange(Duration.zero, Duration.zero), + ], + ), + // Emitted by ready. + VideoEvent(eventType: VideoEventType.bufferingEnd), + ]), + ); + }); + + test('buffering end for ended', () async { + final Stream eventStream = mockPlayerEmitingEvents( + [ + // Trigger a start first, since end is only emitted if it's + // started. + PlaybackStateChangeEvent(state: PlatformPlaybackState.buffering), + PlaybackStateChangeEvent(state: PlatformPlaybackState.ended), + ], + ); + + expect( + eventStream, + emitsInOrder([ + // Emitted by buffering. + VideoEvent(eventType: VideoEventType.bufferingStart), + VideoEvent( + eventType: VideoEventType.bufferingUpdate, + buffered: [ + DurationRange(Duration.zero, Duration.zero), + ], + ), + // Emitted by ended. + VideoEvent(eventType: VideoEventType.completed), + VideoEvent(eventType: VideoEventType.bufferingEnd), + ]), + ); + }); + + test('playback start', () async { + final Stream eventStream = mockPlayerEmitingEvents( + [IsPlayingStateEvent(isPlaying: true)], + ); + + expect( + eventStream, + emitsInOrder([ + VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: true, + ), + ]), + ); + }); + + test('playback stop', () async { + final Stream eventStream = mockPlayerEmitingEvents( + [IsPlayingStateEvent(isPlaying: false)], + ); + + expect( + eventStream, + emitsInOrder([ + VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: false, + ), + ]), + ); + }); }); }); } diff --git a/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart b/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart index 4bc36448aa4..2b75dc1e66e 100644 --- a/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart +++ b/packages/video_player/video_player_android/test/android_video_player_test.mocks.dart @@ -29,11 +29,6 @@ class _FakeTexturePlayerIds_0 extends _i1.SmartFake : super(parent, parentInvocation); } -class _FakePlaybackState_1 extends _i1.SmartFake implements _i2.PlaybackState { - _FakePlaybackState_1(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); -} - /// A class which mocks [AndroidVideoPlayerApi]. /// /// See the documentation for Mockito's code generation for more information. @@ -206,21 +201,20 @@ class MockVideoPlayerInstanceApi extends _i1.Mock as _i4.Future); @override - _i4.Future<_i2.PlaybackState> getPlaybackState() => + _i4.Future getCurrentPosition() => (super.noSuchMethod( - Invocation.method(#getPlaybackState, []), - returnValue: _i4.Future<_i2.PlaybackState>.value( - _FakePlaybackState_1( - this, - Invocation.method(#getPlaybackState, []), - ), - ), - returnValueForMissingStub: _i4.Future<_i2.PlaybackState>.value( - _FakePlaybackState_1( - this, - Invocation.method(#getPlaybackState, []), - ), - ), + Invocation.method(#getCurrentPosition, []), + returnValue: _i4.Future.value(0), + returnValueForMissingStub: _i4.Future.value(0), ) - as _i4.Future<_i2.PlaybackState>); + as _i4.Future); + + @override + _i4.Future getBufferedPosition() => + (super.noSuchMethod( + Invocation.method(#getBufferedPosition, []), + returnValue: _i4.Future.value(0), + returnValueForMissingStub: _i4.Future.value(0), + ) + as _i4.Future); }