-
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. | ||
|
||
| // 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.
Outdated
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; | ||
|
||
|
|
||
| @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.
Outdated
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.
Outdated
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.
Outdated
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 { | ||
|
||
| 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) { | ||
|
||
| var document = parse(htmlString); | ||
ferrazrx marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| String parsedString = parse(document.body.text).documentElement.text; | ||
ferrazrx marked this conversation as resolved.
Outdated
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(':'); | ||
|
||
|
|
||
| int hours = 0; | ||
| int minutes = 0; | ||
| int seconds = 0; | ||
| List<String> styles; | ||
|
|
||
| if (hoursMinutesSeconds.length > 2) { | ||
ferrazrx marked this conversation as resolved.
Outdated
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] | ||
|
||
| // 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]); | ||
| } | ||
|
Contributor
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.
Contributor
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.
Outdated
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.