-
Notifications
You must be signed in to change notification settings - Fork 9.8k
[video_player] VTT Support #2878
Changes from all 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,7 @@ | ||
WEBVTT | ||
|
||
00:00:00.200 --> 00:00:01.750 | ||
[ Birds chirping ] | ||
|
||
00:00:02.300 --> 00:00:05.000 | ||
[ Buzzing ] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
// Copyright 2013 The Flutter 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'; | ||
|
||
import 'package:html/dom.dart'; | ||
|
||
import 'closed_caption_file.dart'; | ||
import 'package:html/parser.dart' as html_parser; | ||
|
||
/// 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(String fileContents) | ||
: _captions = _parseCaptionsFromWebVTTString(fileContents); | ||
|
||
@override | ||
List<Caption> get captions => _captions; | ||
|
||
final List<Caption> _captions; | ||
} | ||
|
||
List<Caption> _parseCaptionsFromWebVTTString(String file) { | ||
final List<Caption> captions = <Caption>[]; | ||
|
||
// Ignore metadata | ||
Set<String> metadata = {'HEADER', 'NOTE', 'REGION', 'WEBVTT'}; | ||
|
||
int captionNumber = 1; | ||
for (List<String> captionLines in _readWebVTTFile(file)) { | ||
// CaptionLines represent a complete caption. | ||
// E.g | ||
// [ | ||
// [00:00.000 --> 01:24.000 align:center] | ||
// ['Introduction'] | ||
// ] | ||
// If caption has just header or time, but no text, `captionLines.length` will be 1. | ||
if (captionLines.length < 2) continue; | ||
cyanglaz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// If caption has header equal metadata, ignore. | ||
String metadaType = captionLines[0].split(' ')[0]; | ||
if (metadata.contains(metadaType)) continue; | ||
|
||
// Caption has header | ||
bool hasHeader = captionLines.length > 2; | ||
if (hasHeader) { | ||
final int? tryParseCaptionNumber = int.tryParse(captionLines[0]); | ||
if (tryParseCaptionNumber != null) { | ||
captionNumber = tryParseCaptionNumber; | ||
} | ||
} | ||
|
||
final _CaptionRange? captionRange = _CaptionRange.fromWebVTTString( | ||
hasHeader ? captionLines[1] : captionLines[0], | ||
); | ||
|
||
if (captionRange == null) { | ||
continue; | ||
} | ||
|
||
final String text = captionLines.sublist(hasHeader ? 2 : 1).join('\n'); | ||
|
||
// TODO(cyanglaz): Handle special syntax in VTT captions. | ||
// https://github.com/flutter/flutter/issues/90007. | ||
final String textWithoutFormat = _extractTextFromHtml(text); | ||
|
||
final Caption newCaption = Caption( | ||
number: captionNumber, | ||
start: captionRange.start, | ||
end: captionRange.end, | ||
text: textWithoutFormat, | ||
); | ||
captions.add(newCaption); | ||
captionNumber++; | ||
} | ||
|
||
return captions; | ||
} | ||
|
||
class _CaptionRange { | ||
final Duration start; | ||
final Duration end; | ||
|
||
_CaptionRange(this.start, this.end); | ||
|
||
// Assumes format from an VTT file. | ||
// For example: | ||
// 00:09.000 --> 00:11.000 | ||
static _CaptionRange? fromWebVTTString(String line) { | ||
final RegExp format = | ||
RegExp(_webVTTTimeStamp + _webVTTArrow + _webVTTTimeStamp); | ||
|
||
if (!format.hasMatch(line)) { | ||
return null; | ||
} | ||
|
||
final List<String> times = line.split(_webVTTArrow); | ||
|
||
final Duration? start = _parseWebVTTTimestamp(times[0]); | ||
final Duration? end = _parseWebVTTTimestamp(times[1]); | ||
|
||
if (start == null || end == null) { | ||
return null; | ||
} | ||
|
||
return _CaptionRange(start, end); | ||
} | ||
} | ||
|
||
String _extractTextFromHtml(String htmlString) { | ||
final Document document = html_parser.parse(htmlString); | ||
final Element? body = document.body; | ||
if (body == null) { | ||
return ''; | ||
} | ||
final Element? bodyElement = html_parser.parse(body.text).documentElement; | ||
return bodyElement?.text ?? ''; | ||
} | ||
|
||
// Parses a time stamp in an VTT file into a Duration. | ||
// | ||
// Returns `null` if `timestampString` is in an invalid format. | ||
// | ||
// For example: | ||
// | ||
// _parseWebVTTTimestamp('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> timeComponents = dotSections[0].split(':'); | ||
|
||
// Validating and parsing the `timestampString`, invalid format will result this method | ||
// to return `null`. See https://www.w3.org/TR/webvtt1/#webvtt-timestamp for valid | ||
// WebVTT timestamp format. | ||
if (timeComponents.length > 3 || timeComponents.length < 2) { | ||
return null; | ||
} | ||
int hours = 0; | ||
if (timeComponents.length == 3) { | ||
final String hourString = timeComponents.removeAt(0); | ||
if (hourString.length < 2) { | ||
return null; | ||
} | ||
hours = int.parse(hourString); | ||
} | ||
final int minutes = int.parse(timeComponents.removeAt(0)); | ||
if (minutes < 0 || minutes > 59) { | ||
return null; | ||
} | ||
final int seconds = int.parse(timeComponents.removeAt(0)); | ||
if (seconds < 0 || seconds > 59) { | ||
return null; | ||
} | ||
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(cyanglaz): Handle caption styles. | ||
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. As I'm reading this again: it's weird that _parseWebVTTTimestamp is where captions would be handled. I think the caller—which is already doing a regex match for timestamps anyway—should just use match groups (the regex would need to be adjusted slightly since right now it's making groups for all the sub-pieces of the timestamp, which isn't actually used) to extract just the timestamps, and then pass those here, removing the need for the But I'm fine with that being something that's cleaned up if/when this functionality is added since I don't want to keep piling changes onto a PR you adopted in the first place :) 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. Sounds good, I'll put a note in the code and link this comment. |
||
// https://github.com/flutter/flutter/issues/90009. | ||
// ```dart | ||
// if (milisecondsStyles.length > 1) { | ||
// List<String> styles = milisecondsStyles.sublist(1); | ||
// } | ||
// ``` | ||
// For a better readable code style, style parsing should happen before | ||
// calling this method. See: https://github.com/flutter/plugins/pull/2878/files#r713381134. | ||
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' --> '; |
Uh oh!
There was an error while loading. Please reload this page.