Skip to content

Commit

Permalink
Feat: Add Support for HLS Interstitials - 1st Version (#125)
Browse files Browse the repository at this point in the history
* feat: Add Support For HLS Interstital Tag V1

* add test for basic  hls interstitial

* add new testvector

* clean up old testvector
  • Loading branch information
Nfrederiksen authored Nov 29, 2024
1 parent b72dfee commit 7f053d8
Show file tree
Hide file tree
Showing 8 changed files with 269 additions and 96 deletions.
165 changes: 105 additions & 60 deletions index.js

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"nyc": "^15.0.1"
},
"dependencies": {
"@eyevinn/m3u8": "^0.5.6",
"@eyevinn/m3u8": "^0.5.8",
"abort-controller": "^3.0.0",
"debug": "^4.1.1",
"node-fetch": "2.6.7"
Expand Down
87 changes: 62 additions & 25 deletions spec/hlsvod_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe("HLSVod standalone", () => {
return fs.createReadStream("testvectors/hls17/" + bandwidth + ".m3u8");
};
});

it("return the correct vod URI", (done) => {
mockVod = new HLSVod("http://mock.com/mock.m3u8");
mockVod.load(mockMasterManifest, mockMediaManifest).then(() => {
Expand Down Expand Up @@ -244,15 +244,16 @@ describe("HLSVod standalone", () => {

it("should fail loading a VOD with different lengths of video segments", (done) => {
mockVod = new HLSVod("http://mock.com/mock.m3u8");
mockVod.load(mockMasterManifest4, mockMediaManifest4)
.then(() => {
expect(mockVod.getVodUri()).toBe("http://mock.com/mock.m3u8");
done();
})
.catch((err) => {
expect(err.toString()).toBe("Error: The VOD loading was rejected because it contains video variants with different segment counts");
done();
})
mockVod
.load(mockMasterManifest4, mockMediaManifest4)
.then(() => {
expect(mockVod.getVodUri()).toBe("http://mock.com/mock.m3u8");
done();
})
.catch((err) => {
expect(err.toString()).toBe("Error: The VOD loading was rejected because it contains video variants with different segment counts");
done();
});
});
});

Expand Down Expand Up @@ -666,8 +667,8 @@ describe("HLSVod with timeline", () => {
mockVod.load(mockMasterManifest, mockMediaManifest).then(() => {
let m3u8 = mockVod.getLiveMediaSequences(0, "2497000", 0);
const lines = m3u8.split("\n");
expect(lines[6]).toEqual(`#EXT-X-PROGRAM-DATE-TIME:${(new Date(now)).toISOString()}`);
expect(lines[9]).toEqual(`#EXT-X-PROGRAM-DATE-TIME:${(new Date(now + 9 * 1000)).toISOString()}`);
expect(lines[6]).toEqual(`#EXT-X-PROGRAM-DATE-TIME:${new Date(now).toISOString()}`);
expect(lines[9]).toEqual(`#EXT-X-PROGRAM-DATE-TIME:${new Date(now + 9 * 1000).toISOString()}`);
done();
});
});
Expand All @@ -679,12 +680,13 @@ describe("HLSVod with timeline", () => {
let m3u8 = mockVod.getLiveMediaSequences(0, "2497000", 0);
const lines = m3u8.split("\n");
expect(lines[6]).toEqual(`#EXTINF:9.000,`);
expect(lines[9]).toEqual(`https://tv4play-i.akamaihd.net/i/mp4root/2018-01-26/pid200032972(3953564_,T3MP445,T3MP435,T3MP425,T3MP415,T3MP48,T3MP43,T3MP4130,).mp4.csmil/segment2_2_av.ts`);
expect(lines[9]).toEqual(
`https://tv4play-i.akamaihd.net/i/mp4root/2018-01-26/pid200032972(3953564_,T3MP445,T3MP435,T3MP425,T3MP415,T3MP48,T3MP43,T3MP4130,).mp4.csmil/segment2_2_av.ts`
);
done();
});
});


it("can handle vod after another vod", (done) => {
const VOD_LENGTH_MS = 2646000 + 6266;
const now = Date.now();
Expand All @@ -706,7 +708,7 @@ describe("HLSVod with timeline", () => {
it("can handle vod after another vod, with later vod disabling timeOffset", (done) => {
const now = 1692110553608;
mockVod = new HLSVod("http://mock.com/mock.m3u8", [], now);
mockVod2 = new HLSVod("http://mock.com/mock2.m3u8", [], timeOffet=null);
mockVod2 = new HLSVod("http://mock.com/mock2.m3u8", [], (timeOffet = null));
mockVod
.load(mockMasterManifest, mockMediaManifest)
.then(() => {
Expand Down Expand Up @@ -912,7 +914,7 @@ describe("HLSVod with not equal usage profiles", () => {
});
mockMasterManifest.push(function () {
return fs.createReadStream("testvectors/hls_abr6/master.m3u8");
})
});
mockMediaManifest.push(function (bandwidth) {
return fs.createReadStream("testvectors/hls1/" + bandwidth + ".m3u8");
});
Expand Down Expand Up @@ -1227,10 +1229,10 @@ describe("HLSVod with not equal usage profiles", () => {
done();
});
});

it("can match by true nearest when options-> alwaysMapBandwidthByNearest is true", (done) => {
mockVod = new HLSVod("http://mock.com/mock.m3u8", null, 0,0, null, { alwaysMapBandwidthByNearest: 1 });
mockVod2 = new HLSVod("http://mock.com/mock2.m3u8", null, 0,0, null, { alwaysMapBandwidthByNearest: 1 });
mockVod = new HLSVod("http://mock.com/mock.m3u8", null, 0, 0, null, { alwaysMapBandwidthByNearest: 1 });
mockVod2 = new HLSVod("http://mock.com/mock2.m3u8", null, 0, 0, null, { alwaysMapBandwidthByNearest: 1 });
mockVod
.load(mockMasterManifest[0], mockMediaManifest[0])
.then(() => {
Expand All @@ -1239,7 +1241,7 @@ describe("HLSVod with not equal usage profiles", () => {
.then(() => {
const seqSegments = mockVod2.getLiveMediaSequenceSegments(0);
const lastIdx = seqSegments["1497000"].length - 1;
expect(seqSegments["1497000"][lastIdx].uri).toEqual("https://mock.vod.media/segment1_0_av.ts");
expect(seqSegments["1497000"][lastIdx].uri).toEqual("https://mock.vod.media/segment1_0_av.ts");
expect(seqSegments["3496000"][lastIdx].uri).toEqual("https://mock.vod.media/segment1_1_av.ts");
expect(seqSegments["4497000"][lastIdx].uri).toEqual("https://mock.vod.media/segment1_2_av.ts");
expect(seqSegments["5544000"][lastIdx].uri).toEqual("https://mock.vod.media/segment1_3_av.ts");
Expand Down Expand Up @@ -1406,11 +1408,11 @@ describe("HLSVod with separate audio variants", () => {
});
});

it("can handle vod after another vod, loading same groupId & languages, but with later vod disabling timeOffset", (done) => {
it("can handle vod after another vod, loading same groupId & languages, but with later vod disabling timeOffset", (done) => {
const now = Date.now();
// # Two demuxed vods with some different languages.
mockVod = new HLSVod("http://mock.com/mock.m3u8", [], now);
mockVod2 = new HLSVod("http://mock.com/mock2.m3u8", [], timeOffest=null);
mockVod2 = new HLSVod("http://mock.com/mock2.m3u8", [], (timeOffest = null));
mockVod
.load(mockMasterManifest, mockMediaManifest, mockAudioManifest)
.then(() => {
Expand Down Expand Up @@ -2266,7 +2268,7 @@ describe("HLSVod serializing", () => {
const bytesToMB = (bytes) => {
const megabytes = bytes / (1024 * 1024);
return megabytes.toFixed(2);
}
};
const expectedJSONSizeInMBFull = "5.26";
const expectedJSONSizeInMBPartial = "0.38";
const expectedJSONSizeInMBFullAgain = expectedJSONSizeInMBFull;
Expand Down Expand Up @@ -2350,7 +2352,7 @@ describe("HLSVod serializing", () => {
const bytesToMB = (bytes) => {
const megabytes = bytes / (1024 * 1024);
return megabytes.toFixed(2);
}
};
const expectedJSONSizeInMBFull = "5.29";
const expectedJSONSizeInMBPartial = "0.38";
const expectedJSONSizeInMBFullAgain = expectedJSONSizeInMBFull;
Expand Down Expand Up @@ -3109,7 +3111,6 @@ describe("HLSVod for demuxed audio, with set option-> sequenceAlwaysContainNewSe
return fs.createReadStream(`testvectors/hls_multiaudiotracks4/${groupId}.m3u8`);
}
};

});

xit("should fail loading a VOD with different lengths of audio segments", (done) => {
Expand Down Expand Up @@ -3725,6 +3726,7 @@ describe("HLSVod delta time and positions", () => {
let mock2_MasterManifest;
let mock2_MediaManifest;
let mock2_AudioManifest;
let mock_vod_3;

beforeEach(() => {
mock1_MasterManifest = function () {
Expand Down Expand Up @@ -3757,6 +3759,17 @@ describe("HLSVod delta time and positions", () => {
return fs.createReadStream(`testvectors/hls_always_4_demux/${groupId}.m3u8`);
}
};
mock_vod_3 = {
master: () => {
return fs.createReadStream("testvectors/hls_cmaf_interstitial_1/master.m3u8");
},
media: (bandwidth) => {
return fs.createReadStream("testvectors/hls_cmaf_interstitial_1/test-video=" + bandwidth + ".m3u8");
},
audio: (groupId, lang) => {
return fs.createReadStream("testvectors/hls_cmaf_interstitial_1/test-audio=256000.m3u8");
},
};
});

it("and there is no matching group ID, then it sets default target group ID to load next VOD segments into", (done) => {
Expand All @@ -3772,5 +3785,29 @@ describe("HLSVod delta time and positions", () => {
});
});
});

it("with Program date time enabled and VODs have HLS Interstitial Tag", (done) => {
mockVod = new HLSVod("http://mock.com/mock.m3u8", null, Date.now(), 0, null, {
sequenceAlwaysContainNewSegments: 0,
forcedDemuxMode: true,
});
mockVod2 = new HLSVod("http://mock.com/mock2.m3u8", null, Date.now() + 90000, 0, null, {
sequenceAlwaysContainNewSegments: 0,
forcedDemuxMode: true,
});
mockVod.load(mock_vod_3.master, mock_vod_3.media, mock_vod_3.audio).then(() => {
mockVod2.loadAfter(mockVod, mock_vod_3.master, mock_vod_3.media, mock_vod_3.audio).then(() => {
let m3u8 = mockVod2.getLiveMediaAudioSequences(0, "audio-aacl-256", "sv", 4);
let lines = m3u8.split("\n");
expect(
lines[16].includes('#EXT-X-DATERANGE:ID="ad1",CLASS="com.apple.hls.interstitial",START-DATE') &&
lines[16].includes(
',DURATION="15.0",X-ASSET-URI="http://example.com/ad1.m3u8",X-RESUME-OFFSET="0",X-RESTRICT="SKIP,JUMP",X-COM-EXAMPLE-BEACON="123"'
)
).toBe(true);
done();
});
});
});
});
});
11 changes: 11 additions & 0 deletions testvectors/hls_cmaf_interstitial_1/master.m3u8
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#EXTM3U
#EXT-X-VERSION:6
## Created with Unified Streaming Platform (version=1.11.20-26889)

# AUDIO groups
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-aacl-256",NAME="Swedish",LANGUAGE="sv",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2",URI="test-audio=256000.m3u8"

# variants
#EXT-X-STREAM-INF:BANDWIDTH=2500000,CODECS="mp4a.40.2,avc1.4D401F",RESOLUTION=1024x576,FRAME-RATE=25,VIDEO-RANGE=SDR,AUDIO="audio-aacl-256",CLOSED-CAPTIONS=NONE
test-video=2500000.m3u8

29 changes: 29 additions & 0 deletions testvectors/hls_cmaf_interstitial_1/test-audio=256000.m3u8
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#EXTM3U
#EXT-X-VERSION:6
## Created with Unified Streaming Platform (version=1.11.20-26889)
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:1
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-TARGETDURATION:3
#EXT-X-PROGRAM-DATE-TIME:1970-01-01T00:00:00.001Z
#EXT-X-MAP:URI="test-audio=256000.m4s"
#EXTINF:10, no desc
test-audio=2500000-1.m4s
#EXTINF:10, no desc
test-audio=2500000-2.m4s
#EXTINF:10, no desc
test-audio=2500000-3.m4s
#EXT-X-DATERANGE:ID="ad1",CLASS="com.apple.hls.interstitial",START-DATE="1970-01-01T00:00:35.001Z",DURATION=15.0,X-ASSET-URI="http://example.com/ad1.m3u8",X-RESUME-OFFSET=0, X-RESTRICT="SKIP,JUMP",X-COM-EXAMPLE-BEACON=123
#EXTINF:10, no desc
test-audio=2500000-4.m4s
#EXTINF:10, no desc
test-audio=2500000-5.m4s
#EXTINF:10, no desc
test-audio=2500000-6.m4s
#EXTINF:10, no desc
test-audio=2500000-7.m4s
#EXTINF:10, no desc
test-audio=2500000-8.m4s
#EXTINF:10, no desc
test-audio=2500000-9.m4s
#EXT-X-ENDLIST
29 changes: 29 additions & 0 deletions testvectors/hls_cmaf_interstitial_1/test-video=2500000.m3u8
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#EXTM3U
#EXT-X-VERSION:6
## Created with Unified Streaming Platform (version=1.11.20-26889)
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:1
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-TARGETDURATION:3
#EXT-X-PROGRAM-DATE-TIME:1970-01-01T00:00:00.001Z
#EXT-X-MAP:URI="test-video=256000.m4s"
#EXTINF:10, no desc
test-video=2500000-1.m4s
#EXTINF:10, no desc
test-video=2500000-2.m4s
#EXTINF:10, no desc
test-video=2500000-3.m4s
#EXT-X-DATERANGE:ID="ad1",CLASS="com.apple.hls.interstitial",START-DATE="1970-01-01T00:00:35.001Z",DURATION=15.0,X-ASSET-URI="http://example.com/ad1.m3u8",X-RESUME-OFFSET=0, X-RESTRICT="SKIP,JUMP",X-COM-EXAMPLE-BEACON=123
#EXTINF:10, no desc
test-video=2500000-4.m4s
#EXTINF:10, no desc
test-video=2500000-5.m4s
#EXTINF:10, no desc
test-video=2500000-6.m4s
#EXTINF:10, no desc
test-video=2500000-7.m4s
#EXTINF:10, no desc
test-video=2500000-8.m4s
#EXTINF:10, no desc
test-video=2500000-9.m4s
#EXT-X-ENDLIST
28 changes: 25 additions & 3 deletions utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,16 +118,17 @@ function segToM3u8(v, i, len, nextSegment, previousSegment) {
m3u8 += "#EXT-X-CUE-IN" + "\n";
}
}
if (v.daterange && i != len - 1) {
if (v.daterange && i != len - 1 && !(v.daterange["CLASS"] == "com.apple.hls.interstitial" && v.daterange["CUE"])) {
const dateRangeAttributes = Object.keys(v.daterange)
.map((key) => daterangeAttribute(key, v.daterange[key]))
.join(",");
if ((nextSegment && !nextSegment.timelinePosition) && v.daterange["start-date"]) {
if (nextSegment && !nextSegment.timelinePosition && v.daterange["start-date"]) {
m3u8 += "#EXT-X-PROGRAM-DATE-TIME:" + v.daterange["start-date"] + "\n";
}
m3u8 += "#EXT-X-DATERANGE:" + dateRangeAttributes + "\n";
}
}

return m3u8;
}

Expand Down Expand Up @@ -221,6 +222,25 @@ const inspectForVodTransition = (list) => {
return [count, foundVodTransition];
};

const playlistItemWithInterstitialsMetadata = (pli) => {
const daterange = pli.attributes.attributes.daterange;
if (daterange && daterange["CLASS"] == "com.apple.hls.interstitial") {
return true;
}
return false;
};

const appendHlsInterstitialLineWithCUE = (m3u8Str, data) => {
// FOR LIVE AND CUED HLS-INTERSTITIAL TAGS, Place them at the bottom of each window
if (data) {
const dateRangeAttributes = Object.keys(data)
.map((key) => daterangeAttribute(key, data[key]))
.join(",");
m3u8Str += "#EXT-X-DATERANGE:" + dateRangeAttributes + "\n";
}
return m3u8Str;
}

module.exports = {
daterangeAttribute,
keysToM3u8,
Expand All @@ -230,5 +250,7 @@ module.exports = {
findIndexReversed,
findBottomSegItem,
fixedNumber,
inspectForVodTransition
inspectForVodTransition,
playlistItemWithInterstitialsMetadata,
appendHlsInterstitialLineWithCUE
}

0 comments on commit 7f053d8

Please sign in to comment.