[video_player] Set playback speed (#3084)
diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md
index 935de9b..f7fad26 100644
--- a/packages/video_player/video_player/CHANGELOG.md
+++ b/packages/video_player/video_player/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 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`
diff --git a/packages/video_player/video_player/README.md b/packages/video_player/video_player/README.md
index ef522a4..9c0e6b1 100644
--- a/packages/video_player/video_player/README.md
+++ b/packages/video_player/video_player/README.md
@@ -127,3 +127,22 @@
}
}
```
+
+## 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 003ca18..78da715 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 @@
}
HashMap toMap() {
- HashMap<String, Object> toMapResult = new HashMap<String, Object>();
+ HashMap<String, Object> 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 @@
}
HashMap toMap() {
- HashMap<String, Object> toMapResult = new HashMap<String, Object>();
+ HashMap<String, Object> toMapResult = new HashMap<>();
toMapResult.put("asset", asset);
toMapResult.put("uri", uri);
toMapResult.put("packageName", packageName);
@@ -92,10 +94,14 @@
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 @@
}
HashMap toMap() {
- HashMap<String, Object> toMapResult = new HashMap<String, Object>();
+ HashMap<String, Object> toMapResult = new HashMap<>();
toMapResult.put("textureId", textureId);
toMapResult.put("isLooping", isLooping);
return toMapResult;
@@ -131,11 +137,13 @@
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 @@
}
HashMap toMap() {
- HashMap<String, Object> toMapResult = new HashMap<String, Object>();
+ HashMap<String, Object> toMapResult = new HashMap<>();
toMapResult.put("textureId", textureId);
toMapResult.put("volume", volume);
return toMapResult;
@@ -171,11 +179,55 @@
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<String, Object> 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 @@
}
HashMap toMap() {
- HashMap<String, Object> toMapResult = new HashMap<String, Object>();
+ HashMap<String, Object> toMapResult = new HashMap<>();
toMapResult.put("textureId", textureId);
toMapResult.put("position", position);
return toMapResult;
@@ -211,14 +263,16 @@
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 @@
}
HashMap toMap() {
- HashMap<String, Object> toMapResult = new HashMap<String, Object>();
+ HashMap<String, Object> 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 @@
void setVolume(VolumeMessage arg);
+ void setPlaybackSpeed(PlaybackSpeedMessage arg);
+
void play(TextureMessage arg);
PositionMessage position(TextureMessage arg);
@@ -271,26 +328,24 @@
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<Object> channel =
- new BasicMessageChannel<Object>(
+ new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.VideoPlayerApi.initialize",
new StandardMessageCodec());
if (api != null) {
channel.setMessageHandler(
- new BasicMessageChannel.MessageHandler<Object>() {
- public void onMessage(Object message, BasicMessageChannel.Reply<Object> reply) {
- HashMap<String, HashMap> wrapped = new HashMap<String, HashMap>();
- try {
- api.initialize();
- wrapped.put("result", null);
- } catch (Exception exception) {
- wrapped.put("error", wrapError(exception));
- }
- reply.reply(wrapped);
+ (message, reply) -> {
+ HashMap<String, 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 @@
}
{
BasicMessageChannel<Object> channel =
- new BasicMessageChannel<Object>(
+ new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.VideoPlayerApi.create",
new StandardMessageCodec());
if (api != null) {
channel.setMessageHandler(
- new BasicMessageChannel.MessageHandler<Object>() {
- public void onMessage(Object message, BasicMessageChannel.Reply<Object> reply) {
+ (message, reply) -> {
+ HashMap<String, HashMap> wrapped = new HashMap<>();
+ try {
+ @SuppressWarnings("ConstantConditions")
CreateMessage input = CreateMessage.fromMap((HashMap) message);
- HashMap<String, HashMap> wrapped = new HashMap<String, 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 @@
}
{
BasicMessageChannel<Object> channel =
- new BasicMessageChannel<Object>(
+ new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.VideoPlayerApi.dispose",
new StandardMessageCodec());
if (api != null) {
channel.setMessageHandler(
- new BasicMessageChannel.MessageHandler<Object>() {
- public void onMessage(Object message, BasicMessageChannel.Reply<Object> reply) {
+ (message, reply) -> {
+ HashMap<String, HashMap> wrapped = new HashMap<>();
+ try {
+ @SuppressWarnings("ConstantConditions")
TextureMessage input = TextureMessage.fromMap((HashMap) message);
- HashMap<String, HashMap> wrapped = new HashMap<String, 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 @@
}
{
BasicMessageChannel<Object> channel =
- new BasicMessageChannel<Object>(
+ new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.VideoPlayerApi.setLooping",
new StandardMessageCodec());
if (api != null) {
channel.setMessageHandler(
- new BasicMessageChannel.MessageHandler<Object>() {
- public void onMessage(Object message, BasicMessageChannel.Reply<Object> reply) {
+ (message, reply) -> {
+ HashMap<String, HashMap> wrapped = new HashMap<>();
+ try {
+ @SuppressWarnings("ConstantConditions")
LoopingMessage input = LoopingMessage.fromMap((HashMap) message);
- HashMap<String, HashMap> wrapped = new HashMap<String, 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,23 @@
}
{
BasicMessageChannel<Object> channel =
- new BasicMessageChannel<Object>(
+ new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.VideoPlayerApi.setVolume",
new StandardMessageCodec());
if (api != null) {
channel.setMessageHandler(
- new BasicMessageChannel.MessageHandler<Object>() {
- public void onMessage(Object message, BasicMessageChannel.Reply<Object> reply) {
+ (message, reply) -> {
+ HashMap<String, HashMap> wrapped = new HashMap<>();
+ try {
+ @SuppressWarnings("ConstantConditions")
VolumeMessage input = VolumeMessage.fromMap((HashMap) message);
- HashMap<String, HashMap> wrapped = new HashMap<String, 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);
@@ -398,24 +449,47 @@
}
{
BasicMessageChannel<Object> channel =
- new BasicMessageChannel<Object>(
+ new BasicMessageChannel<>(
+ binaryMessenger,
+ "dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed",
+ new StandardMessageCodec());
+ if (api != null) {
+ channel.setMessageHandler(
+ (message, reply) -> {
+ HashMap<String, 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);
+ }
+ }
+ {
+ BasicMessageChannel<Object> channel =
+ new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.VideoPlayerApi.play",
new StandardMessageCodec());
if (api != null) {
channel.setMessageHandler(
- new BasicMessageChannel.MessageHandler<Object>() {
- public void onMessage(Object message, BasicMessageChannel.Reply<Object> reply) {
+ (message, reply) -> {
+ HashMap<String, HashMap> wrapped = new HashMap<>();
+ try {
+ @SuppressWarnings("ConstantConditions")
TextureMessage input = TextureMessage.fromMap((HashMap) message);
- HashMap<String, HashMap> wrapped = new HashMap<String, 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 @@
}
{
BasicMessageChannel<Object> channel =
- new BasicMessageChannel<Object>(
+ new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.VideoPlayerApi.position",
new StandardMessageCodec());
if (api != null) {
channel.setMessageHandler(
- new BasicMessageChannel.MessageHandler<Object>() {
- public void onMessage(Object message, BasicMessageChannel.Reply<Object> reply) {
+ (message, reply) -> {
+ HashMap<String, HashMap> wrapped = new HashMap<>();
+ try {
+ @SuppressWarnings("ConstantConditions")
TextureMessage input = TextureMessage.fromMap((HashMap) message);
- HashMap<String, HashMap> wrapped = new HashMap<String, 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 @@
}
{
BasicMessageChannel<Object> channel =
- new BasicMessageChannel<Object>(
+ new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.VideoPlayerApi.seekTo",
new StandardMessageCodec());
if (api != null) {
channel.setMessageHandler(
- new BasicMessageChannel.MessageHandler<Object>() {
- public void onMessage(Object message, BasicMessageChannel.Reply<Object> reply) {
+ (message, reply) -> {
+ HashMap<String, HashMap> wrapped = new HashMap<>();
+ try {
+ @SuppressWarnings("ConstantConditions")
PositionMessage input = PositionMessage.fromMap((HashMap) message);
- HashMap<String, HashMap> wrapped = new HashMap<String, 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 @@
}
{
BasicMessageChannel<Object> channel =
- new BasicMessageChannel<Object>(
+ new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.VideoPlayerApi.pause",
new StandardMessageCodec());
if (api != null) {
channel.setMessageHandler(
- new BasicMessageChannel.MessageHandler<Object>() {
- public void onMessage(Object message, BasicMessageChannel.Reply<Object> reply) {
+ (message, reply) -> {
+ HashMap<String, HashMap> wrapped = new HashMap<>();
+ try {
+ @SuppressWarnings("ConstantConditions")
TextureMessage input = TextureMessage.fromMap((HashMap) message);
- HashMap<String, HashMap> wrapped = new HashMap<String, 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 @@
}
{
BasicMessageChannel<Object> channel =
- new BasicMessageChannel<Object>(
+ new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers",
new StandardMessageCodec());
if (api != null) {
channel.setMessageHandler(
- new BasicMessageChannel.MessageHandler<Object>() {
- public void onMessage(Object message, BasicMessageChannel.Reply<Object> reply) {
+ (message, reply) -> {
+ HashMap<String, HashMap> wrapped = new HashMap<>();
+ try {
+ @SuppressWarnings("ConstantConditions")
MixWithOthersMessage input = MixWithOthersMessage.fromMap((HashMap) message);
- HashMap<String, HashMap> wrapped = new HashMap<String, 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 @@
}
private static HashMap wrapError(Exception exception) {
- HashMap<String, Object> errorMap = new HashMap<String, Object>();
+ HashMap<String, Object> 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 801c2ca..8f8c898 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 @@
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 329370a..7f86305 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 @@
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 ee2fcbd..a99b9da 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 @@
alignment: Alignment.bottomCenter,
children: <Widget>[
VideoPlayer(_controller),
- _PlayPauseOverlay(controller: _controller),
+ _ControlsOverlay(controller: _controller),
VideoProgressIndicator(_controller, allowScrubbing: true),
],
),
@@ -252,7 +252,7 @@
children: <Widget>[
VideoPlayer(_controller),
ClosedCaption(text: _controller.value.caption.text),
- _PlayPauseOverlay(controller: _controller),
+ _ControlsOverlay(controller: _controller),
VideoProgressIndicator(_controller, allowScrubbing: true),
],
),
@@ -264,8 +264,19 @@
}
}
-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 @@
controller.value.isPlaying ? controller.pause() : controller.play();
},
),
+ Align(
+ alignment: Alignment.topRight,
+ child: PopupMenuButton<double>(
+ 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 a834fe3..e6a4f6c 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 @@
_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 @@
[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 27025ef..3c68b3d 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 <Foundation/Foundation.h>
@protocol FlutterBinaryMessenger;
@@ -11,6 +11,7 @@
@class FLTCreateMessage;
@class FLTLoopingMessage;
@class FLTVolumeMessage;
+@class FLTPlaybackSpeedMessage;
@class FLTPositionMessage;
@class FLTMixWithOthersMessage;
@@ -35,6 +36,11 @@
@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 @@
- (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 64fcd75..e71f8b8 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 <Flutter/Flutter.h>
@@ -36,6 +36,10 @@
+ (FLTVolumeMessage *)fromMap:(NSDictionary *)dict;
- (NSDictionary *)toMap;
@end
+@interface FLTPlaybackSpeedMessage ()
++ (FLTPlaybackSpeedMessage *)fromMap:(NSDictionary *)dict;
+- (NSDictionary *)toMap;
+@end
@interface FLTPositionMessage ()
+ (FLTPositionMessage *)fromMap:(NSDictionary *)dict;
- (NSDictionary *)toMap;
@@ -86,9 +90,9 @@
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
@@ -136,6 +140,27 @@
}
@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 != nil ? self.textureId : [NSNull null]),
+ @"textureId", (self.speed != nil ? self.speed : [NSNull null]),
+ @"speed", nil];
+}
+@end
+
@implementation FLTPositionMessage
+ (FLTPositionMessage *)fromMap:(NSDictionary *)dict {
FLTPositionMessage *result = [[FLTPositionMessage alloc] init];
@@ -250,6 +275,21 @@
}
}
{
+ FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel
+ messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed"
+ binaryMessenger:binaryMessenger];
+ if (api) {
+ [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
+ FlutterError *error;
+ FLTPlaybackSpeedMessage *input = [FLTPlaybackSpeedMessage fromMap:message];
+ [api setPlaybackSpeed:input error:&error];
+ callback(wrapResult(nil, error));
+ }];
+ } else {
+ [channel setMessageHandler:nil];
+ }
+ }
+ {
FlutterBasicMessageChannel *channel =
[FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.play"
binaryMessenger:binaryMessenger];
diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart
index 8cd0561..bf98096 100644
--- a/packages/video_player/video_player/lib/video_player.dart
+++ b/packages/video_player/video_player/lib/video_player.dart
@@ -6,11 +6,11 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
-import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import 'package:meta/meta.dart';
-
import 'package:video_player_platform_interface/video_player_platform_interface.dart';
+
export 'package:video_player_platform_interface/video_player_platform_interface.dart'
show DurationRange, DataSourceType, VideoFormat, VideoPlayerOptions;
@@ -37,6 +37,7 @@
this.isLooping = false,
this.isBuffering = false,
this.volume = 1.0,
+ this.playbackSpeed = 1.0,
this.errorDescription,
});
@@ -77,6 +78,9 @@
/// The current volume of the playback.
final double volume;
+ /// The current speed of the playback.
+ final double playbackSpeed;
+
/// A description of the error if present.
///
/// If [hasError] is false this is [null].
@@ -119,6 +123,7 @@
bool isLooping,
bool isBuffering,
double volume,
+ double playbackSpeed,
String errorDescription,
}) {
return VideoPlayerValue(
@@ -131,6 +136,7 @@
isLooping: isLooping ?? this.isLooping,
isBuffering: isBuffering ?? this.isBuffering,
volume: volume ?? this.volume,
+ playbackSpeed: playbackSpeed ?? this.playbackSpeed,
errorDescription: errorDescription ?? this.errorDescription,
);
}
@@ -145,8 +151,9 @@
'buffered: [${buffered.join(', ')}], '
'isPlaying: $isPlaying, '
'isLooping: $isLooping, '
- 'isBuffering: $isBuffering'
+ 'isBuffering: $isBuffering, '
'volume: $volume, '
+ 'playbackSpeed: $playbackSpeed, '
'errorDescription: $errorDescription)';
}
}
@@ -397,6 +404,11 @@
_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 @@
await _videoPlayerPlatform.setVolume(_textureId, value.volume);
}
+ Future<void> _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<Duration> get position async {
if (_isDisposed) {
@@ -445,6 +473,40 @@
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<void> 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 074eef0..427aea2 100644
--- a/packages/video_player/video_player/pigeons/messages.dart
+++ b/packages/video_player/video_player/pigeons/messages.dart
@@ -14,6 +14,11 @@
double volume;
}
+class PlaybackSpeedMessage {
+ int textureId;
+ double speed;
+}
+
class PositionMessage {
int textureId;
int position;
@@ -30,13 +35,14 @@
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 bd6d451..973c0bc 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+5
+version: 0.11.0
homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player
flutter:
@@ -21,12 +21,13 @@
dependencies:
meta: ^1.0.5
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.
# TODO(amirh): Revisit this (either update this part in the design or the pub tool).
# https://github.com/flutter/flutter/issues/46264
- video_player_web: '>=0.1.1 <2.0.0'
+ video_player_web: '>=0.1.4 <2.0.0'
flutter:
sdk: flutter
@@ -35,8 +36,8 @@
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 3cc0879..35cab62 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 @@
@override
String get dataSource => '';
+
@override
DataSourceType get dataSourceType => DataSourceType.file;
+
@override
String get package => null;
+
@override
Future<Duration> get position async => value.position;
@override
Future<void> seekTo(Duration moment) async {}
+
@override
Future<void> setVolume(double volume) async {}
+
+ @override
+ Future<void> setPlaybackSpeed(double speed) async {}
+
@override
Future<void> initialize() async {}
+
@override
Future<void> pause() async {}
+
@override
Future<void> play() async {}
+
@override
Future<void> setLooping(bool looping) async {}
@@ -250,7 +262,14 @@
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 @@
});
});
+ 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 @@
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 @@
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 @@
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);
+ 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: truevolume: 0.5, errorDescription: null)');
+ 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()', () {
@@ -659,7 +719,7 @@
@override
void setPlaybackSpeed(PlaybackSpeedMessage arg) {
- // todo: implement as part of completing https://github.com/flutter/plugins/pull/3031
+ calls.add('setPlaybackSpeed');
}
@override