diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index e21943e4a63..af5acdd746f 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.8.12 + +* Moves buffer position update event generation to Dart. + ## 2.8.11 * Updates kotlin version to 2.2.0 to enable gradle 8.11 support. diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java index c98f787d318..ffd89e6137e 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java @@ -324,6 +324,100 @@ ArrayList toList() { } } + /** 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(); @@ -346,6 +440,8 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { return PlatformVideoViewCreationParams.fromList((ArrayList) readValue(buffer)); case (byte) 132: return CreateMessage.fromList((ArrayList) readValue(buffer)); + case (byte) 133: + return PlaybackState.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); } @@ -365,6 +461,9 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { } else if (value instanceof CreateMessage) { stream.write(132); writeValue(stream, ((CreateMessage) value).toList()); + } else if (value instanceof PlaybackState) { + stream.write(133); + writeValue(stream, ((PlaybackState) value).toList()); } else { super.writeValue(stream, value); } @@ -532,21 +631,26 @@ static void setUp( } /** 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(); - - @NonNull - Long getPosition(); - - void seekTo(@NonNull Long position); - + /** 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() { @@ -668,7 +772,7 @@ static void setUp( BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getPosition" + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.pause" + messageChannelSuffix, getCodec()); if (api != null) { @@ -676,8 +780,8 @@ static void setUp( (message, reply) -> { ArrayList wrapped = new ArrayList<>(); try { - Long output = api.getPosition(); - wrapped.add(0, output); + api.pause(); + wrapped.add(0, null); } catch (Throwable exception) { wrapped = wrapError(exception); } @@ -716,7 +820,7 @@ static void setUp( BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, - "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.pause" + "dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getPlaybackState" + messageChannelSuffix, getCodec()); if (api != null) { @@ -724,8 +828,8 @@ static void setUp( (message, reply) -> { ArrayList wrapped = new ArrayList<>(); try { - api.pause(); - wrapped.add(0, null); + PlaybackState output = api.getPlaybackState(); + wrapped.add(0, output); } catch (Throwable exception) { wrapped = wrapError(exception); } 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 2c4876de6e0..27dc9e95609 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 @@ -66,10 +66,6 @@ public void setDisposeHandler(@Nullable DisposeHandler handler) { protected abstract ExoPlayerEventListener createExoPlayerEventListener( @NonNull ExoPlayer exoPlayer, @Nullable SurfaceProducer surfaceProducer); - void sendBufferingUpdate() { - videoPlayerEvents.onBufferingUpdate(exoPlayer.getBufferedPosition()); - } - private static void setAudioAttributes(ExoPlayer exoPlayer, boolean isMixMode) { exoPlayer.setAudioAttributes( new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MOVIE).build(), @@ -107,12 +103,11 @@ public void setPlaybackSpeed(@NonNull Double speed) { } @Override - public @NonNull Long getPosition() { - long position = exoPlayer.getCurrentPosition(); - // TODO(stuartmorgan): Move this; this is relying on the fact that getPosition is called - // frequently to drive buffering updates, which is a fragile hack. - sendBufferingUpdate(); - return position; + public @NonNull Messages.PlaybackState getPlaybackState() { + return new Messages.PlaybackState.Builder() + .setPlayPosition(exoPlayer.getCurrentPosition()) + .setBufferPosition(exoPlayer.getBufferedPosition()) + .build(); } @Override 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 86003fc4a30..f6dd319500b 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 @@ -126,17 +126,6 @@ public void playsAndPausesProvidedMedia() { videoPlayer.dispose(); } - @Test - public void sendsBufferingUpdatesOnDemand() { - VideoPlayer videoPlayer = createVideoPlayer(); - - when(mockExoPlayer.getBufferedPosition()).thenReturn(10L); - videoPlayer.sendBufferingUpdate(); - verify(mockEvents).onBufferingUpdate(10L); - - videoPlayer.dispose(); - } - @Test public void togglesLoopingEnablesAndDisablesRepeatMode() { VideoPlayer videoPlayer = createVideoPlayer(); @@ -177,14 +166,29 @@ public void setPlaybackSpeedSetsPlaybackParametersWithValue() { } @Test - public void seekAndGetPosition() { + public void seekTo() { VideoPlayer videoPlayer = createVideoPlayer(); videoPlayer.seekTo(10L); verify(mockExoPlayer).seekTo(10); - when(mockExoPlayer.getCurrentPosition()).thenReturn(20L); - assertEquals(20L, videoPlayer.getPosition().longValue()); + videoPlayer.dispose(); + } + + @Test + public void getPlaybackState() { + VideoPlayer videoPlayer = createVideoPlayer(); + + final long playbackPosition = 20L; + final long bufferedPosition = 10L; + when(mockExoPlayer.getCurrentPosition()).thenReturn(playbackPosition); + when(mockExoPlayer.getBufferedPosition()).thenReturn(bufferedPosition); + + final Messages.PlaybackState state = videoPlayer.getPlaybackState(); + assertEquals(playbackPosition, state.getPlayPosition().longValue()); + assertEquals(bufferedPosition, state.getBufferPosition().longValue()); + + videoPlayer.dispose(); } @Test diff --git a/packages/video_player/video_player_android/lib/src/android_video_player.dart b/packages/video_player/video_player_android/lib/src/android_video_player.dart index fb07e03f2a6..a3f147c31de 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 @@ -11,6 +11,12 @@ import 'package:video_player_platform_interface/video_player_platform_interface. import 'messages.g.dart'; 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()); @@ -32,13 +38,7 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { //overridden for testing. final VideoPlayerInstanceApi Function(int playerId) _playerProvider; - /// A map that associates player ID with a view state. - /// This is used to determine which view type to use when building a view. - final Map _playerViewStates = - {}; - - final Map _players = - {}; + final Map _players = {}; /// Registers this class as the default instance of [PathProviderPlatform]. static void registerWith() { @@ -52,9 +52,9 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { @override Future dispose(int playerId) async { + final _PlayerInstance? player = _players.remove(playerId); await _api.dispose(playerId); - _playerViewStates.remove(playerId); - _players.remove(playerId); + await player?.dispose(); } @override @@ -109,14 +109,7 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { ); final int playerId = await _api.create(message); - _playerViewStates[playerId] = switch (options.viewType) { - // playerId is also the textureId when using texture view. - VideoViewType.textureView => _VideoPlayerTextureViewState( - textureId: playerId, - ), - VideoViewType.platformView => const _VideoPlayerPlatformViewState(), - }; - ensureApiInitialized(playerId); + ensureApiInitialized(playerId, options.viewType); return playerId; } @@ -134,12 +127,24 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { return httpHeaders[userAgentKey] ?? defaultUserAgent; } - /// Returns the API instance for [playerId], creating it if it doesn't already - /// exist. + /// Returns the player instance for [playerId], creating it if it doesn't + /// already exist. @visibleForTesting - VideoPlayerInstanceApi ensureApiInitialized(int playerId) { - return _players.putIfAbsent(playerId, () { - return _playerProvider(playerId); + void ensureApiInitialized(int playerId, VideoViewType viewType) { + _players.putIfAbsent(playerId, () { + final _VideoPlayerViewState viewState = switch (viewType) { + // playerId is also the textureId when using texture view. + VideoViewType.textureView => _VideoPlayerTextureViewState( + textureId: playerId, + ), + VideoViewType.platformView => const _VideoPlayerPlatformViewState(), + }; + final String eventChannelName = '$_videoEventChannelNameBase$playerId'; + return _PlayerInstance( + _playerProvider(playerId), + viewState, + eventChannelName: eventChannelName, + ); }); } @@ -172,52 +177,17 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { @override Future seekTo(int playerId, Duration position) { - return _playerWith(id: playerId).seekTo(position.inMilliseconds); + return _playerWith(id: playerId).seekTo(position); } @override Future getPosition(int playerId) async { - final int position = await _playerWith(id: playerId).getPosition(); - return Duration(milliseconds: position); + return _playerWith(id: playerId).getPosition(); } @override Stream videoEventsFor(int playerId) { - return _eventChannelFor(playerId).receiveBroadcastStream().map(( - dynamic event, - ) { - final Map map = event as Map; - return 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: [ - DurationRange( - Duration.zero, - Duration(milliseconds: 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), - }; - }); + return _playerWith(id: playerId).videoEvents(); } @override @@ -228,17 +198,13 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { @override Widget buildViewWithOptions(VideoViewOptions options) { final int playerId = options.playerId; - final _VideoPlayerViewState? viewState = _playerViewStates[playerId]; + final _VideoPlayerViewState viewState = _playerWith(id: playerId).viewState; return switch (viewState) { _VideoPlayerTextureViewState(:final int textureId) => Texture( textureId: textureId, ), _VideoPlayerPlatformViewState() => PlatformViewPlayer(playerId: playerId), - null => - throw Exception( - 'Could not find corresponding view type for playerId: $playerId', - ), }; } @@ -247,12 +213,8 @@ class AndroidVideoPlayer extends VideoPlayerPlatform { return _api.setMixWithOthers(mixWithOthers); } - EventChannel _eventChannelFor(int playerId) { - return EventChannel('flutter.io/videoPlayer/videoEvents$playerId'); - } - - VideoPlayerInstanceApi _playerWith({required int id}) { - final VideoPlayerInstanceApi? player = _players[id]; + _PlayerInstance _playerWith({required int id}) { + final _PlayerInstance? player = _players[id]; return player ?? (throw StateError('No active player with ID $id.')); } @@ -280,6 +242,130 @@ PlatformVideoViewType _platformVideoViewTypeFromVideoViewType( }; } +/// An instance of a video player, corresponding to a single player ID in +/// [AndroidVideoPlayer]. +class _PlayerInstance { + /// Creates a new instance of [_PlayerInstance] corresponding to the given + /// API instance. + _PlayerInstance( + this._api, + this.viewState, { + required String eventChannelName, + }) { + _eventChannel = EventChannel(eventChannelName); + _eventSubscription = _eventChannel.receiveBroadcastStream().listen( + _onStreamEvent, + onError: (Object e) { + _eventStreamController.addError(e); + }, + ); + } + + final VideoPlayerInstanceApi _api; + late final EventChannel _eventChannel; + final StreamController _eventStreamController = + StreamController(); + late final StreamSubscription _eventSubscription; + int _lastBufferPosition = -1; + + final _VideoPlayerViewState viewState; + + Future setLooping(bool looping) { + return _api.setLooping(looping); + } + + Future play() { + return _api.play(); + } + + Future pause() { + return _api.pause(); + } + + Future setVolume(double volume) { + return _api.setVolume(volume); + } + + Future setPlaybackSpeed(double speed) { + return _api.setPlaybackSpeed(speed); + } + + Future seekTo(Duration position) { + return _api.seekTo(position.inMilliseconds); + } + + 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); + } + + Stream videoEvents() { + return _eventStreamController.stream; + } + + Future dispose() async { + await _eventSubscription.cancel(); + } + + /// Sends a buffering update if the buffer position has changed since the + /// last check. + void _updateBufferingState(int bufferPosition) { + if (bufferPosition != _lastBufferPosition) { + _lastBufferPosition = bufferPosition; + _eventStreamController.add( + VideoEvent( + eventType: VideoEventType.bufferingUpdate, + buffered: _bufferRangeForPosition(bufferPosition), + ), + ); + } + } + + 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), + }); + } + + // Turns a single buffer position, which is what ExoPlayer reports, into the + // DurationRange array expected by [VideoEventType.bufferingUpdate]. + List _bufferRangeForPosition(int milliseconds) { + return [ + DurationRange(Duration.zero, Duration(milliseconds: milliseconds)), + ]; + } +} + /// Base class representing the state of a video player view. @immutable sealed class _VideoPlayerViewState { 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 e5921e61175..e576b0336a4 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 @@ -135,6 +135,48 @@ class CreateMessage { 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 @@ -154,6 +196,9 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is CreateMessage) { buffer.putUint8(132); writeValue(buffer, value.encode()); + } else if (value is PlaybackState) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -172,6 +217,8 @@ class _PigeonCodec extends StandardMessageCodec { return PlatformVideoViewCreationParams.decode(readValue(buffer)!); case 132: return CreateMessage.decode(readValue(buffer)!); + case 133: + return PlaybackState.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -354,6 +401,7 @@ class VideoPlayerInstanceApi { final String pigeonVar_messageChannelSuffix; + /// Sets whether to automatically loop playback of the video. Future setLooping(bool looping) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; @@ -381,6 +429,7 @@ class VideoPlayerInstanceApi { } } + /// Sets the volume, with 0.0 being muted and 1.0 being full volume. Future setVolume(double volume) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; @@ -408,6 +457,7 @@ class VideoPlayerInstanceApi { } } + /// Sets the playback speed as a multiple of normal speed. Future setPlaybackSpeed(double speed) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; @@ -435,6 +485,7 @@ class VideoPlayerInstanceApi { } } + /// Begins playback if the video is not currently playing. Future play() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; @@ -460,9 +511,10 @@ class VideoPlayerInstanceApi { } } - Future getPosition() async { + /// Pauses playback if the video is currently playing. + Future pause() async { final String pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getPosition$pigeonVar_messageChannelSuffix'; + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, @@ -480,16 +532,12 @@ class VideoPlayerInstanceApi { 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?)!; + return; } } + /// Seeks to the given playback position, in milliseconds. Future seekTo(int position) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; @@ -517,9 +565,13 @@ class VideoPlayerInstanceApi { } } - Future pause() async { + /// 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 { final String pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; + 'dev.flutter.pigeon.video_player_android.VideoPlayerInstanceApi.getPlaybackState$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, @@ -537,8 +589,13 @@ class VideoPlayerInstanceApi { 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; + return (pigeonVar_replyList[0] as PlaybackState?)!; } } } diff --git a/packages/video_player/video_player_android/pigeons/messages.dart b/packages/video_player/video_player_android/pigeons/messages.dart index fc1f601bf29..b2246ec6d33 100644 --- a/packages/video_player/video_player_android/pigeons/messages.dart +++ b/packages/video_player/video_player_android/pigeons/messages.dart @@ -35,6 +35,16 @@ class CreateMessage { PlatformVideoViewType? viewType; } +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(); @@ -46,11 +56,27 @@ abstract class AndroidVideoPlayerApi { @HostApi() abstract class VideoPlayerInstanceApi { + /// Sets whether to automatically loop playback of the video. void setLooping(bool looping); + + /// Sets the volume, with 0.0 being muted and 1.0 being full volume. void setVolume(double volume); + + /// Sets the playback speed as a multiple of normal speed. void setPlaybackSpeed(double speed); + + /// Begins playback if the video is not currently playing. void play(); - int getPosition(); - void seekTo(int position); + + /// Pauses playback if the video is currently playing. void pause(); + + /// 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(); } diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index a9e239057b8..356b9af7848 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.11 +version: 2.8.12 environment: sdk: ^3.7.0 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 c544da8227c..ac32c50496d 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 @@ -29,7 +29,7 @@ void main() { pluginApi: pluginApi, playerProvider: (_) => instanceApi, ); - player.ensureApiInitialized(playerId); + player.ensureApiInitialized(playerId, VideoViewType.platformView); return (player, pluginApi, instanceApi); } @@ -529,28 +529,27 @@ void main() { verify(playerApi.seekTo(positionMilliseconds)); }); - test('getPosition', () async { + test('getPlaybackState', () async { final ( AndroidVideoPlayer player, _, MockVideoPlayerInstanceApi playerApi, ) = setUpMockPlayer(playerId: 1); const int positionMilliseconds = 12345; - when( - playerApi.getPosition(), - ).thenAnswer((_) async => positionMilliseconds); + when(playerApi.getPlaybackState()).thenAnswer( + (_) async => PlaybackState( + playPosition: positionMilliseconds, + bufferPosition: 0, + ), + ); final Duration position = await player.getPosition(1); expect(position, const Duration(milliseconds: positionMilliseconds)); }); test('videoEventsFor', () async { - final ( - AndroidVideoPlayer player, - MockAndroidVideoPlayerApi api, - _, - ) = setUpMockPlayer(playerId: 1); - const String mockChannel = 'flutter.io/videoPlayer/videoEvents123'; + 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() @@ -669,8 +668,17 @@ void main() { 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); + expect( - player.videoEventsFor(123), + player.videoEventsFor(playerId), emitsInOrder([ VideoEvent( eventType: VideoEventType.initialized, 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 68be21719be..4d7f49d18de 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 @@ -23,6 +23,11 @@ import 'package:video_player_android/src/messages.g.dart' as _i2; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +class _FakePlaybackState_0 extends _i1.SmartFake implements _i2.PlaybackState { + _FakePlaybackState_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + /// A class which mocks [AndroidVideoPlayerApi]. /// /// See the documentation for Mockito's code generation for more information. @@ -155,15 +160,6 @@ class MockVideoPlayerInstanceApi extends _i1.Mock ) as _i4.Future); - @override - _i4.Future getPosition() => - (super.noSuchMethod( - Invocation.method(#getPosition, []), - returnValue: _i4.Future.value(0), - returnValueForMissingStub: _i4.Future.value(0), - ) - as _i4.Future); - @override _i4.Future seekTo(int? position) => (super.noSuchMethod( @@ -181,4 +177,23 @@ class MockVideoPlayerInstanceApi extends _i1.Mock returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); + + @override + _i4.Future<_i2.PlaybackState> getPlaybackState() => + (super.noSuchMethod( + Invocation.method(#getPlaybackState, []), + returnValue: _i4.Future<_i2.PlaybackState>.value( + _FakePlaybackState_0( + this, + Invocation.method(#getPlaybackState, []), + ), + ), + returnValueForMissingStub: _i4.Future<_i2.PlaybackState>.value( + _FakePlaybackState_0( + this, + Invocation.method(#getPlaybackState, []), + ), + ), + ) + as _i4.Future<_i2.PlaybackState>); }