Skip to content

Commit

Permalink
feat: added subtitle support (#47)
Browse files Browse the repository at this point in the history
* feat: added subtitle support
made it possible to add ads, bumpers and interstitials to vod with subtitles
made it so ads and bumpers without subs are added but with a dummy url that is defined in constructor options

* fix: handle ad subtitle parsing when source video tracks not loaded berore

* chore: add warning for when no dummy sub endpoint is set

---------

Co-authored-by: Nicholas Frederiksen <nicholas.frederiksen@eyevinn.se>
  • Loading branch information
Sgtfishtank and Nfrederiksen committed May 12, 2023
1 parent 7812dbe commit 1249a27
Show file tree
Hide file tree
Showing 9 changed files with 764 additions and 134 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
node_modules
.nyc_output
.vscode
.DS_STORE
618 changes: 484 additions & 134 deletions index.js

Large diffs are not rendered by default.

202 changes: 202 additions & 0 deletions spec/hls_splice_subtitle_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
const HLSSpliceVod = require("../index.js");
const fs = require("fs");

describe("HLSSpliceVod with subs", () => {
let mockMasterManifestSubs;
let mockMediaManifestSubs;
let mockSubtitleManifestSubs;
let mockMasterManifestNoSubs;
let mockMediaManifestNoSubs;
let mockAdMasterManifestSubs;
let mockAdMediaManifestSubs;
let mockAdSubtitleManifestSubs;
let mockAdMasterManifestNoSubs;
let mockAdMediaManifestNoSubs;

beforeEach(() => {
mockMasterManifestSubs = () => {
return fs.createReadStream("testvectors/hls_subtitles/hls1/index.m3u8");
};
mockMediaManifestSubs = () => {
return fs.createReadStream("testvectors/hls_subtitles/hls1/video.m3u8");
};
mockSubtitleManifestSubs = () => {
return fs.createReadStream("testvectors/hls_subtitles/hls1/sub.m3u8");
};
mockMasterManifestNoSubs = () => {
return fs.createReadStream("testvectors/hls1/master.m3u8");
};
mockMediaManifestNoSubs = () => {
return fs.createReadStream("testvectors/hls1/index_0_av.m3u8");
};
mockAdMasterManifestSubs = () => {
return fs.createReadStream("testvectors/hls_subtitles/ad1_subs/index.m3u8");
};
mockAdMediaManifestSubs = () => {
return fs.createReadStream("testvectors/hls_subtitles/ad1_subs/video.m3u8");
};
mockAdSubtitleManifestSubs = () => {
return fs.createReadStream("testvectors/hls_subtitles/ad1_subs/sub.m3u8");
};
mockAdMasterManifestNoSubs = () => {
return fs.createReadStream("testvectors/ad1/master.m3u8");
};
mockAdMediaManifestNoSubs = () => {
return fs.createReadStream("testvectors/ad1/index_0_av.m3u8");
};
});

it("contains a 8 second splice at 12 seconds from start, both has subs", (done) => {
const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8");
mockVod
.load(mockMasterManifestSubs, mockMediaManifestSubs, null, mockSubtitleManifestSubs)
.then(() => {
return mockVod.insertAdAt(12000, "http://mock.com/ad/mockad.m3u8", mockAdMasterManifestSubs, mockAdMediaManifestSubs, null, mockAdSubtitleManifestSubs);
})
.then(() => {
const m3u8 = mockVod.getMediaManifest(3370400);
let substrings = m3u8.split("\n")
const m3u8Subs = mockVod.getSubtitleManifest("subs", "fr");
let substringsSubs = m3u8Subs.split("\n")

expect(substrings[12]).toBe("#EXT-X-DISCONTINUITY");
expect(substrings[13]).toBe("#EXT-X-CUE-OUT:DURATION=8");
expect(substrings[14]).toBe("#EXTINF:4.0000,");
expect(substrings[15]).toBe("http://mock.com/ad/ad00.ts");
expect(substrings[19]).toBe("#EXT-X-CUE-IN");

expect(substringsSubs[11]).toBe("#EXT-X-DISCONTINUITY");
expect(substringsSubs[12]).toBe("#EXT-X-CUE-OUT:DURATION=8");
expect(substringsSubs[13]).toBe("#EXTINF:4.0000,");
expect(substringsSubs[14]).toBe("http://mock.com/ad/ad0.webvtt");
expect(substringsSubs[18]).toBe("#EXT-X-CUE-IN");
done();
});
});

it("contains a 15 second splice at 12 seconds from start, ad does not have subs", (done) => {
const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8", { dummySubtitleEndpoint: "/dummy" });
mockVod
.load(mockMasterManifestSubs, mockMediaManifestSubs, null, mockSubtitleManifestSubs)
.then(() => {
return mockVod.insertAdAt(12000, "http://mock.com/ad/mockad.m3u8", mockAdMasterManifestNoSubs, mockAdMediaManifestNoSubs);
})
.then(() => {
const m3u8 = mockVod.getMediaManifest(3370400);
let substrings = m3u8.split("\n")
const m3u8Subs = mockVod.getSubtitleManifest("subs", "fr");
let substringsSubs = m3u8Subs.split("\n")
expect(substrings[12]).toBe("#EXT-X-DISCONTINUITY");
expect(substrings[13]).toBe("#EXT-X-CUE-OUT:DURATION=15");
expect(substrings[14]).toBe("#EXTINF:3.0000,");
expect(substrings[15]).toBe("http://mock.com/ad/ad1_0_av.ts");
expect(substrings[25]).toBe("#EXT-X-CUE-IN");

expect(substringsSubs[11]).toBe("#EXT-X-DISCONTINUITY");
expect(substringsSubs[12]).toBe("#EXT-X-CUE-OUT:DURATION=15");
expect(substringsSubs[13]).toBe("#EXTINF:3.0000,");
expect(substringsSubs[14]).toBe("/dummy");
expect(substringsSubs[24]).toBe("#EXT-X-CUE-IN");
done();
});
});

it("contains a 9 second splice at 12 seconds from start, source does not have subs", (done) => {
const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8");
mockVod
.load(mockMasterManifestNoSubs, mockMediaManifestNoSubs)
.then(() => {
return mockVod.insertAdAt(9000, "http://mock.com/ad/mockad.m3u8", mockAdMasterManifestSubs, mockAdMediaManifestSubs, null, mockAdSubtitleManifestSubs);
})
.then(() => {
const m3u8 = mockVod.getMediaManifest(4497000);
let substrings = m3u8.split("\n")
expect(substrings[9]).toBe("#EXT-X-DISCONTINUITY");
expect(substrings[10]).toBe("#EXT-X-CUE-OUT:DURATION=8");
expect(substrings[11]).toBe("#EXTINF:4.0000,");
expect(substrings[12]).toBe("http://mock.com/ad/ad00.ts");
expect(substrings[16]).toBe("#EXT-X-CUE-IN");
try {
mockVod.getSubtitleManifest("subs", "fr")
} catch (error) {
const m = err.message.match(/Error: Failed to get manifest./)
expect(m).nor.toBe(null);
}
done();
});
});


it("insert Interstitial at 8 sec", (done) => {
const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8");
mockVod
.load(mockMasterManifestSubs, mockMediaManifestSubs, null, mockSubtitleManifestSubs)
.then(() => {
return mockVod.insertInterstitialAt(8000, "001", "http://mock.com/assetlist", true);
})
.then(() => {
const m3u8 = mockVod.getMediaManifest(3370400);
let substrings = m3u8.split("\n")
const m3u8Subs = mockVod.getSubtitleManifest("subs", "fr");
let substringsSubs = m3u8Subs.split("\n")
expect(substrings[11]).toBe(`#EXT-X-DATERANGE:ID="001",CLASS="com.apple.hls.interstitial",START-DATE="1970-01-01T00:00:08.001Z",X-ASSET-LIST="http://mock.com/assetlist"`);
expect(substringsSubs[10]).toBe(`#EXT-X-DATERANGE:ID="001",CLASS="com.apple.hls.interstitial",START-DATE="1970-01-01T00:00:08.001Z",X-ASSET-LIST="http://mock.com/assetlist"`);

done();
});
});

it("insert bumper, both has subs", (done) => {
const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8");
mockVod
.load(mockMasterManifestSubs, mockMediaManifestSubs, null, mockSubtitleManifestSubs)
.then(() => {
return mockVod.insertBumper(
"http://mock.com/ad/mockbumper.m3u8",
mockAdMasterManifestSubs,
mockAdMediaManifestSubs,
null,
mockAdSubtitleManifestSubs

);
})
.then(() => {
const m3u8 = mockVod.getMediaManifest(3370400);
let substrings = m3u8.split("\n")
const m3u8Subs = mockVod.getSubtitleManifest("subs", "fr");
let substringsSubs = m3u8Subs.split("\n")
expect(substrings[7]).toBe("http://mock.com/ad/ad00.ts");
expect(substrings[10]).toBe("#EXT-X-DISCONTINUITY");

expect(substringsSubs[6]).toBe("http://mock.com/ad/ad0.webvtt");
expect(substringsSubs[9]).toBe("#EXT-X-DISCONTINUITY");
done();
});
});

it("insert bumper, only source has subs", (done) => {
const mockVod = new HLSSpliceVod("http://mock.com/mock.m3u8", { dummySubtitleEndpoint: "/dummy" });
mockVod
.load(mockMasterManifestSubs, mockMediaManifestSubs, null, mockSubtitleManifestSubs)
.then(() => {
return mockVod.insertBumper(
"http://mock.com/ad/mockbumper.m3u8",
mockAdMasterManifestNoSubs,
mockAdMediaManifestNoSubs,
);
})
.then(() => {
const m3u8 = mockVod.getMediaManifest(3370400);
let substrings = m3u8.split("\n")
const m3u8Subs = mockVod.getSubtitleManifest("subs", "fr");
let substringsSubs = m3u8Subs.split("\n")
expect(substrings[7]).toBe("http://mock.com/ad/ad1_0_av.ts");
expect(substrings[16]).toBe("#EXT-X-DISCONTINUITY");

expect(substringsSubs[6]).toBe("/dummy");
expect(substringsSubs[15]).toBe("#EXT-X-DISCONTINUITY");
done();
});
});

});
7 changes: 7 additions & 0 deletions testvectors/hls_subtitles/ad1_subs/index.m3u8
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#EXTM3U
#EXT-X-VERSION:6

#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="French",FORCED=NO,AUTOSELECT=YES,URI="sub.m3u8",LANGUAGE="fr"

#EXT-X-STREAM-INF:BANDWIDTH=3370400,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2",SUBTITLES="subs"
video.m3u8
8 changes: 8 additions & 0 deletions testvectors/hls_subtitles/ad1_subs/sub.m3u8
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:4.0,
ad0.webvtt
#EXTINF:4.0,
ad1.webvtt
11 changes: 11 additions & 0 deletions testvectors/hls_subtitles/ad1_subs/video.m3u8
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-INDEPENDENT-SEGMENTS
#EXTINF:4.000000,
ad00.ts
#EXTINF:4.000000,
ad01.ts
#EXT-X-ENDLIST
7 changes: 7 additions & 0 deletions testvectors/hls_subtitles/hls1/index.m3u8
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#EXTM3U
#EXT-X-VERSION:6

#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="French",FORCED=NO,AUTOSELECT=YES,URI="sub.m3u8",LANGUAGE="fr"

#EXT-X-STREAM-INF:BANDWIDTH=3370400,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2",SUBTITLES="subs"
video.m3u8
20 changes: 20 additions & 0 deletions testvectors/hls_subtitles/hls1/sub.m3u8
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:4.0,
0.webvtt
#EXTINF:4.0,
1.webvtt
#EXTINF:4.0,
2.webvtt
#EXTINF:4.0,
3.webvtt
#EXTINF:4.0,
4.webvtt
#EXTINF:4.0,
5.webvtt
#EXTINF:4.0,
6.webvtt
#EXTINF:2.0,
7.webvtt
23 changes: 23 additions & 0 deletions testvectors/hls_subtitles/hls1/video.m3u8
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-INDEPENDENT-SEGMENTS
#EXTINF:4.000000,
seg-00.ts
#EXTINF:4.000000,
seg-01.ts
#EXTINF:4.000000,
seg-02.ts
#EXTINF:4.000000,
seg-03.ts
#EXTINF:4.000000,
seg-04.ts
#EXTINF:4.000000,
seg-05.ts
#EXTINF:4.000000,
seg-06.ts
#EXTINF:2.000000,
seg-07.ts
#EXT-X-ENDLIST

0 comments on commit 1249a27

Please sign in to comment.