diff --git a/src/playlist-merge.js b/src/playlist-merge.js index ab87ba0..1754a92 100644 --- a/src/playlist-merge.js +++ b/src/playlist-merge.js @@ -115,9 +115,15 @@ export const updateSequenceNumbers = ({ oldPlaylists, newPlaylists, timelineStar } // Since we don't yet support early available timelines, we don't need to support - // playlists with no segments. + // playlists with no segments. Playlists with an unresolved SIDX will have an empty + // segments list until the SIDX is parsed and segment references are generated. + if (!playlist.segments || !playlist.segments.length) { + return; + } + const firstNewSegment = playlist.segments[0]; - const oldMatchingSegmentIndex = oldPlaylist.segments.findIndex(function(oldSegment) { + const oldPlaylistSegments = oldPlaylist.segments || []; + const oldMatchingSegmentIndex = oldPlaylistSegments.findIndex(function(oldSegment) { return ( Math.abs(oldSegment.presentationTime - firstNewSegment.presentationTime) < TIME_FUDGE ); @@ -130,7 +136,7 @@ export const updateSequenceNumbers = ({ oldPlaylists, newPlaylists, timelineStar if (oldMatchingSegmentIndex === -1) { updateMediaSequenceForPlaylist({ playlist, - mediaSequence: oldPlaylist.mediaSequence + oldPlaylist.segments.length + mediaSequence: oldPlaylist.mediaSequence + oldPlaylistSegments.length }); playlist.segments[0].discontinuity = true; playlist.discontinuityStarts.unshift(0); @@ -154,9 +160,9 @@ export const updateSequenceNumbers = ({ oldPlaylists, newPlaylists, timelineStar // // A period may also have a duration of zero, so the case of no segments is handled // here even though we don't yet support early available periods. - if ((!oldPlaylist.segments.length && playlist.timeline > oldPlaylist.timeline) || - (oldPlaylist.segments.length && playlist.timeline > - oldPlaylist.segments[oldPlaylist.segments.length - 1].timeline)) { + if ((!oldPlaylistSegments.length && playlist.timeline > oldPlaylist.timeline) || + (oldPlaylistSegments.length && playlist.timeline > + oldPlaylistSegments[oldPlaylistSegments.length - 1].timeline)) { playlist.discontinuitySequence--; } return; @@ -173,7 +179,7 @@ export const updateSequenceNumbers = ({ oldPlaylists, newPlaylists, timelineStar // Period while the "timeline" exists, HLS keeps track of it via the discontinuity // sequence, and that first segment is an indicator, but can be removed before that // timeline is gone. - const oldMatchingSegment = oldPlaylist.segments[oldMatchingSegmentIndex]; + const oldMatchingSegment = oldPlaylistSegments[oldMatchingSegmentIndex]; if (oldMatchingSegment.discontinuity && !firstNewSegment.discontinuity) { firstNewSegment.discontinuity = true; @@ -183,7 +189,7 @@ export const updateSequenceNumbers = ({ oldPlaylists, newPlaylists, timelineStar updateMediaSequenceForPlaylist({ playlist, - mediaSequence: oldPlaylist.segments[oldMatchingSegmentIndex].number + mediaSequence: oldPlaylistSegments[oldMatchingSegmentIndex].number }); }); }; diff --git a/src/toM3u8.js b/src/toM3u8.js index 6d058a7..b4f3b72 100644 --- a/src/toM3u8.js +++ b/src/toM3u8.js @@ -147,6 +147,7 @@ export const formatAudioPlaylist = ({ export const formatVttPlaylist = ({ attributes, segments, + sidx, mediaSequence, discontinuityStarts, discontinuitySequence @@ -192,6 +193,10 @@ export const formatVttPlaylist = ({ vttPlaylist.attributes.serviceLocation = attributes.serviceLocation; } + if (sidx) { + vttPlaylist.sidx = sidx; + } + return vttPlaylist; }; diff --git a/test/playlist-merge.test.js b/test/playlist-merge.test.js index fc98e2d..63a48a1 100644 --- a/test/playlist-merge.test.js +++ b/test/playlist-merge.test.js @@ -333,6 +333,118 @@ QUnit.test('complete refresh of matching playlist', function(assert) { ); }); +QUnit.test('new playlists without segments only update discontinuity sequence', function(assert) { + const oldPlaylists = [{ + discontinuitySequence: 1, + discontinuityStarts: [], + mediaSequence: 5, + timeline: 5, + attributes: { NAME: 'A' }, + segments: [] + }, { + discontinuitySequence: 2, + discontinuityStarts: [], + mediaSequence: 3, + timeline: 10, + attributes: { NAME: 'B' } + }]; + const newPlaylists = [{ + discontinuitySequence: 0, + discontinuityStarts: [], + mediaSequence: 0, + timeline: 5, + attributes: { NAME: 'A' }, + segments: [] + }, { + discontinuitySequence: 0, + discontinuityStarts: [], + mediaSequence: 0, + timeline: 10, + attributes: { NAME: 'B' } + }]; + const timelineStarts = [ + { start: 0, timeline: 0 }, + { start: 5, timeline: 5 }, + { start: 10, timeline: 10 } + ]; + + updateSequenceNumbers({ oldPlaylists, newPlaylists, timelineStarts }); + + assert.deepEqual( + newPlaylists, + [{ + discontinuitySequence: 1, + discontinuityStarts: [], + mediaSequence: 0, + timeline: 5, + attributes: { NAME: 'A' }, + segments: [] + }, { + discontinuitySequence: 2, + discontinuityStarts: [], + mediaSequence: 0, + timeline: 10, + attributes: { NAME: 'B' } + }], + 'new playlists unchanged apart from discontinuity sequence' + ); +}); + +QUnit.test('matching old playlist without segments is treated as a refresh', function(assert) { + const oldPlaylists = [{ + discontinuitySequence: 1, + discontinuityStarts: [], + mediaSequence: 3, + timeline: 5, + attributes: { NAME: 'A' } + }]; + const newPlaylists = [{ + discontinuitySequence: 0, + discontinuityStarts: [], + mediaSequence: 0, + timeline: 10, + attributes: { NAME: 'A' }, + segments: [{ + presentationTime: 10, + timeline: 10, + number: 0 + }, { + presentationTime: 12, + timeline: 10, + number: 1 + }] + }]; + const timelineStarts = [ + { start: 0, timeline: 0 }, + { start: 5, timeline: 5 }, + { start: 10, timeline: 10 } + ]; + + updateSequenceNumbers({ oldPlaylists, newPlaylists, timelineStarts }); + + assert.deepEqual( + newPlaylists, + [{ + discontinuitySequence: 1, + discontinuityStarts: [0], + mediaSequence: 3, + timeline: 10, + attributes: { NAME: 'A' }, + segments: [{ + discontinuity: true, + presentationTime: 10, + timeline: 10, + number: 3 + }, { + presentationTime: 12, + timeline: 10, + number: 4 + }] + }], + 'new playlist updated as a complete refresh' + ); +}); + QUnit.module('positionManifestOnTimeline'); QUnit.test('handles multiple playlists, including added and removed', function(assert) { diff --git a/test/toM3u8.test.js b/test/toM3u8.test.js index 9d765a8..994e403 100644 --- a/test/toM3u8.test.js +++ b/test/toM3u8.test.js @@ -1342,6 +1342,97 @@ QUnit.test('playlists with sidx and sidxMapping', function(assert) { assert.deepEqual(toM3u8({ dashPlaylists, sidxMapping }).playlists, expected); }); +QUnit.test('vtt playlists with sidx and sidxMapping', function(assert) { + const dashPlaylists = [{ + attributes: { + sourceDuration: 100, + id: 'en', + lang: 'en', + codecs: 'wvtt', + duration: 0, + bandwidth: 10000, + periodStart: 0, + mimeType: 'text/vtt', + type: 'static' + }, + segments: [], + sidx: { + byterange: { + offset: 10, + length: 10 + }, + uri: 'sidx.mp4', + resolvedUri: 'http://example.com/sidx.mp4', + timeline: 0, + duration: 10 + }, + uri: 'http://example.com/fmp4.mp4' + }]; + + const sidxMapping = { + 'sidx.mp4-10-19': { + sidx: { + timescale: 1, + firstOffset: 0, + references: [{ + referenceType: 0, + referencedSize: 5, + subsegmentDuration: 2 + }] + } + } + }; + + const expected = [{ + attributes: { + BANDWIDTH: 10000, + CODECS: 'wvtt', + NAME: 'en', + ['PROGRAM-ID']: 1 + }, + sidx: { + byterange: { + offset: 10, + length: 10 + }, + uri: 'sidx.mp4', + resolvedUri: 'http://example.com/sidx.mp4', + timeline: 0, + duration: 10 + }, + targetDuration: 0, + timeline: 0, + timelineStarts: [{ start: 0, timeline: 0 }], + uri: '', + segments: [{ + map: { + resolvedUri: 'http://example.com/sidx.mp4', + uri: '' + }, + byterange: { + offset: 20, + length: 5 + }, + uri: 'http://example.com/sidx.mp4', + resolvedUri: 'http://example.com/sidx.mp4', + duration: 2, + number: 0, + presentationTime: 0, + timeline: 0 + }], + endList: true, + mediaSequence: 0, + discontinuitySequence: 0, + discontinuityStarts: [], + resolvedUri: '' + }]; + + assert.deepEqual( + toM3u8({ dashPlaylists, sidxMapping }).mediaGroups.SUBTITLES.subs.en.playlists, + expected + ); +}); + QUnit.test('playlists without minimumUpdatePeriod dont assign default value', function(assert) { const dashPlaylists = [{ attributes: {