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

[Closed Captioning] Create SubRip file parser and dart closed caption data object #2473

Merged
merged 21 commits into from
Jan 28, 2020
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// 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 'sub_rip.dart';
export 'sub_rip.dart' show SubRipCaptionFile;

/// A structured representation of a parsed closed caption file.
///
/// A closed caption file includes a list of captions, each with a start and end
/// time for when the given closed caption should be displayed.
///
/// The [captions] are a list of all captions in a file, in the order that they
/// appeared in the file.
///
/// See:
/// * [SubRipCaptionFile].
abstract class ClosedCaptionFile {
/// The full list of captions from a given file.
///
/// The [captions] will be in the order that they appear in the given file.
List<Caption> get captions;
}

/// A representation of a single caption.
///
/// A typical closed captioning file will include several [Caption]s, each
/// linked to a start and end time.
class Caption {
/// Creates a new [Caption] object.
///
/// This is not recommended for direct use unless you are writing a parser for
/// a new closed captioning file type.
const Caption({this.number, this.start, this.end, this.text});

/// The number that this caption was assigned.
final int number;

/// When in the given video should this [Caption] begin displaying.
final Duration start;

/// When in the given video should this [Caption] be dismissed.
final Duration end;

/// The actual text that should appear on screen to be read between [start]
/// and [end].
final String text;
}
132 changes: 132 additions & 0 deletions packages/video_player/video_player/lib/src/sub_rip.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// 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';

import 'closed_caption_file.dart';

/// Represents a [ClosedCaptionFile], parsed from the SubRip file format.
/// See: https://en.wikipedia.org/wiki/SubRip
class SubRipCaptionFile extends ClosedCaptionFile {
/// Parses a string into a [ClosedCaptionFile], assuming [fileContents] is in
/// the SubRip file format.
/// * See: https://en.wikipedia.org/wiki/SubRip
SubRipCaptionFile(this.fileContents)
: _captions = _parseCaptionsFromSubRipString(fileContents);

/// The entire body of the SubRip file.
final String fileContents;

@override
List<Caption> get captions => _captions;

final List<Caption> _captions;
}

List<Caption> _parseCaptionsFromSubRipString(String file) {
final List<Caption> captions = <Caption>[];
for (List<String> captionLines in _readSubRipFile(file)) {
if (captionLines.length < 3) break;

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

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

final Caption newCaption = Caption(
number: captionNumber,
start: startAndEnd.start,
end: startAndEnd.end,
text: text,
);

if (newCaption.start != null && newCaption.end != null) {
captions.add(newCaption);
}
}

return captions;
}

class _StartAndEnd {
final Duration start;
final Duration end;

_StartAndEnd(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) {
final RegExp format =
RegExp(_subRipTimeStamp + _subRipArrow + _subRipTimeStamp);

if (!format.hasMatch(line)) {
return _StartAndEnd(null, null);
}

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

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

return _StartAndEnd(start, end);
}
}

// Parses a time stamp in an SubRip file into a Duration.
// For example:
//
// _parseSubRipTimestamp('00:01:59,084')
// returns
// Duration(hours: 0, minutes: 1, seconds: 59, milliseconds: 084)
Duration _parseSubRipTimestamp(String timestampString) {
if (!RegExp(_subRipTimeStamp).hasMatch(timestampString)) {
return null;
}

final List<String> commaSections = timestampString.split(',');
final List<String> hoursMinutesSeconds = commaSections[0].split(':');

final int hours = int.parse(hoursMinutesSeconds[0]);
final int minutes = int.parse(hoursMinutesSeconds[1]);
final int seconds = int.parse(hoursMinutesSeconds[2]);
final int milliseconds = int.parse(commaSections[1]);

return Duration(
hours: hours,
minutes: minutes,
seconds: seconds,
milliseconds: milliseconds,
);
}

// Reads on SubRip file and splits it into Lists of strings where each list is one
// caption.
List<List<String>> _readSubRipFile(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 _subRipTimeStamp = r'\d\d:\d\d:\d\d,\d\d\d';
const String _subRipArrow = r' --> ';
2 changes: 2 additions & 0 deletions packages/video_player/video_player/lib/video_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import 'package:video_player_platform_interface/video_player_platform_interface.
export 'package:video_player_platform_interface/video_player_platform_interface.dart'
show DurationRange, DataSourceType, VideoFormat;

export 'src/closed_caption_file.dart';

final VideoPlayerPlatform _videoPlayerPlatform = VideoPlayerPlatform.instance
// This will clear all open videos on the platform when a full restart is
// performed.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
1
00:00:06,000 --> 00:00:12,074
This is a test file

2
00:01:54,724 --> 00:01:56,760
- Hello.
- Yes?

3
00:01:56,884 --> 00:01:58,954
These are more test lines
Yes, these are more test lines.

4
01:01:59,084 --> 01:02:01,552
- [ Machinery Beeping ]
- I'm not sure what that was,
95 changes: 95 additions & 0 deletions packages/video_player/video_player/test/sub_rip_file_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// 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:io';

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 SubRip file', () {
final File file = File('test/data/sample_sub_rip_file.srt');
final SubRipCaptionFile parsedFile =
SubRipCaptionFile(file.readAsStringSync());

expect(parsedFile.captions.length, 4);

final Caption firstCaption = parsedFile.captions.first;
expect(firstCaption.number, 1);
expect(firstCaption.start, Duration(seconds: 6));
expect(firstCaption.end, Duration(seconds: 12, milliseconds: 74));
expect(firstCaption.text, 'This is a test file');

final Caption secondCaption = parsedFile.captions[1];
expect(secondCaption.number, 2);
expect(
secondCaption.start,
Duration(minutes: 1, seconds: 54, milliseconds: 724),
);
expect(
secondCaption.end,
Duration(minutes: 1, seconds: 56, milliseconds: 760),
);
expect(secondCaption.text, '- Hello.\n- Yes?');

final Caption thirdCaption = parsedFile.captions[2];
expect(thirdCaption.number, 3);
expect(
thirdCaption.start,
Duration(minutes: 1, seconds: 56, milliseconds: 884),
);
expect(
thirdCaption.end,
Duration(minutes: 1, seconds: 58, milliseconds: 954),
);
expect(
thirdCaption.text,
'These are more test lines\nYes, these are more test lines.',
);

final Caption fourthCaption = parsedFile.captions[3];
expect(fourthCaption.number, 4);
expect(
fourthCaption.start,
Duration(hours: 1, minutes: 1, seconds: 59, milliseconds: 84),
);
expect(
fourthCaption.end,
Duration(hours: 1, minutes: 2, seconds: 1, milliseconds: 552),
);
expect(
fourthCaption.text,
'- [ Machinery Beeping ]\n- I\'m not sure what that was,',
);
});

test('Parses SubRip file with malformed input', () {
final ClosedCaptionFile parsedFile = SubRipCaptionFile(_malformedSubRip);

expect(parsedFile.captions.length, 1);

final Caption firstCaption = parsedFile.captions.single;
expect(firstCaption.number, 2);
expect(firstCaption.start, Duration(seconds: 15));
expect(firstCaption.end, Duration(seconds: 17, milliseconds: 74));
expect(firstCaption.text, 'This one is valid');
});
}

const String _malformedSubRip = '''
1
00:00:06,000--> 00:00:12,074
This one should be ignored because the
arrow needs a space.

2
00:00:15,000 --> 00:00:17,074
This one is valid

3
00:01:54,724 --> 00:01:6,760
This one should be ignored because the
ned time is missing a digit.
''';