[video_player] Added the setCaptionOffset (#3275)
diff --git a/packages/video_player/video_player/AUTHORS b/packages/video_player/video_player/AUTHORS
index 493a0b4..02a9c69 100644
--- a/packages/video_player/video_player/AUTHORS
+++ b/packages/video_player/video_player/AUTHORS
@@ -64,3 +64,4 @@
Anton Borries <mail@antonborri.es>
Alex Li <google@alexv525.com>
Rahul Raj <64.rahulraj@gmail.com>
+Koen Van Looveren <vanlooverenkoen.dev@gmail.com>
diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md
index b7225a8..e28ef83 100644
--- a/packages/video_player/video_player/CHANGELOG.md
+++ b/packages/video_player/video_player/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.2.16
+
+* Introduces `setCaptionOffset` to offset the caption display based on a Duration.
+
## 2.2.15
* Updates README discussion of permissions.
diff --git a/packages/video_player/video_player/example/lib/main.dart b/packages/video_player/video_player/example/lib/main.dart
index 0429827..9297cc6 100644
--- a/packages/video_player/video_player/example/lib/main.dart
+++ b/packages/video_player/video_player/example/lib/main.dart
@@ -269,6 +269,17 @@
const _ControlsOverlay({Key? key, required this.controller})
: super(key: key);
+ static const _exampleCaptionOffsets = [
+ Duration(seconds: -10),
+ Duration(seconds: -3),
+ Duration(seconds: -1, milliseconds: -500),
+ Duration(milliseconds: -250),
+ Duration(milliseconds: 0),
+ Duration(milliseconds: 250),
+ Duration(seconds: 1, milliseconds: 500),
+ Duration(seconds: 3),
+ Duration(seconds: 10),
+ ];
static const _examplePlaybackRates = [
0.25,
0.5,
@@ -309,6 +320,35 @@
},
),
Align(
+ alignment: Alignment.topLeft,
+ child: PopupMenuButton<Duration>(
+ initialValue: controller.value.captionOffset,
+ tooltip: 'Caption Offset',
+ onSelected: (delay) {
+ controller.setCaptionOffset(delay);
+ },
+ itemBuilder: (context) {
+ return [
+ for (final offsetDuration in _exampleCaptionOffsets)
+ PopupMenuItem(
+ value: offsetDuration,
+ child: Text('${offsetDuration.inMilliseconds}ms'),
+ )
+ ];
+ },
+ 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.captionOffset.inMilliseconds}ms'),
+ ),
+ ),
+ ),
+ Align(
alignment: Alignment.topRight,
child: PopupMenuButton<double>(
initialValue: controller.value.playbackSpeed,
diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart
index 3d4e1c6..8fd2162 100644
--- a/packages/video_player/video_player/lib/video_player.dart
+++ b/packages/video_player/video_player/lib/video_player.dart
@@ -43,6 +43,7 @@
this.size = Size.zero,
this.position = Duration.zero,
this.caption = Caption.none,
+ this.captionOffset = Duration.zero,
this.buffered = const <DurationRange>[],
this.isInitialized = false,
this.isPlaying = false,
@@ -78,6 +79,11 @@
/// [position], this will be a [Caption.none] object.
final Caption caption;
+ /// The [Duration] that should be used to offset the current [position] to get the correct [Caption].
+ ///
+ /// Defaults to Duration.zero.
+ final Duration captionOffset;
+
/// The currently buffered ranges.
final List<DurationRange> buffered;
@@ -135,6 +141,7 @@
Size? size,
Duration? position,
Caption? caption,
+ Duration? captionOffset,
List<DurationRange>? buffered,
bool? isInitialized,
bool? isPlaying,
@@ -149,6 +156,7 @@
size: size ?? this.size,
position: position ?? this.position,
caption: caption ?? this.caption,
+ captionOffset: captionOffset ?? this.captionOffset,
buffered: buffered ?? this.buffered,
isInitialized: isInitialized ?? this.isInitialized,
isPlaying: isPlaying ?? this.isPlaying,
@@ -169,6 +177,7 @@
'size: $size, '
'position: $position, '
'caption: $caption, '
+ 'captionOffset: $captionOffset, '
'buffered: [${buffered.join(', ')}], '
'isInitialized: $isInitialized, '
'isPlaying: $isPlaying, '
@@ -581,6 +590,22 @@
await _applyPlaybackSpeed();
}
+ /// Sets the caption offset.
+ ///
+ /// The [offset] will be used when getting the correct caption for a specific position.
+ /// The [offset] can be positive or negative.
+ ///
+ /// The values will be handled as follows:
+ /// * 0: This is the default behaviour. No offset will be applied.
+ /// * >0: The caption will have a negative offset. So you will get caption text from the past.
+ /// * <0: The caption will have a positive offset. So you will get caption text from the future.
+ void setCaptionOffset(Duration offset) {
+ value = value.copyWith(
+ captionOffset: offset,
+ caption: _getCaptionAt(value.position),
+ );
+ }
+
/// The closed caption based on the current [position] in the video.
///
/// If there are no closed captions at the current [position], this will
@@ -593,9 +618,10 @@
return Caption.none;
}
+ final delayedPosition = position + value.captionOffset;
// TODO: This would be more efficient as a binary search.
for (final caption in _closedCaptionFile!.captions) {
- if (caption.start <= position && caption.end >= position) {
+ if (caption.start <= delayedPosition && caption.end >= delayedPosition) {
return caption;
}
}
@@ -604,8 +630,10 @@
}
void _updatePosition(Duration position) {
- value = value.copyWith(position: position);
- value = value.copyWith(caption: _getCaptionAt(position));
+ value = value.copyWith(
+ position: position,
+ caption: _getCaptionAt(position),
+ );
}
@override
diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml
index 858a7ce..63520f3 100644
--- a/packages/video_player/video_player/pubspec.yaml
+++ b/packages/video_player/video_player/pubspec.yaml
@@ -3,7 +3,7 @@
widgets on Android, iOS, and web.
repository: https://github.com/flutter/plugins/tree/main/packages/video_player/video_player
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
-version: 2.2.15
+version: 2.2.16
environment:
sdk: ">=2.14.0 <3.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 08eecb6..3bce821 100644
--- a/packages/video_player/video_player/test/video_player_test.dart
+++ b/packages/video_player/video_player/test/video_player_test.dart
@@ -69,6 +69,9 @@
@override
VideoPlayerOptions? get videoPlayerOptions => null;
+
+ @override
+ void setCaptionOffset(Duration delay) {}
}
Future<ClosedCaptionFile> _loadClosedCaption() async =>
@@ -557,11 +560,92 @@
await controller.seekTo(const Duration(milliseconds: 300));
expect(controller.value.caption.text, 'two');
+ await controller.seekTo(const Duration(milliseconds: 301));
+ expect(controller.value.caption.text, 'two');
+
await controller.seekTo(const Duration(milliseconds: 500));
expect(controller.value.caption.text, '');
await controller.seekTo(const Duration(milliseconds: 300));
expect(controller.value.caption.text, 'two');
+
+ await controller.seekTo(const Duration(milliseconds: 301));
+ expect(controller.value.caption.text, 'two');
+ });
+
+ test('works when seeking with captionOffset positive', () async {
+ final VideoPlayerController controller = VideoPlayerController.network(
+ 'https://127.0.0.1',
+ closedCaptionFile: _loadClosedCaption(),
+ );
+
+ await controller.initialize();
+ controller.setCaptionOffset(Duration(milliseconds: 100));
+ expect(controller.value.position, const Duration());
+ expect(controller.value.caption.text, '');
+
+ await controller.seekTo(const Duration(milliseconds: 100));
+ expect(controller.value.caption.text, 'one');
+
+ await controller.seekTo(const Duration(milliseconds: 101));
+ expect(controller.value.caption.text, '');
+
+ await controller.seekTo(const Duration(milliseconds: 250));
+ expect(controller.value.caption.text, 'two');
+
+ await controller.seekTo(const Duration(milliseconds: 300));
+ expect(controller.value.caption.text, 'two');
+
+ await controller.seekTo(const Duration(milliseconds: 301));
+ expect(controller.value.caption.text, '');
+
+ await controller.seekTo(const Duration(milliseconds: 500));
+ expect(controller.value.caption.text, '');
+
+ await controller.seekTo(const Duration(milliseconds: 300));
+ expect(controller.value.caption.text, 'two');
+
+ await controller.seekTo(const Duration(milliseconds: 301));
+ expect(controller.value.caption.text, '');
+ });
+
+ test('works when seeking with captionOffset negative', () async {
+ final VideoPlayerController controller = VideoPlayerController.network(
+ 'https://127.0.0.1',
+ closedCaptionFile: _loadClosedCaption(),
+ );
+
+ await controller.initialize();
+ controller.setCaptionOffset(Duration(milliseconds: -100));
+ expect(controller.value.position, const Duration());
+ expect(controller.value.caption.text, '');
+
+ await controller.seekTo(const Duration(milliseconds: 100));
+ expect(controller.value.caption.text, '');
+
+ await controller.seekTo(const Duration(milliseconds: 200));
+ expect(controller.value.caption.text, 'one');
+
+ await controller.seekTo(const Duration(milliseconds: 250));
+ expect(controller.value.caption.text, 'one');
+
+ await controller.seekTo(const Duration(milliseconds: 300));
+ expect(controller.value.caption.text, 'one');
+
+ await controller.seekTo(const Duration(milliseconds: 301));
+ expect(controller.value.caption.text, '');
+
+ await controller.seekTo(const Duration(milliseconds: 400));
+ expect(controller.value.caption.text, 'two');
+
+ await controller.seekTo(const Duration(milliseconds: 500));
+ expect(controller.value.caption.text, 'two');
+
+ await controller.seekTo(const Duration(milliseconds: 600));
+ expect(controller.value.caption.text, '');
+
+ await controller.seekTo(const Duration(milliseconds: 300));
+ expect(controller.value.caption.text, 'one');
});
});
@@ -655,6 +739,7 @@
expect(uninitialized.duration, equals(Duration.zero));
expect(uninitialized.position, equals(Duration.zero));
expect(uninitialized.caption, equals(Caption.none));
+ expect(uninitialized.captionOffset, equals(Duration.zero));
expect(uninitialized.buffered, isEmpty);
expect(uninitialized.isPlaying, isFalse);
expect(uninitialized.isLooping, isFalse);
@@ -675,6 +760,7 @@
expect(error.duration, equals(Duration.zero));
expect(error.position, equals(Duration.zero));
expect(error.caption, equals(Caption.none));
+ expect(error.captionOffset, equals(Duration.zero));
expect(error.buffered, isEmpty);
expect(error.isPlaying, isFalse);
expect(error.isLooping, isFalse);
@@ -694,6 +780,7 @@
const Duration position = Duration(seconds: 1);
const Caption caption = Caption(
text: 'foo', number: 0, start: Duration.zero, end: Duration.zero);
+ const Duration captionOffset = Duration(milliseconds: 250);
final List<DurationRange> buffered = <DurationRange>[
DurationRange(const Duration(seconds: 0), const Duration(seconds: 4))
];
@@ -709,6 +796,7 @@
size: size,
position: position,
caption: caption,
+ captionOffset: captionOffset,
buffered: buffered,
isInitialized: isInitialized,
isPlaying: isPlaying,
@@ -724,6 +812,7 @@
'size: Size(400.0, 300.0), '
'position: 0:00:01.000000, '
'caption: Caption(number: 0, start: 0:00:00.000000, end: 0:00:00.000000, text: foo), '
+ 'captionOffset: 0:00:00.250000, '
'buffered: [DurationRange(start: 0:00:00.000000, end: 0:00:04.000000)], '
'isInitialized: true, '
'isPlaying: true, '