diff --git a/index.js b/index.js index 6653905..e1aa59a 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,16 @@ 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 +508,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,28 +614,46 @@ 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}` + ); + } } } diff --git a/package-lock.json b/package-lock.json index 02a4ebc..0af813b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.5.1", "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 bf15e13..6f9913b 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" } } diff --git a/spec/hls_splice_spec.js b/spec/hls_splice_spec.js index 86f3a71..9b81093 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,66 @@ describe("HLSSpliceVod", () => { }); }); + 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) + .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[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) + .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 @@ -1463,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 @@ -1547,7 +1682,6 @@ 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 +1734,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,11 +2153,35 @@ 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"' ); 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(); + }); + }); });