diff --git a/.gitignore b/.gitignore index c9106a7..aa292fa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules .nyc_output +dist \ No newline at end of file diff --git a/README.md b/README.md index cf5aaa6..b19dee3 100644 --- a/README.md +++ b/README.md @@ -75,13 +75,76 @@ segment2_0_av.ts #EXT-X-ENDLIST ``` +You can also provide a start-time offset. + +``` +const hlsVod = new HLSTruncateVod('http://testcontent.eyevinn.technology/slates/30seconds/playlist.m3u8', 4, { offset: 10 }); +hlsVod.load() +.then(() => { + const mediaManifest = hlsVod.getMediaManifest(4928000); + console.log(mediaManifest); +}); +``` + +The library will first remove 10 seconds (approx depending on segment length) and then take the next 4 seconds of the original playlist. For example consider this playlist: + +``` +#EXTM3U +#EXT-X-TARGETDURATION:3 +#EXT-X-ALLOW-CACHE:YES +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-VERSION:3 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MEDIA-SEQUENCE:1 +#EXTINF:3.000, +segment1_0_av.ts +#EXTINF:3.000, +segment2_0_av.ts +#EXTINF:3.000, +segment3_0_av.ts +#EXTINF:3.000, +segment4_0_av.ts +#EXTINF:3.000, +segment5_0_av.ts +#EXTINF:3.000, +segment6_0_av.ts +#EXTINF:3.000, +segment7_0_av.ts +#EXTINF:3.000, +segment8_0_av.ts +#EXTINF:3.000, +segment9_0_av.ts +#EXTINF:3.000, +segment10_0_av.ts +#EXT-X-ENDLIST +``` + +will result in: + +``` +#EXTM3U +#EXT-X-TARGETDURATION:3 +#EXT-X-ALLOW-CACHE:YES +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-VERSION:3 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MEDIA-SEQUENCE:1 +#EXTINF:3.000, +segment5_0_av.ts +#EXTINF:3.000, +segment6_0_av.ts +#EXT-X-ENDLIST +``` + +It will remove 12 seconds from the start (3 segments is 9 seconds and 4 segments is 12 seconds) and then truncate the remaining part to get a duration of 6 seconds (2 segments i 6 segments which is close to 4 seconds as requested). + # Authors This open source project is maintained by Eyevinn Technology. ## Contributors -- Jonas Rydholm Birmé (jonas.birme@eyevinn.se) +- Jonas Birmé (jonas.birme@eyevinn.se) - Alan Allard (alan.allard@eyevinn.se) # [Contributing](CONTRIBUTING.md) diff --git a/index.js b/index.js index 9b22a41..c26b4dc 100644 --- a/index.js +++ b/index.js @@ -8,8 +8,12 @@ class HLSTruncateVod { this.playlists = {}; this.duration = duration; this.durationAudio = 0; + this.startVideoOffset = 0; this.bandwiths = []; this.audioSegments = {}; + if (options && options.offset) { + this.startOffset = options.offset; + } } load(_injectMasterManifest, _injectMediaManifest, _injectAudioManifest) { @@ -147,8 +151,21 @@ class HLSTruncateVod { let accDuration = 0; let prevAccDuration = 0; let pos = 0; + let startPos = 0; + + if (this.startOffset) { + let accStartOffset = 0; + m3u.items.PlaylistItem.map((item => { + if (accStartOffset + item.get('duration') <= this.startOffset) { + accStartOffset += item.get('duration'); + startPos++; + } + })); + + this.startVideoOffset = this.startVideoOffset === 0 ? accStartOffset : this.startVideoOffset; + } - m3u.items.PlaylistItem.map((item => { + m3u.items.PlaylistItem.slice(startPos).map((item => { if (accDuration <= this.duration) { prevAccDuration = accDuration; accDuration += item.get('duration'); @@ -166,7 +183,7 @@ class HLSTruncateVod { this.durationAudio = this.durationAudio === 0 ? accDuration : this.durationAudio; - this.playlists[bandwidth].items.PlaylistItem = m3u.items.PlaylistItem.slice(0, pos); + this.playlists[bandwidth].items.PlaylistItem = m3u.items.PlaylistItem.slice(startPos, startPos + pos); resolve(); }); parser.on('error', (err) => { @@ -195,8 +212,22 @@ class HLSTruncateVod { let accDuration = 0; let prevAccDuration = 0; let pos = 0; + let startPos = 0; - m3u.items.PlaylistItem.map((item => { + if (this.startOffset) { + let accStartOffset = 0; + m3u.items.PlaylistItem.map((item => { + if (accStartOffset + item.get('duration') <= this.startOffset) { + accStartOffset += item.get('duration'); + startPos++; + } + })); + if ((accStartOffset > this.startVideoOffset) && startPos > 1) { + startPos--; + } + } + + m3u.items.PlaylistItem.slice(startPos).map((item => { if (accDuration <= this.durationAudio) { prevAccDuration = accDuration; accDuration += item.get('duration'); @@ -210,7 +241,8 @@ class HLSTruncateVod { if (this._similarSegItemDuration() && (accDuration - this.durationAudio) >= (this.durationAudio - prevAccDuration) && pos > 1) { pos--; } - this.audioSegments[audioGroupId][audioLang].items.PlaylistItem = m3u.items.PlaylistItem.slice(0, pos); + + this.audioSegments[audioGroupId][audioLang].items.PlaylistItem = m3u.items.PlaylistItem.slice(startPos, startPos + pos); resolve(); }); parser.on('error', (err) => { diff --git a/spec/hls_truncate_cmaf_spec.js b/spec/hls_truncate_cmaf_spec.js index fb53873..8fc0d74 100644 --- a/spec/hls_truncate_cmaf_spec.js +++ b/spec/hls_truncate_cmaf_spec.js @@ -290,5 +290,25 @@ describe("HLSTruncateVod", () => { done(); }) }); + + it("cuts to the closest segment when requesting unaligned duration with equal time between them and has start offset", done => { + const mockVod = new HLSTruncateVod('http://mock.com/mock.m3u8', 7.5, { offset: 5 }); + + mockVod.load(mockMasterManifest, mockMediaManifest, mockAudioManifest) + .then(() => { + const bandwidths = mockVod.getBandwidths(); + const videoManifest = mockVod.getMediaManifest(bandwidths[0]); + const audioManifest = mockVod.getAudioManifest("audio-aacl-256", "sv"); + const linesVideo = videoManifest.split("\n"); + const linesAudio = audioManifest.split("\n"); + const durationVideo = calcDuration(videoManifest); + const durationAudio = calcDuration(audioManifest); + expect(durationVideo).toEqual(6); + expect(durationAudio).toEqual(7.68); + expect(linesVideo[9]).toEqual("test-video=2500000-2.m4s"); + expect(linesAudio[9]).toEqual("test-audio=256000-2.m4s"); + done(); + }) + }); }); }); \ No newline at end of file diff --git a/spec/hls_truncate_spec.js b/spec/hls_truncate_spec.js index d17bb76..3fe2603 100644 --- a/spec/hls_truncate_spec.js +++ b/spec/hls_truncate_spec.js @@ -105,6 +105,22 @@ describe("HLSTruncateVod for muxed TS HLS Vods", () => { done(); }) }); + + it("can trim the beginning if start offset is requested", done => { + const mockVod = new HLSTruncateVod('http://mock.com/mock.m3u8', 6, { offset: 10 }); + + mockVod.load(mockMasterManifest, mockMediaManifest) + .then(() => { + const bandwidths = mockVod.getBandwidths(); + const manifest = mockVod.getMediaManifest(bandwidths[0]); + const lines = manifest.split("\n"); + expect(lines[8]).toEqual("segment4_0_av.ts"); + expect(lines[10]).toEqual("segment5_0_av.ts"); + const duration = calcDuration(manifest); + expect(duration).toEqual(6); + done(); + }) + }); }); describe("HLSTruncateVod,", () => { describe("for Demuxed TS HLS Vods", () => { @@ -178,6 +194,27 @@ describe("HLSTruncateVod,", () => { done(); }) }); + + it("can trim the beginning if start offset is requested", done => { + const mockVod = new HLSTruncateVod('http://mock.com/mock.m3u8', 30, { offset: 30 }); + + mockVod.load(mockMasterManifest, mockMediaManifest, mockAudioManifest) + .then(() => { + const bandwidths = mockVod.getBandwidths(); + const manifest = mockVod.getMediaManifest(bandwidths[0]); + const lines = manifest.split("\n"); + expect(lines[8]).toEqual("level1/seg_36.ts"); + expect(lines[11]).toEqual("level1/seg_37.ts"); + const duration = calcDuration(manifest); + expect(duration).toEqual(30.03); + + const audioManifest = mockVod.getAudioManifest("aac", "en"); + const audioLines = audioManifest.split("\n"); + expect(audioLines[8]).toEqual("audio/seg_en_36.ts"); + expect(audioLines[11]).toEqual("audio/seg_en_37.ts"); + done(); + }) + }); }); describe("for Demuxed TS HLS Vods which have different segments durations,", () => { let mockMasterManifest;