Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit eb6e912

Browse files
[video_player] Added the setCaptionOffset (#3275)
1 parent 411f09e commit eb6e912

File tree

6 files changed

+166
-4
lines changed

6 files changed

+166
-4
lines changed

packages/video_player/video_player/AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,4 @@ Aleksandr Yurkovskiy <sanekyy@gmail.com>
6464
Anton Borries <mail@antonborri.es>
6565
Alex Li <google@alexv525.com>
6666
Rahul Raj <64.rahulraj@gmail.com>
67+
Koen Van Looveren <vanlooverenkoen.dev@gmail.com>

packages/video_player/video_player/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.2.16
2+
3+
* Introduces `setCaptionOffset` to offset the caption display based on a Duration.
4+
15
## 2.2.15
26

37
* Updates README discussion of permissions.

packages/video_player/video_player/example/lib/main.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,17 @@ class _ControlsOverlay extends StatelessWidget {
269269
const _ControlsOverlay({Key? key, required this.controller})
270270
: super(key: key);
271271

272+
static const _exampleCaptionOffsets = [
273+
Duration(seconds: -10),
274+
Duration(seconds: -3),
275+
Duration(seconds: -1, milliseconds: -500),
276+
Duration(milliseconds: -250),
277+
Duration(milliseconds: 0),
278+
Duration(milliseconds: 250),
279+
Duration(seconds: 1, milliseconds: 500),
280+
Duration(seconds: 3),
281+
Duration(seconds: 10),
282+
];
272283
static const _examplePlaybackRates = [
273284
0.25,
274285
0.5,
@@ -308,6 +319,35 @@ class _ControlsOverlay extends StatelessWidget {
308319
controller.value.isPlaying ? controller.pause() : controller.play();
309320
},
310321
),
322+
Align(
323+
alignment: Alignment.topLeft,
324+
child: PopupMenuButton<Duration>(
325+
initialValue: controller.value.captionOffset,
326+
tooltip: 'Caption Offset',
327+
onSelected: (delay) {
328+
controller.setCaptionOffset(delay);
329+
},
330+
itemBuilder: (context) {
331+
return [
332+
for (final offsetDuration in _exampleCaptionOffsets)
333+
PopupMenuItem(
334+
value: offsetDuration,
335+
child: Text('${offsetDuration.inMilliseconds}ms'),
336+
)
337+
];
338+
},
339+
child: Padding(
340+
padding: const EdgeInsets.symmetric(
341+
// Using less vertical padding as the text is also longer
342+
// horizontally, so it feels like it would need more spacing
343+
// horizontally (matching the aspect ratio of the video).
344+
vertical: 12,
345+
horizontal: 16,
346+
),
347+
child: Text('${controller.value.captionOffset.inMilliseconds}ms'),
348+
),
349+
),
350+
),
311351
Align(
312352
alignment: Alignment.topRight,
313353
child: PopupMenuButton<double>(

packages/video_player/video_player/lib/video_player.dart

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class VideoPlayerValue {
4343
this.size = Size.zero,
4444
this.position = Duration.zero,
4545
this.caption = Caption.none,
46+
this.captionOffset = Duration.zero,
4647
this.buffered = const <DurationRange>[],
4748
this.isInitialized = false,
4849
this.isPlaying = false,
@@ -78,6 +79,11 @@ class VideoPlayerValue {
7879
/// [position], this will be a [Caption.none] object.
7980
final Caption caption;
8081

82+
/// The [Duration] that should be used to offset the current [position] to get the correct [Caption].
83+
///
84+
/// Defaults to Duration.zero.
85+
final Duration captionOffset;
86+
8187
/// The currently buffered ranges.
8288
final List<DurationRange> buffered;
8389

@@ -135,6 +141,7 @@ class VideoPlayerValue {
135141
Size? size,
136142
Duration? position,
137143
Caption? caption,
144+
Duration? captionOffset,
138145
List<DurationRange>? buffered,
139146
bool? isInitialized,
140147
bool? isPlaying,
@@ -149,6 +156,7 @@ class VideoPlayerValue {
149156
size: size ?? this.size,
150157
position: position ?? this.position,
151158
caption: caption ?? this.caption,
159+
captionOffset: captionOffset ?? this.captionOffset,
152160
buffered: buffered ?? this.buffered,
153161
isInitialized: isInitialized ?? this.isInitialized,
154162
isPlaying: isPlaying ?? this.isPlaying,
@@ -169,6 +177,7 @@ class VideoPlayerValue {
169177
'size: $size, '
170178
'position: $position, '
171179
'caption: $caption, '
180+
'captionOffset: $captionOffset, '
172181
'buffered: [${buffered.join(', ')}], '
173182
'isInitialized: $isInitialized, '
174183
'isPlaying: $isPlaying, '
@@ -581,6 +590,22 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
581590
await _applyPlaybackSpeed();
582591
}
583592

593+
/// Sets the caption offset.
594+
///
595+
/// The [offset] will be used when getting the correct caption for a specific position.
596+
/// The [offset] can be positive or negative.
597+
///
598+
/// The values will be handled as follows:
599+
/// * 0: This is the default behaviour. No offset will be applied.
600+
/// * >0: The caption will have a negative offset. So you will get caption text from the past.
601+
/// * <0: The caption will have a positive offset. So you will get caption text from the future.
602+
void setCaptionOffset(Duration offset) {
603+
value = value.copyWith(
604+
captionOffset: offset,
605+
caption: _getCaptionAt(value.position),
606+
);
607+
}
608+
584609
/// The closed caption based on the current [position] in the video.
585610
///
586611
/// If there are no closed captions at the current [position], this will
@@ -593,9 +618,10 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
593618
return Caption.none;
594619
}
595620

621+
final delayedPosition = position + value.captionOffset;
596622
// TODO: This would be more efficient as a binary search.
597623
for (final caption in _closedCaptionFile!.captions) {
598-
if (caption.start <= position && caption.end >= position) {
624+
if (caption.start <= delayedPosition && caption.end >= delayedPosition) {
599625
return caption;
600626
}
601627
}
@@ -604,8 +630,10 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
604630
}
605631

606632
void _updatePosition(Duration position) {
607-
value = value.copyWith(position: position);
608-
value = value.copyWith(caption: _getCaptionAt(position));
633+
value = value.copyWith(
634+
position: position,
635+
caption: _getCaptionAt(position),
636+
);
609637
}
610638

611639
@override

packages/video_player/video_player/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter
33
widgets on Android, iOS, and web.
44
repository: https://github.com/flutter/plugins/tree/main/packages/video_player/video_player
55
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
6-
version: 2.2.15
6+
version: 2.2.16
77

88
environment:
99
sdk: ">=2.14.0 <3.0.0"

packages/video_player/video_player/test/video_player_test.dart

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ class FakeController extends ValueNotifier<VideoPlayerValue>
6969

7070
@override
7171
VideoPlayerOptions? get videoPlayerOptions => null;
72+
73+
@override
74+
void setCaptionOffset(Duration delay) {}
7275
}
7376

7477
Future<ClosedCaptionFile> _loadClosedCaption() async =>
@@ -557,11 +560,92 @@ void main() {
557560
await controller.seekTo(const Duration(milliseconds: 300));
558561
expect(controller.value.caption.text, 'two');
559562

563+
await controller.seekTo(const Duration(milliseconds: 301));
564+
expect(controller.value.caption.text, 'two');
565+
566+
await controller.seekTo(const Duration(milliseconds: 500));
567+
expect(controller.value.caption.text, '');
568+
569+
await controller.seekTo(const Duration(milliseconds: 300));
570+
expect(controller.value.caption.text, 'two');
571+
572+
await controller.seekTo(const Duration(milliseconds: 301));
573+
expect(controller.value.caption.text, 'two');
574+
});
575+
576+
test('works when seeking with captionOffset positive', () async {
577+
final VideoPlayerController controller = VideoPlayerController.network(
578+
'https://127.0.0.1',
579+
closedCaptionFile: _loadClosedCaption(),
580+
);
581+
582+
await controller.initialize();
583+
controller.setCaptionOffset(Duration(milliseconds: 100));
584+
expect(controller.value.position, const Duration());
585+
expect(controller.value.caption.text, '');
586+
587+
await controller.seekTo(const Duration(milliseconds: 100));
588+
expect(controller.value.caption.text, 'one');
589+
590+
await controller.seekTo(const Duration(milliseconds: 101));
591+
expect(controller.value.caption.text, '');
592+
593+
await controller.seekTo(const Duration(milliseconds: 250));
594+
expect(controller.value.caption.text, 'two');
595+
596+
await controller.seekTo(const Duration(milliseconds: 300));
597+
expect(controller.value.caption.text, 'two');
598+
599+
await controller.seekTo(const Duration(milliseconds: 301));
600+
expect(controller.value.caption.text, '');
601+
560602
await controller.seekTo(const Duration(milliseconds: 500));
561603
expect(controller.value.caption.text, '');
562604

563605
await controller.seekTo(const Duration(milliseconds: 300));
564606
expect(controller.value.caption.text, 'two');
607+
608+
await controller.seekTo(const Duration(milliseconds: 301));
609+
expect(controller.value.caption.text, '');
610+
});
611+
612+
test('works when seeking with captionOffset negative', () async {
613+
final VideoPlayerController controller = VideoPlayerController.network(
614+
'https://127.0.0.1',
615+
closedCaptionFile: _loadClosedCaption(),
616+
);
617+
618+
await controller.initialize();
619+
controller.setCaptionOffset(Duration(milliseconds: -100));
620+
expect(controller.value.position, const Duration());
621+
expect(controller.value.caption.text, '');
622+
623+
await controller.seekTo(const Duration(milliseconds: 100));
624+
expect(controller.value.caption.text, '');
625+
626+
await controller.seekTo(const Duration(milliseconds: 200));
627+
expect(controller.value.caption.text, 'one');
628+
629+
await controller.seekTo(const Duration(milliseconds: 250));
630+
expect(controller.value.caption.text, 'one');
631+
632+
await controller.seekTo(const Duration(milliseconds: 300));
633+
expect(controller.value.caption.text, 'one');
634+
635+
await controller.seekTo(const Duration(milliseconds: 301));
636+
expect(controller.value.caption.text, '');
637+
638+
await controller.seekTo(const Duration(milliseconds: 400));
639+
expect(controller.value.caption.text, 'two');
640+
641+
await controller.seekTo(const Duration(milliseconds: 500));
642+
expect(controller.value.caption.text, 'two');
643+
644+
await controller.seekTo(const Duration(milliseconds: 600));
645+
expect(controller.value.caption.text, '');
646+
647+
await controller.seekTo(const Duration(milliseconds: 300));
648+
expect(controller.value.caption.text, 'one');
565649
});
566650
});
567651

@@ -655,6 +739,7 @@ void main() {
655739
expect(uninitialized.duration, equals(Duration.zero));
656740
expect(uninitialized.position, equals(Duration.zero));
657741
expect(uninitialized.caption, equals(Caption.none));
742+
expect(uninitialized.captionOffset, equals(Duration.zero));
658743
expect(uninitialized.buffered, isEmpty);
659744
expect(uninitialized.isPlaying, isFalse);
660745
expect(uninitialized.isLooping, isFalse);
@@ -675,6 +760,7 @@ void main() {
675760
expect(error.duration, equals(Duration.zero));
676761
expect(error.position, equals(Duration.zero));
677762
expect(error.caption, equals(Caption.none));
763+
expect(error.captionOffset, equals(Duration.zero));
678764
expect(error.buffered, isEmpty);
679765
expect(error.isPlaying, isFalse);
680766
expect(error.isLooping, isFalse);
@@ -694,6 +780,7 @@ void main() {
694780
const Duration position = Duration(seconds: 1);
695781
const Caption caption = Caption(
696782
text: 'foo', number: 0, start: Duration.zero, end: Duration.zero);
783+
const Duration captionOffset = Duration(milliseconds: 250);
697784
final List<DurationRange> buffered = <DurationRange>[
698785
DurationRange(const Duration(seconds: 0), const Duration(seconds: 4))
699786
];
@@ -709,6 +796,7 @@ void main() {
709796
size: size,
710797
position: position,
711798
caption: caption,
799+
captionOffset: captionOffset,
712800
buffered: buffered,
713801
isInitialized: isInitialized,
714802
isPlaying: isPlaying,
@@ -724,6 +812,7 @@ void main() {
724812
'size: Size(400.0, 300.0), '
725813
'position: 0:00:01.000000, '
726814
'caption: Caption(number: 0, start: 0:00:00.000000, end: 0:00:00.000000, text: foo), '
815+
'captionOffset: 0:00:00.250000, '
727816
'buffered: [DurationRange(start: 0:00:00.000000, end: 0:00:04.000000)], '
728817
'isInitialized: true, '
729818
'isPlaying: true, '

0 commit comments

Comments
 (0)