diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 4e2df4ebaef3..f7fad2648b3e 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.11.0 + +* Added option to set the video playback speed on the video controller. +* **Minor breaking change**: fixed `VideoPlayerValue.toString` to insert a comma after `isBuffering`. + +## 0.10.12+5 + +* Depend on `video_player_platform_interface` version that contains the new `TestHostVideoPlayerApi` + in order for tests to pass using the latest dependency. + ## 0.10.12+4 * Keep handling deprecated Android v1 classes for backward compatibility. diff --git a/packages/video_player/video_player/README.md b/packages/video_player/video_player/README.md index ef522a455c21..9c0e6b1abb28 100644 --- a/packages/video_player/video_player/README.md +++ b/packages/video_player/video_player/README.md @@ -127,3 +127,22 @@ class _VideoAppState extends State { } } ``` + +## Usage + +The following section contains usage information that goes beyond what is included in the +documentation in order to give a more elaborate overview of the API. + +This is not complete as of now. You can contribute to this section by [opening a pull request](https://github.com/flutter/plugins/pulls). + +### Playback speed + +You can set the playback speed on your `_controller` (instance of `VideoPlayerController`) by +calling `_controller.setPlaybackSpeed`. `setPlaybackSpeed` takes a `double` speed value indicating +the rate of playback for your video. +For example, when given a value of `2.0`, your video will play at 2x the regular playback speed +and so on. + +To learn about playback speed limitations, see the [`setPlaybackSpeed` method documentation](https://pub.dev/documentation/video_player/latest/video_player/VideoPlayerController/setPlaybackSpeed.html). + +Furthermore, see the example app for an example playback speed implementation. diff --git a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java index 003ca18f23cb..78da7150edf0 100644 --- a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java +++ b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v0.1.0-experimental.11), do not edit directly. +// Autogenerated from Pigeon (v0.1.7), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.videoplayer; @@ -9,6 +9,7 @@ import java.util.HashMap; /** Generated class from Pigeon. */ +@SuppressWarnings("unused") public class Messages { /** Generated class from Pigeon that represents data sent in messages. */ @@ -24,17 +25,18 @@ public void setTextureId(Long setterArg) { } HashMap toMap() { - HashMap toMapResult = new HashMap(); + HashMap toMapResult = new HashMap<>(); toMapResult.put("textureId", textureId); return toMapResult; } static TextureMessage fromMap(HashMap map) { TextureMessage fromMapResult = new TextureMessage(); + Object textureId = map.get("textureId"); fromMapResult.textureId = - (map.get("textureId") instanceof Integer) - ? (Integer) map.get("textureId") - : (Long) map.get("textureId"); + (textureId == null) + ? null + : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId); return fromMapResult; } } @@ -82,7 +84,7 @@ public void setFormatHint(String setterArg) { } HashMap toMap() { - HashMap toMapResult = new HashMap(); + HashMap toMapResult = new HashMap<>(); toMapResult.put("asset", asset); toMapResult.put("uri", uri); toMapResult.put("packageName", packageName); @@ -92,10 +94,14 @@ HashMap toMap() { static CreateMessage fromMap(HashMap map) { CreateMessage fromMapResult = new CreateMessage(); - fromMapResult.asset = (String) map.get("asset"); - fromMapResult.uri = (String) map.get("uri"); - fromMapResult.packageName = (String) map.get("packageName"); - fromMapResult.formatHint = (String) map.get("formatHint"); + Object asset = map.get("asset"); + fromMapResult.asset = (String) asset; + Object uri = map.get("uri"); + fromMapResult.uri = (String) uri; + Object packageName = map.get("packageName"); + fromMapResult.packageName = (String) packageName; + Object formatHint = map.get("formatHint"); + fromMapResult.formatHint = (String) formatHint; return fromMapResult; } } @@ -123,7 +129,7 @@ public void setIsLooping(Boolean setterArg) { } HashMap toMap() { - HashMap toMapResult = new HashMap(); + HashMap toMapResult = new HashMap<>(); toMapResult.put("textureId", textureId); toMapResult.put("isLooping", isLooping); return toMapResult; @@ -131,11 +137,13 @@ HashMap toMap() { static LoopingMessage fromMap(HashMap map) { LoopingMessage fromMapResult = new LoopingMessage(); + Object textureId = map.get("textureId"); fromMapResult.textureId = - (map.get("textureId") instanceof Integer) - ? (Integer) map.get("textureId") - : (Long) map.get("textureId"); - fromMapResult.isLooping = (Boolean) map.get("isLooping"); + (textureId == null) + ? null + : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId); + Object isLooping = map.get("isLooping"); + fromMapResult.isLooping = (Boolean) isLooping; return fromMapResult; } } @@ -163,7 +171,7 @@ public void setVolume(Double setterArg) { } HashMap toMap() { - HashMap toMapResult = new HashMap(); + HashMap toMapResult = new HashMap<>(); toMapResult.put("textureId", textureId); toMapResult.put("volume", volume); return toMapResult; @@ -171,11 +179,55 @@ HashMap toMap() { static VolumeMessage fromMap(HashMap map) { VolumeMessage fromMapResult = new VolumeMessage(); + Object textureId = map.get("textureId"); fromMapResult.textureId = - (map.get("textureId") instanceof Integer) - ? (Integer) map.get("textureId") - : (Long) map.get("textureId"); - fromMapResult.volume = (Double) map.get("volume"); + (textureId == null) + ? null + : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId); + Object volume = map.get("volume"); + fromMapResult.volume = (Double) volume; + return fromMapResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class PlaybackSpeedMessage { + private Long textureId; + + public Long getTextureId() { + return textureId; + } + + public void setTextureId(Long setterArg) { + this.textureId = setterArg; + } + + private Double speed; + + public Double getSpeed() { + return speed; + } + + public void setSpeed(Double setterArg) { + this.speed = setterArg; + } + + HashMap toMap() { + HashMap toMapResult = new HashMap<>(); + toMapResult.put("textureId", textureId); + toMapResult.put("speed", speed); + return toMapResult; + } + + static PlaybackSpeedMessage fromMap(HashMap map) { + PlaybackSpeedMessage fromMapResult = new PlaybackSpeedMessage(); + Object textureId = map.get("textureId"); + fromMapResult.textureId = + (textureId == null) + ? null + : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId); + Object speed = map.get("speed"); + fromMapResult.speed = (Double) speed; return fromMapResult; } } @@ -203,7 +255,7 @@ public void setPosition(Long setterArg) { } HashMap toMap() { - HashMap toMapResult = new HashMap(); + HashMap toMapResult = new HashMap<>(); toMapResult.put("textureId", textureId); toMapResult.put("position", position); return toMapResult; @@ -211,14 +263,16 @@ HashMap toMap() { static PositionMessage fromMap(HashMap map) { PositionMessage fromMapResult = new PositionMessage(); + Object textureId = map.get("textureId"); fromMapResult.textureId = - (map.get("textureId") instanceof Integer) - ? (Integer) map.get("textureId") - : (Long) map.get("textureId"); + (textureId == null) + ? null + : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId); + Object position = map.get("position"); fromMapResult.position = - (map.get("position") instanceof Integer) - ? (Integer) map.get("position") - : (Long) map.get("position"); + (position == null) + ? null + : ((position instanceof Integer) ? (Integer) position : (Long) position); return fromMapResult; } } @@ -236,14 +290,15 @@ public void setMixWithOthers(Boolean setterArg) { } HashMap toMap() { - HashMap toMapResult = new HashMap(); + HashMap toMapResult = new HashMap<>(); toMapResult.put("mixWithOthers", mixWithOthers); return toMapResult; } static MixWithOthersMessage fromMap(HashMap map) { MixWithOthersMessage fromMapResult = new MixWithOthersMessage(); - fromMapResult.mixWithOthers = (Boolean) map.get("mixWithOthers"); + Object mixWithOthers = map.get("mixWithOthers"); + fromMapResult.mixWithOthers = (Boolean) mixWithOthers; return fromMapResult; } } @@ -260,6 +315,8 @@ public interface VideoPlayerApi { void setVolume(VolumeMessage arg); + void setPlaybackSpeed(PlaybackSpeedMessage arg); + void play(TextureMessage arg); PositionMessage position(TextureMessage arg); @@ -271,26 +328,24 @@ public interface VideoPlayerApi { void setMixWithOthers(MixWithOthersMessage arg); /** Sets up an instance of `VideoPlayerApi` to handle messages through the `binaryMessenger` */ - public static void setup(BinaryMessenger binaryMessenger, VideoPlayerApi api) { + static void setup(BinaryMessenger binaryMessenger, VideoPlayerApi api) { { BasicMessageChannel channel = - new BasicMessageChannel( + new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.initialize", new StandardMessageCodec()); if (api != null) { channel.setMessageHandler( - new BasicMessageChannel.MessageHandler() { - public void onMessage(Object message, BasicMessageChannel.Reply reply) { - HashMap wrapped = new HashMap(); - try { - api.initialize(); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); + (message, reply) -> { + HashMap wrapped = new HashMap<>(); + try { + api.initialize(); + wrapped.put("result", null); + } catch (Exception exception) { + wrapped.put("error", wrapError(exception)); } + reply.reply(wrapped); }); } else { channel.setMessageHandler(null); @@ -298,24 +353,23 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { } { BasicMessageChannel channel = - new BasicMessageChannel( + new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.create", new StandardMessageCodec()); if (api != null) { channel.setMessageHandler( - new BasicMessageChannel.MessageHandler() { - public void onMessage(Object message, BasicMessageChannel.Reply reply) { + (message, reply) -> { + HashMap wrapped = new HashMap<>(); + try { + @SuppressWarnings("ConstantConditions") CreateMessage input = CreateMessage.fromMap((HashMap) message); - HashMap wrapped = new HashMap(); - try { - TextureMessage output = api.create(input); - wrapped.put("result", output.toMap()); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); + TextureMessage output = api.create(input); + wrapped.put("result", output.toMap()); + } catch (Exception exception) { + wrapped.put("error", wrapError(exception)); } + reply.reply(wrapped); }); } else { channel.setMessageHandler(null); @@ -323,24 +377,23 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { } { BasicMessageChannel channel = - new BasicMessageChannel( + new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.dispose", new StandardMessageCodec()); if (api != null) { channel.setMessageHandler( - new BasicMessageChannel.MessageHandler() { - public void onMessage(Object message, BasicMessageChannel.Reply reply) { + (message, reply) -> { + HashMap wrapped = new HashMap<>(); + try { + @SuppressWarnings("ConstantConditions") TextureMessage input = TextureMessage.fromMap((HashMap) message); - HashMap wrapped = new HashMap(); - try { - api.dispose(input); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); + api.dispose(input); + wrapped.put("result", null); + } catch (Exception exception) { + wrapped.put("error", wrapError(exception)); } + reply.reply(wrapped); }); } else { channel.setMessageHandler(null); @@ -348,24 +401,23 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { } { BasicMessageChannel channel = - new BasicMessageChannel( + new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.setLooping", new StandardMessageCodec()); if (api != null) { channel.setMessageHandler( - new BasicMessageChannel.MessageHandler() { - public void onMessage(Object message, BasicMessageChannel.Reply reply) { + (message, reply) -> { + HashMap wrapped = new HashMap<>(); + try { + @SuppressWarnings("ConstantConditions") LoopingMessage input = LoopingMessage.fromMap((HashMap) message); - HashMap wrapped = new HashMap(); - try { - api.setLooping(input); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); + api.setLooping(input); + wrapped.put("result", null); + } catch (Exception exception) { + wrapped.put("error", wrapError(exception)); } + reply.reply(wrapped); }); } else { channel.setMessageHandler(null); @@ -373,24 +425,47 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { } { BasicMessageChannel channel = - new BasicMessageChannel( + new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.setVolume", new StandardMessageCodec()); if (api != null) { channel.setMessageHandler( - new BasicMessageChannel.MessageHandler() { - public void onMessage(Object message, BasicMessageChannel.Reply reply) { + (message, reply) -> { + HashMap wrapped = new HashMap<>(); + try { + @SuppressWarnings("ConstantConditions") VolumeMessage input = VolumeMessage.fromMap((HashMap) message); - HashMap wrapped = new HashMap(); - try { - api.setVolume(input); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); + api.setVolume(input); + wrapped.put("result", null); + } catch (Exception exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed", + new StandardMessageCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + HashMap wrapped = new HashMap<>(); + try { + @SuppressWarnings("ConstantConditions") + PlaybackSpeedMessage input = PlaybackSpeedMessage.fromMap((HashMap) message); + api.setPlaybackSpeed(input); + wrapped.put("result", null); + } catch (Exception exception) { + wrapped.put("error", wrapError(exception)); } + reply.reply(wrapped); }); } else { channel.setMessageHandler(null); @@ -398,24 +473,23 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { } { BasicMessageChannel channel = - new BasicMessageChannel( + new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.play", new StandardMessageCodec()); if (api != null) { channel.setMessageHandler( - new BasicMessageChannel.MessageHandler() { - public void onMessage(Object message, BasicMessageChannel.Reply reply) { + (message, reply) -> { + HashMap wrapped = new HashMap<>(); + try { + @SuppressWarnings("ConstantConditions") TextureMessage input = TextureMessage.fromMap((HashMap) message); - HashMap wrapped = new HashMap(); - try { - api.play(input); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); + api.play(input); + wrapped.put("result", null); + } catch (Exception exception) { + wrapped.put("error", wrapError(exception)); } + reply.reply(wrapped); }); } else { channel.setMessageHandler(null); @@ -423,24 +497,23 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { } { BasicMessageChannel channel = - new BasicMessageChannel( + new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.position", new StandardMessageCodec()); if (api != null) { channel.setMessageHandler( - new BasicMessageChannel.MessageHandler() { - public void onMessage(Object message, BasicMessageChannel.Reply reply) { + (message, reply) -> { + HashMap wrapped = new HashMap<>(); + try { + @SuppressWarnings("ConstantConditions") TextureMessage input = TextureMessage.fromMap((HashMap) message); - HashMap wrapped = new HashMap(); - try { - PositionMessage output = api.position(input); - wrapped.put("result", output.toMap()); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); + PositionMessage output = api.position(input); + wrapped.put("result", output.toMap()); + } catch (Exception exception) { + wrapped.put("error", wrapError(exception)); } + reply.reply(wrapped); }); } else { channel.setMessageHandler(null); @@ -448,24 +521,23 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { } { BasicMessageChannel channel = - new BasicMessageChannel( + new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.seekTo", new StandardMessageCodec()); if (api != null) { channel.setMessageHandler( - new BasicMessageChannel.MessageHandler() { - public void onMessage(Object message, BasicMessageChannel.Reply reply) { + (message, reply) -> { + HashMap wrapped = new HashMap<>(); + try { + @SuppressWarnings("ConstantConditions") PositionMessage input = PositionMessage.fromMap((HashMap) message); - HashMap wrapped = new HashMap(); - try { - api.seekTo(input); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); + api.seekTo(input); + wrapped.put("result", null); + } catch (Exception exception) { + wrapped.put("error", wrapError(exception)); } + reply.reply(wrapped); }); } else { channel.setMessageHandler(null); @@ -473,24 +545,23 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { } { BasicMessageChannel channel = - new BasicMessageChannel( + new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.pause", new StandardMessageCodec()); if (api != null) { channel.setMessageHandler( - new BasicMessageChannel.MessageHandler() { - public void onMessage(Object message, BasicMessageChannel.Reply reply) { + (message, reply) -> { + HashMap wrapped = new HashMap<>(); + try { + @SuppressWarnings("ConstantConditions") TextureMessage input = TextureMessage.fromMap((HashMap) message); - HashMap wrapped = new HashMap(); - try { - api.pause(input); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); + api.pause(input); + wrapped.put("result", null); + } catch (Exception exception) { + wrapped.put("error", wrapError(exception)); } + reply.reply(wrapped); }); } else { channel.setMessageHandler(null); @@ -498,24 +569,23 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { } { BasicMessageChannel channel = - new BasicMessageChannel( + new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers", new StandardMessageCodec()); if (api != null) { channel.setMessageHandler( - new BasicMessageChannel.MessageHandler() { - public void onMessage(Object message, BasicMessageChannel.Reply reply) { + (message, reply) -> { + HashMap wrapped = new HashMap<>(); + try { + @SuppressWarnings("ConstantConditions") MixWithOthersMessage input = MixWithOthersMessage.fromMap((HashMap) message); - HashMap wrapped = new HashMap(); - try { - api.setMixWithOthers(input); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); + api.setMixWithOthers(input); + wrapped.put("result", null); + } catch (Exception exception) { + wrapped.put("error", wrapError(exception)); } + reply.reply(wrapped); }); } else { channel.setMessageHandler(null); @@ -525,7 +595,7 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { } private static HashMap wrapError(Exception exception) { - HashMap errorMap = new HashMap(); + HashMap errorMap = new HashMap<>(); errorMap.put("message", exception.toString()); errorMap.put("code", null); errorMap.put("details", null); diff --git a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index 801c2ca3bff2..8f8c898dea27 100644 --- a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -11,6 +11,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.EventListener; import com.google.android.exoplayer2.SimpleExoPlayer; @@ -233,6 +234,14 @@ void setVolume(double value) { exoPlayer.setVolume(bracketedValue); } + void setPlaybackSpeed(double value) { + // 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(((float) value)); + + exoPlayer.setPlaybackParameters(playbackParameters); + } + void seekTo(int location) { exoPlayer.seekTo(location); } diff --git a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java index 329370a47477..7f863051f616 100644 --- a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java +++ b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java @@ -14,6 +14,7 @@ import io.flutter.plugins.videoplayer.Messages.CreateMessage; import io.flutter.plugins.videoplayer.Messages.LoopingMessage; import io.flutter.plugins.videoplayer.Messages.MixWithOthersMessage; +import io.flutter.plugins.videoplayer.Messages.PlaybackSpeedMessage; import io.flutter.plugins.videoplayer.Messages.PositionMessage; import io.flutter.plugins.videoplayer.Messages.TextureMessage; import io.flutter.plugins.videoplayer.Messages.VideoPlayerApi; @@ -154,6 +155,11 @@ public void setVolume(VolumeMessage arg) { player.setVolume(arg.getVolume()); } + public void setPlaybackSpeed(PlaybackSpeedMessage arg) { + VideoPlayer player = videoPlayers.get(arg.getTextureId()); + player.setPlaybackSpeed(arg.getSpeed()); + } + public void play(TextureMessage arg) { VideoPlayer player = videoPlayers.get(arg.getTextureId()); player.play(); diff --git a/packages/video_player/video_player/example/lib/main.dart b/packages/video_player/video_player/example/lib/main.dart index ee2fcbdc9632..a99b9da6bd0c 100644 --- a/packages/video_player/video_player/example/lib/main.dart +++ b/packages/video_player/video_player/example/lib/main.dart @@ -188,7 +188,7 @@ class _ButterFlyAssetVideoState extends State<_ButterFlyAssetVideo> { alignment: Alignment.bottomCenter, children: [ VideoPlayer(_controller), - _PlayPauseOverlay(controller: _controller), + _ControlsOverlay(controller: _controller), VideoProgressIndicator(_controller, allowScrubbing: true), ], ), @@ -252,7 +252,7 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { children: [ VideoPlayer(_controller), ClosedCaption(text: _controller.value.caption.text), - _PlayPauseOverlay(controller: _controller), + _ControlsOverlay(controller: _controller), VideoProgressIndicator(_controller, allowScrubbing: true), ], ), @@ -264,8 +264,19 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { } } -class _PlayPauseOverlay extends StatelessWidget { - const _PlayPauseOverlay({Key key, this.controller}) : super(key: key); +class _ControlsOverlay extends StatelessWidget { + const _ControlsOverlay({Key key, this.controller}) : super(key: key); + + static const _examplePlaybackRates = [ + 0.25, + 0.5, + 1.0, + 1.5, + 2.0, + 3.0, + 5.0, + 10.0, + ]; final VideoPlayerController controller; @@ -294,6 +305,35 @@ class _PlayPauseOverlay extends StatelessWidget { controller.value.isPlaying ? controller.pause() : controller.play(); }, ), + Align( + alignment: Alignment.topRight, + child: PopupMenuButton( + initialValue: controller.value.playbackSpeed, + tooltip: 'Playback speed', + onSelected: (speed) { + controller.setPlaybackSpeed(speed); + }, + itemBuilder: (context) { + return [ + for (final speed in _examplePlaybackRates) + PopupMenuItem( + value: speed, + child: Text('${speed}x'), + ) + ]; + }, + child: Padding( + padding: const EdgeInsets.symmetric( + // Using less vertical padding as the text is also longer + // horizontally, so it feels like it would need more spacing + // horizontally (matching the aspect ratio of the video). + vertical: 12, + horizontal: 16, + ), + child: Text('${controller.value.playbackSpeed}x'), + ), + ), + ), ], ); } diff --git a/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m b/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m index a834fe32b87b..e6a4f6ccb0b7 100644 --- a/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m +++ b/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m @@ -359,6 +359,30 @@ - (void)setVolume:(double)volume { _player.volume = (float)((volume < 0.0) ? 0.0 : ((volume > 1.0) ? 1.0 : volume)); } +- (void)setPlaybackSpeed:(double)speed { + // See https://developer.apple.com/library/archive/qa/qa1772/_index.html for an explanation of + // these checks. + if (speed > 2.0 && !_player.currentItem.canPlayFastForward) { + if (_eventSink != nil) { + _eventSink([FlutterError errorWithCode:@"VideoError" + message:@"Video cannot be fast-forwarded beyond 2.0x" + details:nil]); + } + return; + } + + if (speed < 1.0 && !_player.currentItem.canPlaySlowForward) { + if (_eventSink != nil) { + _eventSink([FlutterError errorWithCode:@"VideoError" + message:@"Video cannot be slow-forwarded" + details:nil]); + } + return; + } + + _player.rate = speed; +} + - (CVPixelBufferRef)copyPixelBuffer { CMTime outputItemTime = [_videoOutput itemTimeForHostTime:CACurrentMediaTime()]; if ([_videoOutput hasNewPixelBufferForItemTime:outputItemTime]) { @@ -538,6 +562,11 @@ - (void)setVolume:(FLTVolumeMessage*)input error:(FlutterError**)error { [player setVolume:[input.volume doubleValue]]; } +- (void)setPlaybackSpeed:(FLTPlaybackSpeedMessage*)input error:(FlutterError**)error { + FLTVideoPlayer* player = _players[input.textureId]; + [player setPlaybackSpeed:[input.speed doubleValue]]; +} + - (void)play:(FLTTextureMessage*)input error:(FlutterError**)error { FLTVideoPlayer* player = _players[input.textureId]; [player play]; diff --git a/packages/video_player/video_player/ios/Classes/messages.h b/packages/video_player/video_player/ios/Classes/messages.h index 27025ef27413..3c68b3dd24d4 100644 --- a/packages/video_player/video_player/ios/Classes/messages.h +++ b/packages/video_player/video_player/ios/Classes/messages.h @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v0.1.0-experimental.11), do not edit directly. +// Autogenerated from Pigeon (v0.1.7), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @protocol FlutterBinaryMessenger; @@ -11,6 +11,7 @@ NS_ASSUME_NONNULL_BEGIN @class FLTCreateMessage; @class FLTLoopingMessage; @class FLTVolumeMessage; +@class FLTPlaybackSpeedMessage; @class FLTPositionMessage; @class FLTMixWithOthersMessage; @@ -35,6 +36,11 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, strong, nullable) NSNumber *volume; @end +@interface FLTPlaybackSpeedMessage : NSObject +@property(nonatomic, strong, nullable) NSNumber *textureId; +@property(nonatomic, strong, nullable) NSNumber *speed; +@end + @interface FLTPositionMessage : NSObject @property(nonatomic, strong, nullable) NSNumber *textureId; @property(nonatomic, strong, nullable) NSNumber *position; @@ -51,6 +57,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)dispose:(FLTTextureMessage *)input error:(FlutterError *_Nullable *_Nonnull)error; - (void)setLooping:(FLTLoopingMessage *)input error:(FlutterError *_Nullable *_Nonnull)error; - (void)setVolume:(FLTVolumeMessage *)input error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setPlaybackSpeed:(FLTPlaybackSpeedMessage *)input + error:(FlutterError *_Nullable *_Nonnull)error; - (void)play:(FLTTextureMessage *)input error:(FlutterError *_Nullable *_Nonnull)error; - (nullable FLTPositionMessage *)position:(FLTTextureMessage *)input error:(FlutterError *_Nullable *_Nonnull)error; diff --git a/packages/video_player/video_player/ios/Classes/messages.m b/packages/video_player/video_player/ios/Classes/messages.m index 64fcd75cc6bf..b651a6137c11 100644 --- a/packages/video_player/video_player/ios/Classes/messages.m +++ b/packages/video_player/video_player/ios/Classes/messages.m @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v0.1.0-experimental.11), do not edit directly. +// Autogenerated from Pigeon (v0.1.7), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "messages.h" #import @@ -36,6 +36,10 @@ @interface FLTVolumeMessage () + (FLTVolumeMessage *)fromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end +@interface FLTPlaybackSpeedMessage () ++ (FLTPlaybackSpeedMessage *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end @interface FLTPositionMessage () + (FLTPositionMessage *)fromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @@ -55,9 +59,9 @@ + (FLTTextureMessage *)fromMap:(NSDictionary *)dict { return result; } - (NSDictionary *)toMap { - return [NSDictionary - dictionaryWithObjectsAndKeys:(self.textureId != nil ? self.textureId : [NSNull null]), - @"textureId", nil]; + return + [NSDictionary dictionaryWithObjectsAndKeys:(self.textureId ? self.textureId : [NSNull null]), + @"textureId", nil]; } @end @@ -86,9 +90,9 @@ - (NSDictionary *)toMap { return [NSDictionary dictionaryWithObjectsAndKeys:(self.asset ? self.asset : [NSNull null]), @"asset", (self.uri ? self.uri : [NSNull null]), @"uri", - (self.packageName != nil ? self.packageName : [NSNull null]), + (self.packageName ? self.packageName : [NSNull null]), @"packageName", - (self.formatHint != nil ? self.formatHint : [NSNull null]), + (self.formatHint ? self.formatHint : [NSNull null]), @"formatHint", nil]; } @end @@ -108,10 +112,9 @@ + (FLTLoopingMessage *)fromMap:(NSDictionary *)dict { } - (NSDictionary *)toMap { return [NSDictionary - dictionaryWithObjectsAndKeys:(self.textureId != nil ? self.textureId : [NSNull null]), - @"textureId", - (self.isLooping != nil ? self.isLooping : [NSNull null]), - @"isLooping", nil]; + dictionaryWithObjectsAndKeys:(self.textureId ? self.textureId : [NSNull null]), @"textureId", + (self.isLooping ? self.isLooping : [NSNull null]), @"isLooping", + nil]; } @end @@ -130,9 +133,28 @@ + (FLTVolumeMessage *)fromMap:(NSDictionary *)dict { } - (NSDictionary *)toMap { return [NSDictionary - dictionaryWithObjectsAndKeys:(self.textureId != nil ? self.textureId : [NSNull null]), - @"textureId", (self.volume != nil ? self.volume : [NSNull null]), - @"volume", nil]; + dictionaryWithObjectsAndKeys:(self.textureId ? self.textureId : [NSNull null]), @"textureId", + (self.volume ? self.volume : [NSNull null]), @"volume", nil]; +} +@end + +@implementation FLTPlaybackSpeedMessage ++ (FLTPlaybackSpeedMessage *)fromMap:(NSDictionary *)dict { + FLTPlaybackSpeedMessage *result = [[FLTPlaybackSpeedMessage alloc] init]; + result.textureId = dict[@"textureId"]; + if ((NSNull *)result.textureId == [NSNull null]) { + result.textureId = nil; + } + result.speed = dict[@"speed"]; + if ((NSNull *)result.speed == [NSNull null]) { + result.speed = nil; + } + return result; +} +- (NSDictionary *)toMap { + return [NSDictionary + dictionaryWithObjectsAndKeys:(self.textureId ? self.textureId : [NSNull null]), @"textureId", + (self.speed ? self.speed : [NSNull null]), @"speed", nil]; } @end @@ -151,10 +173,9 @@ + (FLTPositionMessage *)fromMap:(NSDictionary *)dict { } - (NSDictionary *)toMap { return [NSDictionary - dictionaryWithObjectsAndKeys:(self.textureId != nil ? self.textureId : [NSNull null]), - @"textureId", - (self.position != nil ? self.position : [NSNull null]), - @"position", nil]; + dictionaryWithObjectsAndKeys:(self.textureId ? self.textureId : [NSNull null]), @"textureId", + (self.position ? self.position : [NSNull null]), @"position", + nil]; } @end @@ -169,7 +190,7 @@ + (FLTMixWithOthersMessage *)fromMap:(NSDictionary *)dict { } - (NSDictionary *)toMap { return [NSDictionary - dictionaryWithObjectsAndKeys:(self.mixWithOthers != nil ? self.mixWithOthers : [NSNull null]), + dictionaryWithObjectsAndKeys:(self.mixWithOthers ? self.mixWithOthers : [NSNull null]), @"mixWithOthers", nil]; } @end @@ -249,6 +270,21 @@ void FLTVideoPlayerApiSetup(id binaryMessenger, id { _updatePosition(newPosition); }, ); + + // This ensures that the correct playback speed is always applied when + // playing back. This is necessary because we do not set playback speed + // when paused. + await _applyPlaybackSpeed(); } else { _timer?.cancel(); await _videoPlayerPlatform.pause(_textureId); @@ -410,6 +422,22 @@ class VideoPlayerController extends ValueNotifier { await _videoPlayerPlatform.setVolume(_textureId, value.volume); } + Future _applyPlaybackSpeed() async { + if (!value.initialized || _isDisposed) { + return; + } + + // Setting the playback speed on iOS will trigger the video to play. We + // prevent this from happening by not applying the playback speed until + // the video is manually played from Flutter. + if (!value.isPlaying) return; + + await _videoPlayerPlatform.setPlaybackSpeed( + _textureId, + value.playbackSpeed, + ); + } + /// The position in the current video. Future get position async { if (_isDisposed) { @@ -445,6 +473,40 @@ class VideoPlayerController extends ValueNotifier { await _applyVolume(); } + /// Sets the playback speed of [this]. + /// + /// [speed] indicates a speed value with different platforms accepting + /// different ranges for speed values. The [speed] must be greater than 0. + /// + /// The values will be handled as follows: + /// * On web, the audio will be muted at some speed when the browser + /// determines that the sound would not be useful anymore. For example, + /// "Gecko mutes the sound outside the range `0.25` to `5.0`" (see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/playbackRate). + /// * On Android, some very extreme speeds will not be played back accurately. + /// Instead, your video will still be played back, but the speed will be + /// clamped by ExoPlayer (but the values are allowed by the player, like on + /// web). + /// * On iOS, you can sometimes not go above `2.0` playback speed on a video. + /// An error will be thrown for if the option is unsupported. It is also + /// possible that your specific video cannot be slowed down, in which case + /// the plugin also reports errors. + Future setPlaybackSpeed(double speed) async { + if (speed < 0) { + throw ArgumentError.value( + speed, + 'Negative playback speeds are generally unsupported.', + ); + } else if (speed == 0) { + throw ArgumentError.value( + speed, + 'Zero playback speed is generally unsupported. Consider using [pause].', + ); + } + + value = value.copyWith(playbackSpeed: speed); + await _applyPlaybackSpeed(); + } + /// The closed caption based on the current [position] in the video. /// /// If there are no closed captions at the current [position], this will diff --git a/packages/video_player/video_player/pigeons/messages.dart b/packages/video_player/video_player/pigeons/messages.dart index 074eef023b94..427aea279071 100644 --- a/packages/video_player/video_player/pigeons/messages.dart +++ b/packages/video_player/video_player/pigeons/messages.dart @@ -14,6 +14,11 @@ class VolumeMessage { double volume; } +class PlaybackSpeedMessage { + int textureId; + double speed; +} + class PositionMessage { int textureId; int position; @@ -30,13 +35,14 @@ class MixWithOthersMessage { bool mixWithOthers; } -@HostApi() +@HostApi(dartHostTestHandler: 'TestHostVideoPlayerApi') abstract class VideoPlayerApi { void initialize(); TextureMessage create(CreateMessage msg); void dispose(TextureMessage msg); void setLooping(LoopingMessage msg); void setVolume(VolumeMessage msg); + void setPlaybackSpeed(PlaybackSpeedMessage msg); void play(TextureMessage msg); PositionMessage position(TextureMessage msg); void seekTo(PositionMessage msg); diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 6eac285006a9..09dc9a9bb984 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -1,10 +1,10 @@ name: video_player description: Flutter plugin for displaying inline video with other Flutter - widgets on Android and iOS. + widgets on Android, iOS, and web. # 0.10.y+z is compatible with 1.0.0, if you land a breaking change bump # the version to 2.0.0. # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.10.12+4 +version: 0.11.0 homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player flutter: @@ -20,7 +20,8 @@ flutter: dependencies: meta: ^1.0.5 - video_player_platform_interface: ^2.1.0 + video_player_platform_interface: ^2.2.0 + # The design on https://flutter.dev/go/federated-plugins was to leave # this constraint as "any". We cannot do it right now as it fails pub publish # validation, so we set a ^ constraint. @@ -35,8 +36,8 @@ dev_dependencies: flutter_test: sdk: flutter pedantic: ^1.8.0 - pigeon: 0.1.0-experimental.11 + pigeon: 0.1.7 environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.8.0 <3.0.0" flutter: ">=1.12.13+hotfix.5 <2.0.0" diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index f2d4c35a1ce8..35cab6204965 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -4,12 +4,13 @@ import 'dart:async'; import 'dart:io'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'package:video_player/video_player.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:video_player/video_player.dart'; import 'package:video_player_platform_interface/messages.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; @@ -27,23 +28,34 @@ class FakeController extends ValueNotifier @override String get dataSource => ''; + @override DataSourceType get dataSourceType => DataSourceType.file; + @override String get package => null; + @override Future get position async => value.position; @override Future seekTo(Duration moment) async {} + @override Future setVolume(double volume) async {} + + @override + Future setPlaybackSpeed(double speed) async {} + @override Future initialize() async {} + @override Future pause() async {} + @override Future play() async {} + @override Future setLooping(bool looping) async {} @@ -250,7 +262,14 @@ void main() { await controller.play(); expect(controller.value.isPlaying, isTrue); - expect(fakeVideoPlayerPlatform.calls.last, 'play'); + + // The two last calls will be "play" and then "setPlaybackSpeed". The + // reason for this is that "play" calls "setPlaybackSpeed" internally. + expect( + fakeVideoPlayerPlatform + .calls[fakeVideoPlayerPlatform.calls.length - 2], + 'play'); + expect(fakeVideoPlayerPlatform.calls.last, 'setPlaybackSpeed'); }); test('setLooping', () async { @@ -335,6 +354,31 @@ void main() { }); }); + group('setPlaybackSpeed', () { + test('works', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + expect(controller.value.playbackSpeed, 1.0); + + const double speed = 1.5; + await controller.setPlaybackSpeed(speed); + + expect(controller.value.playbackSpeed, speed); + }); + + test('rejects negative values', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + expect(controller.value.playbackSpeed, 1.0); + + expect(() => controller.setPlaybackSpeed(-1), throwsArgumentError); + }); + }); + group('caption', () { test('works when seeking', () async { final VideoPlayerController controller = VideoPlayerController.network( @@ -458,6 +502,7 @@ void main() { expect(uninitialized.isLooping, isFalse); expect(uninitialized.isBuffering, isFalse); expect(uninitialized.volume, 1.0); + expect(uninitialized.playbackSpeed, 1.0); expect(uninitialized.errorDescription, isNull); expect(uninitialized.size, isNull); expect(uninitialized.size, isNull); @@ -478,6 +523,7 @@ void main() { expect(error.isLooping, isFalse); expect(error.isBuffering, isFalse); expect(error.volume, 1.0); + expect(error.playbackSpeed, 1.0); expect(error.errorDescription, errorMessage); expect(error.size, isNull); expect(error.size, isNull); @@ -498,20 +544,34 @@ void main() { const bool isLooping = true; const bool isBuffering = true; const double volume = 0.5; + const double playbackSpeed = 1.5; final VideoPlayerValue value = VideoPlayerValue( - duration: duration, - size: size, - position: position, - caption: caption, - buffered: buffered, - isPlaying: isPlaying, - isLooping: isLooping, - isBuffering: isBuffering, - volume: volume); - - expect(value.toString(), - 'VideoPlayerValue(duration: 0:00:05.000000, size: Size(400.0, 300.0), position: 0:00:01.000000, caption: Instance of \'Caption\', buffered: [DurationRange(start: 0:00:00.000000, end: 0:00:04.000000)], isPlaying: true, isLooping: true, isBuffering: truevolume: 0.5, errorDescription: null)'); + duration: duration, + size: size, + position: position, + caption: caption, + buffered: buffered, + isPlaying: isPlaying, + isLooping: isLooping, + isBuffering: isBuffering, + volume: volume, + playbackSpeed: playbackSpeed, + ); + + expect( + value.toString(), + 'VideoPlayerValue(duration: 0:00:05.000000, ' + 'size: Size(400.0, 300.0), ' + 'position: 0:00:01.000000, ' + 'caption: Instance of \'Caption\', ' + 'buffered: [DurationRange(start: 0:00:00.000000, end: 0:00:04.000000)], ' + 'isPlaying: true, ' + 'isLooping: true, ' + 'isBuffering: true, ' + 'volume: 0.5, ' + 'playbackSpeed: 1.5, ' + 'errorDescription: null)'); }); test('copyWith()', () { @@ -588,9 +648,9 @@ void main() { }); } -class FakeVideoPlayerPlatform extends VideoPlayerApiTest { +class FakeVideoPlayerPlatform extends TestHostVideoPlayerApi { FakeVideoPlayerPlatform() { - VideoPlayerApiTestSetup(this); + TestHostVideoPlayerApi.setup(this); } Completer initialized = Completer(); @@ -657,6 +717,11 @@ class FakeVideoPlayerPlatform extends VideoPlayerApiTest { calls.add('setVolume'); } + @override + void setPlaybackSpeed(PlaybackSpeedMessage arg) { + calls.add('setPlaybackSpeed'); + } + @override void setMixWithOthers(MixWithOthersMessage arg) { calls.add('setMixWithOthers'); diff --git a/packages/video_player/video_player_platform_interface/CHANGELOG.md b/packages/video_player/video_player_platform_interface/CHANGELOG.md index 2af6f01ffe88..8af22f783675 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.2.0 + +* Added option to set the video playback speed on the video controller. + ## 2.1.1 * Fix mixWithOthers test channel. diff --git a/packages/video_player/video_player_platform_interface/lib/messages.dart b/packages/video_player/video_player_platform_interface/lib/messages.dart index c5e8cd413b70..bfe65f1fd2ea 100644 --- a/packages/video_player/video_player_platform_interface/lib/messages.dart +++ b/packages/video_player/video_player_platform_interface/lib/messages.dart @@ -1,8 +1,10 @@ -// Autogenerated from Pigeon (v0.1.0-experimental.11), do not edit directly. +// Autogenerated from Pigeon (v0.1.7), 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 +// @dart = 2.8 import 'dart:async'; import 'package:flutter/services.dart'; +import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; class TextureMessage { int textureId; @@ -15,6 +17,9 @@ class TextureMessage { // ignore: unused_element static TextureMessage _fromMap(Map pigeonMap) { + if (pigeonMap == null) { + return null; + } final TextureMessage result = TextureMessage(); result.textureId = pigeonMap['textureId']; return result; @@ -38,6 +43,9 @@ class CreateMessage { // ignore: unused_element static CreateMessage _fromMap(Map pigeonMap) { + if (pigeonMap == null) { + return null; + } final CreateMessage result = CreateMessage(); result.asset = pigeonMap['asset']; result.uri = pigeonMap['uri']; @@ -60,6 +68,9 @@ class LoopingMessage { // ignore: unused_element static LoopingMessage _fromMap(Map pigeonMap) { + if (pigeonMap == null) { + return null; + } final LoopingMessage result = LoopingMessage(); result.textureId = pigeonMap['textureId']; result.isLooping = pigeonMap['isLooping']; @@ -80,6 +91,9 @@ class VolumeMessage { // ignore: unused_element static VolumeMessage _fromMap(Map pigeonMap) { + if (pigeonMap == null) { + return null; + } final VolumeMessage result = VolumeMessage(); result.textureId = pigeonMap['textureId']; result.volume = pigeonMap['volume']; @@ -87,6 +101,29 @@ class VolumeMessage { } } +class PlaybackSpeedMessage { + int textureId; + double speed; + // ignore: unused_element + Map _toMap() { + final Map pigeonMap = {}; + pigeonMap['textureId'] = textureId; + pigeonMap['speed'] = speed; + return pigeonMap; + } + + // ignore: unused_element + static PlaybackSpeedMessage _fromMap(Map pigeonMap) { + if (pigeonMap == null) { + return null; + } + final PlaybackSpeedMessage result = PlaybackSpeedMessage(); + result.textureId = pigeonMap['textureId']; + result.speed = pigeonMap['speed']; + return result; + } +} + class PositionMessage { int textureId; int position; @@ -100,6 +137,9 @@ class PositionMessage { // ignore: unused_element static PositionMessage _fromMap(Map pigeonMap) { + if (pigeonMap == null) { + return null; + } final PositionMessage result = PositionMessage(); result.textureId = pigeonMap['textureId']; result.position = pigeonMap['position']; @@ -118,128 +158,15 @@ class MixWithOthersMessage { // ignore: unused_element static MixWithOthersMessage _fromMap(Map pigeonMap) { + if (pigeonMap == null) { + return null; + } final MixWithOthersMessage result = MixWithOthersMessage(); result.mixWithOthers = pigeonMap['mixWithOthers']; return result; } } -abstract class VideoPlayerApiTest { - void initialize(); - TextureMessage create(CreateMessage arg); - void dispose(TextureMessage arg); - void setLooping(LoopingMessage arg); - void setVolume(VolumeMessage arg); - void play(TextureMessage arg); - PositionMessage position(TextureMessage arg); - void seekTo(PositionMessage arg); - void pause(TextureMessage arg); - void setMixWithOthers(MixWithOthersMessage arg); -} - -void VideoPlayerApiTestSetup(VideoPlayerApiTest api) { - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.initialize', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - api.initialize(); - return {}; - }); - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.create', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = message as Map; - final CreateMessage input = CreateMessage._fromMap(mapMessage); - final TextureMessage output = api.create(input); - return {'result': output._toMap()}; - }); - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.dispose', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = message as Map; - final TextureMessage input = TextureMessage._fromMap(mapMessage); - api.dispose(input); - return {}; - }); - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setLooping', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = message as Map; - final LoopingMessage input = LoopingMessage._fromMap(mapMessage); - api.setLooping(input); - return {}; - }); - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setVolume', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = message as Map; - final VolumeMessage input = VolumeMessage._fromMap(mapMessage); - api.setVolume(input); - return {}; - }); - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.play', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = message as Map; - final TextureMessage input = TextureMessage._fromMap(mapMessage); - api.play(input); - return {}; - }); - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.position', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = message as Map; - final TextureMessage input = TextureMessage._fromMap(mapMessage); - final PositionMessage output = api.position(input); - return {'result': output._toMap()}; - }); - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.seekTo', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = message as Map; - final PositionMessage input = PositionMessage._fromMap(mapMessage); - api.seekTo(input); - return {}; - }); - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.pause', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = message as Map; - final TextureMessage input = TextureMessage._fromMap(mapMessage); - api.pause(input); - return {}; - }); - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers', - StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = message as Map; - final MixWithOthersMessage input = - MixWithOthersMessage._fromMap(mapMessage); - api.setMixWithOthers(input); - return {}; - }); - } -} - class VideoPlayerApi { Future initialize() async { const BasicMessageChannel channel = BasicMessageChannel( @@ -350,6 +277,29 @@ class VideoPlayerApi { } } + Future setPlaybackSpeed(PlaybackSpeedMessage arg) async { + final Map requestMap = arg._toMap(); + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed', + StandardMessageCodec()); + + final Map replyMap = await channel.send(requestMap); + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null); + } else if (replyMap['error'] != null) { + final Map error = replyMap['error']; + throw PlatformException( + code: error['code'], + message: error['message'], + details: error['details']); + } else { + // noop + } + } + Future play(TextureMessage arg) async { final Map requestMap = arg._toMap(); const BasicMessageChannel channel = BasicMessageChannel( @@ -461,3 +411,144 @@ class VideoPlayerApi { } } } + +abstract class TestHostVideoPlayerApi { + void initialize(); + TextureMessage create(CreateMessage arg); + void dispose(TextureMessage arg); + void setLooping(LoopingMessage arg); + void setVolume(VolumeMessage arg); + void setPlaybackSpeed(PlaybackSpeedMessage arg); + void play(TextureMessage arg); + PositionMessage position(TextureMessage arg); + void seekTo(PositionMessage arg); + void pause(TextureMessage arg); + void setMixWithOthers(MixWithOthersMessage arg); + static void setup(TestHostVideoPlayerApi api) { + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.initialize', + StandardMessageCodec()); + channel.setMockMessageHandler((dynamic message) async { + api.initialize(); + return {}; + }); + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.create', StandardMessageCodec()); + channel.setMockMessageHandler((dynamic message) async { + final Map mapMessage = + message as Map; + final CreateMessage input = CreateMessage._fromMap(mapMessage); + final TextureMessage output = api.create(input); + return {'result': output._toMap()}; + }); + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.dispose', StandardMessageCodec()); + channel.setMockMessageHandler((dynamic message) async { + final Map mapMessage = + message as Map; + final TextureMessage input = TextureMessage._fromMap(mapMessage); + api.dispose(input); + return {}; + }); + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.setLooping', + StandardMessageCodec()); + channel.setMockMessageHandler((dynamic message) async { + final Map mapMessage = + message as Map; + final LoopingMessage input = LoopingMessage._fromMap(mapMessage); + api.setLooping(input); + return {}; + }); + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.setVolume', + StandardMessageCodec()); + channel.setMockMessageHandler((dynamic message) async { + final Map mapMessage = + message as Map; + final VolumeMessage input = VolumeMessage._fromMap(mapMessage); + api.setVolume(input); + return {}; + }); + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed', + StandardMessageCodec()); + channel.setMockMessageHandler((dynamic message) async { + final Map mapMessage = + message as Map; + final PlaybackSpeedMessage input = + PlaybackSpeedMessage._fromMap(mapMessage); + api.setPlaybackSpeed(input); + return {}; + }); + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.play', StandardMessageCodec()); + channel.setMockMessageHandler((dynamic message) async { + final Map mapMessage = + message as Map; + final TextureMessage input = TextureMessage._fromMap(mapMessage); + api.play(input); + return {}; + }); + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.position', StandardMessageCodec()); + channel.setMockMessageHandler((dynamic message) async { + final Map mapMessage = + message as Map; + final TextureMessage input = TextureMessage._fromMap(mapMessage); + final PositionMessage output = api.position(input); + return {'result': output._toMap()}; + }); + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.seekTo', StandardMessageCodec()); + channel.setMockMessageHandler((dynamic message) async { + final Map mapMessage = + message as Map; + final PositionMessage input = PositionMessage._fromMap(mapMessage); + api.seekTo(input); + return {}; + }); + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.pause', StandardMessageCodec()); + channel.setMockMessageHandler((dynamic message) async { + final Map mapMessage = + message as Map; + final TextureMessage input = TextureMessage._fromMap(mapMessage); + api.pause(input); + return {}; + }); + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers', + StandardMessageCodec()); + channel.setMockMessageHandler((dynamic message) async { + final Map mapMessage = + message as Map; + final MixWithOthersMessage input = + MixWithOthersMessage._fromMap(mapMessage); + api.setMixWithOthers(input); + return {}; + }); + } + } +} diff --git a/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart b/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart index 8c0f1de39661..0ea443fb6e12 100644 --- a/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart +++ b/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart @@ -71,6 +71,15 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { ..volume = volume); } + @override + Future setPlaybackSpeed(int textureId, double speed) { + assert(speed > 0); + + return _api.setPlaybackSpeed(PlaybackSpeedMessage() + ..textureId = textureId + ..speed = speed); + } + @override Future seekTo(int textureId, Duration position) { return _api.seekTo(PositionMessage() diff --git a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart index 279810aaaf63..2757fb135af6 100644 --- a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart +++ b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart @@ -100,6 +100,11 @@ abstract class VideoPlayerPlatform { throw UnimplementedError('seekTo() has not been implemented.'); } + /// Sets the playback speed to a [speed] value indicating the playback rate. + Future setPlaybackSpeed(int textureId, double speed) { + throw UnimplementedError('setPlaybackSpeed() has not been implemented.'); + } + /// Gets the video position as [Duration] from the start. Future getPosition(int textureId) { throw UnimplementedError('getPosition() has not been implemented.'); @@ -184,7 +189,7 @@ enum DataSourceType { network, /// The video was loaded off of the local filesystem. - file + file, } /// The file format of the given video. @@ -199,7 +204,7 @@ enum VideoFormat { ss, /// Any format other than the other ones defined in this enum. - other + other, } /// Event emitted from the platform implementation. diff --git a/packages/video_player/video_player_platform_interface/pubspec.yaml b/packages/video_player/video_player_platform_interface/pubspec.yaml index 38bc6851f376..cc3cd79f1f33 100644 --- a/packages/video_player/video_player_platform_interface/pubspec.yaml +++ b/packages/video_player/video_player_platform_interface/pubspec.yaml @@ -3,7 +3,7 @@ description: A common platform interface for the video_player plugin. homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_platform_interface # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.1.1 +version: 2.2.0 dependencies: flutter: @@ -17,5 +17,5 @@ dev_dependencies: pedantic: ^1.8.0 environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.8.0 <3.0.0" flutter: ">=1.10.0 <2.0.0" diff --git a/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart b/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart index 185953163350..c4791001ad92 100644 --- a/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart +++ b/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart @@ -4,21 +4,21 @@ import 'dart:ui'; -import 'package:mockito/mockito.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - +import 'package:mockito/mockito.dart'; +import 'package:video_player_platform_interface/messages.dart'; import 'package:video_player_platform_interface/method_channel_video_player.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; -import 'package:video_player_platform_interface/messages.dart'; -class _ApiLogger implements VideoPlayerApiTest { +class _ApiLogger implements TestHostVideoPlayerApi { final List log = []; TextureMessage textureMessage; CreateMessage createMessage; PositionMessage positionMessage; LoopingMessage loopingMessage; VolumeMessage volumeMessage; + PlaybackSpeedMessage playbackSpeedMessage; MixWithOthersMessage mixWithOthersMessage; @override @@ -81,6 +81,12 @@ class _ApiLogger implements VideoPlayerApiTest { log.add('setVolume'); volumeMessage = arg; } + + @override + void setPlaybackSpeed(PlaybackSpeedMessage arg) { + log.add('setPlaybackSpeed'); + playbackSpeedMessage = arg; + } } void main() { @@ -116,7 +122,7 @@ void main() { setUp(() { log = _ApiLogger(); - VideoPlayerApiTestSetup(log); + TestHostVideoPlayerApi.setup(log); }); test('init', () async { @@ -203,6 +209,13 @@ void main() { expect(log.volumeMessage.volume, 0.7); }); + test('setPlaybackSpeed', () async { + await player.setPlaybackSpeed(1, 1.5); + expect(log.log.last, 'setPlaybackSpeed'); + expect(log.playbackSpeedMessage.textureId, 1); + expect(log.playbackSpeedMessage.speed, 1.5); + }); + test('seekTo', () async { await player.seekTo(1, const Duration(milliseconds: 12345)); expect(log.log.last, 'seekTo'); diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index 9c500e951122..d18504913d89 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.4 + +* Added option to set the video playback speed on the video controller. + ## 0.1.3+2 * Allow users to set the 'muted' attribute on video elements by setting their volume to 0. diff --git a/packages/video_player/video_player_web/lib/video_player_web.dart b/packages/video_player/video_player_web/lib/video_player_web.dart index 3a849f45e0c1..251da3779e7f 100644 --- a/packages/video_player/video_player_web/lib/video_player_web.dart +++ b/packages/video_player/video_player_web/lib/video_player_web.dart @@ -119,6 +119,13 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { return _videoPlayers[textureId].setVolume(volume); } + @override + Future setPlaybackSpeed(int textureId, double speed) async { + assert(speed > 0); + + return _videoPlayers[textureId].setPlaybackSpeed(speed); + } + @override Future seekTo(int textureId, Duration position) async { return _videoPlayers[textureId].seekTo(position); @@ -232,6 +239,12 @@ class _VideoPlayer { videoElement.volume = value; } + void setPlaybackSpeed(double speed) { + assert(speed > 0); + + videoElement.playbackRate = speed; + } + void seekTo(Duration position) { videoElement.currentTime = position.inMilliseconds.toDouble() / 1000; } diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index 891430d7483b..98191bf6ba85 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -1,10 +1,10 @@ name: video_player_web -description: Web platform implementation of video_player +description: Web platform implementation of video_player. homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_web # 0.1.y+z is compatible with 1.0.0, if you land a breaking change bump # the version to 2.0.0. # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.1.3+2 +version: 0.1.4 flutter: plugin: @@ -19,7 +19,7 @@ dependencies: flutter_web_plugins: sdk: flutter meta: ^1.1.7 - video_player_platform_interface: ^2.0.0 + video_player_platform_interface: ^2.2.0 dev_dependencies: flutter_test: @@ -29,5 +29,5 @@ dev_dependencies: pedantic: ^1.8.0 environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.8.0 <3.0.0" flutter: ">=1.12.8 <2.0.0" diff --git a/packages/video_player/video_player_web/test/video_player_web_test.dart b/packages/video_player/video_player_web/test/video_player_web_test.dart index ef6dc028c529..453079bfcd40 100644 --- a/packages/video_player/video_player_web/test/video_player_web_test.dart +++ b/packages/video_player/video_player_web/test/video_player_web_test.dart @@ -109,6 +109,13 @@ void main() { expect(VideoPlayerPlatform.instance.setVolume(textureId, 0.8), completes); }); + test('can set playback speed', () { + expect( + VideoPlayerPlatform.instance.setPlaybackSpeed(textureId, 2.0), + completes, + ); + }); + test('can seek to position', () { expect( VideoPlayerPlatform.instance.seekTo(textureId, Duration(seconds: 1)),