From 50731731b3c87a481ec287914b1bdff52ed000dc Mon Sep 17 00:00:00 2001 From: Nicholas Frederiksen Date: Mon, 25 Nov 2024 16:39:47 +0100 Subject: [PATCH 1/3] Fix: Extend HLS Interstitial Tag Insertion (#50) * interstitial: handle more attributes and set start-date more accurately * fix if statement * update unit tests --- index.js | 120 ++++++++++++++++++++++++++++++---------- spec/hls_splice_spec.js | 26 +++++---- 2 files changed, 108 insertions(+), 38 deletions(-) diff --git a/index.js b/index.js index ad5273c..6653905 100644 --- a/index.js +++ b/index.js @@ -20,7 +20,7 @@ const NOT_MULTIVARIANT_ERROR_MSG = "Error: Source is Not a Multivariant Manifest let DUMMY_SUBTITLE_COUNT = 0; const getDummySubtitleSegmentId = () => { return DUMMY_SUBTITLE_COUNT++; -} +}; const findNearestGroupAndLang = (_group, _language, _playlist) => { const groups = Object.keys(_playlist); @@ -93,7 +93,6 @@ class HLSSpliceVod { } this.cmafMapUri = { video: {}, audio: {}, subtitle: {} }; - } loadMasterManifest(_injectMasterManifest, _injectMediaManifest, _injectAudioManifest, _injectSubtitleManifest) { @@ -214,7 +213,9 @@ class HLSSpliceVod { subtitleItemUri = subtitleItem.get("uri"); } const subtitleItemGroupId = subtitleItem.get("group-id"); - const subtitleItemLanguage = subtitleItem.get("language") ? subtitleItem.get("language") : subtitleItem.get("name"); + const subtitleItemLanguage = subtitleItem.get("language") + ? subtitleItem.get("language") + : subtitleItem.get("name"); if (loadedSubtitleGroupLangs.includes(`${subtitleItemGroupId}-${subtitleItemLanguage}`)) { continue; } else { @@ -222,7 +223,12 @@ class HLSSpliceVod { } const subtitleManifestUrl = url.resolve(baseUrl, subtitleItemUri); mediaManifestPromises.push( - this.loadSubtitleManifest(subtitleManifestUrl, subtitleItemGroupId, subtitleItemLanguage, _injectSubtitleManifest) + this.loadSubtitleManifest( + subtitleManifestUrl, + subtitleItemGroupId, + subtitleItemLanguage, + _injectSubtitleManifest + ) ); } Promise.all(mediaManifestPromises).then(resolve).catch(reject); @@ -243,16 +249,16 @@ class HLSSpliceVod { _createFakeSubtitles(videoPlaylist) { let bw = Object.keys(videoPlaylist)[0]; - const [nearestGroup, nearestLang] = findNearestGroupAndLang("temp", "temp", this.playlistsSubtitle) + const [nearestGroup, nearestLang] = findNearestGroupAndLang("temp", "temp", this.playlistsSubtitle); let subtitleItems = {}; subtitleItems[nearestGroup] = {}; subtitleItems[nearestGroup][nearestLang] = {}; - const vp = videoPlaylist[bw] + const vp = videoPlaylist[bw]; let m3u = m3u8.M3U.create(); - vp.items.PlaylistItem.forEach(element => m3u.addPlaylistItem(element.properties)) + vp.items.PlaylistItem.forEach((element) => m3u.addPlaylistItem(element.properties)); subtitleItems[nearestGroup][nearestLang] = m3u; const playlist = subtitleItems[nearestGroup][nearestLang]; @@ -263,14 +269,17 @@ class HLSSpliceVod { for (let index = 0; index < playlist.items.PlaylistItem.length; index++) { if (playlist.items.PlaylistItem[index].get("duration")) { duration += playlist.items.PlaylistItem[index].get("duration"); - playlist.items.PlaylistItem[index].set("uri", this.dummySubtitleEndpoint + `?id=${getDummySubtitleSegmentId()}`); + playlist.items.PlaylistItem[index].set( + "uri", + this.dummySubtitleEndpoint + `?id=${getDummySubtitleSegmentId()}` + ); } } return [subtitleItems, duration]; } _insertAdAtExtraMedia(startOffset, offset, playlists, adPlaylists, targetDuration, adDuration, isPostRoll) { - let groups = Object.keys(playlists) + let groups = Object.keys(playlists); if (isPostRoll) { let duration = 0; const langs = Object.keys(playlists[groups[0]]); @@ -333,7 +342,14 @@ class HLSSpliceVod { } } - insertAdAt(offset, adMasterManifestUri, _injectAdMasterManifest, _injectAdMediaManifest, _injectAdAudioManifest, _injectAdSubtitleManifest) { + insertAdAt( + offset, + adMasterManifestUri, + _injectAdMasterManifest, + _injectAdMediaManifest, + _injectAdAudioManifest, + _injectAdSubtitleManifest + ) { this.ad = {}; return new Promise((resolve, reject) => { this._parseAdMasterManifest( @@ -416,11 +432,27 @@ class HLSSpliceVod { const audioGroups = Object.keys(this.playlistsAudio); const adAudioGroups = Object.keys(ad.playlistAudio); if (audioGroups.length > 0 && adAudioGroups.length > 0) { - this._insertAdAtExtraMedia(startOffset, offset, this.playlistsAudio, ad.playlistAudio, this.targetDurationAudio, ad.durationAudio, isPostRoll) + this._insertAdAtExtraMedia( + startOffset, + offset, + this.playlistsAudio, + ad.playlistAudio, + this.targetDurationAudio, + ad.durationAudio, + isPostRoll + ); } if (subtitleGroups.length > 0) { - this._insertAdAtExtraMedia(startOffset, offset, this.playlistsSubtitle, ad.playlistSubtitle, this.targetDurationSubtitle, ad.durationSubtile, isPostRoll) + this._insertAdAtExtraMedia( + startOffset, + offset, + this.playlistsSubtitle, + ad.playlistSubtitle, + this.targetDurationSubtitle, + ad.durationSubtile, + isPostRoll + ); } resolve(); }) @@ -428,7 +460,7 @@ class HLSSpliceVod { }); } - _insertInterstitialAtExtraMedia(offset, id, uri, isAssetList, extraAttrs, plannedDuration, playlists) { + _insertInterstitialAtExtraMedia(offset, id, uri, isAssetList, extraAttrs, startDate, playlists, opts) { const groups = Object.keys(playlists); for (let i = 0; i < groups.length; i++) { const group = groups[i]; @@ -442,12 +474,13 @@ class HLSSpliceVod { while (pos < offset && idx < playlist.items.PlaylistItem.length) { const plItem = playlist.items.PlaylistItem[idx]; pos += plItem.get("duration") * 1000; - idx++; + if (pos <= offset) { + idx++; + } } - let startDate = new Date(1 + offset).toISOString(); let durationTag = ""; - if (plannedDuration) { - durationTag = `,DURATION=${plannedDuration / 1000}`; + if (opts && opts.plannedDuration) { + durationTag = `,DURATION=${opts.plannedDuration / 1000}`; } if (isAssetList) { playlist.items.PlaylistItem[idx].set( @@ -469,7 +502,7 @@ class HLSSpliceVod { if (this.bumperDuration) { offset = this.bumperDuration + offset; } - + let startDate; let extraAttrs = ""; if (opts) { if (opts.resumeOffset !== undefined) { @@ -481,6 +514,31 @@ class HLSSpliceVod { if (opts.snap === "IN" || opts.snap === "OUT") { extraAttrs += `,X-SNAP="${opts.snap}"`; } + if (opts.restrict !== undefined) { + if (opts.restrict.includes("SKIP") || opts.restrict.includes("JUMP")) { + extraAttrs += `,X-RESTRICT="${opts.restrict}"`; + } + } + if (opts.contentmayvary === "YES" || opts.contentmayvary === "NO") { + extraAttrs += `,X-CONTENT-MAY-VARY="${opts.contentmayvary}"`; + } + if (opts.timelineoccupies === "POINT" || opts.timelineoccupies === "RANGE") { + extraAttrs += `,X-TIMELINE-OCCUPIES="${opts.timelineoccupies}"`; + } + if (opts.timelinestyle === "HIGHLIGHT" || opts.timelinestyle === "PRIMARY") { + extraAttrs += `,X-TIMELINE-STYLE="${opts.timelinestyle}"`; + } + if (opts.custombeacon !== undefined) { + let custombeacon = ""; + if (opts.custombeacon.includes("%")) { + custombeacon = decodeURIComponent(opts.custombeacon); + } else { + custombeacon = opts.custombeacon; + } + if (custombeacon.charAt(0) === "X") { + extraAttrs += `,${custombeacon}`; + } + } } const bandwidths = Object.keys(this.playlists); @@ -492,9 +550,11 @@ class HLSSpliceVod { while (pos < offset && i < this.playlists[bw].items.PlaylistItem.length) { const plItem = this.playlists[bw].items.PlaylistItem[i]; pos += plItem.get("duration") * 1000; - i++; + if (pos <= offset) { + i++; + } } - let startDate = new Date(1 + offset).toISOString(); + startDate = new Date(1 + Number(offset)).toISOString(); let durationTag = ""; if (opts && opts.plannedDuration) { durationTag = `,DURATION=${opts.plannedDuration / 1000}`; @@ -512,11 +572,9 @@ class HLSSpliceVod { } } - let plannedDuration = (opts && opts.plannedDuration) ? opts.plannedDuration : 0; - - this._insertInterstitialAtExtraMedia(offset, id, uri, isAssetList, extraAttrs, plannedDuration, this.playlistsAudio) + this._insertInterstitialAtExtraMedia(offset, id, uri, isAssetList, extraAttrs, startDate, this.playlistsAudio, opts); - this._insertInterstitialAtExtraMedia(offset, id, uri, isAssetList, extraAttrs, plannedDuration, this.playlistsSubtitle) + this._insertInterstitialAtExtraMedia(offset, id, uri, isAssetList, extraAttrs, startDate, this.playlistsSubtitle, opts); resolve(); }); @@ -585,9 +643,9 @@ class HLSSpliceVod { this.playlists[bw].set("targetDuration", this.targetDuration); } - this._insertBumperExtraMedia(this.playlistsAudio, bumper.playlistAudio, this.targetDurationAudio) + this._insertBumperExtraMedia(this.playlistsAudio, bumper.playlistAudio, this.targetDurationAudio); - this._insertBumperExtraMedia(this.playlistsSubtitle, bumper.playlistSubtitle, this.targetDurationSubtitle) + this._insertBumperExtraMedia(this.playlistsSubtitle, bumper.playlistSubtitle, this.targetDurationSubtitle); resolve(); }) @@ -901,7 +959,7 @@ class HLSSpliceVod { this.targetDurationSubtitle = targetDuration; } this.playlistsSubtitle[group][lang].set("targetDuration", this.targetDurationSubtitle); - + const initSegUri = this._getCmafMapUri(m3u, subtitleManifestUri, this.baseUrl); if (initSegUri) { if (!this.cmafMapUri.subtitle[group]) { @@ -925,7 +983,13 @@ class HLSSpliceVod { }); } - _parseAdMasterManifest(manifestUri, _injectAdMasterManifest, _injectAdMediaManifest, _injectAdAudioManifest, _injectAdSubtitleManifest) { + _parseAdMasterManifest( + manifestUri, + _injectAdMasterManifest, + _injectAdMediaManifest, + _injectAdAudioManifest, + _injectAdSubtitleManifest + ) { return new Promise((resolve, reject) => { let ad = {}; const parser = m3u8.createStream(); diff --git a/spec/hls_splice_spec.js b/spec/hls_splice_spec.js index 560aad0..86f3a71 100644 --- a/spec/hls_splice_spec.js +++ b/spec/hls_splice_spec.js @@ -1,6 +1,10 @@ const HLSSpliceVod = require("../index.js"); const fs = require("fs"); +const ll = (log_lines) => { + log_lines.map((line, idx) => console.log(line, idx)); +} + describe("HLSSpliceVod", () => { let mockMasterManifest; let mockMediaManifest; @@ -439,7 +443,7 @@ describe("HLSSpliceVod", () => { .then(() => { const m3u8 = mockVod.getMediaManifest(4497000); const lines = m3u8.split("\n"); - expect(lines[12]).toEqual( + expect(lines[10]).toEqual( '#EXT-X-DATERANGE:ID="001",CLASS="com.apple.hls.interstitial",START-DATE="1970-01-01T00:00:16.001Z",X-ASSET-LIST="http://mock.com/assetlist"' ); done(); @@ -456,7 +460,7 @@ describe("HLSSpliceVod", () => { .then(() => { const m3u8 = mockVod.getMediaManifest(4497000); const lines = m3u8.split("\n"); - expect(lines[12]).toEqual( + expect(lines[10]).toEqual( '#EXT-X-DATERANGE:ID="001",CLASS="com.apple.hls.interstitial",START-DATE="1970-01-01T00:00:16.001Z",X-ASSET-LIST="/assetlist/sdfsdfjlsdfsdf"' ); done(); @@ -1379,12 +1383,12 @@ describe("HLSSpliceVod with Demuxed Audio Tracks,", () => { .then(() => { const m3u8 = mockVod.getMediaManifest(4497000); let lines = m3u8.split("\n"); - expect(lines[12]).toEqual( + expect(lines[10]).toEqual( '#EXT-X-DATERANGE:ID="001",CLASS="com.apple.hls.interstitial",START-DATE="1970-01-01T00:00:16.001Z",X-ASSET-LIST="http://mock.com/assetlist"' ); const m3u8Audio = mockVod.getAudioManifest("stereo", "sv"); lines = m3u8Audio.split("\n"); - expect(lines[12]).toEqual( + expect(lines[10]).toEqual( '#EXT-X-DATERANGE:ID="001",CLASS="com.apple.hls.interstitial",START-DATE="1970-01-01T00:00:16.001Z",X-ASSET-LIST="http://mock.com/assetlist"' ); done(); @@ -1401,12 +1405,12 @@ describe("HLSSpliceVod with Demuxed Audio Tracks,", () => { .then(() => { const m3u8 = mockVod.getMediaManifest(4497000); let lines = m3u8.split("\n"); - expect(lines[12]).toEqual( + expect(lines[10]).toEqual( '#EXT-X-DATERANGE:ID="001",CLASS="com.apple.hls.interstitial",START-DATE="1970-01-01T00:00:16.001Z",X-ASSET-LIST="/assetlist/sdfsdfjlsdfsdf"' ); const m3u8Audio = mockVod.getAudioManifest("stereo", "sv"); lines = m3u8Audio.split("\n"); - expect(lines[12]).toEqual( + expect(lines[10]).toEqual( '#EXT-X-DATERANGE:ID="001",CLASS="com.apple.hls.interstitial",START-DATE="1970-01-01T00:00:16.001Z",X-ASSET-LIST="/assetlist/sdfsdfjlsdfsdf"' ); done(); @@ -1543,6 +1547,7 @@ describe("HLSSpliceVod with Demuxed Audio Tracks,", () => { .then(() => { const m3u8 = mockVod.getMediaManifest(4497000); let lines = m3u8.split("\n"); + expect(lines[12]).toEqual( '#EXT-X-DATERANGE:ID="001",CLASS="com.apple.hls.interstitial",START-DATE="1970-01-01T00:00:18.001Z",X-ASSET-URI="http://mock.com/asseturi",X-SNAP="OUT"' ); @@ -1980,13 +1985,12 @@ test-audio=256000-6.m4s`; .then(() => { const m3u8 = mockVod.getMediaManifest(4497000); let lines = m3u8.split("\n"); - expect(lines[22]).toEqual( + expect(lines[20]).toEqual( '#EXT-X-DATERANGE:ID="001",CLASS="com.apple.hls.interstitial",START-DATE="1970-01-01T00:00:16.001Z",X-ASSET-LIST="http://mock.com/assetlist"' ); const m3u8Audio = mockVod.getAudioManifest("stereo", "sv"); lines = m3u8Audio.split("\n"); - //lines.map((l, i) => console.log(l, i)); - expect(lines[28]).toEqual( + expect(lines[26]).toEqual( '#EXT-X-DATERANGE:ID="001",CLASS="com.apple.hls.interstitial",START-DATE="1970-01-01T00:00:16.001Z",X-ASSET-LIST="http://mock.com/assetlist"' ); done(); @@ -2005,12 +2009,14 @@ test-audio=256000-6.m4s`; .then(() => { const m3u8 = mockVod.getMediaManifest(4497000); let lines = m3u8.split("\n"); + expect(lines[22]).toEqual( '#EXT-X-DATERANGE:ID="001",CLASS="com.apple.hls.interstitial",START-DATE="1970-01-01T00:00:18.001Z",DURATION=30,X-ASSET-LIST="http://mock.com/asseturi"' ); const m3u8Audio = mockVod.getAudioManifest("stereo", "sv"); lines = m3u8Audio.split("\n"); - expect(lines[30]).toEqual( + + expect(lines[28]).toEqual( '#EXT-X-DATERANGE:ID="001",CLASS="com.apple.hls.interstitial",START-DATE="1970-01-01T00:00:18.001Z",DURATION=30,X-ASSET-LIST="http://mock.com/asseturi"' ); done(); From 2007119cff03b712d3c002e40d9357477251deb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Birm=C3=A9?= Date: Tue, 26 Nov 2024 17:01:36 +0100 Subject: [PATCH 2/3] 5.0.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index bf4ae62..b9b7f11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@eyevinn/hls-splice", - "version": "0.4.12", + "version": "5.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@eyevinn/hls-splice", - "version": "0.4.12", + "version": "5.0.1", "license": "MIT", "dependencies": { "@eyevinn/m3u8": "^0.5.6", diff --git a/package.json b/package.json index 635a2ea..baa6cca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eyevinn/hls-splice", - "version": "0.4.12", + "version": "5.0.1", "description": "NPM library to splice HLS VOD", "repository": "https://github.com/Eyevinn/hls-splice", "main": "index.js", From 41ed4ac4fda26a840648ee9e008290333fa9b846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Birm=C3=A9?= Date: Tue, 26 Nov 2024 17:02:11 +0100 Subject: [PATCH 3/3] 0.5.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b9b7f11..02a4ebc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@eyevinn/hls-splice", - "version": "5.0.1", + "version": "0.5.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@eyevinn/hls-splice", - "version": "5.0.1", + "version": "0.5.1", "license": "MIT", "dependencies": { "@eyevinn/m3u8": "^0.5.6", diff --git a/package.json b/package.json index baa6cca..bf15e13 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eyevinn/hls-splice", - "version": "5.0.1", + "version": "0.5.1", "description": "NPM library to splice HLS VOD", "repository": "https://github.com/Eyevinn/hls-splice", "main": "index.js",