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

[video_player] VTT Support #2878

Merged
merged 76 commits into from
Sep 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
9788b63
Web VTT Supported Added
Jul 15, 2020
a66f5d7
Fixes
Jul 15, 2020
6481331
VTT with header added
Jul 15, 2020
8f8b095
VTT with header added
Jul 15, 2020
da1c6e4
VTT with header added
Jul 15, 2020
1a21621
VTT with header added
Jul 15, 2020
0d3549e
New tests added
Jul 15, 2020
b018ea9
tests organized by group
Jul 15, 2020
b92f305
Changelog updated
Jul 15, 2020
1573670
Sample vtt renamed
Jul 15, 2020
1e53fc5
Sample vtt renamed
Jul 15, 2020
9034263
Todo Styles commented
Jul 15, 2020
d9647a9
Sample VTT file fixed
Jul 16, 2020
2ebf99f
Sample VTT file fixed
Jul 16, 2020
43abfd6
Sample VTT file fixed
Jul 16, 2020
a44edea
Credentials added
ferrazrx Jul 16, 2020
d92e820
Credentials added
ferrazrx Jul 16, 2020
10e717c
Merge branch 'ferrazrx/web_vtt' of github.com:ferrazrx/plugins into f…
ferrazrx Jul 16, 2020
98e21c1
[image_picker] Add web support to the example app. (#2816)
ferrazrx Jun 5, 2020
e146366
Fix bug in example (#2801)
ferrazrx Jun 6, 2020
c05a6f1
Update Linux desktop Dockerfile for GTK switch (#2826)
ferrazrx Jun 13, 2020
50044cc
[image_picker] fixes for iOS which doesn't present camera/albums with…
ferrazrx Jun 15, 2020
2aacc0f
[path_provider] Updated documentation reflecting changes needed for t…
ferrazrx Jun 16, 2020
928bfb4
[url_launcher] docs: note about encoding URIs (#2172)
ferrazrx Jun 17, 2020
ee9e13a
[In_App_Purchase]queryPastPurchases() shouldn't block transaction upd…
ferrazrx Jun 22, 2020
bf3e958
[e2e] Fix e2e pixel ratio (#2842)
ferrazrx Jun 23, 2020
91bba0f
Update README for plugin list (#2843)
ferrazrx Jun 24, 2020
459b7f8
[connectivity_for_web] Introduce connectivity_for_web package. (#2820)
ferrazrx Jun 30, 2020
b43b1a8
[url_launcher_web] Adds "tel" and "sms" URL support (#2847)
ferrazrx Jun 30, 2020
db547b6
[shared_preferences_linux] Add support for Linux (#2836)
ferrazrx Jun 30, 2020
7864399
[e2e] Use SettableFuture instead of CompletableFuture (#2854)
ferrazrx Jul 1, 2020
7afa2c2
[connectivity] Endorse connectivity_for_web. (#2853)
ferrazrx Jul 1, 2020
a8a5994
[e2e] Bump version to 0.6.0 (#2855)
ferrazrx Jul 1, 2020
0fdc514
[multiple] Improve video playback in image_picker example (#2819)
ferrazrx Jul 1, 2020
1ca27a7
[image_picker] updated VALID_ARCHS to support iPhone simulator (#2761)
ferrazrx Jul 6, 2020
39ba0ae
[url_launcher_linux] Add Linux url_launcher plugin (#2857)
ferrazrx Jul 7, 2020
7391c73
[url_launcher] Endorse url_launcher_linux (#2863)
ferrazrx Jul 7, 2020
00efa43
[shared_preferences_linux] Add iOS stub (#2865)
ferrazrx Jul 8, 2020
8e1dcad
[shared_preferences] Shared preferences linux endorsement (#2864)
ferrazrx Jul 8, 2020
9fdcb05
[connectivity_for_web] Fix JS Interop in release mode. (#2869)
ferrazrx Jul 13, 2020
f0e286b
[google_sign_in] Bridge google_sign_in and googleapis. (#2824)
ferrazrx Jul 14, 2020
1350f8e
Web VTT Supported Added
ferrazrx Jul 15, 2020
c72a068
Fixes
ferrazrx Jul 15, 2020
226b9c6
VTT with header added
ferrazrx Jul 15, 2020
4554771
VTT with header added
ferrazrx Jul 15, 2020
0b7dde4
VTT with header added
ferrazrx Jul 15, 2020
7aa17ba
VTT with header added
ferrazrx Jul 15, 2020
2f686c2
New tests added
ferrazrx Jul 15, 2020
5d09170
tests organized by group
ferrazrx Jul 15, 2020
78098ca
Changelog updated
ferrazrx Jul 15, 2020
46aecd8
Sample vtt renamed
ferrazrx Jul 15, 2020
cacf4dc
Sample vtt renamed
ferrazrx Jul 15, 2020
5524931
Todo Styles commented
ferrazrx Jul 15, 2020
110c115
Sample VTT file fixed
ferrazrx Jul 16, 2020
808b1a0
Sample VTT file fixed
ferrazrx Jul 16, 2020
a041f54
Sample VTT file fixed
ferrazrx Jul 16, 2020
2a16e2e
Credentials added
ferrazrx Jul 16, 2020
acf7417
Merge branch 'ferrazrx/web_vtt' of github.com:ferrazrx/plugins into f…
ferrazrx Jul 16, 2020
de3c973
Comments fixed
ferrazrx Jul 17, 2020
495b37c
Comments fixed
ferrazrx Jul 17, 2020
fc0d049
Version Changed
ferrazrx Jul 17, 2020
9db4ec1
Version Changed
ferrazrx Jul 17, 2020
c5bbba6
Comments fixed
ferrazrx Jul 22, 2020
7bb0d53
Public comments fixed
ferrazrx Aug 20, 2020
d47ea7f
CHANGELOG.md updated
ferrazrx Aug 20, 2020
4c66c38
Merge remote-tracking branch 'upstream/master' into ferrazrx/web_vtt
ferrazrx Aug 20, 2020
db79449
Format fixed
ferrazrx Aug 20, 2020
fa0d6dc
Version updated
ferrazrx Aug 20, 2020
a4fcbf7
merge master, nnbd migration
Sep 13, 2021
61a22a5
add issue links for TODOs
Sep 13, 2021
14468e5
Merge branch 'master' into ferrazrx/web_vtt
Sep 16, 2021
0d68872
review
Sep 21, 2021
c97876c
Merge branch 'ferrazrx/web_vtt' of github.com:ferrazrx/plugins into f…
Sep 21, 2021
6c457a5
Merge branch 'master' into ferrazrx/web_vtt
Sep 21, 2021
5ffff31
review 2
Sep 21, 2021
c8f7c2c
fix license header
Sep 22, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/video_player/video_player/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.2.5

* Support to closed caption WebVTT format added.

## 2.2.4

* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0.
Expand Down
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 ]
5 changes: 3 additions & 2 deletions packages/video_player/video_player/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,9 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> {

Future<ClosedCaptionFile> _loadCaptions() async {
final String fileContents = await DefaultAssetBundle.of(context)
.loadString('assets/bumble_bee_captions.srt');
return SubRipCaptionFile(fileContents);
.loadString('assets/bumble_bee_captions.vtt');
return WebVTTCaptionFile(
fileContents); // For vtt files, use WebVTTCaptionFile
}

@override
Expand Down
7 changes: 4 additions & 3 deletions packages/video_player/video_player/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dev_dependencies:
flutter:
uses-material-design: true
assets:
- assets/flutter-mark-square-64.png
- assets/Butterfly-209.mp4
- assets/bumble_bee_captions.srt
- assets/flutter-mark-square-64.png
- assets/Butterfly-209.mp4
- assets/bumble_bee_captions.srt
- assets/bumble_bee_captions.vtt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import 'sub_rip.dart';
export 'sub_rip.dart' show SubRipCaptionFile;

import 'web_vtt.dart';
export 'web_vtt.dart' show WebVTTCaptionFile;

/// A structured representation of a parsed closed caption file.
///
/// A closed caption file includes a list of captions, each with a start and end
Expand All @@ -15,6 +18,7 @@ export 'sub_rip.dart' show SubRipCaptionFile;
///
/// See:
/// * [SubRipCaptionFile].
/// * [WebVTTCaptionFile].
abstract class ClosedCaptionFile {
/// The full list of captions from a given file.
///
Expand Down
20 changes: 11 additions & 9 deletions packages/video_player/video_player/lib/src/sub_rip.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ class SubRipCaptionFile extends ClosedCaptionFile {
: _captions = _parseCaptionsFromSubRipString(fileContents);

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

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

final int captionNumber = int.parse(captionLines[0]);
final _StartAndEnd startAndEnd =
_StartAndEnd.fromSubRipString(captionLines[1]);
final _CaptionRange captionRange =
_CaptionRange.fromSubRipString(captionLines[1]);

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

final Caption newCaption = Caption(
number: captionNumber,
start: startAndEnd.start,
end: startAndEnd.end,
start: captionRange.start,
end: captionRange.end,
text: text,
);
if (newCaption.start != newCaption.end) {
Expand All @@ -49,29 +51,29 @@ List<Caption> _parseCaptionsFromSubRipString(String file) {
return captions;
}

class _StartAndEnd {
class _CaptionRange {
final Duration start;
final Duration end;

_StartAndEnd(this.start, this.end);
_CaptionRange(this.start, this.end);

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

if (!format.hasMatch(line)) {
return _StartAndEnd(Duration.zero, Duration.zero);
return _CaptionRange(Duration.zero, Duration.zero);
}

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

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

return _StartAndEnd(start, end);
return _CaptionRange(start, end);
}
}

Expand Down
211 changes: 211 additions & 0 deletions packages/video_player/video_player/lib/src/web_vtt.dart
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 {
/// 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;

// 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)
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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once the above is removed, this can all condense to:

int hours = 0;
if (hoursMinutesSeconds.length == 3) {
  hours = int.parse(timeComponents.removeAt(0));
}
final int minutes = int.parse(timeComponents.removeAt(0));
final int seconds = int.parse(timeComponents.removeAt(0));

It would be good to add a safety check that the length is either 2 or 3 and returns null first, as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


List<String> milisecondsStyles = dotSections[1].split(" ");

// TODO(cyanglaz): Handle caption styles.
Copy link
Contributor

@stuartmorgan-g stuartmorgan-g Sep 21, 2021

Choose a reason for hiding this comment

The 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 milisecondsStyles = dotSections[1].split(" "). Then the caption-handling TODO would go in the calling function, not here.

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 :)

Copy link
Contributor

Choose a reason for hiding this comment

The 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' --> ';
3 changes: 2 additions & 1 deletion packages/video_player/video_player/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter
widgets on Android, iOS, and web.
repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
version: 2.2.4
version: 2.2.5

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

dev_dependencies:
flutter_test:
Expand Down
Loading