Skip to content

Commit

Permalink
feat: #7: support for the insertion of a video bumper that should not…
Browse files Browse the repository at this point in the history
… be tagged as an ad break and always be first
  • Loading branch information
birme committed Jun 21, 2021
1 parent 06935dd commit a79bda7
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 0 deletions.
30 changes: 30 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class HLSSpliceVod {
this.baseUrl = null;
this.targetDuration = 0;
this.mergeBreaks = false; // Merge ad breaks at the same position into one single break
this.bumperDuration = null;
if (options && options.baseUrl) {
this.baseUrl = options.baseUrl;
}
Expand Down Expand Up @@ -102,6 +103,10 @@ class HLSSpliceVod {
duration += (plItem.get('duration') * 1000);
});
offset = duration;
} else {
if (this.bumperDuration) {
offset = this.bumperDuration + offset;
}
}

for (let b = 0; b < bandwidths.length; b++) {
Expand Down Expand Up @@ -144,6 +149,31 @@ class HLSSpliceVod {
});
}

insertBumper(bumperMasterManifestUri, _injectBumperMasterManifest, _injectBumperMediaManifest) {
return new Promise((resolve, reject) => {
this._parseAdMasterManifest(bumperMasterManifestUri, _injectBumperMasterManifest, _injectBumperMediaManifest)
.then(bumper => {
const bandwidths = Object.keys(this.playlists);
for (let b = 0; b < bandwidths.length; b++) {
const bw = bandwidths[b];

const bumperPlaylist = bumper.playlist[findNearestBw(bw, Object.keys(bumper.playlist))];
const bumperLength = bumperPlaylist.items.PlaylistItem.length;
let i = 0;
this.bumperDuration = 0;
for (let j = 0; j < bumperLength; j++) {
this.playlists[bw].items.PlaylistItem.splice(i + j, 0, bumperPlaylist.items.PlaylistItem[j]);
this.bumperDuration += (bumperPlaylist.items.PlaylistItem[j].get('duration') * 1000);
}
this.playlists[bw].items.PlaylistItem[i + bumperLength].set('discontinuity', true);
this.playlists[bw].set('targetDuration', this.targetDuration);
this.bumperOffset = bumperLength;
}
resolve();
}).catch(reject);
});
}

getMasterManifest() {
return this.m3u.toString();
}
Expand Down
88 changes: 88 additions & 0 deletions spec/hls_splice_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ describe("HLSSpliceVod", () => {
}
return fs.createReadStream(`testvectors/ad3/index_${bwmap[bw]}_av.m3u8`);
};
mockBumperMasterManifest = () => {
return fs.createReadStream('testvectors/ad1/master.m3u8')
};
mockBumperMediaManifest = (bw) => {
const bwmap = {
4497000: "0",
2497000: "1"
}
return fs.createReadStream(`testvectors/ad1/index_${bwmap[bw]}_av.m3u8`);
};
});

it("can prepend a baseurl on each segment", done => {
Expand Down Expand Up @@ -238,4 +248,82 @@ describe("HLSSpliceVod", () => {
done();
});
});

it("handles video bumper without any ads", done => {
const mockVod = new HLSSpliceVod('http://mock.com/mock.m3u8');
mockVod.load(mockMasterManifest, mockMediaManifest)
.then(() => {
return mockVod.insertBumper('http://mock.com/ad/mockbumper.m3u8', mockBumperMasterManifest, mockBumperMediaManifest);
})
.then(() => {
const m3u8 = mockVod.getMediaManifest(4497000);
const lines = m3u8.split('\n');
expect(lines[8]).toEqual("http://mock.com/ad/ad1_0_av.ts");
expect(lines[17]).toEqual("#EXT-X-DISCONTINUITY");
expect(lines[18]).not.toEqual("#EXT-X-CUE-IN");
done();
})
});

it("handles video bumper with one ad", done => {
const mockVod = new HLSSpliceVod('http://mock.com/mock.m3u8');
mockVod.load(mockMasterManifest, mockMediaManifest)
.then(() => {
return mockVod.insertAdAt(0, 'http://mock.com/ad/mockad.m3u8', mockAdMasterManifest, mockAdMediaManifest);
})
.then(() => {
return mockVod.insertBumper('http://mock.com/ad/mockbumper.m3u8', mockBumperMasterManifest, mockBumperMediaManifest);
})
.then(() => {
const m3u8 = mockVod.getMediaManifest(4497000);
const lines = m3u8.split('\n');
expect(lines[8]).toEqual("http://mock.com/ad/ad1_0_av.ts");
expect(lines[17]).toEqual("#EXT-X-DISCONTINUITY");
expect(lines[18]).not.toEqual("#EXT-X-CUE-IN");
expect(lines[18]).toEqual("#EXT-X-CUE-OUT:DURATION=15");
done();
})
});

it("handles video bumper and two ads in a row merged into one break", done => {
const mockVod = new HLSSpliceVod('http://mock.com/mock.m3u8', { merge: true });
mockVod.load(mockMasterManifest, mockMediaManifest)
.then(() => {
return mockVod.insertAdAt(0, 'http://mock.com/ad/mockad.m3u8', mockAdMasterManifest, mockAdMediaManifest);
})
.then(() => {
// This one will go first
return mockVod.insertAdAt(0, 'http://mock.com/ad/mockad.m3u8', mockAdMasterManifest3, mockAdMediaManifest3);
})
.then(() => {
return mockVod.insertBumper('http://mock.com/ad/mockbumper.m3u8', mockBumperMasterManifest, mockBumperMediaManifest);
})
.then(() => {
const m3u8 = mockVod.getMediaManifest(4497000);
const lines = m3u8.split('\n');
expect(lines[10+8]).toEqual("#EXT-X-CUE-OUT:DURATION=18");
expect(lines[lines.length - 2]).toEqual("#EXT-X-ENDLIST");
done();
});
});

it("ensures that video bumper is always first", done => {
const mockVod = new HLSSpliceVod('http://mock.com/mock.m3u8');
mockVod.load(mockMasterManifest, mockMediaManifest)
.then(() => {
return mockVod.insertBumper('http://mock.com/ad/mockbumper.m3u8', mockBumperMasterManifest, mockBumperMediaManifest);
})
.then(() => {
return mockVod.insertAdAt(0, 'http://mock.com/ad/mockad.m3u8', mockAdMasterManifest, mockAdMediaManifest);
})
.then(() => {
const m3u8 = mockVod.getMediaManifest(4497000);
const lines = m3u8.split('\n');
expect(lines[8]).toEqual("http://mock.com/ad/ad1_0_av.ts");
expect(lines[17]).toEqual("#EXT-X-DISCONTINUITY");
expect(lines[18]).not.toEqual("#EXT-X-CUE-IN");
expect(lines[18]).toEqual("#EXT-X-CUE-OUT:DURATION=15");
done();
})
});
});

0 comments on commit a79bda7

Please sign in to comment.