From 492fb87a4e82c34f13e9895674d673c9bae1f5ff Mon Sep 17 00:00:00 2001 From: Nfrederiksen Date: Thu, 21 Nov 2024 17:33:44 +0100 Subject: [PATCH 01/10] interstitial: handle more attributes and set start-date more accurately --- index.js | 120 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 92 insertions(+), 28 deletions(-) diff --git a/index.js b/index.js index ad5273c..45e866f 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.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(); From 9cf09fea86dd33e327df4988680095aaf5a742ca Mon Sep 17 00:00:00 2001 From: Nfrederiksen Date: Thu, 21 Nov 2024 17:36:14 +0100 Subject: [PATCH 02/10] fix if statement --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 45e866f..884b08e 100644 --- a/index.js +++ b/index.js @@ -479,7 +479,7 @@ class HLSSpliceVod { } } let durationTag = ""; - if (opts.plannedDuration) { + if (opts && opts.plannedDuration) { durationTag = `,DURATION=${opts.plannedDuration / 1000}`; } if (isAssetList) { From 487c0003a86710142ebd08675d5810dfc9f1af4a Mon Sep 17 00:00:00 2001 From: Nfrederiksen Date: Mon, 25 Nov 2024 16:35:10 +0100 Subject: [PATCH 03/10] update unit tests --- index.js | 4 ++-- spec/hls_splice_spec.js | 26 ++++++++++++++++---------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/index.js b/index.js index 884b08e..6653905 100644 --- a/index.js +++ b/index.js @@ -474,7 +474,7 @@ class HLSSpliceVod { while (pos < offset && idx < playlist.items.PlaylistItem.length) { const plItem = playlist.items.PlaylistItem[idx]; pos += plItem.get("duration") * 1000; - if (pos < offset) { + if (pos <= offset) { idx++; } } @@ -550,7 +550,7 @@ 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; - if (pos < offset) { + if (pos <= offset) { i++; } } 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 31b773528bcbe126988864fe18b30c2bb6547c33 Mon Sep 17 00:00:00 2001 From: Nfrederiksen Date: Mon, 25 Nov 2024 16:41:11 +0100 Subject: [PATCH 04/10] v0.5.0 --- 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..1db5f09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@eyevinn/hls-splice", - "version": "0.4.12", + "version": "0.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@eyevinn/hls-splice", - "version": "0.4.12", + "version": "0.5.0", "license": "MIT", "dependencies": { "@eyevinn/m3u8": "^0.5.6", diff --git a/package.json b/package.json index 635a2ea..7c2f7b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eyevinn/hls-splice", - "version": "0.4.12", + "version": "0.5.0", "description": "NPM library to splice HLS VOD", "repository": "https://github.com/Eyevinn/hls-splice", "main": "index.js", From 0c61c2b87046d32f05b6cc083b6cc42fcc810385 Mon Sep 17 00:00:00 2001 From: Nfrederiksen Date: Wed, 27 Nov 2024 09:41:30 +0100 Subject: [PATCH 05/10] add support for daterange cue tags --- index.js | 147 +++++++++++++++++++++++++++++++++------- spec/hls_splice_spec.js | 42 +++++++++--- 2 files changed, 157 insertions(+), 32 deletions(-) diff --git a/index.js b/index.js index 6653905..7a185ab 100644 --- a/index.js +++ b/index.js @@ -22,6 +22,26 @@ const getDummySubtitleSegmentId = () => { return DUMMY_SUBTITLE_COUNT++; }; +const _parseValidCueValues = (cueStr) => { + if (cueStr.includes(",")) { + const splitCueVals = cueStr.split(","); + if (splitCueVals.length > 0) { + const validVals = []; + for (let cueVal of splitCueVals) { + if (["PRE", "POST", "ONCE"].includes(cueVal)) { + validVals.push(cueVal); + } + } + return validVals.join(","); + } + } else { + if (["PRE", "POST", "ONCE"].includes(cueStr)) { + return cueStr; + } + } + return null; +}; + const findNearestGroupAndLang = (_group, _language, _playlist) => { const groups = Object.keys(_playlist); let group = groups[0]; // default @@ -461,6 +481,14 @@ class HLSSpliceVod { } _insertInterstitialAtExtraMedia(offset, id, uri, isAssetList, extraAttrs, startDate, playlists, opts) { + let HAS_CUE_ATTR = false; + if (opts && opts.cue) { + const cueValue = _parseValidCueValues(opts.cue); + if (cueValue) { + extraAttrs += `,CUE="${cueValue}"`; + HAS_CUE_ATTR = true; + } + } const groups = Object.keys(playlists); for (let i = 0; i < groups.length; i++) { const group = groups[i]; @@ -478,33 +506,70 @@ class HLSSpliceVod { idx++; } } + if (HAS_CUE_ATTR) { + pos = playlist.items.PlaylistItem.length - 1; + } let durationTag = ""; if (opts && opts.plannedDuration) { durationTag = `,DURATION=${opts.plannedDuration / 1000}`; } - if (isAssetList) { - playlist.items.PlaylistItem[idx].set( - "daterange", - `ID=${id},CLASS="com.apple.hls.interstitial",START-DATE="${startDate}"${durationTag},X-ASSET-LIST="${uri}"${extraAttrs}` - ); + + if (HAS_CUE_ATTR) { + if (isAssetList) { + playlist.addPlaylistItem({ + daterange: `ID=${id},CLASS="com.apple.hls.interstitial",START-DATE="${startDate}"${durationTag},X-ASSET-LIST="${uri}"${extraAttrs}`, + }); + } else { + playlist.addPlaylistItem({ + daterange: `ID=${id},CLASS="com.apple.hls.interstitial",START-DATE="${startDate}"${durationTag},X-ASSET-URI="${uri}"${extraAttrs}`, + }); + } } else { - playlist.items.PlaylistItem[idx].set( - "daterange", - `ID=${id},CLASS="com.apple.hls.interstitial",START-DATE="${startDate}"${durationTag},X-ASSET-URI="${uri}"${extraAttrs}` - ); + if (isAssetList) { + playlist.items.PlaylistItem[idx].set( + "daterange", + `ID=${id},CLASS="com.apple.hls.interstitial",START-DATE="${startDate}"${durationTag},X-ASSET-LIST="${uri}"${extraAttrs}` + ); + } else { + playlist.items.PlaylistItem[idx].set( + "daterange", + `ID=${id},CLASS="com.apple.hls.interstitial",START-DATE="${startDate}"${durationTag},X-ASSET-URI="${uri}"${extraAttrs}` + ); + } } } } } - insertInterstitialAt(offset, id, uri, isAssetList, opts) { + insertInterstitialAt(offset, id, uri, isAssetList, opts={ + resumeOffset: undefined, + playoutLimit: undefined, + snap: undefined, + restrict: undefined, + contentmayvary: undefined, + timelineoccupies: undefined, + timelinestyle: undefined, + custombeacon: undefined, + cue: undefined, + }) { + let HAS_CUE_ATTR = false; + return new Promise((resolve, reject) => { if (this.bumperDuration) { offset = this.bumperDuration + offset; } - let startDate; + let startDate; let extraAttrs = ""; if (opts) { + + if (opts.cue) { + const cueValue = _parseValidCueValues(opts.cue); + if (cueValue) { + extraAttrs += `,CUE="${cueValue}"`; + HAS_CUE_ATTR = true; + } + } + if (opts.resumeOffset !== undefined) { extraAttrs += `,X-RESUME-OFFSET=${opts.resumeOffset / 1000}`; } @@ -547,34 +612,70 @@ class HLSSpliceVod { let pos = 0; let i = 0; this.playlists[bw].items.PlaylistItem[0].set("date", new Date(1)); - while (pos < offset && i < this.playlists[bw].items.PlaylistItem.length) { + const playlistSize = this.playlists[bw].items.PlaylistItem.length; + while (pos < offset && i < playlistSize) { const plItem = this.playlists[bw].items.PlaylistItem[i]; pos += plItem.get("duration") * 1000; if (pos <= offset) { i++; } } + + if (HAS_CUE_ATTR) { + pos = playlistSize - 1; + } + startDate = new Date(1 + Number(offset)).toISOString(); let durationTag = ""; if (opts && opts.plannedDuration) { durationTag = `,DURATION=${opts.plannedDuration / 1000}`; } if (isAssetList) { - this.playlists[bw].items.PlaylistItem[i].set( - "daterange", - `ID=${id},CLASS="com.apple.hls.interstitial",START-DATE="${startDate}"${durationTag},X-ASSET-LIST="${uri}"${extraAttrs}` - ); + if (HAS_CUE_ATTR) { + this.playlists[bw].addPlaylistItem({ + daterange: `ID=${id},CLASS="com.apple.hls.interstitial",START-DATE="${startDate}"${durationTag},X-ASSET-LIST="${uri}"${extraAttrs}`, + }); + } else { + this.playlists[bw].items.PlaylistItem[i].set( + "daterange", + `ID=${id},CLASS="com.apple.hls.interstitial",START-DATE="${startDate}"${durationTag},X-ASSET-LIST="${uri}"${extraAttrs}` + ); + } } else { - this.playlists[bw].items.PlaylistItem[i].set( - "daterange", - `ID=${id},CLASS="com.apple.hls.interstitial",START-DATE="${startDate}"${durationTag},X-ASSET-URI="${uri}"${extraAttrs}` - ); + if (HAS_CUE_ATTR) { + this.playlists[bw].addPlaylistItem({ + daterange: `ID=${id},CLASS="com.apple.hls.interstitial",START-DATE="${startDate}"${durationTag},X-ASSET-URI="${uri}"${extraAttrs}`, + }); + } else { + this.playlists[bw].items.PlaylistItem[i].set( + "daterange", + `ID=${id},CLASS="com.apple.hls.interstitial",START-DATE="${startDate}"${durationTag},X-ASSET-URI="${uri}"${extraAttrs}` + ); + } } } - this._insertInterstitialAtExtraMedia(offset, id, uri, isAssetList, extraAttrs, startDate, this.playlistsAudio, opts); - - this._insertInterstitialAtExtraMedia(offset, id, uri, isAssetList, extraAttrs, startDate, this.playlistsSubtitle, opts); + this._insertInterstitialAtExtraMedia( + offset, + id, + uri, + isAssetList, + extraAttrs, + startDate, + this.playlistsAudio, + opts + ); + + this._insertInterstitialAtExtraMedia( + offset, + id, + uri, + isAssetList, + extraAttrs, + startDate, + this.playlistsSubtitle, + opts + ); resolve(); }); diff --git a/spec/hls_splice_spec.js b/spec/hls_splice_spec.js index 86f3a71..78c12ff 100644 --- a/spec/hls_splice_spec.js +++ b/spec/hls_splice_spec.js @@ -3,7 +3,7 @@ const fs = require("fs"); const ll = (log_lines) => { log_lines.map((line, idx) => console.log(line, idx)); -} +}; describe("HLSSpliceVod", () => { let mockMasterManifest; @@ -503,6 +503,27 @@ describe("HLSSpliceVod", () => { }); }); + it("can insert interstitial with an asset uri and a resume offset and CUE tag", (done) => { + const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8"); + mockVod + .load(mockMasterManifest, mockMediaManifest) + .then(() => { + return mockVod.insertInterstitialAt(18000, "001", "http://mock.com/asseturi", false, { + resumeOffset: 10500, + cue: "PRE,ONCE", + }); + }) + .then(() => { + const m3u8 = mockVod.getMediaManifest(4497000); + const lines = m3u8.split("\n"); + // ll(lines); + expect(lines[29]).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",CUE="PRE,ONCE",X-RESUME-OFFSET=10.5' + ); + done(); + }); + }); + it("can insert interstitial with an asset uri and a resume offset that is 0", (done) => { const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8"); mockVod @@ -1547,7 +1568,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"' ); @@ -1600,14 +1621,18 @@ describe("HLSSpliceVod with Demuxed Audio Tracks,", () => { .then(() => { const m3u8 = mockVod.getMediaManifest(4497000); let lines = m3u8.split("\n"); - expect(lines[31]).toBe("segment5_0_av.ts") - expect(lines[32]).not.toBe("#EXT-X-CUE-IN") - expect(lines[33]).not.toBe(`#EXT-X-DATERANGE:ID="1-804",START-DATE="1970-01-01T00:13:24Z",PLANNED-DURATION="0",SCTE35-OUT="0xFC302000000000000000FFF00F05000000017FFFFE000000000000000000007A3D9BBD"`) + expect(lines[31]).toBe("segment5_0_av.ts"); + expect(lines[32]).not.toBe("#EXT-X-CUE-IN"); + expect(lines[33]).not.toBe( + `#EXT-X-DATERANGE:ID="1-804",START-DATE="1970-01-01T00:13:24Z",PLANNED-DURATION="0",SCTE35-OUT="0xFC302000000000000000FFF00F05000000017FFFFE000000000000000000007A3D9BBD"` + ); const m3u8Audio = mockVod.getAudioManifest("stereo", "en"); lines = m3u8Audio.split("\n"); - expect(lines[31]).toBe("segment5_sen_a.ts") - expect(lines[32]).not.toBe("#EXT-X-CUE-IN") - expect(lines[33]).not.toBe(`#EXT-X-DATERANGE:ID="1-804",START-DATE="1970-01-01T00:13:24Z",PLANNED-DURATION="0",SCTE35-OUT="0xFC302000000000000000FFF00F05000000017FFFFE000000000000000000007A3D9BBD"`) + expect(lines[31]).toBe("segment5_sen_a.ts"); + expect(lines[32]).not.toBe("#EXT-X-CUE-IN"); + expect(lines[33]).not.toBe( + `#EXT-X-DATERANGE:ID="1-804",START-DATE="1970-01-01T00:13:24Z",PLANNED-DURATION="0",SCTE35-OUT="0xFC302000000000000000FFF00F05000000017FFFFE000000000000000000007A3D9BBD"` + ); done(); }); }); @@ -2015,7 +2040,6 @@ test-audio=256000-6.m4s`; ); const m3u8Audio = mockVod.getAudioManifest("stereo", "sv"); lines = m3u8Audio.split("\n"); - 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"' ); From fdfcc726582782ce23c4cbed0b1aa7c578e07b1b Mon Sep 17 00:00:00 2001 From: Nfrederiksen Date: Wed, 27 Nov 2024 10:26:32 +0100 Subject: [PATCH 06/10] bump node-m3u8 version and clear unit tests --- package-lock.json | 10 +++++++--- package.json | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1db5f09..733d4c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.5.0", "license": "MIT", "dependencies": { - "@eyevinn/m3u8": "^0.5.6", + "@eyevinn/m3u8": "^0.5.7", "request": "^2.88.2" }, "devDependencies": { @@ -313,7 +313,9 @@ } }, "node_modules/@eyevinn/m3u8": { - "version": "0.5.6", + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@eyevinn/m3u8/-/m3u8-0.5.7.tgz", + "integrity": "sha512-3Jb7xT7fwzdZvd22xcLYPqVU4ffHHZkYRLV8Bl82MOcY9M2xnsfPZQVILgoqJkZ+nbs2UXRP1+2QJ7s2xQwCDA==", "dependencies": { "chunked-stream": "~0.0.2" } @@ -2236,7 +2238,9 @@ } }, "@eyevinn/m3u8": { - "version": "0.5.6", + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@eyevinn/m3u8/-/m3u8-0.5.7.tgz", + "integrity": "sha512-3Jb7xT7fwzdZvd22xcLYPqVU4ffHHZkYRLV8Bl82MOcY9M2xnsfPZQVILgoqJkZ+nbs2UXRP1+2QJ7s2xQwCDA==", "requires": { "chunked-stream": "~0.0.2" } diff --git a/package.json b/package.json index 7c2f7b4..fcdb86f 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "nyc": "^15.0.1" }, "dependencies": { - "@eyevinn/m3u8": "^0.5.6", + "@eyevinn/m3u8": "^0.5.7", "request": "^2.88.2" } } From 45fc6a3f22c704d9a843c39261cb39ee94eb7ca0 Mon Sep 17 00:00:00 2001 From: Nfrederiksen Date: Wed, 27 Nov 2024 10:34:52 +0100 Subject: [PATCH 07/10] test: add more coverage --- spec/hls_splice_spec.js | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/spec/hls_splice_spec.js b/spec/hls_splice_spec.js index f742f04..25763e6 100644 --- a/spec/hls_splice_spec.js +++ b/spec/hls_splice_spec.js @@ -503,7 +503,7 @@ describe("HLSSpliceVod", () => { }); }); - it("can insert interstitial with an asset uri and a resume offset and CUE tag", (done) => { + it("can insert interstitial with an asset uri and a resume offset and CUE attribute", (done) => { const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8"); mockVod .load(mockMasterManifest, mockMediaManifest) @@ -516,7 +516,6 @@ describe("HLSSpliceVod", () => { .then(() => { const m3u8 = mockVod.getMediaManifest(4497000); const lines = m3u8.split("\n"); - // ll(lines); expect(lines[29]).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",CUE="PRE,ONCE",X-RESUME-OFFSET=10.5' ); @@ -1484,13 +1483,38 @@ describe("HLSSpliceVod with Demuxed Audio Tracks,", () => { }); }); + it("can insert interstitial with an asset uri and a resume offset and CUE attribute", (done) => { + const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8"); + mockVod + .load(mockMasterManifest, mockMediaManifest, mockAudioManifest) + .then(() => { + return mockVod.insertInterstitialAt(18000, "001", "http://mock.com/asseturi", false, { + resumeOffset: 10500, + cue: "POST,ONCE" + }); + }) + .then(() => { + const m3u8 = mockVod.getMediaManifest(4497000); + let lines = m3u8.split("\n"); + expect(lines[29]).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",CUE="POST,ONCE",X-RESUME-OFFSET=10.5' + ); + const m3u8Audio = mockVod.getAudioManifest("stereo", "sv"); + lines = m3u8Audio.split("\n"); + expect(lines[29]).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",CUE="POST,ONCE",X-RESUME-OFFSET=10.5' + ); + done(); + }); + }); + it("can insert interstitial with an asset uri and a resume offset that is 0", (done) => { const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8"); mockVod .load(mockMasterManifest, mockMediaManifest, mockAudioManifest) .then(() => { return mockVod.insertInterstitialAt(18000, "001", "http://mock.com/asseturi", false, { - resumeOffset: 0, + resumeOffset: 0 }); }) .then(() => { From ab23aebeecd5628466ceaa08c0da51c68f2bcba5 Mon Sep 17 00:00:00 2001 From: Nfrederiksen Date: Wed, 27 Nov 2024 10:45:26 +0100 Subject: [PATCH 08/10] test: one more coverage --- spec/hls_splice_spec.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/spec/hls_splice_spec.js b/spec/hls_splice_spec.js index 25763e6..d5947fd 100644 --- a/spec/hls_splice_spec.js +++ b/spec/hls_splice_spec.js @@ -2069,4 +2069,29 @@ test-audio=256000-6.m4s`; done(); }); }); + + it("can insert interstitial with an asset uri and a resume offset and CUE attribute", (done) => { + const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8"); + mockVod + .load(mockCmafMasterManifest, mockCmafMediaManifest, mockCmafAudioManifest) + .then(() => { + return mockVod.insertInterstitialAt(18000, "001", "http://mock.com/asseturi", false, { + resumeOffset: 10500, + cue: "POST,ONCE" + }); + }) + .then(() => { + const m3u8 = mockVod.getMediaManifest(4497000); + let lines = m3u8.split("\n"); + expect(lines[129]).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",CUE="POST,ONCE",X-RESUME-OFFSET=10.5' + ); + const m3u8Audio = mockVod.getAudioManifest("stereo", "sv"); + lines = m3u8Audio.split("\n"); + expect(lines[195]).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",CUE="POST,ONCE",X-RESUME-OFFSET=10.5' + ); + done(); + }); + }); }); From d2b11a68f11bafd6e7bc4c17e5accddc959b88bb Mon Sep 17 00:00:00 2001 From: Nfrederiksen Date: Wed, 27 Nov 2024 10:56:54 +0100 Subject: [PATCH 09/10] test: add even more coverage --- spec/hls_splice_spec.js | 100 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 5 deletions(-) diff --git a/spec/hls_splice_spec.js b/spec/hls_splice_spec.js index d5947fd..9b81093 100644 --- a/spec/hls_splice_spec.js +++ b/spec/hls_splice_spec.js @@ -523,6 +523,46 @@ describe("HLSSpliceVod", () => { }); }); + it("can insert interstitial with an asset uri and a resume offset and invalid CUE attribute", (done) => { + const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8"); + mockVod + .load(mockMasterManifest, mockMediaManifest) + .then(() => { + return mockVod.insertInterstitialAt(18000, "001", "http://mock.com/asseturi", false, { + resumeOffset: 10500, + cue: "pre-once", + }); + }) + .then(() => { + const m3u8 = mockVod.getMediaManifest(4497000); + const 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-RESUME-OFFSET=10.5' + ); + done(); + }); + }); + + it("can insert interstitial with an asset uri and a resume offset and single CUE attribute", (done) => { + const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8"); + mockVod + .load(mockMasterManifest, mockMediaManifest) + .then(() => { + return mockVod.insertInterstitialAt(18000, "001", "http://mock.com/asseturi", false, { + resumeOffset: 10500, + cue: "POST", + }); + }) + .then(() => { + const m3u8 = mockVod.getMediaManifest(4497000); + const lines = m3u8.split("\n"); + expect(lines[29]).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",CUE="POST",X-RESUME-OFFSET=10.5' + ); + done(); + }); + }); + it("can insert interstitial with an asset uri and a resume offset that is 0", (done) => { const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8"); mockVod @@ -1490,19 +1530,69 @@ describe("HLSSpliceVod with Demuxed Audio Tracks,", () => { .then(() => { return mockVod.insertInterstitialAt(18000, "001", "http://mock.com/asseturi", false, { resumeOffset: 10500, - cue: "POST,ONCE" + cue: "PRE,ONCE", }); }) .then(() => { const m3u8 = mockVod.getMediaManifest(4497000); let lines = m3u8.split("\n"); expect(lines[29]).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",CUE="POST,ONCE",X-RESUME-OFFSET=10.5' + '#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",CUE="PRE,ONCE",X-RESUME-OFFSET=10.5' ); const m3u8Audio = mockVod.getAudioManifest("stereo", "sv"); lines = m3u8Audio.split("\n"); expect(lines[29]).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",CUE="POST,ONCE",X-RESUME-OFFSET=10.5' + '#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",CUE="PRE,ONCE",X-RESUME-OFFSET=10.5' + ); + done(); + }); + }); + + it("can insert interstitial with an asset uri and a resume offset and invalid CUE attribute", (done) => { + const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8"); + mockVod + .load(mockMasterManifest, mockMediaManifest, mockAudioManifest) + .then(() => { + return mockVod.insertInterstitialAt(18000, "001", "http://mock.com/asseturi", false, { + resumeOffset: 10500, + cue: "ROST,TWICE", + }); + }) + .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-RESUME-OFFSET=10.5' + ); + const m3u8Audio = mockVod.getAudioManifest("stereo", "sv"); + lines = m3u8Audio.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-RESUME-OFFSET=10.5' + ); + done(); + }); + }); + + it("can insert interstitial with an asset uri and a resume offset and partially valid CUE attribute", (done) => { + const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8"); + mockVod + .load(mockMasterManifest, mockMediaManifest, mockAudioManifest) + .then(() => { + return mockVod.insertInterstitialAt(18000, "001", "http://mock.com/asseturi", false, { + resumeOffset: 10500, + cue: "POST,THRICE", + }); + }) + .then(() => { + const m3u8 = mockVod.getMediaManifest(4497000); + let lines = m3u8.split("\n"); + expect(lines[29]).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",CUE="POST",X-RESUME-OFFSET=10.5' + ); + const m3u8Audio = mockVod.getAudioManifest("stereo", "sv"); + lines = m3u8Audio.split("\n"); + expect(lines[29]).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",CUE="POST",X-RESUME-OFFSET=10.5' ); done(); }); @@ -1514,7 +1604,7 @@ describe("HLSSpliceVod with Demuxed Audio Tracks,", () => { .load(mockMasterManifest, mockMediaManifest, mockAudioManifest) .then(() => { return mockVod.insertInterstitialAt(18000, "001", "http://mock.com/asseturi", false, { - resumeOffset: 0 + resumeOffset: 0, }); }) .then(() => { @@ -2077,7 +2167,7 @@ test-audio=256000-6.m4s`; .then(() => { return mockVod.insertInterstitialAt(18000, "001", "http://mock.com/asseturi", false, { resumeOffset: 10500, - cue: "POST,ONCE" + cue: "POST,ONCE", }); }) .then(() => { From 4a08df0fbd50db0724044968559908c450f9c2e0 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 10/10] 5.0.1 test: add more coverage test: one more coverage test: add even more coverage --- package-lock.json | 4 +- package.json | 2 +- spec/hls_splice_spec.js | 143 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 144 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 733d4c8..0af813b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@eyevinn/hls-splice", - "version": "0.5.0", + "version": "0.5.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@eyevinn/hls-splice", - "version": "0.5.0", + "version": "0.5.1", "license": "MIT", "dependencies": { "@eyevinn/m3u8": "^0.5.7", diff --git a/package.json b/package.json index fcdb86f..6f9913b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eyevinn/hls-splice", - "version": "0.5.0", + "version": "0.5.1", "description": "NPM library to splice HLS VOD", "repository": "https://github.com/Eyevinn/hls-splice", "main": "index.js", diff --git a/spec/hls_splice_spec.js b/spec/hls_splice_spec.js index 78c12ff..7dd1320 100644 --- a/spec/hls_splice_spec.js +++ b/spec/hls_splice_spec.js @@ -503,7 +503,7 @@ describe("HLSSpliceVod", () => { }); }); - it("can insert interstitial with an asset uri and a resume offset and CUE tag", (done) => { + it("can insert interstitial with an asset uri and a resume offset and CUE attribute", (done) => { const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8"); mockVod .load(mockMasterManifest, mockMediaManifest) @@ -516,7 +516,6 @@ describe("HLSSpliceVod", () => { .then(() => { const m3u8 = mockVod.getMediaManifest(4497000); const lines = m3u8.split("\n"); - // ll(lines); expect(lines[29]).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",CUE="PRE,ONCE",X-RESUME-OFFSET=10.5' ); @@ -524,6 +523,46 @@ describe("HLSSpliceVod", () => { }); }); + it("can insert interstitial with an asset uri and a resume offset and invalid CUE attribute", (done) => { + const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8"); + mockVod + .load(mockMasterManifest, mockMediaManifest) + .then(() => { + return mockVod.insertInterstitialAt(18000, "001", "http://mock.com/asseturi", false, { + resumeOffset: 10500, + cue: "pre-once", + }); + }) + .then(() => { + const m3u8 = mockVod.getMediaManifest(4497000); + const 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-RESUME-OFFSET=10.5' + ); + done(); + }); + }); + + it("can insert interstitial with an asset uri and a resume offset and single CUE attribute", (done) => { + const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8"); + mockVod + .load(mockMasterManifest, mockMediaManifest) + .then(() => { + return mockVod.insertInterstitialAt(18000, "001", "http://mock.com/asseturi", false, { + resumeOffset: 10500, + cue: "POST", + }); + }) + .then(() => { + const m3u8 = mockVod.getMediaManifest(4497000); + const lines = m3u8.split("\n"); + expect(lines[29]).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",CUE="POST",X-RESUME-OFFSET=10.5' + ); + done(); + }); + }); + it("can insert interstitial with an asset uri and a resume offset that is 0", (done) => { const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8"); mockVod @@ -1484,6 +1523,81 @@ describe("HLSSpliceVod with Demuxed Audio Tracks,", () => { }); }); + it("can insert interstitial with an asset uri and a resume offset and CUE attribute", (done) => { + const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8"); + mockVod + .load(mockMasterManifest, mockMediaManifest, mockAudioManifest) + .then(() => { + return mockVod.insertInterstitialAt(18000, "001", "http://mock.com/asseturi", false, { + resumeOffset: 10500, + cue: "PRE,ONCE", + }); + }) + .then(() => { + const m3u8 = mockVod.getMediaManifest(4497000); + let lines = m3u8.split("\n"); + expect(lines[29]).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",CUE="PRE,ONCE",X-RESUME-OFFSET=10.5' + ); + const m3u8Audio = mockVod.getAudioManifest("stereo", "sv"); + lines = m3u8Audio.split("\n"); + expect(lines[29]).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",CUE="PRE,ONCE",X-RESUME-OFFSET=10.5' + ); + done(); + }); + }); + + it("can insert interstitial with an asset uri and a resume offset and invalid CUE attribute", (done) => { + const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8"); + mockVod + .load(mockMasterManifest, mockMediaManifest, mockAudioManifest) + .then(() => { + return mockVod.insertInterstitialAt(18000, "001", "http://mock.com/asseturi", false, { + resumeOffset: 10500, + cue: "ROST,TWICE", + }); + }) + .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-RESUME-OFFSET=10.5' + ); + const m3u8Audio = mockVod.getAudioManifest("stereo", "sv"); + lines = m3u8Audio.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-RESUME-OFFSET=10.5' + ); + done(); + }); + }); + + it("can insert interstitial with an asset uri and a resume offset and partially valid CUE attribute", (done) => { + const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8"); + mockVod + .load(mockMasterManifest, mockMediaManifest, mockAudioManifest) + .then(() => { + return mockVod.insertInterstitialAt(18000, "001", "http://mock.com/asseturi", false, { + resumeOffset: 10500, + cue: "POST,THRICE", + }); + }) + .then(() => { + const m3u8 = mockVod.getMediaManifest(4497000); + let lines = m3u8.split("\n"); + expect(lines[29]).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",CUE="POST",X-RESUME-OFFSET=10.5' + ); + const m3u8Audio = mockVod.getAudioManifest("stereo", "sv"); + lines = m3u8Audio.split("\n"); + expect(lines[29]).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",CUE="POST",X-RESUME-OFFSET=10.5' + ); + done(); + }); + }); + it("can insert interstitial with an asset uri and a resume offset that is 0", (done) => { const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8"); mockVod @@ -2046,4 +2160,29 @@ test-audio=256000-6.m4s`; done(); }); }); + + it("can insert interstitial with an asset uri and a resume offset and CUE attribute", (done) => { + const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8"); + mockVod + .load(mockCmafMasterManifest, mockCmafMediaManifest, mockCmafAudioManifest) + .then(() => { + return mockVod.insertInterstitialAt(18000, "001", "http://mock.com/asseturi", false, { + resumeOffset: 10500, + cue: "POST,ONCE", + }); + }) + .then(() => { + const m3u8 = mockVod.getMediaManifest(4497000); + let lines = m3u8.split("\n"); + expect(lines[129]).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",CUE="POST,ONCE",X-RESUME-OFFSET=10.5' + ); + const m3u8Audio = mockVod.getAudioManifest("stereo", "sv"); + lines = m3u8Audio.split("\n"); + expect(lines[195]).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",CUE="POST,ONCE",X-RESUME-OFFSET=10.5' + ); + done(); + }); + }); });