From 609beb3400bde4f3c5469acdc0a2c681c736c287 Mon Sep 17 00:00:00 2001 From: Joe Forbes Date: Thu, 10 Jan 2019 10:58:47 -0800 Subject: [PATCH] feat: expose custom M3U8 mapper API (#325) --- README.md | 7 +++++++ package-lock.json | 14 +++++++++++--- package.json | 2 +- src/playlist-loader.js | 7 +++++++ src/videojs-http-streaming.js | 4 +++- test/configuration.test.js | 31 ++++++++++++++++++++++++++----- test/playlist-loader.test.js | 16 ++++++++++++++-- 7 files changed, 69 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 4e6434e51..c28d62432 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ Video.js Compatibility: 6.0, 7.0 - [smoothQualityChange](#smoothqualitychange) - [allowSeeksWithinUnsafeLiveWindow](#allowseekswithinunsafelivewindow) - [customTagParsers](#customtagparsers) + - [customTagMappers](#customtagmappers) - [Runtime Properties](#runtime-properties) - [hls.playlists.master](#hlsplaylistsmaster) - [hls.playlists.media](#hlsplaylistsmedia) @@ -401,6 +402,12 @@ The property defaults to `false`. With `customTagParsers` you can pass an array of custom m3u8 tag parser objects. See https://github.com/videojs/m3u8-parser#custom-parsers +##### customTagMappers +* Type: `Array` +* can be used as a source option + +Similar to `customTagParsers`, with `customTagMappers` you can pass an array of custom m3u8 tag mapper objects. See https://github.com/videojs/m3u8-parser#custom-parsers + ### Runtime Properties Runtime properties are attached to the tech object when HLS is in use. You can get a reference to the HLS source handler like this: diff --git a/package-lock.json b/package-lock.json index 83c5af70c..f29028f9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,11 @@ "video.js": "^6.8.0 || ^7.0.0" }, "dependencies": { + "m3u8-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.2.0.tgz", + "integrity": "sha512-LVHw0U6IPJjwk9i9f7Xe26NqaUHTNlIt4SSWoEfYFROeVKHN6MIjOhbRheI3dg8Jbq5WCuMFQ0QU3EgZpmzFPg==" + }, "mpd-parser": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.6.1.tgz", @@ -6217,9 +6222,12 @@ "dev": true }, "m3u8-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.2.0.tgz", - "integrity": "sha512-LVHw0U6IPJjwk9i9f7Xe26NqaUHTNlIt4SSWoEfYFROeVKHN6MIjOhbRheI3dg8Jbq5WCuMFQ0QU3EgZpmzFPg==" + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.3.0.tgz", + "integrity": "sha512-bVbjuBMoVIgFL1vpXVIxjeaoB5TPDJRb0m5qiTdM738SGqv/LAmsnVVPlKjM4fulm/rr1XZsKM+owHm+zvqxYA==", + "requires": { + "global": "^4.3.2" + } }, "magic-string": { "version": "0.22.5", diff --git a/package.json b/package.json index 1414e4ebb..c9be73aba 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "dependencies": { "aes-decrypter": "3.0.0", "global": "^4.3.0", - "m3u8-parser": "4.2.0", + "m3u8-parser": "4.3.0", "mpd-parser": "0.7.0", "mux.js": "5.0.1", "url-toolkit": "^2.1.3", diff --git a/src/playlist-loader.js b/src/playlist-loader.js index bf60034f7..f8e994219 100644 --- a/src/playlist-loader.js +++ b/src/playlist-loader.js @@ -206,6 +206,7 @@ export default class PlaylistLoader extends EventTarget { const options = hls.options_; this.customTagParsers = (options && options.customTagParsers) || []; + this.customTagMappers = (options && options.customTagMappers) || []; if (!this.srcUrl) { throw new Error('A non-empty playlist URL is required'); @@ -273,6 +274,9 @@ export default class PlaylistLoader extends EventTarget { // adding custom tag parsers this.customTagParsers.forEach(customParser => parser.addParser(customParser)); + // adding custom tag mappers + this.customTagMappers.forEach(mapper => parser.addTagMapper(mapper)); + parser.push(xhr.responseText); parser.end(); parser.manifest.uri = url; @@ -515,6 +519,9 @@ export default class PlaylistLoader extends EventTarget { // adding custom tag parsers this.customTagParsers.forEach(customParser => parser.addParser(customParser)); + // adding custom tag mappers + this.customTagMappers.forEach(mapper => parser.addTagMapper(mapper)); + parser.push(req.responseText); parser.end(); diff --git a/src/videojs-http-streaming.js b/src/videojs-http-streaming.js index 6d0707b48..36137cd14 100644 --- a/src/videojs-http-streaming.js +++ b/src/videojs-http-streaming.js @@ -402,6 +402,7 @@ class HlsHandler extends Component { this.source_.useBandwidthFromLocalStorage : this.options_.useBandwidthFromLocalStorage || false; this.options_.customTagParsers = this.options_.customTagParsers || []; + this.options_.customTagMappers = this.options_.customTagMappers || []; if (typeof this.options_.blacklistDuration !== 'number') { this.options_.blacklistDuration = 5 * 60; @@ -439,7 +440,8 @@ class HlsHandler extends Component { 'limitRenditionByPlayerDimensions', 'bandwidth', 'smoothQualityChange', - 'customTagParsers' + 'customTagParsers', + 'customTagMappers' ].forEach((option) => { if (typeof this.source_[option] !== 'undefined') { this.options_[option] = this.source_[option]; diff --git a/test/configuration.test.js b/test/configuration.test.js index 645ef256b..9cbac8050 100644 --- a/test/configuration.test.js +++ b/test/configuration.test.js @@ -38,6 +38,27 @@ const options = [{ default: false, test: true, alt: false +}, { + name: 'useBandwidthFromLocalStorage', + default: false, + test: true +}, { + name: 'customTagParsers', + default: [], + test: [{ + expression: /#PARSER/, + customType: 'test', + segment: true + }] +}, { + name: 'customTagMappers', + default: [], + test: [{ + expression: /#MAPPER/, + map(line) { + return `#FOO`; + } + }] }]; const CONFIG_KEYS = Object.keys(Config); @@ -270,7 +291,7 @@ options.forEach((opt) => { let hls = this.player.tech_.hls; - assert.equal(hls.options_[opt.name], + assert.deepEqual(hls.options_[opt.name], opt.default, `${opt.name} should be default`); }); @@ -306,7 +327,7 @@ options.forEach((opt) => { let hls = this.player.tech_.hls; - assert.equal(hls.options_[opt.name], + assert.deepEqual(hls.options_[opt.name], opt.test, `${opt.name} should be equal to sourceHandler Option`); }); @@ -325,7 +346,7 @@ options.forEach((opt) => { let hls = this.player.tech_.hls; - assert.equal(hls.options_[opt.name], + assert.deepEqual(hls.options_[opt.name], opt.test, `${opt.name} should be equal to src option`); }); @@ -345,7 +366,7 @@ options.forEach((opt) => { let hls = this.player.tech_.hls; - assert.equal(hls.options_[opt.name], + assert.deepEqual(hls.options_[opt.name], opt.test, `${opt.name} should be equal to sourchHandler option`); }); @@ -366,7 +387,7 @@ options.forEach((opt) => { let hls = this.player.tech_.hls; - assert.equal(hls.options_[opt.name], + assert.deepEqual(hls.options_[opt.name], opt.test, `${opt.name} should be equal to sourchHandler option`); }); diff --git a/test/playlist-loader.test.js b/test/playlist-loader.test.js index e11d16ac8..b4a0cf320 100644 --- a/test/playlist-loader.test.js +++ b/test/playlist-loader.test.js @@ -862,14 +862,24 @@ QUnit.test('logs warning for master playlist with invalid STREAM-INF', function( 'logged a warning'); }); -QUnit.test('executes custom tag parsers', function(assert) { +QUnit.test('executes custom parsers and mappers', function(assert) { const customTagParsers = [{ expression: /#PARSER/, customType: 'test', segment: true }]; + const customTagMappers = [{ + expression: /#MAPPER/, + map(line) { + const regex = /#MAPPER:(\d+)/g; + const match = regex.exec(line); + const ISOdate = new Date(Number(match[1])).toISOString(); + + return `#EXT-X-PROGRAM-DATE-TIME:${ISOdate}`; + } + }]; - this.fakeHls.options_ = { customTagParsers }; + this.fakeHls.options_ = { customTagParsers, customTagMappers }; let loader = new PlaylistLoader('master.m3u8', this.fakeHls); @@ -877,6 +887,7 @@ QUnit.test('executes custom tag parsers', function(assert) { this.requests.pop().respond(200, null, '#EXTM3U\n' + '#PARSER:parsed\n' + + '#MAPPER:1511816599485\n' + '#EXTINF:10,\n' + '0.ts\n' + '#EXT-X-ENDLIST\n'); @@ -884,6 +895,7 @@ QUnit.test('executes custom tag parsers', function(assert) { const segment = loader.master.playlists[0].segments[0]; assert.strictEqual(segment.custom.test, '#PARSER:parsed', 'parsed custom tag'); + assert.ok(segment.dateTimeObject, 'converted and parsed custom time'); delete this.fakeHls.options_; });