From 5e9b4f1a62c3a10c52b1b9bef3ef2e19b6cec7af Mon Sep 17 00:00:00 2001 From: Garrett Singer Date: Wed, 10 Feb 2021 15:37:23 -0500 Subject: [PATCH] fix: use a separate ProgramDateTime mapping to player time per timeline (#1063) Previously, the ProgramDateTime would map to time 0 at the start of playback, and that mapping would be used for the rest of playback. However, in the case of a discontinuity, ProgramDateTime can jump (as in, can be a larger difference in time than a single segment duration, e.g., if the encoder went down for a period of time). Because the ProgramDateTime to player time mapping never changed, this led to seeking errors. This fix uses a mapping of ProgramDateTime to player time per timeline, allowing those "jumps" in time on discontinuities. --- docs/creating-content.md | 19 +++ src/segment-loader.js | 14 +- src/sync-controller.js | 45 ++++-- test/segment-loader.test.js | 236 +++++++++++++++++++++++++++++++ test/segments/videoOneSecond.ts | Bin 0 -> 2256 bytes test/segments/videoOneSecond1.ts | Bin 0 -> 2256 bytes test/segments/videoOneSecond2.ts | Bin 0 -> 2256 bytes test/segments/videoOneSecond3.ts | Bin 0 -> 2256 bytes test/segments/videoOneSecond4.ts | Bin 0 -> 2256 bytes test/sync-controller.test.js | 116 +++++++++++++-- test/test-helpers.js | 29 ++-- 11 files changed, 426 insertions(+), 33 deletions(-) create mode 100644 test/segments/videoOneSecond.ts create mode 100644 test/segments/videoOneSecond1.ts create mode 100644 test/segments/videoOneSecond2.ts create mode 100644 test/segments/videoOneSecond3.ts create mode 100644 test/segments/videoOneSecond4.ts diff --git a/docs/creating-content.md b/docs/creating-content.md index 3ac4e1488..ec1e16c53 100644 --- a/docs/creating-content.md +++ b/docs/creating-content.md @@ -33,6 +33,25 @@ Copy only the first two video frames, leave out audio. $ ffmpeg -i index0.ts -vframes 2 -an -vcodec copy video.ts ``` +### videoOneSecond.ts + +Blank video for 1 second, MMS-Small resolution, start at 0 PTS/DTS, 2 frames per second + +``` +$ ffmpeg -f lavfi -i color=c=black:s=128x96:r=2:d=1 -muxdelay 0 -c:v libx264 videoOneSecond.ts +``` + +### videoOneSecond1.ts through videoOneSecond4.ts + +Same as videoOneSecond.ts, but follows timing in sequence, with videoOneSecond.ts acting as the 0 index. Each segment starts at the second that its index indicates (e.g., videoOneSecond2.ts has a start time of 2 seconds). + +``` +$ ffmpeg -i videoOneSecond.ts -muxdelay 0 -output_ts_offset 1 -vcodec copy videoOneSecond1.ts +$ ffmpeg -i videoOneSecond.ts -muxdelay 0 -output_ts_offset 2 -vcodec copy videoOneSecond2.ts +$ ffmpeg -i videoOneSecond.ts -muxdelay 0 -output_ts_offset 3 -vcodec copy videoOneSecond3.ts +$ ffmpeg -i videoOneSecond.ts -muxdelay 0 -output_ts_offset 4 -vcodec copy videoOneSecond4.ts +``` + ### audio.ts Copy only the first two audio frames, leave out video. diff --git a/src/segment-loader.js b/src/segment-loader.js index b6aa3f54d..be9ff8f6c 100644 --- a/src/segment-loader.js +++ b/src/segment-loader.js @@ -855,9 +855,6 @@ export default class SegmentLoader extends videojs.EventTarget { return; } - // not sure if this is the best place for this - this.syncController_.setDateTimeMapping(this.playlist_); - // if all the configuration is ready, initialize and begin loading if (this.state === 'INIT' && this.couldBeginLoading_()) { return this.init_(); @@ -916,6 +913,17 @@ export default class SegmentLoader extends videojs.EventTarget { mediaSequence: newPlaylist.mediaSequence, time: 0 }; + // Setting the date time mapping means mapping the program date time (if available) + // to time 0 on the player's timeline. The playlist's syncInfo serves a similar + // purpose, mapping the initial mediaSequence to time zero. Since the syncInfo can + // be updated as the playlist is refreshed before the loader starts loading, the + // program date time mapping needs to be updated as well. + // + // This mapping is only done for the main loader because a program date time should + // map equivalently between playlists. + if (this.loaderType_ === 'main') { + this.syncController_.setDateTimeMappingForStart(newPlaylist); + } } let oldId = null; diff --git a/src/sync-controller.js b/src/sync-controller.js index afd502e4a..9c215b613 100644 --- a/src/sync-controller.js +++ b/src/sync-controller.js @@ -27,7 +27,7 @@ export const syncPointStrategies = [ { name: 'ProgramDateTime', run: (syncController, playlist, duration, currentTimeline, currentTime) => { - if (!syncController.datetimeToDisplayTime) { + if (!Object.keys(syncController.timelineToDatetimeMappings).length) { return null; } @@ -39,10 +39,16 @@ export const syncPointStrategies = [ for (let i = 0; i < segments.length; i++) { const segment = segments[i]; + const datetimeMapping = + syncController.timelineToDatetimeMappings[segment.timeline]; + + if (!datetimeMapping) { + continue; + } if (segment.dateTimeObject) { const segmentTime = segment.dateTimeObject.getTime() / 1000; - const segmentStart = segmentTime + syncController.datetimeToDisplayTime; + const segmentStart = segmentTime + datetimeMapping; const distance = Math.abs(currentTime - segmentStart); // Once the distance begins to increase, or if distance is 0, we have passed @@ -161,7 +167,7 @@ export default class SyncController extends videojs.EventTarget { // ...for synching across variants this.timelines = []; this.discontinuities = []; - this.datetimeToDisplayTime = null; + this.timelineToDatetimeMappings = {}; this.logger_ = logger('SyncController'); } @@ -351,19 +357,25 @@ export default class SyncController extends videojs.EventTarget { } /** - * Save the mapping from playlist's ProgramDateTime to display. This should - * only ever happen once at the start of playback. + * Save the mapping from playlist's ProgramDateTime to display. This should only happen + * before segments start to load. * * @param {Playlist} playlist - The currently active playlist */ - setDateTimeMapping(playlist) { - if (!this.datetimeToDisplayTime && - playlist.segments && + setDateTimeMappingForStart(playlist) { + // It's possible for the playlist to be updated before playback starts, meaning time + // zero is not yet set. If, during these playlist refreshes, a discontinuity is + // crossed, then the old time zero mapping (for the prior timeline) would be retained + // unless the mappings are cleared. + this.timelineToDatetimeMappings = {}; + + if (playlist.segments && playlist.segments.length && playlist.segments[0].dateTimeObject) { - const playlistTimestamp = playlist.segments[0].dateTimeObject.getTime() / 1000; + const firstSegment = playlist.segments[0]; + const playlistTimestamp = firstSegment.dateTimeObject.getTime() / 1000; - this.datetimeToDisplayTime = -playlistTimestamp; + this.timelineToDatetimeMappings[firstSegment.timeline] = -playlistTimestamp; } } @@ -377,7 +389,7 @@ export default class SyncController extends videojs.EventTarget { * The current active request information * @param {boolean} options.shouldSaveTimelineMapping * If there's a timeline change, determines if the timeline mapping should be - * saved in timelines. + * saved for timeline mapping and program date time mappings. */ saveSegmentTimingInfo({ segmentInfo, shouldSaveTimelineMapping }) { const didCalculateSegmentTimeMapping = this.calculateSegmentTimeMapping_( @@ -385,6 +397,7 @@ export default class SyncController extends videojs.EventTarget { segmentInfo.timingInfo, shouldSaveTimelineMapping ); + const segment = segmentInfo.segment; if (didCalculateSegmentTimeMapping) { this.saveDiscontinuitySyncInfo_(segmentInfo); @@ -394,10 +407,16 @@ export default class SyncController extends videojs.EventTarget { if (!segmentInfo.playlist.syncInfo) { segmentInfo.playlist.syncInfo = { mediaSequence: segmentInfo.playlist.mediaSequence + segmentInfo.mediaIndex, - time: segmentInfo.segment.start + time: segment.start }; } } + + const dateTime = segment.dateTimeObject; + + if (segment.discontinuity && shouldSaveTimelineMapping && dateTime) { + this.timelineToDatetimeMappings[segment.timeline] = -(dateTime.getTime() / 1000); + } } timestampOffsetForTimeline(timeline) { @@ -433,7 +452,7 @@ export default class SyncController extends videojs.EventTarget { const segment = segmentInfo.segment; let mappingObj = this.timelines[segmentInfo.timeline]; - if (segmentInfo.timestampOffset !== null) { + if (typeof segmentInfo.timestampOffset === 'number') { mappingObj = { time: segmentInfo.startOfSegment, mapping: segmentInfo.startOfSegment - timingInfo.start diff --git a/test/segment-loader.test.js b/test/segment-loader.test.js index ee76c0008..a72ebf3b1 100644 --- a/test/segment-loader.test.js +++ b/test/segment-loader.test.js @@ -27,6 +27,10 @@ import { oneSecond as oneSecondSegment, audio as audioSegment, video as videoSegment, + videoOneSecond as videoOneSecondSegment, + videoOneSecond1 as videoOneSecond1Segment, + videoOneSecond2 as videoOneSecond2Segment, + videoOneSecond3 as videoOneSecond3Segment, videoLargeOffset as videoLargeOffsetSegment, videoLargeOffset2 as videoLargeOffset2Segment, videoMaxOffset as videoMaxOffsetSegment, @@ -3860,6 +3864,238 @@ QUnit.module('SegmentLoader', function(hooks) { ); }); }); + + QUnit.test('PDT mapping updated before loader starts loading', function(assert) { + const targetDuration = 1; + const playlistOptions = { + targetDuration, + discontinuityStarts: [2], + // make it a live playlist so that removing segments from beginning is allowed + endList: false + }; + const playlistDuration = 4; + const playlist1 = playlistWithDuration( + playlistDuration, + // need different URIs to ensure the playlists are considered different + videojs.mergeOptions(playlistOptions, { uri: 'playlist1.m3u8' }) + ); + const playlist2 = playlistWithDuration( + playlistDuration, + videojs.mergeOptions(playlistOptions, { uri: 'playlist2.m3u8' }) + ); + + const segmentDurationMs = targetDuration * 1000; + + const playlist1Start = new Date('2021-01-01T00:00:00.000-0500'); + + playlist1.segments[0].dateTimeObject = playlist1Start; + playlist1.segments[1].dateTimeObject = new Date(playlist1Start.getTime() + segmentDurationMs); + // jump of 0.5 seconds after disco (0.5 seconds of missing real world time, e.g., + // an encoder went down briefly), should have a PDT mapping difference of -3.5 + // seconds from first mapping + playlist1.segments[2].dateTimeObject = new Date(playlist1.segments[1].dateTimeObject.getTime() + segmentDurationMs + 500); + playlist1.segments[3].dateTimeObject = new Date(playlist1.segments[2].dateTimeObject.getTime() + segmentDurationMs); + + // offset by 0.25 seconds from playlist1 + const playlist2Start = new Date('2021-01-01T00:00:00.250-0500'); + + playlist2.segments[0].dateTimeObject = playlist2Start; + playlist2.segments[1].dateTimeObject = new Date(playlist2Start.getTime() + segmentDurationMs); + // jump of 0.5 seconds after disco (0.5 seconds of missing real world time, e.g., + // an encoder went down briefly), should have a PDT mapping difference of -3.5 + // seconds from first mapping + playlist2.segments[2].dateTimeObject = new Date(playlist2.segments[1].dateTimeObject.getTime() + segmentDurationMs + 500); + playlist2.segments[3].dateTimeObject = new Date(playlist2.segments[2].dateTimeObject.getTime() + segmentDurationMs); + + const { + mediaSource_: mediaSource, + sourceUpdater_: sourceUpdater + } = loader; + const mediaSettings = { isVideoOnly: true }; + + return setupMediaSource(mediaSource, sourceUpdater, mediaSettings).then(() => { + loader.playlist(playlist1); + + // uses private property of sync controller because there isn't a great way + // to really check without a whole bunch of other code + assert.deepEqual( + loader.syncController_.timelineToDatetimeMappings, + { 0: -1609477200 }, + 'set date time mapping to start of playlist1' + ); + + // change of playlist before load should set new 0 point + loader.playlist(playlist2); + + assert.deepEqual( + loader.syncController_.timelineToDatetimeMappings, + // offset of 0.25 seconds + { 0: -1609477200.25 }, + 'set date time mapping to start of playlist2' + ); + + // changes back, because why not + loader.playlist(playlist1); + + assert.deepEqual( + loader.syncController_.timelineToDatetimeMappings, + { 0: -1609477200 }, + 'set date time mapping to start of playlist1' + ); + + playlist1.segments.shift(); + playlist1.mediaSequence++; + // playlist update, first segment removed + loader.playlist(playlist1); + + assert.deepEqual( + loader.syncController_.timelineToDatetimeMappings, + // 1 second later + { 0: -1609477201 }, + 'set date time mapping to new start of playlist1' + ); + + playlist1.segments.shift(); + playlist1.mediaSequence++; + // playlist update, first two segments now removed + loader.playlist(playlist1); + + assert.deepEqual( + loader.syncController_.timelineToDatetimeMappings, + // 2.5 seconds later, as this is a disco and the PDT jumped + // note also the timeline jumped in the mapping key + { 1: -1609477202.5 }, + 'set date time mapping to post disco of playlist1' + ); + + loader.load(); + }); + }); + + QUnit.test('handles PDT mappings for different timelines', function(assert) { + const playlistDuration = 5; + const targetDuration = 1; + const playlistOptions = { + targetDuration, + discontinuityStarts: [3] + }; + let currentTime = 0; + // In a normal mediaIndex++ situation, the timing values will be OK even though the + // PDT mapping changes, but when changing renditions over a timeline change, the new + // mapping will lead to an incorrect value if the different timeline mappings are + // not accounted for. + // + // This is mainly an issue with smooth quality change, as that is when the loader + // will overlap content. + const playlist1 = playlistWithDuration( + playlistDuration, + // need different URIs to ensure the playlists are considered different + videojs.mergeOptions(playlistOptions, { uri: 'playlist1.m3u8' }) + ); + const playlist2 = playlistWithDuration( + playlistDuration, + videojs.mergeOptions(playlistOptions, { uri: 'playlist2.m3u8' }) + ); + + loader.currentTime_ = () => currentTime; + + const segmentDurationMs = targetDuration * 1000; + const segment0Start = new Date('2021-01-01T00:00:00.000-0500'); + const segment1Start = new Date(segment0Start.getTime() + segmentDurationMs); + const segment2Start = new Date(segment1Start.getTime() + segmentDurationMs); + // jump of 0.5 seconds after disco (0.5 seconds of missing real world time, e.g., + // an encoder went down briefly), should have a PDT mapping difference of -3.5 + // seconds from first mapping + const segment3Start = new Date(segment2Start.getTime() + segmentDurationMs + 500); + + [playlist1, playlist2].forEach((playlist) => { + playlist.dateTimeObject = segment0Start; + playlist.segments[0].dateTimeObject = segment0Start; + playlist.segments[1].dateTimeObject = segment1Start; + playlist.segments[2].dateTimeObject = segment2Start; + playlist.segments[3].dateTimeObject = segment3Start; + }); + + const { + mediaSource_: mediaSource, + sourceUpdater_: sourceUpdater + } = loader; + const mediaSettings = { isVideoOnly: true }; + + return setupMediaSource(mediaSource, sourceUpdater, mediaSettings).then(() => { + loader.playlist(playlist1); + loader.load(); + + this.clock.tick(1); + standardXHRResponse(this.requests.shift(), videoOneSecondSegment()); + + return new Promise((resolve, reject) => { + loader.one('appended', resolve); + loader.one('error', reject); + }); + }).then(() => { + this.clock.tick(1); + + standardXHRResponse(this.requests.shift(), videoOneSecond1Segment()); + + return new Promise((resolve, reject) => { + loader.one('appended', resolve); + loader.one('error', reject); + }); + }).then(() => { + this.clock.tick(1); + + standardXHRResponse(this.requests.shift(), videoOneSecond2Segment()); + + return new Promise((resolve, reject) => { + loader.one('appended', resolve); + loader.one('error', reject); + }); + }).then(() => { + this.clock.tick(1); + + // responding with the first segment post discontinuity + standardXHRResponse(this.requests.shift(), videoOneSecond3Segment()); + + return new Promise((resolve, reject) => { + loader.one('appended', resolve); + loader.one('error', reject); + }); + }).then(() => { + // The time needs to be at a point in time where the ProgramDateTime strategy + // is chosen. In this case, the segments go: + // + // 0.ts: 0 => 1 + // 1.ts: 1 => 2 + // 2.ts: 2 => 3 + // DISCO + // 3.ts: 3 => 4 + // + // By setting the current time to 2.8, 2.ts should be chosen, since the closest + // sync point will be ProgramDateTime, at a time of 3.5, though this time value is + // wrong, since the gap in ProgramDateTime was not accounted for. + currentTime = 2.8; + loader.playlist(playlist2); + // smoothQualityChange will reset loader after changing renditions, so need to + // mimic that behavior here in order for content to be overlayed over already + // buffered content. + loader.resetLoader(); + this.clock.tick(1); + + standardXHRResponse(this.requests.shift(), videoOneSecond2Segment()); + + return new Promise((resolve, reject) => { + loader.one('appended', resolve); + loader.one('error', reject); + }); + }).then(() => { + assert.deepEqual( + playlist1.segments[2], + playlist2.segments[2], + 'segments are equal' + ); + }); + }); }); }); diff --git a/test/segments/videoOneSecond.ts b/test/segments/videoOneSecond.ts new file mode 100644 index 0000000000000000000000000000000000000000..950cab4ff796883473b5a9247c0c04e1aecd3edc GIT binary patch literal 2256 zcmdT_&1)1%6t6b%BSKhb)RQ0;7cXjNs(Z#69VnPdHcs{+MiwG_YO1SirrY#KrMlwG z1POESARYu+@UXB~LD)lfK|Be167(N1;K4-^b{8}^A&BFvNz_x$Ce`%oSMT%ps$W$# z>$yW{YW*a#o**=^2FUu@KQf!Q_FcFTOVJ+tMW|~b7sT#dpMEyjS)*A;hY(sFA7V$= zIy&{&=-)dF;M8 z1z!8{-TSUnDdS1ZQzfunv9T%{ID2P`gHI1ha%iy-ZNjE!9ZZwo5n`G!R{!EqTCIvCMkl3e;6TwF?BCfZ#an z3g0ymZSZgvOWK0k70d&b#>@p*8*3$^D9nI6*FEQR4Gdq5DFFz~UrrN29l)Nq&)7C@ zu}r%%TL`7G+6=NN-L%!pgr<`Y)`0@A3<`+Sbb$phalgoHoJFvszX=FV)G!YZW2Oz3 zFwsIq42(eTMN5kHT%N{~X_#|ZCaqYQKnO5Y%-FOPi-pPOxv~d*=u-!H<}+z3@m#=u z*b7-^5`{_(>+6WH9cT~qsT7IZP9!mc z*VFm&4Ya&`_11A@{cKe)H=6ZaZWo{LaC+?8^F6nV&-b&x!OeQ*&C2)a`fWEFH2=uYU&qo$~wu literal 0 HcmV?d00001 diff --git a/test/segments/videoOneSecond1.ts b/test/segments/videoOneSecond1.ts new file mode 100644 index 0000000000000000000000000000000000000000..30571a55ac820a7bb7e4f2bc76b5c333a8f60b6d GIT binary patch literal 2256 zcmdT_&1)M+6rXMFxGtDjYI-V!u}dzAEzPbJDHRMzb{she6Nf;cr)73_q}_Ty#jr^awV3cJ$^%|TOk+3?%baLZE(DP%{rPyXnSIa z9a;P6{QZ$%ju*ru(KMF<`$yaPA@-MOAAMoS=Q~R`k90XUie>|mv1NpQ{&fQ(WW7MB z-yfYqqmQP{aeUvvknokQqjWEnDYI)84<=z_Peo=ran=G5q5@YCMH?>nz| z7awlTJjc6V{_$tmsg&_7=BX0cuGmmFFeol9FEnS%xN&nHEPcU2 zF`vp_Bw89fgp_6-;t&u9TFY9o*y(f%w?bc}O$+5BmK0LeE}Be*K*tf-rBa7!lGQM0 z9^(|!iWV(lUwBc6U7wYuhCp3mS+n&elBOs#s<{a0TirpV|ci zPC#%Rc7^Ynh&Fh*iY0A9?F#0BN@M1NtBti1Q50stUFu%)xdw)>#*_dA=C7xTpblWq z+cUO}TP)MA%vM5atPX>$N;hq_GNI|LgLR<5D}w@}G+ki<9Nd3pHqIiL=>G)-Cu*37 zhcVLzOPFY(A_hhv_o6k$dM;06$uyifT$5HTOdtdpDrRh2ip9d@^IX{jKJ=*rJoA|} zm3S^-|I79xmq-|S64`*DFzkDKU!?&jK!&KBL9y~bTuh;(p2Q2io literal 0 HcmV?d00001 diff --git a/test/segments/videoOneSecond2.ts b/test/segments/videoOneSecond2.ts new file mode 100644 index 0000000000000000000000000000000000000000..97401aebe6a06dca2042e1fef83cbc45caac948a GIT binary patch literal 2256 zcmdT_PiRy}7@ub~t(DO1YEObpEM9D~JMZnfs|y3W#<=N0Oc4T}9`okS?tA9_narEG zyFtPpdMF->Ab1eG3PKNQq2v^>r-Gh(GoTO=1Qlv-LXfQAY|{3Wv&kO5{pS1o`{sS$ zH_du(3|-tjgRDOh8dw8lee9o@$y+0rFUL}}k6sh%X2=DxJGZ8u4EEQrSw~|Ct&9$_ zBWn|#{q6AY`wQZpXqvNt{m@E&i2WtnL?;dTcJ0dCo-X@F(QF{{la5?atM8aEJF@|lRtCM=z@2zo}PXT%*c_!;L-ExH*2rg zXIAe`Jj3hX{qw%-RLXb~^Hd3JS8S|G#Hr%4?buGW3W}Km7!+lt!1rP>~uPXo1rh#riF45OA4uK7fq%@pyLSaQmMl<$!eH0 zk8uiVMT?fOFT5z_^VF`98o?}KQ7;pebW1hTwe1p)1r0oL@#gew5b_Mf5r7?5C)y7(hC<-&+&UerITm!=wVoCr4^Ow>@PzSK* z?Frk)EtY9lX7iymRy#o!q?@){nb367!8%ajl|cbfn$EKT4(`7)8)p$r^#1~a6E)1k z!ZRYzNu{eJVxbwo?h(9iasGdC`OT)Hz3XSvL+7kY`*Zf-h=X zCI+cu=4O@10`OMc{jXx9SDbRy!m$U}Ybt{SbxH+-7fd~r6e_?<6Lz6mo@qfch~8pMOFp2HPM0y88Ix+vlgBKA5KC>yya(8=-+UK-TB}*nHmFJ2w|g(LQilsOupY#P0k&^JK8Se$6^MgwX2f5IeHg z(TN9p{@h*=w?xxi0PMR~^F!<}(Kf!RAU7~Fe4`{vQ>wfTp) zPd&qH$KQYGI+Ze>!aP+1+Z7wD5^<`yY&*77t%6~`01m|q3$x9sGHzU%0Z(6WFwCT~ z7m1d}4k4u}hd2b3f!4BCEOt7b!u8M>Y12Zvh$V$owTq@wA<%IIeyP-9nq)Q1na4PV zvZ6&x*cV=u@_O1h;Q>DqP)$ASi;i?d}9xGI)e4?=;u%BOaLKoAfd zhh5>jCZi1zu3|}BP`iS8pwgJR5NczsL==S?aHqSceXfDwOEDz?f%z+GBB%q{^Y$s* z#x0go5w-*EfjyNXaoec`>!wmd_@d}Re(FqEfa%O zF>|s?WC27g?tbgo*cGQ-wQ%gg@tV#+piZek@RI3=l0pSoY4Q&LS(jGMfFjdE(lQPu zd@CIS_OeFb;rgX8L(l|Ug84Jkg!*u4#IL`gvB8&5|6F|h$J^%ZL}rp3Cjv_Z?o39s9oLcJTW?eG9fz`^OvKW9qlOXfXUQ bzV4dCcR!j}YF;4a%Mg(C&XsWAgrhC&LmFkK! z6GTSvuz1+Zf`CVhDQ>V5uR^?R?I z_1qXbvwj>|{~$E52FUu*KQNcKcAq;JOVQqYNvNwK7sT#dn|V6eT2Hf%#t>Q=9b!k; zI-2}z>M6W-hqeSSt}lVFui(?kS&ZVEAH82|!@}a+(P00QS6n z(zbDnW!jb5LMV;Z=OByHOhIx1x zGi|Vhi54niU<7h6T2id%@-&uA!LV%%S#-^oMEKEMnl|A4?pE|%ZpGi}R z=K}WqUdS?&C{$utUq^)PKzpE1rAXX%Dna{MD8YR}^dLTUrpXu9jl%@w85fD*OPZF6 zL8_R!StYUnycKu|)J@%@tvkMI8b{OIbPSvo;(J=>T& zyXUu`Pvl28(DL#xw~ip|C#!m~(X8ik+jzgj_Sm-jJ-3bb`}FT)Xy?I^>))g6Hyt!+ c{uf{VHHWT1XMFwFH2#=~8pV$fCj0gJCnPlU;{X5v literal 0 HcmV?d00001 diff --git a/test/sync-controller.test.js b/test/sync-controller.test.js index 89f1d2cbe..ad15b7dc1 100644 --- a/test/sync-controller.test.js +++ b/test/sync-controller.test.js @@ -43,11 +43,11 @@ QUnit.test('returns correct sync point for ProgramDateTime strategy', function(a syncPoint = strategy.run(this.syncController, playlist, duration, timeline); - assert.equal(syncPoint, null, 'no syncpoint when datetimeToDisplayTime not set'); + assert.equal(syncPoint, null, 'no syncpoint when no date time to display time mapping'); playlist.segments[0].dateTimeObject = datetime; - this.syncController.setDateTimeMapping(playlist); + this.syncController.setDateTimeMappingForStart(playlist); const newPlaylist = playlistWithDuration(40); @@ -74,13 +74,13 @@ QUnit.test('ProgramDateTime strategy finds nearest segment for sync', function(a syncPoint = strategy.run(this.syncController, playlist, duration, timeline, 170); - assert.equal(syncPoint, null, 'no syncpoint when datetimeToDisplayTime not set'); + assert.equal(syncPoint, null, 'no syncpoint when no date time to display time mapping'); playlist.segments.forEach((segment, index) => { segment.dateTimeObject = new Date(2012, 11, 12, 12, 12, 12 + (index * 10)); }); - this.syncController.setDateTimeMapping(playlist); + this.syncController.setDateTimeMappingForStart(playlist); const newPlaylist = playlistWithDuration(200); @@ -114,18 +114,118 @@ QUnit.test( playlist.segments[1].dateTimeObject = new Date(2012, 11, 12, 12, 12, 12); - this.syncController.setDateTimeMapping(playlist); + this.syncController.setDateTimeMappingForStart(playlist); - assert.notOk(this.syncController.datetimeToDisplayTime, 'did not set datetime mapping'); + assert.equal( + Object.keys(this.syncController.timelineToDatetimeMappings).length, + 0, + 'did not set datetime mapping' + ); playlist.segments[0].dateTimeObject = new Date(2012, 11, 12, 12, 12, 2); - this.syncController.setDateTimeMapping(playlist); + this.syncController.setDateTimeMappingForStart(playlist); - assert.ok(this.syncController.datetimeToDisplayTime, 'did set date time mapping'); + assert.equal( + Object.keys(this.syncController.timelineToDatetimeMappings).length, + 1, + 'did set datetime mapping' + ); } ); +QUnit.test('uses separate date time to display time mapping for each timeline', function(assert) { + const playlist = playlistWithDuration(40, { discontinuityStarts: [1, 3] }); + + playlist.segments[0].dateTimeObject = new Date(2020, 1, 1, 1, 1, 1); + // 20 seconds later (10 more than default) + playlist.segments[1].dateTimeObject = new Date(2020, 1, 1, 1, 1, 21); + playlist.segments[2].dateTimeObject = new Date(2020, 1, 1, 1, 1, 31); + // 30 seconds later (20 more than default) + playlist.segments[3].dateTimeObject = new Date(2020, 1, 1, 1, 2, 1); + + // after this call, the initial playlist mapping will be provided + this.syncController.setDateTimeMappingForStart(playlist); + + // since this segment does not have a discontinuity, there should be no additional or + // changed mappings from the initial + this.syncController.saveSegmentTimingInfo({ + segmentInfo: { + playlist, + segment: playlist.segments[0], + timeline: 0, + mediaIndex: 0 + }, + shouldSaveTimelineMapping: true + }); + + assert.deepEqual( + this.syncController.timelineToDatetimeMappings, + { + 0: -(playlist.segments[0].dateTimeObject.getTime() / 1000) + }, + 'has correct mapping for timeline 0' + ); + + this.syncController.saveSegmentTimingInfo({ + segmentInfo: { + playlist, + segment: playlist.segments[1], + timeline: 1, + mediaIndex: 1 + }, + shouldSaveTimelineMapping: true + }); + + assert.deepEqual( + this.syncController.timelineToDatetimeMappings, + { + 0: -(playlist.segments[0].dateTimeObject.getTime() / 1000), + 1: -(playlist.segments[1].dateTimeObject.getTime() / 1000) + }, + 'has correct mapping for timelines 0 and 1' + ); + + this.syncController.saveSegmentTimingInfo({ + segmentInfo: { + playlist, + segment: playlist.segments[2], + timeline: 1, + mediaIndex: 2 + }, + shouldSaveTimelineMapping: true + }); + + assert.deepEqual( + this.syncController.timelineToDatetimeMappings, + { + 0: -(playlist.segments[0].dateTimeObject.getTime() / 1000), + 1: -(playlist.segments[1].dateTimeObject.getTime() / 1000) + }, + 'does not add a new timeline mapping when no disco' + ); + + this.syncController.saveSegmentTimingInfo({ + segmentInfo: { + playlist, + segment: playlist.segments[3], + timeline: 2, + mediaIndex: 3 + }, + shouldSaveTimelineMapping: true + }); + + assert.deepEqual( + this.syncController.timelineToDatetimeMappings, + { + 0: -(playlist.segments[0].dateTimeObject.getTime() / 1000), + 1: -(playlist.segments[1].dateTimeObject.getTime() / 1000), + 2: -(playlist.segments[3].dateTimeObject.getTime() / 1000) + }, + 'has correct mappings for timelines 0, 1, and 2' + ); +}); + QUnit.test('returns correct sync point for Segment strategy', function(assert) { const strategy = getStrategy('Segment'); const playlist = { diff --git a/test/test-helpers.js b/test/test-helpers.js index 67ec13cbd..a88e42b74 100644 --- a/test/test-helpers.js +++ b/test/test-helpers.js @@ -419,8 +419,10 @@ export const standardXHRResponse = function(request, data) { }; export const playlistWithDuration = function(time, conf) { + const targetDuration = conf && typeof conf.targetDuration === 'number' ? + conf.targetDuration : 10; const result = { - targetDuration: 10, + targetDuration, mediaSequence: conf && conf.mediaSequence ? conf.mediaSequence : 0, discontinuityStarts: conf && conf.discontinuityStarts ? conf.discontinuityStarts : [], segments: [], @@ -433,8 +435,8 @@ export const playlistWithDuration = function(time, conf) { result.id = result.uri; - const count = Math.floor(time / 10); - const remainder = time % 10; + const count = Math.floor(time / targetDuration); + const remainder = time % targetDuration; let i; const isEncrypted = conf && conf.isEncrypted; const extension = conf && conf.extension ? conf.extension : '.ts'; @@ -442,24 +444,33 @@ export const playlistWithDuration = function(time, conf) { let discontinuityStartsIndex = 0; for (i = 0; i < count; i++) { - if (result.discontinuityStarts && - result.discontinuityStarts[discontinuityStartsIndex] === i) { + const isDiscontinuity = result.discontinuityStarts && + result.discontinuityStarts[discontinuityStartsIndex] === i; + + if (isDiscontinuity) { timeline++; discontinuityStartsIndex++; } - result.segments.push({ + const segment = { uri: i + extension, resolvedUri: i + extension, - duration: 10, + duration: targetDuration, timeline - }); + }; + if (isEncrypted) { - result.segments[i].key = { + segment.key = { uri: i + '-key.php', resolvedUri: i + '-key.php' }; } + + if (isDiscontinuity) { + segment.discontinuity = true; + } + + result.segments.push(segment); } if (remainder) { result.segments.push({