Skip to content

Commit ed2e7a7

Browse files
authored
[video_player] VTT Support (#2878)
1 parent 42a17dc commit ed2e7a7

File tree

9 files changed

+507
-15
lines changed

9 files changed

+507
-15
lines changed

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.5
2+
3+
* Support to closed caption WebVTT format added.
4+
15
## 2.2.4
26

37
* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
WEBVTT
2+
3+
00:00:00.200 --> 00:00:01.750
4+
[ Birds chirping ]
5+
6+
00:00:02.300 --> 00:00:05.000
7+
[ Buzzing ]

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,9 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> {
210210

211211
Future<ClosedCaptionFile> _loadCaptions() async {
212212
final String fileContents = await DefaultAssetBundle.of(context)
213-
.loadString('assets/bumble_bee_captions.srt');
214-
return SubRipCaptionFile(fileContents);
213+
.loadString('assets/bumble_bee_captions.vtt');
214+
return WebVTTCaptionFile(
215+
fileContents); // For vtt files, use WebVTTCaptionFile
215216
}
216217

217218
@override

packages/video_player/video_player/example/pubspec.yaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ dev_dependencies:
3030
flutter:
3131
uses-material-design: true
3232
assets:
33-
- assets/flutter-mark-square-64.png
34-
- assets/Butterfly-209.mp4
35-
- assets/bumble_bee_captions.srt
33+
- assets/flutter-mark-square-64.png
34+
- assets/Butterfly-209.mp4
35+
- assets/bumble_bee_captions.srt
36+
- assets/bumble_bee_captions.vtt

packages/video_player/video_player/lib/src/closed_caption_file.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import 'sub_rip.dart';
66
export 'sub_rip.dart' show SubRipCaptionFile;
77

8+
import 'web_vtt.dart';
9+
export 'web_vtt.dart' show WebVTTCaptionFile;
10+
811
/// A structured representation of a parsed closed caption file.
912
///
1013
/// A closed caption file includes a list of captions, each with a start and end
@@ -15,6 +18,7 @@ export 'sub_rip.dart' show SubRipCaptionFile;
1518
///
1619
/// See:
1720
/// * [SubRipCaptionFile].
21+
/// * [WebVTTCaptionFile].
1822
abstract class ClosedCaptionFile {
1923
/// The full list of captions from a given file.
2024
///

packages/video_player/video_player/lib/src/sub_rip.dart

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ class SubRipCaptionFile extends ClosedCaptionFile {
1616
: _captions = _parseCaptionsFromSubRipString(fileContents);
1717

1818
/// The entire body of the SubRip file.
19+
// TODO(cyanglaz): Remove this public member as it doesn't seem need to exist.
20+
// https://github.com/flutter/flutter/issues/90471
1921
final String fileContents;
2022

2123
@override
@@ -30,15 +32,15 @@ List<Caption> _parseCaptionsFromSubRipString(String file) {
3032
if (captionLines.length < 3) break;
3133

3234
final int captionNumber = int.parse(captionLines[0]);
33-
final _StartAndEnd startAndEnd =
34-
_StartAndEnd.fromSubRipString(captionLines[1]);
35+
final _CaptionRange captionRange =
36+
_CaptionRange.fromSubRipString(captionLines[1]);
3537

3638
final String text = captionLines.sublist(2).join('\n');
3739

3840
final Caption newCaption = Caption(
3941
number: captionNumber,
40-
start: startAndEnd.start,
41-
end: startAndEnd.end,
42+
start: captionRange.start,
43+
end: captionRange.end,
4244
text: text,
4345
);
4446
if (newCaption.start != newCaption.end) {
@@ -49,29 +51,29 @@ List<Caption> _parseCaptionsFromSubRipString(String file) {
4951
return captions;
5052
}
5153

52-
class _StartAndEnd {
54+
class _CaptionRange {
5355
final Duration start;
5456
final Duration end;
5557

56-
_StartAndEnd(this.start, this.end);
58+
_CaptionRange(this.start, this.end);
5759

5860
// Assumes format from an SubRip file.
5961
// For example:
6062
// 00:01:54,724 --> 00:01:56,760
61-
static _StartAndEnd fromSubRipString(String line) {
63+
static _CaptionRange fromSubRipString(String line) {
6264
final RegExp format =
6365
RegExp(_subRipTimeStamp + _subRipArrow + _subRipTimeStamp);
6466

6567
if (!format.hasMatch(line)) {
66-
return _StartAndEnd(Duration.zero, Duration.zero);
68+
return _CaptionRange(Duration.zero, Duration.zero);
6769
}
6870

6971
final List<String> times = line.split(_subRipArrow);
7072

7173
final Duration start = _parseSubRipTimestamp(times[0]);
7274
final Duration end = _parseSubRipTimestamp(times[1]);
7375

74-
return _StartAndEnd(start, end);
76+
return _CaptionRange(start, end);
7577
}
7678
}
7779

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:convert';
6+
7+
import 'package:html/dom.dart';
8+
9+
import 'closed_caption_file.dart';
10+
import 'package:html/parser.dart' as html_parser;
11+
12+
/// Represents a [ClosedCaptionFile], parsed from the WebVTT file format.
13+
/// See: https://en.wikipedia.org/wiki/WebVTT
14+
class WebVTTCaptionFile extends ClosedCaptionFile {
15+
/// Parses a string into a [ClosedCaptionFile], assuming [fileContents] is in
16+
/// the WebVTT file format.
17+
/// * See: https://en.wikipedia.org/wiki/WebVTT
18+
WebVTTCaptionFile(String fileContents)
19+
: _captions = _parseCaptionsFromWebVTTString(fileContents);
20+
21+
@override
22+
List<Caption> get captions => _captions;
23+
24+
final List<Caption> _captions;
25+
}
26+
27+
List<Caption> _parseCaptionsFromWebVTTString(String file) {
28+
final List<Caption> captions = <Caption>[];
29+
30+
// Ignore metadata
31+
Set<String> metadata = {'HEADER', 'NOTE', 'REGION', 'WEBVTT'};
32+
33+
int captionNumber = 1;
34+
for (List<String> captionLines in _readWebVTTFile(file)) {
35+
// CaptionLines represent a complete caption.
36+
// E.g
37+
// [
38+
// [00:00.000 --> 01:24.000 align:center]
39+
// ['Introduction']
40+
// ]
41+
// If caption has just header or time, but no text, `captionLines.length` will be 1.
42+
if (captionLines.length < 2) continue;
43+
44+
// If caption has header equal metadata, ignore.
45+
String metadaType = captionLines[0].split(' ')[0];
46+
if (metadata.contains(metadaType)) continue;
47+
48+
// Caption has header
49+
bool hasHeader = captionLines.length > 2;
50+
if (hasHeader) {
51+
final int? tryParseCaptionNumber = int.tryParse(captionLines[0]);
52+
if (tryParseCaptionNumber != null) {
53+
captionNumber = tryParseCaptionNumber;
54+
}
55+
}
56+
57+
final _CaptionRange? captionRange = _CaptionRange.fromWebVTTString(
58+
hasHeader ? captionLines[1] : captionLines[0],
59+
);
60+
61+
if (captionRange == null) {
62+
continue;
63+
}
64+
65+
final String text = captionLines.sublist(hasHeader ? 2 : 1).join('\n');
66+
67+
// TODO(cyanglaz): Handle special syntax in VTT captions.
68+
// https://github.com/flutter/flutter/issues/90007.
69+
final String textWithoutFormat = _extractTextFromHtml(text);
70+
71+
final Caption newCaption = Caption(
72+
number: captionNumber,
73+
start: captionRange.start,
74+
end: captionRange.end,
75+
text: textWithoutFormat,
76+
);
77+
captions.add(newCaption);
78+
captionNumber++;
79+
}
80+
81+
return captions;
82+
}
83+
84+
class _CaptionRange {
85+
final Duration start;
86+
final Duration end;
87+
88+
_CaptionRange(this.start, this.end);
89+
90+
// Assumes format from an VTT file.
91+
// For example:
92+
// 00:09.000 --> 00:11.000
93+
static _CaptionRange? fromWebVTTString(String line) {
94+
final RegExp format =
95+
RegExp(_webVTTTimeStamp + _webVTTArrow + _webVTTTimeStamp);
96+
97+
if (!format.hasMatch(line)) {
98+
return null;
99+
}
100+
101+
final List<String> times = line.split(_webVTTArrow);
102+
103+
final Duration? start = _parseWebVTTTimestamp(times[0]);
104+
final Duration? end = _parseWebVTTTimestamp(times[1]);
105+
106+
if (start == null || end == null) {
107+
return null;
108+
}
109+
110+
return _CaptionRange(start, end);
111+
}
112+
}
113+
114+
String _extractTextFromHtml(String htmlString) {
115+
final Document document = html_parser.parse(htmlString);
116+
final Element? body = document.body;
117+
if (body == null) {
118+
return '';
119+
}
120+
final Element? bodyElement = html_parser.parse(body.text).documentElement;
121+
return bodyElement?.text ?? '';
122+
}
123+
124+
// Parses a time stamp in an VTT file into a Duration.
125+
//
126+
// Returns `null` if `timestampString` is in an invalid format.
127+
//
128+
// For example:
129+
//
130+
// _parseWebVTTTimestamp('00:01:08.430')
131+
// returns
132+
// Duration(hours: 0, minutes: 1, seconds: 8, milliseconds: 430)
133+
Duration? _parseWebVTTTimestamp(String timestampString) {
134+
if (!RegExp(_webVTTTimeStamp).hasMatch(timestampString)) {
135+
return null;
136+
}
137+
138+
final List<String> dotSections = timestampString.split('.');
139+
final List<String> timeComponents = dotSections[0].split(':');
140+
141+
// Validating and parsing the `timestampString`, invalid format will result this method
142+
// to return `null`. See https://www.w3.org/TR/webvtt1/#webvtt-timestamp for valid
143+
// WebVTT timestamp format.
144+
if (timeComponents.length > 3 || timeComponents.length < 2) {
145+
return null;
146+
}
147+
int hours = 0;
148+
if (timeComponents.length == 3) {
149+
final String hourString = timeComponents.removeAt(0);
150+
if (hourString.length < 2) {
151+
return null;
152+
}
153+
hours = int.parse(hourString);
154+
}
155+
final int minutes = int.parse(timeComponents.removeAt(0));
156+
if (minutes < 0 || minutes > 59) {
157+
return null;
158+
}
159+
final int seconds = int.parse(timeComponents.removeAt(0));
160+
if (seconds < 0 || seconds > 59) {
161+
return null;
162+
}
163+
164+
List<String> milisecondsStyles = dotSections[1].split(" ");
165+
166+
// TODO(cyanglaz): Handle caption styles.
167+
// https://github.com/flutter/flutter/issues/90009.
168+
// ```dart
169+
// if (milisecondsStyles.length > 1) {
170+
// List<String> styles = milisecondsStyles.sublist(1);
171+
// }
172+
// ```
173+
// For a better readable code style, style parsing should happen before
174+
// calling this method. See: https://github.com/flutter/plugins/pull/2878/files#r713381134.
175+
int milliseconds = int.parse(milisecondsStyles[0]);
176+
177+
return Duration(
178+
hours: hours,
179+
minutes: minutes,
180+
seconds: seconds,
181+
milliseconds: milliseconds,
182+
);
183+
}
184+
185+
// Reads on VTT file and splits it into Lists of strings where each list is one
186+
// caption.
187+
List<List<String>> _readWebVTTFile(String file) {
188+
final List<String> lines = LineSplitter.split(file).toList();
189+
190+
final List<List<String>> captionStrings = <List<String>>[];
191+
List<String> currentCaption = <String>[];
192+
int lineIndex = 0;
193+
for (final String line in lines) {
194+
final bool isLineBlank = line.trim().isEmpty;
195+
if (!isLineBlank) {
196+
currentCaption.add(line);
197+
}
198+
199+
if (isLineBlank || lineIndex == lines.length - 1) {
200+
captionStrings.add(currentCaption);
201+
currentCaption = <String>[];
202+
}
203+
204+
lineIndex += 1;
205+
}
206+
207+
return captionStrings;
208+
}
209+
210+
const String _webVTTTimeStamp = r'(\d+):(\d{2})(:\d{2})?\.(\d{3})';
211+
const String _webVTTArrow = r' --> ';

packages/video_player/video_player/pubspec.yaml

Lines changed: 2 additions & 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/master/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.4
6+
version: 2.2.5
77

88
environment:
99
sdk: ">=2.14.0 <3.0.0"
@@ -32,6 +32,7 @@ dependencies:
3232
# TODO(amirh): Revisit this (either update this part in the design or the pub tool).
3333
# https://github.com/flutter/flutter/issues/46264
3434
video_player_web: ^2.0.0
35+
html: ^0.15.0
3536

3637
dev_dependencies:
3738
flutter_test:

0 commit comments

Comments
 (0)