-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[video_player] VTT Support #2878
Changes from 2 commits
9788b63
a66f5d7
6481331
8f8b095
da1c6e4
1a21621
0d3549e
b018ea9
b92f305
1573670
1e53fc5
9034263
d9647a9
2ebf99f
43abfd6
a44edea
d92e820
10e717c
98e21c1
e146366
c05a6f1
50044cc
2aacc0f
928bfb4
ee9e13a
bf3e958
91bba0f
459b7f8
b43b1a8
db547b6
7864399
7afa2c2
a8a5994
0fdc514
1ca27a7
39ba0ae
7391c73
00efa43
8e1dcad
9fdcb05
f0e286b
1350f8e
c72a068
226b9c6
4554771
0b7dde4
7aa17ba
2f686c2
5d09170
78098ca
46aecd8
cacf4dc
5524931
110c115
808b1a0
a041f54
2a16e2e
acf7417
de3c973
495b37c
fc0d049
9db4ec1
c5bbba6
7bb0d53
d47ea7f
4c66c38
db79449
fa0d6dc
a4fcbf7
61a22a5
14468e5
0d68872
c97876c
6c457a5
5ffff31
c8f7c2c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
WEBVTT | ||
|
||
00:00.500 --> 00:01.000 | ||
<v Roger Bingham>We are in New York City | ||
|
||
00:01.200 --> 00:02.000 | ||
<v Roger Bingham>We’re actually at the Lucern Hotel, just down the street | ||
|
||
00:16.000 --> 00:18.000 | ||
<v Roger Bingham>from the American Museum of Natural History | ||
|
||
00:18.000 --> 00:20.000 | ||
<v Roger Bingham>And with me is Neil deGrasse Tyson | ||
|
||
00:20.000 --> 00:22.000 | ||
<v Roger Bingham>Astrophysicist, Director of the Hayden Planetarium | ||
|
||
00:22.000 --> 00:24.000 | ||
<v Roger Bingham>at the AMNH. | ||
|
||
00:24.000 --> 00:26.000 | ||
<v Roger Bingham>Thank you for walking down here. | ||
|
||
00:27.000 --> 00:30.000 | ||
<v Roger Bingham>And I want to do a follow-up on the last conversation we did. | ||
|
||
00:30.000 --> 00:31.500 align:right size:50% | ||
<v Roger Bingham>When we e-mailed— | ||
|
||
00:30.500 --> 00:32.500 align:left size:50% | ||
<v Neil deGrasse Tyson>Didn’t we talk about enough in that conversation? | ||
|
||
00:32.000 --> 00:35.500 align:right size:50% | ||
<v Roger Bingham>No! No no no no; 'cos 'cos obviously 'cos | ||
|
||
00:32.500 --> 00:33.500 align:left size:50% | ||
<v Neil deGrasse Tyson><i>Laughs</i> | ||
|
||
00:35.500 --> 00:38.000 | ||
<v Roger Bingham>You know I’m so excited my glasses are falling off here. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
// Copyright 2020 The Chromium Authors. All rights reserved. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This needs to be "Copyright 2013 The Flutter Authors." in both new files, which is why There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
// Use of this source code is governed by a BSD-style license that can be | ||
// found in the LICENSE file. | ||
|
||
import 'dart:convert'; | ||
cyanglaz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
import 'closed_caption_file.dart'; | ||
import 'package:html/parser.dart'; | ||
ferrazrx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/// Represents a [ClosedCaptionFile], parsed from the WebVTT file format. | ||
/// See: https://en.wikipedia.org/wiki/WebVTT | ||
class WebVTTCaptionFile extends ClosedCaptionFile { | ||
ferrazrx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// Parses a string into a [ClosedCaptionFile], assuming [fileContents] is in | ||
/// the WebVTT file format. | ||
/// * See: https://en.wikipedia.org/wiki/WebVTT | ||
WebVTTCaptionFile(this.fileContents) | ||
: _captions = _parseCaptionsFromWebVTTString(fileContents); | ||
|
||
/// The entire body of the VTT file. | ||
final String fileContents; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this public? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was probably a copy paste from SubRipCaptionFile in sub_rip.dart. Removed I'm not sure how to keep track of this tho. We might forgot about this when doing breaking changes in the plugin. |
||
|
||
@override | ||
List<Caption> get captions => _captions; | ||
|
||
final List<Caption> _captions; | ||
} | ||
|
||
List<Caption> _parseCaptionsFromWebVTTString(String file) { | ||
final List<Caption> captions = <Caption>[]; | ||
int number = 1; | ||
ferrazrx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for (List<String> captionLines in _readWebVTTFile(file)) { | ||
if (captionLines.length < 2) continue; | ||
cyanglaz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
print(captionLines); | ||
ferrazrx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
final int captionNumber = number; | ||
final _StartAndEnd startAndEnd = | ||
_StartAndEnd.fromWebVTTString(captionLines[0]); | ||
|
||
final String text = captionLines.sublist(1).join('\n'); | ||
|
||
//TODO: Handle text format | ||
ferrazrx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
final String textWithoutFormat = _parseHtmlString(text); | ||
|
||
final Caption newCaption = Caption( | ||
number: captionNumber, | ||
start: startAndEnd.start, | ||
end: startAndEnd.end, | ||
text: textWithoutFormat, | ||
); | ||
|
||
if (newCaption.start != null && newCaption.end != null) { | ||
captions.add(newCaption); | ||
number++; | ||
} | ||
} | ||
|
||
return captions; | ||
} | ||
|
||
class _StartAndEnd { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a weird name; could we call it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another copy paste from sub_rip, will fix it there too |
||
final Duration start; | ||
final Duration end; | ||
|
||
_StartAndEnd(this.start, this.end); | ||
|
||
// Assumes format from an VTT file. | ||
// For example: | ||
// 00:09.000 --> 00:11.000 | ||
static _StartAndEnd fromWebVTTString(String line) { | ||
final RegExp format = | ||
RegExp(_webVTTTimeStamp + _webVTTArrow + _webVTTTimeStamp); | ||
|
||
if (!format.hasMatch(line)) { | ||
return _StartAndEnd(null, null); | ||
} | ||
|
||
final List<String> times = line.split(_webVTTArrow); | ||
|
||
final Duration start = _parseWebVTTTimestamp(times[0]); | ||
final Duration end = _parseWebVTTTimestamp(times[1]); | ||
|
||
return _StartAndEnd(start, end); | ||
} | ||
} | ||
|
||
String _parseHtmlString(String htmlString) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's call this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
var document = parse(htmlString); | ||
ferrazrx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
String parsedString = parse(document.body.text).documentElement.text; | ||
ferrazrx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return parsedString; | ||
} | ||
|
||
// Parses a time stamp in an VTT file into a Duration. | ||
// For example: | ||
// | ||
// _parseWebVTTimestamp('00:01:08.430') | ||
// returns | ||
// Duration(hours: 0, minutes: 1, seconds: 8, milliseconds: 430) | ||
cyanglaz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Duration _parseWebVTTTimestamp(String timestampString) { | ||
if (!RegExp(_webVTTTimeStamp).hasMatch(timestampString)) { | ||
return null; | ||
} | ||
|
||
final List<String> dotSections = timestampString.split('.'); | ||
final List<String> hoursMinutesSeconds = dotSections[0].split(':'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
|
||
int hours = 0; | ||
int minutes = 0; | ||
int seconds = 0; | ||
List<String> styles; | ||
|
||
if (hoursMinutesSeconds.length > 2) { | ||
ferrazrx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Timestamp takes the form of [hours]:[minutes]:[seconds].[milliseconds] | ||
hours = int.parse(hoursMinutesSeconds[0]); | ||
minutes = int.parse(hoursMinutesSeconds[1]); | ||
seconds = int.parse(hoursMinutesSeconds[2]); | ||
} else if (int.parse(hoursMinutesSeconds[0]) > 59) { | ||
// Timestamp takes the form of [hours]:[minutes].[milliseconds] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seemed deeply strange to me so I checked the spec, and this isn't a valid timestamp. We shouldn't handle invalid values unless there's substantial real-world evidence that lots of people have written this (which seems unlikely, because even if other parsers allow this it is incapable of expressing any time within the first 59 hours of a video) |
||
// First position is hours as it's over 59. | ||
hours = int.parse(hoursMinutesSeconds[0]); | ||
minutes = int.parse(hoursMinutesSeconds[1]); | ||
} else { | ||
// Timestamp takes the form of [minutes]:[seconds].[milliseconds] | ||
minutes = int.parse(hoursMinutesSeconds[0]); | ||
seconds = int.parse(hoursMinutesSeconds[1]); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Once the above is removed, this can all condense to:
It would be good to add a safety check that the length is either 2 or 3 and returns null first, as well. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
|
||
List<String> milisecondsStyles = dotSections[1].split(" "); | ||
//TODO: Handle styles data on timestamp | ||
if (milisecondsStyles.length > 1) { | ||
styles = milisecondsStyles.sublist(1); | ||
} | ||
int milliseconds = int.parse(milisecondsStyles[0]); | ||
|
||
return Duration( | ||
hours: hours, | ||
minutes: minutes, | ||
seconds: seconds, | ||
milliseconds: milliseconds, | ||
); | ||
} | ||
|
||
// Reads on VTT file and splits it into Lists of strings where each list is one | ||
// caption. | ||
List<List<String>> _readWebVTTFile(String file) { | ||
final List<String> lines = LineSplitter.split(file).toList(); | ||
|
||
final List<List<String>> captionStrings = <List<String>>[]; | ||
List<String> currentCaption = <String>[]; | ||
int lineIndex = 0; | ||
for (final String line in lines) { | ||
final bool isLineBlank = line.trim().isEmpty; | ||
if (!isLineBlank) { | ||
currentCaption.add(line); | ||
} | ||
|
||
if (isLineBlank || lineIndex == lines.length - 1) { | ||
captionStrings.add(currentCaption); | ||
currentCaption = <String>[]; | ||
} | ||
|
||
lineIndex += 1; | ||
} | ||
|
||
return captionStrings; | ||
} | ||
|
||
const String _webVTTTimeStamp = r'(\d+):(\d{2})(:\d{2})?\.(\d{3})'; | ||
const String _webVTTArrow = r' --> '; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
// Copyright 2020 The Chromium Authors. All rights reserved. | ||
ferrazrx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Use of this source code is governed by a BSD-style license that can be | ||
// found in the LICENSE file. | ||
|
||
import 'package:flutter_test/flutter_test.dart'; | ||
import 'package:video_player/src/closed_caption_file.dart'; | ||
import 'package:video_player/video_player.dart'; | ||
|
||
void main() { | ||
test('Parses VTT file', () { | ||
final WebVTTCaptionFile parsedFile = WebVTTCaptionFile(_validVTT); | ||
|
||
expect(parsedFile.captions.length, 13); | ||
|
||
final Caption firstCaption = parsedFile.captions.first; | ||
expect(firstCaption.number, 1); | ||
expect(firstCaption.start, Duration(seconds: 9)); | ||
expect(firstCaption.end, Duration(seconds: 11, milliseconds: 430)); | ||
expect(firstCaption.text, 'We are in New York City'); | ||
|
||
final Caption secondCaption = parsedFile.captions[1]; | ||
expect(secondCaption.number, 2); | ||
expect( | ||
secondCaption.start, | ||
Duration(minutes: 0, seconds: 13, milliseconds: 0), | ||
); | ||
expect( | ||
secondCaption.end, | ||
Duration(minutes: 0, seconds: 16, milliseconds: 0), | ||
); | ||
expect(secondCaption.text, | ||
"We're actually at the Lucern Hotel, just down the street"); | ||
|
||
//With styles on timestamp | ||
final Caption lastCaption = parsedFile.captions[12]; | ||
expect(lastCaption.number, 13); | ||
expect( | ||
lastCaption.start, | ||
Duration(minutes: 0, seconds: 35, milliseconds: 500), | ||
); | ||
expect( | ||
lastCaption.end, | ||
Duration(minutes: 0, seconds: 38, milliseconds: 0), | ||
); | ||
expect(lastCaption.text, | ||
"You know I'm so excited my glasses are falling off here."); | ||
}); | ||
|
||
test('Parses VTT file with malformed input', () { | ||
final ClosedCaptionFile parsedFile = WebVTTCaptionFile(_malformedVTT); | ||
|
||
expect(parsedFile.captions.length, 1); | ||
|
||
final Caption firstCaption = parsedFile.captions.single; | ||
expect(firstCaption.number, 1); | ||
expect(firstCaption.start, Duration(seconds: 13)); | ||
expect(firstCaption.end, Duration(seconds: 16, milliseconds: 0)); | ||
expect(firstCaption.text, 'Valid'); | ||
}); | ||
} | ||
|
||
const String _validVTT = ''' | ||
WEBVTT Kind: captions; Language: en | ||
|
||
00:09.000 --> 00:11.430 | ||
<v Roger Bingham>We are in New York City | ||
|
||
00:13.000 --> 00:16.000 | ||
<v Roger Bingham>We're actually at the Lucern Hotel, just down the street | ||
|
||
00:16.000 --> 00:18.000 | ||
<v Roger Bingham>from the American Museum of Natural History | ||
|
||
00:18.000 --> 00:20.000 | ||
<v Roger Bingham>And with me is Neil deGrasse Tyson | ||
|
||
00:20.000 --> 00:22.000 | ||
<v Roger Bingham>Astrophysicist, Director of the Hayden Planetarium | ||
|
||
00:22.000 --> 00:24.000 | ||
<v Roger Bingham>at the AMNH. | ||
|
||
00:24.000 --> 00:26.000 | ||
<v Roger Bingham>Thank you for walking down here. | ||
|
||
00:27.000 --> 00:30.000 | ||
<v Roger Bingham>And I want to do a follow-up on the last conversation we did. | ||
|
||
00:30.000 --> 00:31.500 align:end size:50% | ||
<v Roger Bingham>When we e-mailed— | ||
|
||
00:30.500 --> 00:32.500 align:start size:50% | ||
<v Neil deGrasse Tyson>Didn't we talk about enough in that conversation? | ||
|
||
00:32.000 --> 00:35.500 align:end size:50% | ||
<v Roger Bingham>No! No no no no; 'cos 'cos obviously 'cos | ||
|
||
00:32.500 --> 00:33.500 align:start size:50% | ||
<v Neil deGrasse Tyson><i>Laughs</i> | ||
|
||
00:35.500 --> 00:38.000 align:start size:50% | ||
<v Roger Bingham>You know I'm so excited my glasses are falling off here. | ||
|
||
'''; | ||
|
||
const String _malformedVTT = ''' | ||
|
||
WEBVTT Kind: captions; Language: en | ||
|
||
00:09.000--> 00:11.430 | ||
<Test>This one should be ignored because the arrow needs a space. | ||
|
||
00:13.000 --> 00:16.000 | ||
<Test>Valid | ||
|
||
00:16.000 --> 00:8.000 | ||
<Test>This one should be ignored because the time is missing a digit. | ||
|
||
'''; |
Uh oh!
There was an error while loading. Please reload this page.