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

Commit ec39e65

Browse files
johnsonmhMichael Klimushyn
authored andcommitted
[Closed Captioning] Create SubRip file parser and dart closed caption data object (#2473)
This PR specifies a dart object that represents a "Closed Caption". This will be useful in a follow up PR, where I will add closed caption support the the `VideoPlayerController`.
1 parent 569a5b7 commit ec39e65

File tree

6 files changed

+301
-1
lines changed

6 files changed

+301
-1
lines changed

packages/video_player/video_player/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.10.6
2+
3+
* `ClosedCaptionFile` and `SubRipCaptionFile` classes added to read
4+
[SubRip](https://en.wikipedia.org/wiki/SubRip) files into dart objects.
5+
16
## 0.10.5+3
27

38
* Add integration instructions for the `web` platform.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2020 The Chromium 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 'sub_rip.dart';
6+
export 'sub_rip.dart' show SubRipCaptionFile;
7+
8+
/// A structured representation of a parsed closed caption file.
9+
///
10+
/// A closed caption file includes a list of captions, each with a start and end
11+
/// time for when the given closed caption should be displayed.
12+
///
13+
/// The [captions] are a list of all captions in a file, in the order that they
14+
/// appeared in the file.
15+
///
16+
/// See:
17+
/// * [SubRipCaptionFile].
18+
abstract class ClosedCaptionFile {
19+
/// The full list of captions from a given file.
20+
///
21+
/// The [captions] will be in the order that they appear in the given file.
22+
List<Caption> get captions;
23+
}
24+
25+
/// A representation of a single caption.
26+
///
27+
/// A typical closed captioning file will include several [Caption]s, each
28+
/// linked to a start and end time.
29+
class Caption {
30+
/// Creates a new [Caption] object.
31+
///
32+
/// This is not recommended for direct use unless you are writing a parser for
33+
/// a new closed captioning file type.
34+
const Caption({this.number, this.start, this.end, this.text});
35+
36+
/// The number that this caption was assigned.
37+
final int number;
38+
39+
/// When in the given video should this [Caption] begin displaying.
40+
final Duration start;
41+
42+
/// When in the given video should this [Caption] be dismissed.
43+
final Duration end;
44+
45+
/// The actual text that should appear on screen to be read between [start]
46+
/// and [end].
47+
final String text;
48+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright 2020 The Chromium 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 'closed_caption_file.dart';
8+
9+
/// Represents a [ClosedCaptionFile], parsed from the SubRip file format.
10+
/// See: https://en.wikipedia.org/wiki/SubRip
11+
class SubRipCaptionFile extends ClosedCaptionFile {
12+
/// Parses a string into a [ClosedCaptionFile], assuming [fileContents] is in
13+
/// the SubRip file format.
14+
/// * See: https://en.wikipedia.org/wiki/SubRip
15+
SubRipCaptionFile(this.fileContents)
16+
: _captions = _parseCaptionsFromSubRipString(fileContents);
17+
18+
/// The entire body of the SubRip file.
19+
final String fileContents;
20+
21+
@override
22+
List<Caption> get captions => _captions;
23+
24+
final List<Caption> _captions;
25+
}
26+
27+
List<Caption> _parseCaptionsFromSubRipString(String file) {
28+
final List<Caption> captions = <Caption>[];
29+
for (List<String> captionLines in _readSubRipFile(file)) {
30+
if (captionLines.length < 3) break;
31+
32+
final int captionNumber = int.parse(captionLines[0]);
33+
final _StartAndEnd startAndEnd =
34+
_StartAndEnd.fromSubRipString(captionLines[1]);
35+
36+
final String text = captionLines.sublist(2).join('\n');
37+
38+
final Caption newCaption = Caption(
39+
number: captionNumber,
40+
start: startAndEnd.start,
41+
end: startAndEnd.end,
42+
text: text,
43+
);
44+
45+
if (newCaption.start != null && newCaption.end != null) {
46+
captions.add(newCaption);
47+
}
48+
}
49+
50+
return captions;
51+
}
52+
53+
class _StartAndEnd {
54+
final Duration start;
55+
final Duration end;
56+
57+
_StartAndEnd(this.start, this.end);
58+
59+
// Assumes format from an SubRip file.
60+
// For example:
61+
// 00:01:54,724 --> 00:01:56,760
62+
static _StartAndEnd fromSubRipString(String line) {
63+
final RegExp format =
64+
RegExp(_subRipTimeStamp + _subRipArrow + _subRipTimeStamp);
65+
66+
if (!format.hasMatch(line)) {
67+
return _StartAndEnd(null, null);
68+
}
69+
70+
final List<String> times = line.split(_subRipArrow);
71+
72+
final Duration start = _parseSubRipTimestamp(times[0]);
73+
final Duration end = _parseSubRipTimestamp(times[1]);
74+
75+
return _StartAndEnd(start, end);
76+
}
77+
}
78+
79+
// Parses a time stamp in an SubRip file into a Duration.
80+
// For example:
81+
//
82+
// _parseSubRipTimestamp('00:01:59,084')
83+
// returns
84+
// Duration(hours: 0, minutes: 1, seconds: 59, milliseconds: 084)
85+
Duration _parseSubRipTimestamp(String timestampString) {
86+
if (!RegExp(_subRipTimeStamp).hasMatch(timestampString)) {
87+
return null;
88+
}
89+
90+
final List<String> commaSections = timestampString.split(',');
91+
final List<String> hoursMinutesSeconds = commaSections[0].split(':');
92+
93+
final int hours = int.parse(hoursMinutesSeconds[0]);
94+
final int minutes = int.parse(hoursMinutesSeconds[1]);
95+
final int seconds = int.parse(hoursMinutesSeconds[2]);
96+
final int milliseconds = int.parse(commaSections[1]);
97+
98+
return Duration(
99+
hours: hours,
100+
minutes: minutes,
101+
seconds: seconds,
102+
milliseconds: milliseconds,
103+
);
104+
}
105+
106+
// Reads on SubRip file and splits it into Lists of strings where each list is one
107+
// caption.
108+
List<List<String>> _readSubRipFile(String file) {
109+
final List<String> lines = LineSplitter.split(file).toList();
110+
111+
final List<List<String>> captionStrings = <List<String>>[];
112+
List<String> currentCaption = <String>[];
113+
int lineIndex = 0;
114+
for (final String line in lines) {
115+
final bool isLineBlank = line.trim().isEmpty;
116+
if (!isLineBlank) {
117+
currentCaption.add(line);
118+
}
119+
120+
if (isLineBlank || lineIndex == lines.length - 1) {
121+
captionStrings.add(currentCaption);
122+
currentCaption = <String>[];
123+
}
124+
125+
lineIndex += 1;
126+
}
127+
128+
return captionStrings;
129+
}
130+
131+
const String _subRipTimeStamp = r'\d\d:\d\d:\d\d,\d\d\d';
132+
const String _subRipArrow = r' --> ';

packages/video_player/video_player/lib/video_player.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import 'package:video_player_platform_interface/video_player_platform_interface.
1414
export 'package:video_player_platform_interface/video_player_platform_interface.dart'
1515
show DurationRange, DataSourceType, VideoFormat;
1616

17+
export 'src/closed_caption_file.dart';
18+
1719
final VideoPlayerPlatform _videoPlayerPlatform = VideoPlayerPlatform.instance
1820
// This will clear all open videos on the platform when a full restart is
1921
// performed.

packages/video_player/video_player/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: video_player
22
description: Flutter plugin for displaying inline video with other Flutter
33
widgets on Android and iOS.
4-
version: 0.10.5+3
4+
version: 0.10.6
55
homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player
66

77
flutter:
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright 2020 The Chromium 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 'package:flutter_test/flutter_test.dart';
6+
import 'package:video_player/src/closed_caption_file.dart';
7+
import 'package:video_player/video_player.dart';
8+
9+
void main() {
10+
test('Parses SubRip file', () {
11+
final SubRipCaptionFile parsedFile = SubRipCaptionFile(_validSubRip);
12+
13+
expect(parsedFile.captions.length, 4);
14+
15+
final Caption firstCaption = parsedFile.captions.first;
16+
expect(firstCaption.number, 1);
17+
expect(firstCaption.start, Duration(seconds: 6));
18+
expect(firstCaption.end, Duration(seconds: 12, milliseconds: 74));
19+
expect(firstCaption.text, 'This is a test file');
20+
21+
final Caption secondCaption = parsedFile.captions[1];
22+
expect(secondCaption.number, 2);
23+
expect(
24+
secondCaption.start,
25+
Duration(minutes: 1, seconds: 54, milliseconds: 724),
26+
);
27+
expect(
28+
secondCaption.end,
29+
Duration(minutes: 1, seconds: 56, milliseconds: 760),
30+
);
31+
expect(secondCaption.text, '- Hello.\n- Yes?');
32+
33+
final Caption thirdCaption = parsedFile.captions[2];
34+
expect(thirdCaption.number, 3);
35+
expect(
36+
thirdCaption.start,
37+
Duration(minutes: 1, seconds: 56, milliseconds: 884),
38+
);
39+
expect(
40+
thirdCaption.end,
41+
Duration(minutes: 1, seconds: 58, milliseconds: 954),
42+
);
43+
expect(
44+
thirdCaption.text,
45+
'These are more test lines\nYes, these are more test lines.',
46+
);
47+
48+
final Caption fourthCaption = parsedFile.captions[3];
49+
expect(fourthCaption.number, 4);
50+
expect(
51+
fourthCaption.start,
52+
Duration(hours: 1, minutes: 1, seconds: 59, milliseconds: 84),
53+
);
54+
expect(
55+
fourthCaption.end,
56+
Duration(hours: 1, minutes: 2, seconds: 1, milliseconds: 552),
57+
);
58+
expect(
59+
fourthCaption.text,
60+
'- [ Machinery Beeping ]\n- I\'m not sure what that was,',
61+
);
62+
});
63+
64+
test('Parses SubRip file with malformed input', () {
65+
final ClosedCaptionFile parsedFile = SubRipCaptionFile(_malformedSubRip);
66+
67+
expect(parsedFile.captions.length, 1);
68+
69+
final Caption firstCaption = parsedFile.captions.single;
70+
expect(firstCaption.number, 2);
71+
expect(firstCaption.start, Duration(seconds: 15));
72+
expect(firstCaption.end, Duration(seconds: 17, milliseconds: 74));
73+
expect(firstCaption.text, 'This one is valid');
74+
});
75+
}
76+
77+
const String _validSubRip = '''
78+
1
79+
00:00:06,000 --> 00:00:12,074
80+
This is a test file
81+
82+
2
83+
00:01:54,724 --> 00:01:56,760
84+
- Hello.
85+
- Yes?
86+
87+
3
88+
00:01:56,884 --> 00:01:58,954
89+
These are more test lines
90+
Yes, these are more test lines.
91+
92+
4
93+
01:01:59,084 --> 01:02:01,552
94+
- [ Machinery Beeping ]
95+
- I'm not sure what that was,
96+
97+
''';
98+
99+
const String _malformedSubRip = '''
100+
1
101+
00:00:06,000--> 00:00:12,074
102+
This one should be ignored because the
103+
arrow needs a space.
104+
105+
2
106+
00:00:15,000 --> 00:00:17,074
107+
This one is valid
108+
109+
3
110+
00:01:54,724 --> 00:01:6,760
111+
This one should be ignored because the
112+
ned time is missing a digit.
113+
''';

0 commit comments

Comments
 (0)