From d791c6195a6f4ac5616f83014787ed1193614b8d Mon Sep 17 00:00:00 2001 From: Alvaro Velad Date: Wed, 9 Dec 2020 14:36:08 +0100 Subject: [PATCH 01/15] Add LyRiCs (LRC) support --- README.md | 2 + build/types/text | 1 + lib/player.js | 56 +++++++++++--- lib/text/lrc_text_parser.js | 121 ++++++++++++++++++++++++++++++ shaka-player.uncompiled.js | 1 + test/text/lrc_text_parser_unit.js | 85 +++++++++++++++++++++ 6 files changed, 255 insertions(+), 11 deletions(-) create mode 100644 lib/text/lrc_text_parser.js create mode 100644 test/text/lrc_text_parser_unit.js diff --git a/README.md b/README.md index 10af263b3e..438b597fd1 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,8 @@ Shaka Player supports: - Supported embedded in MP4 - SubRip (SRT) - UTF-8 encoding only + - LyRiCs (LRC) + - UTF-8 encoding only Subtitles are rendered by the browser by default. Applications can create a [text display plugin][] for customer rendering to go beyond browser-supported diff --git a/build/types/text b/build/types/text index 2eea94e4d6..62f6d94c38 100644 --- a/build/types/text +++ b/build/types/text @@ -1,5 +1,6 @@ # Optional plugins related to text parsing and displaying. ++../../lib/text/lrc_text_parser.js +../../lib/text/mp4_ttml_parser.js +../../lib/text/mp4_vtt_parser.js +../../lib/text/srt_text_parser.js diff --git a/lib/player.js b/lib/player.js index ed8dff3802..df0a81d132 100644 --- a/lib/player.js +++ b/lib/player.js @@ -3871,6 +3871,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { 'vtt': 'text/vtt', 'webvtt': 'text/vtt', 'ttml': 'application/ttml+xml', + 'lrc': 'application/x-subtitle-lrc', }[extension]; if (!mimeType) { @@ -4004,6 +4005,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { 'vtt': 'text/vtt', 'webvtt': 'text/vtt', 'ttml': 'application/ttml+xml', + 'lrc': 'application/x-subtitle-lrc', }[extension]; if (!mimeType) { @@ -4034,10 +4036,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget { try { goog.asserts.assert( this.networkingEngine_, 'Need networking engine.'); - const stringData = await this.getTextData_(uri, + const data = await this.getTextData_(uri, this.networkingEngine_, this.config_.streaming.retryParameters); - const vvtText = this.convertToWebVTT_(stringData, mimeType); + const vvtText = this.convertToWebVTT_(data, mimeType); const blob = new Blob([vvtText], {type: 'text/vtt'}); uri = URL.createObjectURL(blob); mimeType = 'text/vtt'; @@ -4131,7 +4133,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { * @param {string} uri * @param {!shaka.net.NetworkingEngine} netEngine * @param {shaka.extern.RetryParameters} retryParams - * @return {!Promise.} + * @return {!Promise.} * @private */ async getTextData_(uri, netEngine, retryParams) { @@ -4142,14 +4144,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget { const response = await netEngine.request(type, request).promise; - return shaka.util.StringUtils.fromUTF8(response.data); + return response.data; } /** * Converts an input string to a WebVTT format string. * - * @param {string} data + * @param {BufferSource} data * @param {string} mimeType * @return {string} * @private @@ -4159,13 +4161,45 @@ shaka.Player = class extends shaka.util.FakeEventTarget { if (mimeType === 'text/srt') { const SrtTextParser = shaka.text.SrtTextParser; if (SrtTextParser) { - return SrtTextParser.srt2webvtt(data); + const string = shaka.util.StringUtils.fromUTF8(data); + return SrtTextParser.srt2webvtt(string); + } else { + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.TEXT, + shaka.util.Error.Code.MISSING_TEXT_PLUGIN, + mimeType); + } + } + if (mimeType === 'application/x-subtitle-lrc') { + const LrcTextParser = shaka.text.LrcTextParser; + if (LrcTextParser) { + const cues = LrcTextParser.getCues(data); + let webvttString = 'WEBVTT\n\n'; + for (const cue of cues) { + const webvttTimeString = (time) => { + const hours = Math.floor(time / 3600); + const minutes = Math.floor(time / 60 % 60); + const seconds = Math.floor(time % 60); + const milliseconds = Math.floor(time * 1000 % 1000); + return (hours < 10 ? '0' : '') + hours + ':' + + (minutes < 10 ? '0' : '') + minutes + ':' + + (seconds < 10 ? '0' : '') + seconds + '.' + + (milliseconds < 100 ? (milliseconds < 10 ? '00' : '0') : '') + + milliseconds; + }; + webvttString += webvttTimeString(cue.startTime) + ' --> ' + + webvttTimeString(cue.endTime) + '\n'; + webvttString += cue.payload + '\n\n'; + } + return webvttString; + } else { + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.TEXT, + shaka.util.Error.Code.MISSING_TEXT_PLUGIN, + mimeType); } - throw new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.TEXT, - shaka.util.Error.Code.MISSING_TEXT_PLUGIN, - mimeType); } throw new shaka.util.Error( shaka.util.Error.Severity.RECOVERABLE, diff --git a/lib/text/lrc_text_parser.js b/lib/text/lrc_text_parser.js new file mode 100644 index 0000000000..1f38b5d02d --- /dev/null +++ b/lib/text/lrc_text_parser.js @@ -0,0 +1,121 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.text.LrcTextParser'); + +goog.require('goog.asserts'); +goog.require('shaka.log'); +goog.require('shaka.text.Cue'); +goog.require('shaka.text.TextEngine'); +goog.require('shaka.util.StringUtils'); + + +/** + * @implements {shaka.extern.TextParser} + * @export + */ +shaka.text.LrcTextParser = class { + /** + * @override + * @export + */ + parseInit(data) { + goog.asserts.assert(false, 'LRC does not have init segments'); + } + + /** + * @override + * @export + */ + parseMedia(data, time) { + return shaka.text.LrcTextParser.getCues(data); + } + + /** + * @param {BufferSource} data + * @return {!Array.} + * @export + */ + static getCues(data) { + const StringUtils = shaka.util.StringUtils; + const LrcTextParser = shaka.text.LrcTextParser; + + // Get the input as a string. + const str = StringUtils.fromUTF8(data); + + /** @type {shaka.extern.Cue} */ + let prevCue = null; + + /** @type {!Array.} */ + const cues = []; + const parts = str.split(/\r?\n/); + for (const part of parts) { + if (!part || /^\s+$/.test(part)) { + continue; + } + + // LRC content + const match = LrcTextParser.lyricLine_.exec(part); + if (match) { + const startTime = LrcTextParser.parseTime_(match[1]); + // By default we add 2 seconds of duration. + const endTime = startTime + 2; + const payload = match[3]; + const cue = new shaka.text.Cue(startTime, endTime, payload); + + // Update previous + if (prevCue) { + prevCue.endTime = startTime; + cues.push(prevCue); + } + prevCue = cue; + continue; + } + shaka.log.warning('LrcTextParser parser encountered an unknown part.', + part); + } + if (prevCue) { + cues.push(prevCue); + } + + return cues; + } + + /** + * Parses a LRC time from the given parser. + * + * @param {string} string + * @return {number} + * @private + */ + static parseTime_(string) { + const LrcTextParser = shaka.text.LrcTextParser; + const match = LrcTextParser.timeFormat_.exec(string); + const minutes = parseInt(match[1], 10); + const seconds = parseInt(match[2], 10); + const hundredthsOfSeconds = match[4] ? parseInt(match[4], 10) : 0; + return minutes * 60 + seconds + hundredthsOfSeconds / 100; + } +}; + +/** + * @const + * @private {!RegExp} + * @example 50t or 50.5t + */ +shaka.text.LrcTextParser.lyricLine_ = + /^\[(\d{1,2}:\d{1,2}([.,]\d{1,3})?)\](.*)(\r?\n)*$/; + +/** + * @const + * @private {!RegExp} + * @example 50t or 50.5t + */ +shaka.text.LrcTextParser.timeFormat_ = + /^\s*(\d+):(\d{1,2})([.,](\d{1,3}))?\s*$/; + +shaka.text.TextEngine.registerParser( + 'application/x-subtitle-lrc', () => new shaka.text.LrcTextParser()); diff --git a/shaka-player.uncompiled.js b/shaka-player.uncompiled.js index 7c4a3a47c1..f5a9af599e 100644 --- a/shaka-player.uncompiled.js +++ b/shaka-player.uncompiled.js @@ -49,6 +49,7 @@ goog.require('shaka.polyfill.VideoPlaybackQuality'); goog.require('shaka.polyfill'); goog.require('shaka.routing.Walker'); goog.require('shaka.text.Cue'); +goog.require('shaka.text.LrcTextParser'); goog.require('shaka.text.Mp4TtmlParser'); goog.require('shaka.text.Mp4VttParser'); goog.require('shaka.text.TextEngine'); diff --git a/test/text/lrc_text_parser_unit.js b/test/text/lrc_text_parser_unit.js new file mode 100644 index 0000000000..c566ec5561 --- /dev/null +++ b/test/text/lrc_text_parser_unit.js @@ -0,0 +1,85 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.require('shaka.text.LrcTextParser'); +goog.require('shaka.util.BufferUtils'); +goog.require('shaka.util.StringUtils'); + +describe('LrcTextParser', () => { + it('supports no cues', () => { + verifyHelper([], + '', + {periodStart: 0, segmentStart: 0, segmentEnd: 0}); + }); + + it('handles a blank line at the start of the file', () => { + verifyHelper( + [ + {startTime: 0, endTime: 2, payload: 'Test'}, + ], + '\n\n' + + '[00:00.00]Test', + {periodStart: 0, segmentStart: 0, segmentEnd: 0}); + }); + + it('handles a blank line at the end of the file', () => { + verifyHelper( + [ + {startTime: 0, endTime: 2, payload: 'Test'}, + ], + '[00:00.00]Test' + + '\n\n', + {periodStart: 0, segmentStart: 0, segmentEnd: 0}); + }); + + it('handles no blank line at the end of the file', () => { + verifyHelper( + [ + {startTime: 0, endTime: 2, payload: 'Test'}, + ], + '[00:00.00]Test', + {periodStart: 0, segmentStart: 0, segmentEnd: 0, + }); + }); + + it('supports multiple cues', () => { + verifyHelper( + [ + {startTime: 0, endTime: 10, payload: 'Test'}, + {startTime: 10, endTime: 20, payload: 'Test2'}, + {startTime: 20, endTime: 22, payload: 'Test3'}, + ], + '[00:00.00]Test\n' + + '[00:10.00]Test2\n' + + '[00:20.00]Test3', + {periodStart: 0, segmentStart: 0, segmentEnd: 0}); + }); + + /** + * @param {!Array} cues + * @param {string} text + * @param {shaka.extern.TextParser.TimeContext} time + */ + function verifyHelper(cues, text, time) { + const BufferUtils = shaka.util.BufferUtils; + const StringUtils = shaka.util.StringUtils; + + const data = BufferUtils.toUint8(StringUtils.toUTF8(text)); + + const parser = new shaka.text.LrcTextParser(); + const result = parser.parseMedia(data, time); + + const expected = cues.map((cue) => { + if (cue.nestedCues) { + cue.nestedCues = cue.nestedCues.map( + (nestedCue) => jasmine.objectContaining(nestedCue) + ); + } + return jasmine.objectContaining(cue); + }); + expect(result).toEqual(expected); + } +}); From f78191cf5bcf88ad2352d694ec4155eba0dad472 Mon Sep 17 00:00:00 2001 From: Alvaro Velad Date: Wed, 23 Dec 2020 21:36:23 +0100 Subject: [PATCH 02/15] Add link to wikipedia --- lib/text/lrc_text_parser.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/text/lrc_text_parser.js b/lib/text/lrc_text_parser.js index 1f38b5d02d..2fa044c5a8 100644 --- a/lib/text/lrc_text_parser.js +++ b/lib/text/lrc_text_parser.js @@ -14,6 +14,8 @@ goog.require('shaka.util.StringUtils'); /** + * LRC file format: https://en.wikipedia.org/wiki/LRC_(file_format) + * * @implements {shaka.extern.TextParser} * @export */ From 93bb2f1ccaf2dfb3ec5cf0784feba4b111aebc58 Mon Sep 17 00:00:00 2001 From: Alvaro Velad Date: Wed, 23 Dec 2020 21:39:50 +0100 Subject: [PATCH 03/15] Rename parts to lines --- lib/text/lrc_text_parser.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/text/lrc_text_parser.js b/lib/text/lrc_text_parser.js index 2fa044c5a8..fe62527c95 100644 --- a/lib/text/lrc_text_parser.js +++ b/lib/text/lrc_text_parser.js @@ -53,14 +53,14 @@ shaka.text.LrcTextParser = class { /** @type {!Array.} */ const cues = []; - const parts = str.split(/\r?\n/); - for (const part of parts) { - if (!part || /^\s+$/.test(part)) { + const lines = str.split(/\r?\n/); + for (const line of lines) { + if (!line || /^\s+$/.test(line)) { continue; } // LRC content - const match = LrcTextParser.lyricLine_.exec(part); + const match = LrcTextParser.lyricLine_.exec(line); if (match) { const startTime = LrcTextParser.parseTime_(match[1]); // By default we add 2 seconds of duration. @@ -76,8 +76,7 @@ shaka.text.LrcTextParser = class { prevCue = cue; continue; } - shaka.log.warning('LrcTextParser parser encountered an unknown part.', - part); + shaka.log.warning('LrcTextParser encountered an unknown line.', line); } if (prevCue) { cues.push(prevCue); From d03b2e59ba974d99603b0881db4f9ea4bdea1f39 Mon Sep 17 00:00:00 2001 From: Alvaro Velad Date: Wed, 23 Dec 2020 21:48:01 +0100 Subject: [PATCH 04/15] Move some logic from the player to the plugin --- lib/player.js | 20 +------------------- lib/text/lrc_text_parser.js | 36 +++++++++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/lib/player.js b/lib/player.js index e87dc011b1..80332fb765 100644 --- a/lib/player.js +++ b/lib/player.js @@ -4178,25 +4178,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { if (mimeType === 'application/x-subtitle-lrc') { const LrcTextParser = shaka.text.LrcTextParser; if (LrcTextParser) { - const cues = LrcTextParser.getCues(data); - let webvttString = 'WEBVTT\n\n'; - for (const cue of cues) { - const webvttTimeString = (time) => { - const hours = Math.floor(time / 3600); - const minutes = Math.floor(time / 60 % 60); - const seconds = Math.floor(time % 60); - const milliseconds = Math.floor(time * 1000 % 1000); - return (hours < 10 ? '0' : '') + hours + ':' + - (minutes < 10 ? '0' : '') + minutes + ':' + - (seconds < 10 ? '0' : '') + seconds + '.' + - (milliseconds < 100 ? (milliseconds < 10 ? '00' : '0') : '') + - milliseconds; - }; - webvttString += webvttTimeString(cue.startTime) + ' --> ' + - webvttTimeString(cue.endTime) + '\n'; - webvttString += cue.payload + '\n\n'; - } - return webvttString; + return LrcTextParser.convertToWebVTT(data); } else { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, diff --git a/lib/text/lrc_text_parser.js b/lib/text/lrc_text_parser.js index fe62527c95..7280305ab8 100644 --- a/lib/text/lrc_text_parser.js +++ b/lib/text/lrc_text_parser.js @@ -33,15 +33,15 @@ shaka.text.LrcTextParser = class { * @export */ parseMedia(data, time) { - return shaka.text.LrcTextParser.getCues(data); + return shaka.text.LrcTextParser.getCues_(data); } /** * @param {BufferSource} data * @return {!Array.} - * @export + * @private */ - static getCues(data) { + static getCues_(data) { const StringUtils = shaka.util.StringUtils; const LrcTextParser = shaka.text.LrcTextParser; @@ -100,6 +100,36 @@ shaka.text.LrcTextParser = class { const hundredthsOfSeconds = match[4] ? parseInt(match[4], 10) : 0; return minutes * 60 + seconds + hundredthsOfSeconds / 100; } + + /** + * Convert a LRC input to WebVTT + * + * @param {BufferSource} data + * @return {string} + * @export + */ + static convertToWebVTT(data) { + const LrcTextParser = shaka.text.LrcTextParser; + const cues = LrcTextParser.getCues_(data); + let webvttString = 'WEBVTT\n\n'; + for (const cue of cues) { + const webvttTimeString = (time) => { + const hours = Math.floor(time / 3600); + const minutes = Math.floor(time / 60 % 60); + const seconds = Math.floor(time % 60); + const milliseconds = Math.floor(time * 1000 % 1000); + return (hours < 10 ? '0' : '') + hours + ':' + + (minutes < 10 ? '0' : '') + minutes + ':' + + (seconds < 10 ? '0' : '') + seconds + '.' + + (milliseconds < 100 ? (milliseconds < 10 ? '00' : '0') : '') + + milliseconds; + }; + webvttString += webvttTimeString(cue.startTime) + ' --> ' + + webvttTimeString(cue.endTime) + '\n'; + webvttString += cue.payload + '\n\n'; + } + return webvttString; + } }; /** From d030b17cb796d39c6e97a1ca8f03efb76169d9dc Mon Sep 17 00:00:00 2001 From: Alvaro Velad Date: Wed, 23 Dec 2020 21:55:25 +0100 Subject: [PATCH 05/15] Fix regex example --- lib/text/lrc_text_parser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/text/lrc_text_parser.js b/lib/text/lrc_text_parser.js index 7280305ab8..b9491f2e7b 100644 --- a/lib/text/lrc_text_parser.js +++ b/lib/text/lrc_text_parser.js @@ -143,7 +143,7 @@ shaka.text.LrcTextParser.lyricLine_ = /** * @const * @private {!RegExp} - * @example 50t or 50.5t + * @example 00:12.00 or 00:12.0 or 00:12.000 */ shaka.text.LrcTextParser.timeFormat_ = /^\s*(\d+):(\d{1,2})([.,](\d{1,3}))?\s*$/; From f7602ad0021b009e5854e8bb6bdd8982e4ef3049 Mon Sep 17 00:00:00 2001 From: Alvaro Velad Date: Wed, 23 Dec 2020 21:59:09 +0100 Subject: [PATCH 06/15] Fix time parsing --- lib/text/lrc_text_parser.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/text/lrc_text_parser.js b/lib/text/lrc_text_parser.js index b9491f2e7b..af76c623ef 100644 --- a/lib/text/lrc_text_parser.js +++ b/lib/text/lrc_text_parser.js @@ -96,9 +96,8 @@ shaka.text.LrcTextParser = class { const LrcTextParser = shaka.text.LrcTextParser; const match = LrcTextParser.timeFormat_.exec(string); const minutes = parseInt(match[1], 10); - const seconds = parseInt(match[2], 10); - const hundredthsOfSeconds = match[4] ? parseInt(match[4], 10) : 0; - return minutes * 60 + seconds + hundredthsOfSeconds / 100; + const seconds = parseInt(match[2], 10) + parseFloat(match[3]); + return minutes * 60 + seconds; } /** From 20b9b07f50c9ded847039dce609aa5f6f13844ee Mon Sep 17 00:00:00 2001 From: Alvaro Velad Date: Wed, 23 Dec 2020 21:59:18 +0100 Subject: [PATCH 07/15] Add more tests --- test/text/lrc_text_parser_unit.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/text/lrc_text_parser_unit.js b/test/text/lrc_text_parser_unit.js index c566ec5561..11c524f05c 100644 --- a/test/text/lrc_text_parser_unit.js +++ b/test/text/lrc_text_parser_unit.js @@ -58,6 +58,19 @@ describe('LrcTextParser', () => { {periodStart: 0, segmentStart: 0, segmentEnd: 0}); }); + it('supports different time formats', () => { + verifyHelper( + [ + {startTime: 0.1, endTime: 10.001, payload: 'Test'}, + {startTime: 10.001, endTime: 20.02, payload: 'Test2'}, + {startTime: 20.02, endTime: 22.02, payload: 'Test3'}, + ], + '[00:00.10]Test\n' + + '[00:10.001]Test2\n' + + '[00:20.02]Test3', + {periodStart: 0, segmentStart: 0, segmentEnd: 0}); + }); + /** * @param {!Array} cues * @param {string} text From 96bb3757a39aa02d6065a15558b0c3fb967daaf4 Mon Sep 17 00:00:00 2001 From: Alvaro Velad Date: Mon, 28 Dec 2020 07:55:54 +0100 Subject: [PATCH 08/15] Fix example --- lib/text/lrc_text_parser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/text/lrc_text_parser.js b/lib/text/lrc_text_parser.js index af76c623ef..29d2fac867 100644 --- a/lib/text/lrc_text_parser.js +++ b/lib/text/lrc_text_parser.js @@ -134,7 +134,7 @@ shaka.text.LrcTextParser = class { /** * @const * @private {!RegExp} - * @example 50t or 50.5t + * @example [mm:ss.xx]Text */ shaka.text.LrcTextParser.lyricLine_ = /^\[(\d{1,2}:\d{1,2}([.,]\d{1,3})?)\](.*)(\r?\n)*$/; From 07e942568290d88ae91bd39d3542ec94c61c8ba8 Mon Sep 17 00:00:00 2001 From: Alvaro Velad Date: Mon, 28 Dec 2020 08:02:08 +0100 Subject: [PATCH 09/15] Update regex --- lib/text/lrc_text_parser.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/text/lrc_text_parser.js b/lib/text/lrc_text_parser.js index 29d2fac867..195c89ca70 100644 --- a/lib/text/lrc_text_parser.js +++ b/lib/text/lrc_text_parser.js @@ -65,7 +65,7 @@ shaka.text.LrcTextParser = class { const startTime = LrcTextParser.parseTime_(match[1]); // By default we add 2 seconds of duration. const endTime = startTime + 2; - const payload = match[3]; + const payload = match[2]; const cue = new shaka.text.Cue(startTime, endTime, payload); // Update previous @@ -96,7 +96,7 @@ shaka.text.LrcTextParser = class { const LrcTextParser = shaka.text.LrcTextParser; const match = LrcTextParser.timeFormat_.exec(string); const minutes = parseInt(match[1], 10); - const seconds = parseInt(match[2], 10) + parseFloat(match[3]); + const seconds = parseFloat(match[2]); return minutes * 60 + seconds; } @@ -137,7 +137,7 @@ shaka.text.LrcTextParser = class { * @example [mm:ss.xx]Text */ shaka.text.LrcTextParser.lyricLine_ = - /^\[(\d{1,2}:\d{1,2}([.,]\d{1,3})?)\](.*)(\r?\n)*$/; + /^\[(\d{1,2}:\d{1,2}(?:[.,]\d{1,3})?)\](.*)/; /** * @const @@ -145,7 +145,7 @@ shaka.text.LrcTextParser.lyricLine_ = * @example 00:12.00 or 00:12.0 or 00:12.000 */ shaka.text.LrcTextParser.timeFormat_ = - /^\s*(\d+):(\d{1,2})([.,](\d{1,3}))?\s*$/; + /^(\d+):(\d{1,2}(?:[.,]\d{1,3})?)$/; shaka.text.TextEngine.registerParser( 'application/x-subtitle-lrc', () => new shaka.text.LrcTextParser()); From 9d53dcfec817a2cc411ad6b31d565b582ac51206 Mon Sep 17 00:00:00 2001 From: Alvaro Velad Date: Mon, 28 Dec 2020 08:11:55 +0100 Subject: [PATCH 10/15] Fix endTime --- lib/player.js | 2 +- lib/text/lrc_text_parser.js | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/player.js b/lib/player.js index 80332fb765..62e92b2b59 100644 --- a/lib/player.js +++ b/lib/player.js @@ -4178,7 +4178,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { if (mimeType === 'application/x-subtitle-lrc') { const LrcTextParser = shaka.text.LrcTextParser; if (LrcTextParser) { - return LrcTextParser.convertToWebVTT(data); + return LrcTextParser.convertToWebVTT(data, this.video_.duration); } else { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, diff --git a/lib/text/lrc_text_parser.js b/lib/text/lrc_text_parser.js index 195c89ca70..f3a0430349 100644 --- a/lib/text/lrc_text_parser.js +++ b/lib/text/lrc_text_parser.js @@ -33,15 +33,16 @@ shaka.text.LrcTextParser = class { * @export */ parseMedia(data, time) { - return shaka.text.LrcTextParser.getCues_(data); + return shaka.text.LrcTextParser.getCues_(data, time.segmentEnd); } /** * @param {BufferSource} data + * @param {number} segmentEnd * @return {!Array.} * @private */ - static getCues_(data) { + static getCues_(data, segmentEnd) { const StringUtils = shaka.util.StringUtils; const LrcTextParser = shaka.text.LrcTextParser; @@ -63,8 +64,9 @@ shaka.text.LrcTextParser = class { const match = LrcTextParser.lyricLine_.exec(line); if (match) { const startTime = LrcTextParser.parseTime_(match[1]); + // This time can be overwritten by a subsequent cue. // By default we add 2 seconds of duration. - const endTime = startTime + 2; + const endTime = segmentEnd ? segmentEnd : startTime + 2; const payload = match[2]; const cue = new shaka.text.Cue(startTime, endTime, payload); @@ -104,12 +106,13 @@ shaka.text.LrcTextParser = class { * Convert a LRC input to WebVTT * * @param {BufferSource} data + * @param {number} segmentEnd * @return {string} * @export */ - static convertToWebVTT(data) { + static convertToWebVTT(data, segmentEnd) { const LrcTextParser = shaka.text.LrcTextParser; - const cues = LrcTextParser.getCues_(data); + const cues = LrcTextParser.getCues_(data, segmentEnd); let webvttString = 'WEBVTT\n\n'; for (const cue of cues) { const webvttTimeString = (time) => { From 03994c1615d7a1f0ea84e258fe3a269ed87c3991 Mon Sep 17 00:00:00 2001 From: Alvaro Velad Date: Mon, 28 Dec 2020 20:53:27 +0100 Subject: [PATCH 11/15] Add more examples --- lib/text/lrc_text_parser.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/text/lrc_text_parser.js b/lib/text/lrc_text_parser.js index f3a0430349..e229560b17 100644 --- a/lib/text/lrc_text_parser.js +++ b/lib/text/lrc_text_parser.js @@ -137,7 +137,8 @@ shaka.text.LrcTextParser = class { /** * @const * @private {!RegExp} - * @example [mm:ss.xx]Text + * @example [00:12.0]Text or [00:12.00]Text or [00:12.000]Text or + * [00:12,0]Text or [00:12,00]Text or [00:12,000]Text */ shaka.text.LrcTextParser.lyricLine_ = /^\[(\d{1,2}:\d{1,2}(?:[.,]\d{1,3})?)\](.*)/; @@ -145,7 +146,8 @@ shaka.text.LrcTextParser.lyricLine_ = /** * @const * @private {!RegExp} - * @example 00:12.00 or 00:12.0 or 00:12.000 + * @example 00:12.0 or 00:12.00 or 00:12.000 or + * 00:12,0 or 00:12,00 or 00:12,000 */ shaka.text.LrcTextParser.timeFormat_ = /^(\d+):(\d{1,2}(?:[.,]\d{1,3})?)$/; From 4141d8fd5fc10aaea2a886899ad738c82862a37d Mon Sep 17 00:00:00 2001 From: Alvaro Velad Date: Mon, 28 Dec 2020 20:53:38 +0100 Subject: [PATCH 12/15] Add more tests --- test/text/lrc_text_parser_unit.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/text/lrc_text_parser_unit.js b/test/text/lrc_text_parser_unit.js index 11c524f05c..a03c5e3d05 100644 --- a/test/text/lrc_text_parser_unit.js +++ b/test/text/lrc_text_parser_unit.js @@ -63,11 +63,17 @@ describe('LrcTextParser', () => { [ {startTime: 0.1, endTime: 10.001, payload: 'Test'}, {startTime: 10.001, endTime: 20.02, payload: 'Test2'}, - {startTime: 20.02, endTime: 22.02, payload: 'Test3'}, + {startTime: 20.02, endTime: 30.1, payload: 'Test3'}, + {startTime: 30.1, endTime: 40.001, payload: 'Test'}, + {startTime: 40.001, endTime: 50.02, payload: 'Test2'}, + {startTime: 50.02, endTime: 52.02, payload: 'Test3'}, ], - '[00:00.10]Test\n' + + '[00:00.1]Test\n' + '[00:10.001]Test2\n' + - '[00:20.02]Test3', + '[00:20.02]Test3\n' + + '[00:30.1]Test4\n' + + '[00:40.001]Test5\n' + + '[00:50.02]Test6', {periodStart: 0, segmentStart: 0, segmentEnd: 0}); }); From 1242b335509a1528d7bcb880fe17924adce35de1 Mon Sep 17 00:00:00 2001 From: Alvaro Velad Date: Mon, 28 Dec 2020 20:57:01 +0100 Subject: [PATCH 13/15] Fix tests --- test/text/lrc_text_parser_unit.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/text/lrc_text_parser_unit.js b/test/text/lrc_text_parser_unit.js index a03c5e3d05..19d4b7814b 100644 --- a/test/text/lrc_text_parser_unit.js +++ b/test/text/lrc_text_parser_unit.js @@ -64,9 +64,9 @@ describe('LrcTextParser', () => { {startTime: 0.1, endTime: 10.001, payload: 'Test'}, {startTime: 10.001, endTime: 20.02, payload: 'Test2'}, {startTime: 20.02, endTime: 30.1, payload: 'Test3'}, - {startTime: 30.1, endTime: 40.001, payload: 'Test'}, - {startTime: 40.001, endTime: 50.02, payload: 'Test2'}, - {startTime: 50.02, endTime: 52.02, payload: 'Test3'}, + {startTime: 30.1, endTime: 40.001, payload: 'Test4'}, + {startTime: 40.001, endTime: 50.02, payload: 'Test5'}, + {startTime: 50.02, endTime: 52.02, payload: 'Test6'}, ], '[00:00.1]Test\n' + '[00:10.001]Test2\n' + From 86eb708eae2faae7810dec442a64039b90e33a84 Mon Sep 17 00:00:00 2001 From: Alvaro Velad Date: Mon, 28 Dec 2020 22:15:38 +0100 Subject: [PATCH 14/15] Fix test typo --- test/text/lrc_text_parser_unit.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/text/lrc_text_parser_unit.js b/test/text/lrc_text_parser_unit.js index 19d4b7814b..70e87f207d 100644 --- a/test/text/lrc_text_parser_unit.js +++ b/test/text/lrc_text_parser_unit.js @@ -71,9 +71,9 @@ describe('LrcTextParser', () => { '[00:00.1]Test\n' + '[00:10.001]Test2\n' + '[00:20.02]Test3\n' + - '[00:30.1]Test4\n' + - '[00:40.001]Test5\n' + - '[00:50.02]Test6', + '[00:30,1]Test4\n' + + '[00:40,001]Test5\n' + + '[00:50,02]Test6', {periodStart: 0, segmentStart: 0, segmentEnd: 0}); }); From 39ceaf433b4a42eb878b16989ff257f08863ec61 Mon Sep 17 00:00:00 2001 From: Alvaro Velad Date: Mon, 28 Dec 2020 22:17:08 +0100 Subject: [PATCH 15/15] Fix decimal parse with comma --- lib/text/lrc_text_parser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/text/lrc_text_parser.js b/lib/text/lrc_text_parser.js index e229560b17..7905902c40 100644 --- a/lib/text/lrc_text_parser.js +++ b/lib/text/lrc_text_parser.js @@ -98,7 +98,7 @@ shaka.text.LrcTextParser = class { const LrcTextParser = shaka.text.LrcTextParser; const match = LrcTextParser.timeFormat_.exec(string); const minutes = parseInt(match[1], 10); - const seconds = parseFloat(match[2]); + const seconds = parseFloat(match[2].replace(',', '.')); return minutes * 60 + seconds; }