diff --git a/src/playlist.js b/src/playlist.js index 51fefb481..a1a053276 100644 --- a/src/playlist.js +++ b/src/playlist.js @@ -18,13 +18,13 @@ const {createTimeRange} = videojs; * * @return {Array} The part/segment list. */ -const getPartsAndSegments = (playlist) => (playlist.segments || []).reduce((acc, segment, si) => { +export const getPartsAndSegments = (playlist) => (playlist.segments || []).reduce((acc, segment, si) => { if (segment.parts) { segment.parts.forEach(function(part, pi) { - acc.push({duration: part.duration, segmentIndex: si, partIndex: pi}); + acc.push({duration: part.duration, segmentIndex: si, partIndex: pi, part, segment}); }); } else { - acc.push({duration: segment.duration, segmentIndex: si, partIndex: null}); + acc.push({duration: segment.duration, segmentIndex: si, partIndex: null, segment, part: null}); } return acc; }, []); @@ -261,12 +261,13 @@ export const duration = function(playlist, endSequence, expired) { * playlist in which case, the targetDuration of the playlist is used * to approximate the durations of the segments * - * @param {Object} playlist a media playlist object - * @param {number} startIndex - * @param {number} endIndex + * @param {Array} options.durationList list to iterate over for durations. + * @param {number} options.defaultDuration duration to use for elements before or after the durationList + * @param {number} options.startIndex partsAndSegments index to start + * @param {number} options.endIndex partsAndSegments index to end. * @return {number} the number of seconds between startIndex and endIndex */ -export const sumDurations = function(playlist, startIndex, endIndex) { +export const sumDurations = function({defaultDuration, durationList, startIndex, endIndex}) { let durations = 0; if (startIndex > endIndex) { @@ -275,13 +276,13 @@ export const sumDurations = function(playlist, startIndex, endIndex) { if (startIndex < 0) { for (let i = startIndex; i < Math.min(0, endIndex); i++) { - durations += playlist.targetDuration; + durations += defaultDuration; } startIndex = 0; } for (let i = startIndex; i < endIndex; i++) { - durations += playlist.segments[i].duration; + durations += durationList[i].duration; } return durations; @@ -367,38 +368,64 @@ export const seekable = function(playlist, expired, liveEdgePadding) { * Determine the index and estimated starting time of the segment that * contains a specified playback position in a media playlist. * - * @param {Object} playlist the media playlist to query - * @param {number} currentTime The number of seconds since the earliest + * @param {Object} options.playlist the media playlist to query + * @param {number} options.currentTime The number of seconds since the earliest * possible position to determine the containing segment for - * @param {number} startIndex - * @param {number} startTime - * @return {Object} + * @param {number} options.startTime the time when the segment/part starts + * @param {number} options.startingSegmentIndex the segment index to start looking at. + * @param {number?} [options.startingPartIndex] the part index to look at within the segment. + * + * @return {Object} an object with partIndex, segmentIndex, and startTime. */ -export const getMediaInfoForTime = function( +export const getMediaInfoForTime = function({ playlist, currentTime, - startIndex, + startingSegmentIndex, + startingPartIndex, startTime -) { +}) { - const partsAndSegments = getPartsAndSegments(playlist); let time = currentTime - startTime; + const partsAndSegments = getPartsAndSegments(playlist); + + let startIndex = 0; + + for (let i = 0; i < partsAndSegments.length; i++) { + const partAndSegment = partsAndSegments[i]; + + if (startingSegmentIndex !== partAndSegment.segmentIndex) { + continue; + } + + // skip this if part index does not match. + if (typeof startingPartIndex === 'number' && typeof partAndSegment.partIndex === 'number' && startingPartIndex !== partAndSegment.partIndex) { + continue; + } + + startIndex = i; + break; + } if (time < 0) { // Walk backward from startIndex in the playlist, adding durations // until we find a segment that contains `time` and return it if (startIndex > 0) { for (let i = startIndex - 1; i >= 0; i--) { - const segment = partsAndSegments[i]; + const partAndSegment = partsAndSegments[i]; - time += segment.duration; + time += partAndSegment.duration; // TODO: consider not using TIME_FUDGE_FACTOR at all here if ((time + TIME_FUDGE_FACTOR) > 0) { return { - mediaIndex: segment.segmentIndex, - startTime: startTime - sumDurations(playlist, startIndex, segment.segmentIndex), - partIndex: segment.partIndex + partIndex: partAndSegment.partIndex, + segmentIndex: partAndSegment.segmentIndex, + startTime: startTime - sumDurations({ + defaultDuration: playlist.targetDuration, + durationList: partsAndSegments, + startIndex, + endIndex: i + }) }; } } @@ -407,8 +434,8 @@ export const getMediaInfoForTime = function( // We were unable to find a good segment within the playlist // so select the first segment return { - mediaIndex: partsAndSegments[0] && partsAndSegments[0].segmentIndex || 0, partIndex: partsAndSegments[0] && partsAndSegments[0].partIndex || null, + segmentIndex: partsAndSegments[0] && partsAndSegments[0].segmentIndex || 0, startTime: currentTime }; } @@ -421,7 +448,8 @@ export const getMediaInfoForTime = function( time -= playlist.targetDuration; if (time < 0) { return { - mediaIndex: partsAndSegments[0].segmentIndex, + partIndex: partsAndSegments[0] && partsAndSegments[0].partIndex || null, + segmentIndex: partsAndSegments[0] && partsAndSegments[0].segmentIndex || 0, startTime: currentTime }; } @@ -432,23 +460,28 @@ export const getMediaInfoForTime = function( // Walk forward from startIndex in the playlist, subtracting durations // until we find a segment that contains `time` and return it for (let i = startIndex; i < partsAndSegments.length; i++) { - const partSegment = partsAndSegments[i]; + const partAndSegment = partsAndSegments[i]; - time -= partSegment.duration; + time -= partAndSegment.duration; // TODO: consider not using TIME_FUDGE_FACTOR at all here if ((time - TIME_FUDGE_FACTOR) < 0) { return { - mediaIndex: partSegment.segmentIndex, - startTime: startTime + sumDurations(playlist, startIndex, partSegment.segmentIndex), - partIndex: partSegment.partIndex + partIndex: partAndSegment.partIndex, + segmentIndex: partAndSegment.segmentIndex, + startTime: startTime + sumDurations({ + defaultDuration: playlist.targetDuration, + durationList: partsAndSegments, + startIndex, + endIndex: i + }) }; } } // We are out of possible candidates so load the last one... return { - mediaIndex: partsAndSegments[partsAndSegments.length - 1].segmentIndex, + segmentIndex: partsAndSegments[partsAndSegments.length - 1].segmentIndex, partIndex: partsAndSegments[partsAndSegments.length - 1].partIndex, startTime: currentTime }; diff --git a/src/segment-loader.js b/src/segment-loader.js index f24aa2f56..926ce9465 100644 --- a/src/segment-loader.js +++ b/src/segment-loader.js @@ -1400,17 +1400,18 @@ export default class SegmentLoader extends videojs.EventTarget { } } else { // Find the segment containing the end of the buffer or current time. - const mediaSourceInfo = Playlist.getMediaInfoForTime( - this.playlist_, - this.fetchAtBuffer_ ? bufferedEnd : this.currentTime_(), - this.syncPoint_.segmentIndex, - this.syncPoint_.time - ); + const {segmentIndex, startTime, partIndex} = Playlist.getMediaInfoForTime({ + playlist: this.playlist_, + currentTime: this.fetchAtBuffer_ ? bufferedEnd : this.currentTime_(), + startingPartIndex: this.syncPoint_.partIndex, + startingSegmentIndex: this.syncPoint_.segmentIndex, + startTime: this.syncPoint_.time + }); next.getMediaInfoForTime = this.fetchAtBuffer_ ? 'bufferedEnd' : 'currentTime'; - next.mediaIndex = mediaSourceInfo.mediaIndex; - next.startOfSegment = mediaSourceInfo.startTime; - next.partIndex = mediaSourceInfo.partIndex; + next.mediaIndex = segmentIndex; + next.startOfSegment = startTime; + next.partIndex = partIndex; } const nextSegment = segments[next.mediaIndex]; @@ -1479,7 +1480,7 @@ export default class SegmentLoader extends videojs.EventTarget { // The timeline that the segment is in timeline: segment.timeline, // The expected duration of the segment in seconds - duration: segment.duration, + duration: part && part.duration || segment.duration, // retain the segment in case the playlist updates while doing an async process segment, part, diff --git a/src/sync-controller.js b/src/sync-controller.js index 64d5f625e..51d074063 100644 --- a/src/sync-controller.js +++ b/src/sync-controller.js @@ -2,16 +2,10 @@ * @file sync-controller.js */ -import {sumDurations} from './playlist'; +import {sumDurations, getPartsAndSegments} from './playlist'; import videojs from 'video.js'; import logger from './util/logger'; -const getSegmentIndex = (i, playlist, currentTime = 0) => { - const segments = playlist.segments; - - return (playlist.endList || currentTime === 0) ? i : segments.length - (i + 1); -}; - export const syncPointStrategies = [ // Stategy "VOD": Handle the VOD-case where the sync-point is *always* // the equivalence display-time 0 === segment-index 0 @@ -21,7 +15,8 @@ export const syncPointStrategies = [ if (duration !== Infinity) { const syncPoint = { time: 0, - segmentIndex: 0 + segmentIndex: 0, + partIndex: null }; return syncPoint; @@ -37,15 +32,17 @@ export const syncPointStrategies = [ return null; } - const segments = playlist.segments || []; let syncPoint = null; let lastDistance = null; + const partsAndSegments = getPartsAndSegments(playlist); currentTime = currentTime || 0; - - for (let i = 0; i < segments.length; i++) { - const segmentIndex = getSegmentIndex(i, playlist, currentTime); - const segment = segments[segmentIndex]; + for (let i = 0; i < partsAndSegments.length; i++) { + // start from the end and loop backwards for live + // or start from the front and loop forwards for non-live + const index = (playlist.endList || currentTime === 0) ? i : partsAndSegments.length - (i + 1); + const partAndSegment = partsAndSegments[index]; + const segment = partAndSegment.segment; const datetimeMapping = syncController.timelineToDatetimeMappings[segment.timeline]; @@ -55,8 +52,15 @@ export const syncPointStrategies = [ if (segment.dateTimeObject) { const segmentTime = segment.dateTimeObject.getTime() / 1000; - const segmentStart = segmentTime + datetimeMapping; - const distance = Math.abs(currentTime - segmentStart); + let start = segmentTime + datetimeMapping; + + // take part duration into account. + if (segment.parts && typeof partAndSegment.partIndex === 'number') { + for (let z = 0; z < partAndSegment.partIndex; z++) { + start += segment.parts[z].duration; + } + } + const distance = Math.abs(currentTime - start); // Once the distance begins to increase, or if distance is 0, we have passed // currentTime and can stop looking for better candidates @@ -66,8 +70,9 @@ export const syncPointStrategies = [ lastDistance = distance; syncPoint = { - time: segmentStart, - segmentIndex + time: start, + segmentIndex: partAndSegment.segmentIndex, + partIndex: partAndSegment.partIndex }; } } @@ -79,19 +84,22 @@ export const syncPointStrategies = [ { name: 'Segment', run: (syncController, playlist, duration, currentTimeline, currentTime) => { - const segments = playlist.segments || []; let syncPoint = null; let lastDistance = null; currentTime = currentTime || 0; + const partsAndSegments = getPartsAndSegments(playlist); - for (let i = 0; i < segments.length; i++) { - const segmentIndex = getSegmentIndex(i, playlist, currentTime); - const segment = segments[segmentIndex]; + for (let i = 0; i < partsAndSegments.length; i++) { + // start from the end and loop backwards for live + // or start from the front and loop forwards for non-live + const index = (playlist.endList || currentTime === 0) ? i : partsAndSegments.length - (i + 1); + const partAndSegment = partsAndSegments[index]; + const segment = partAndSegment.segment; + const start = partAndSegment.part && partAndSegment.part.start || segment && segment.start; - if (segment.timeline === currentTimeline && - typeof segment.start !== 'undefined') { - const distance = Math.abs(currentTime - segment.start); + if (segment.timeline === currentTimeline && typeof start !== 'undefined') { + const distance = Math.abs(currentTime - start); // Once the distance begins to increase, we have passed // currentTime and can stop looking for better candidates @@ -102,8 +110,9 @@ export const syncPointStrategies = [ if (!syncPoint || lastDistance === null || lastDistance >= distance) { lastDistance = distance; syncPoint = { - time: segment.start, - segmentIndex + time: start, + segmentIndex: partAndSegment.segmentIndex, + partIndex: partAndSegment.partIndex }; } @@ -142,7 +151,8 @@ export const syncPointStrategies = [ lastDistance = distance; syncPoint = { time: discontinuitySync.time, - segmentIndex + segmentIndex, + partIndex: null }; } } @@ -159,7 +169,8 @@ export const syncPointStrategies = [ if (playlist.syncInfo) { const syncPoint = { time: playlist.syncInfo.time, - segmentIndex: playlist.syncInfo.mediaSequence - playlist.mediaSequence + segmentIndex: playlist.syncInfo.mediaSequence - playlist.mediaSequence, + partIndex: null }; return syncPoint; @@ -255,7 +266,12 @@ export default class SyncController extends videojs.EventTarget { syncPoint.time *= -1; } - return Math.abs(syncPoint.time + sumDurations(playlist, syncPoint.segmentIndex, 0)); + return Math.abs(syncPoint.time + sumDurations({ + defaultDuration: playlist.targetDuration, + durationList: playlist.segments, + startIndex: syncPoint.segmentIndex, + endIndex: 0 + })); } /** @@ -330,7 +346,9 @@ export default class SyncController extends videojs.EventTarget { this.logger_(`syncPoint for [${target.key}: ${target.value}] chosen with strategy` + ` [${bestStrategy}]: [time:${bestSyncPoint.time},` + - ` segmentIndex:${bestSyncPoint.segmentIndex}]`); + ` segmentIndex:${bestSyncPoint.segmentIndex}` + + (typeof bestSyncPoint.partIndex === 'number' ? `,partIndex:${bestSyncPoint.partIndex}` : '') + + ']'); return bestSyncPoint; } @@ -457,8 +475,12 @@ export default class SyncController extends videojs.EventTarget { * Returns false if segment time mapping could not be calculated */ calculateSegmentTimeMapping_(segmentInfo, timingInfo, shouldSaveTimelineMapping) { + // TODO: remove side effects const segment = segmentInfo.segment; + const part = segmentInfo.part; let mappingObj = this.timelines[segmentInfo.timeline]; + let start; + let end; if (typeof segmentInfo.timestampOffset === 'number') { mappingObj = { @@ -473,15 +495,31 @@ export default class SyncController extends videojs.EventTarget { `[time: ${mappingObj.time}] [mapping: ${mappingObj.mapping}]`); } - segment.start = segmentInfo.startOfSegment; - segment.end = timingInfo.end + mappingObj.mapping; + start = segmentInfo.startOfSegment; + end = timingInfo.end + mappingObj.mapping; + } else if (mappingObj) { - segment.start = timingInfo.start + mappingObj.mapping; - segment.end = timingInfo.end + mappingObj.mapping; + start = timingInfo.start + mappingObj.mapping; + end = timingInfo.end + mappingObj.mapping; } else { return false; } + if (part) { + part.start = start; + part.end = end; + } + + // If we don't have a segment start yet or the start value we got + // is less than our current segment.start value, save a new start value. + // We have to do this because parts will have segment timing info saved + // multiple times and we want segment start to be the earliest part start + // value for that segment. + if (!segment.start || start < segment.start) { + segment.start = start; + } + segment.end = end; + return true; } @@ -519,17 +557,19 @@ export default class SyncController extends videojs.EventTarget { let time; if (mediaIndexDiff < 0) { - time = segment.start - sumDurations( - playlist, - segmentInfo.mediaIndex, - segmentIndex - ); + time = segment.start - sumDurations({ + defaultDuration: playlist.targetDuration, + durationList: playlist.segments, + startIndex: segmentInfo.mediaIndex, + endIndex: segmentIndex + }); } else { - time = segment.end + sumDurations( - playlist, - segmentInfo.mediaIndex + 1, - segmentIndex - ); + time = segment.end + sumDurations({ + defaultDuration: playlist.targetDuration, + durationList: playlist.segments, + startIndex: segmentInfo.mediaIndex + 1, + endIndex: segmentIndex + }); } this.discontinuities[discontinuity] = { diff --git a/test/playlist.test.js b/test/playlist.test.js index 66ce0414c..bcb95ffb2 100644 --- a/test/playlist.test.js +++ b/test/playlist.test.js @@ -5,6 +5,7 @@ import xhrFactory from '../src/xhr'; import { useFakeEnvironment } from './test-helpers'; // needed for plugin registration import '../src/videojs-http-streaming'; +import {mergeOptions as merge} from 'video.js'; QUnit.module('Playlist', function() { QUnit.module('Duration'); @@ -964,6 +965,11 @@ QUnit.module('Playlist', function() { this.fakeVhs = { xhr: xhrFactory() }; + + this.getMediaInfoForTime = (overrides) => { + return Playlist.getMediaInfoForTime(merge(this.defaults, overrides)); + }; + }, afterEach() { this.env.restore(); @@ -992,24 +998,41 @@ QUnit.module('Playlist', function() { const media = loader.media(); - assert.equal( - Playlist.getMediaInfoForTime(media, -1, 0, 0).mediaIndex, 0, + this.defaults = { + playlist: media, + currentTime: -1, + startingSegmentIndex: 0, + startingPartIndex: null, + startTime: 0 + }; + + assert.deepEqual( + this.getMediaInfoForTime({currentTime: -1}), + {partIndex: null, segmentIndex: 0, startTime: -1}, 'the index is never less than zero' ); - assert.equal( - Playlist.getMediaInfoForTime(media, 0, 0, 0).mediaIndex, 0, + + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 0}), + {partIndex: null, segmentIndex: 0, startTime: 0}, 'time zero is index zero' ); - assert.equal( - Playlist.getMediaInfoForTime(media, 3, 0, 0).mediaIndex, 0, + + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 3}), + {partIndex: null, segmentIndex: 0, startTime: 0}, 'time three is index zero' ); - assert.equal( - Playlist.getMediaInfoForTime(media, 10, 0, 0).mediaIndex, 2, + + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 10}), + {partIndex: null, segmentIndex: 2, startTime: 9}, 'time 10 is index 2' ); - assert.equal( - Playlist.getMediaInfoForTime(media, 22, 0, 0).mediaIndex, 2, + + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 22}), + {partIndex: null, segmentIndex: 2, startTime: 22}, 'time greater than the length is index 2' ); } @@ -1040,25 +1063,80 @@ QUnit.module('Playlist', function() { ); const media = loader.media(); - const fn = Playlist.getMediaInfoForTime; + + this.defaults = { + playlist: media, + currentTime: 2.1, + startingSegmentIndex: 0, + startingPartIndex: null, + startTime: 0 + }; // 1 segment away - assert.equal(fn(media, 2.1, 0, 0).mediaIndex, 1, '1 away 2 is correct'); - assert.equal(fn(media, 4.1, 1, 2).mediaIndex, 2, '1 away 3 is correct '); - assert.equal(fn(media, 6.1, 2, 4).mediaIndex, 3, '1 away 4 is correct'); - assert.equal(fn(media, 8.1, 3, 6).mediaIndex, 4, '1 away 5 is correct'); - assert.equal(fn(media, 10.1, 4, 8).mediaIndex, 5, '1 away 6 is correct'); - - // 2 segment away - assert.equal(fn(media, 4.1, 0, 0).mediaIndex, 2, '2 away 3 is correct '); - assert.equal(fn(media, 6.1, 1, 2).mediaIndex, 3, '2 away 4 is correct'); - assert.equal(fn(media, 8.1, 2, 4).mediaIndex, 4, '2 away 5 is correct'); - assert.equal(fn(media, 10.1, 3, 6).mediaIndex, 5, '2 away 6 is correct'); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 2.1}), + {segmentIndex: 1, startTime: 2, partIndex: null}, + '1 away 2 is correct' + ); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 4.1, startingSegmentIndex: 1, startTime: 2}), + {segmentIndex: 2, startTime: 4, partIndex: null}, + '1 away 3 is correct' + ); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 6.1, startingSegmentIndex: 2, startTime: 4}), + {segmentIndex: 3, startTime: 6, partIndex: null}, + '1 away 4 is correct' + ); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 8.1, startingSegmentIndex: 3, startTime: 6}), + {segmentIndex: 4, startTime: 8, partIndex: null}, + '1 away 5 is correct' + ); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 10.1, startingSegmentIndex: 4, startTime: 8}), + {segmentIndex: 5, startTime: 10, partIndex: null}, + '1 away 6 is correct' + ); + + // 2 segments away + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 4.1, startingSegmentIndex: 0, startTime: 0}), + {segmentIndex: 2, startTime: 4, partIndex: null}, + '2 away 3 is correct' + ); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 6.1, startingSegmentIndex: 1, startTime: 2}), + {segmentIndex: 3, startTime: 6, partIndex: null}, + '2 away 4 is correct' + ); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 8.1, startingSegmentIndex: 2, startTime: 4}), + {segmentIndex: 4, startTime: 8, partIndex: null}, + '2 away 5 is correct' + ); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 10.1, startingSegmentIndex: 3, startTime: 6}), + {segmentIndex: 5, startTime: 10, partIndex: null}, + '2 away 6 is correct' + ); // 3 segments away - assert.equal(fn(media, 6.1, 0, 0).mediaIndex, 3, '3 away 4 is correct'); - assert.equal(fn(media, 8.1, 1, 2).mediaIndex, 4, '3 away 5 is correct'); - assert.equal(fn(media, 10.1, 2, 4).mediaIndex, 5, '3 away 6 is correct'); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 6.1, startingSegmentIndex: 0, startTime: 0}), + {segmentIndex: 3, startTime: 6, partIndex: null}, + '3 away 4 is correct' + ); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 8.1, startingSegmentIndex: 1, startTime: 2}), + {segmentIndex: 4, startTime: 8, partIndex: null}, + '3 away 5 is correct' + ); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 10.1, startingSegmentIndex: 2, startTime: 4}), + {segmentIndex: 5, startTime: 10, partIndex: null}, + '3 away 6 is correct' + ); }); QUnit.test('rounding up works', function(assert) { @@ -1086,25 +1164,81 @@ QUnit.module('Playlist', function() { ); const media = loader.media(); - const fn = Playlist.getMediaInfoForTime; + + this.defaults = { + playlist: media, + currentTime: 2.1, + startingSegmentIndex: 0, + startingPartIndex: null, + startTime: 0 + }; // 1 segment away - assert.equal(fn(media, 0, 1, 2).mediaIndex, 0, '1 away 1 is correct'); - assert.equal(fn(media, 2.1, 2, 4).mediaIndex, 1, '1 away 2 is correct'); - assert.equal(fn(media, 4.1, 3, 6).mediaIndex, 2, '1 away 3 is correct'); - assert.equal(fn(media, 6.1, 4, 8).mediaIndex, 3, '1 away 4 is correct'); - assert.equal(fn(media, 8.1, 5, 10).mediaIndex, 4, '1 away 5 is correct'); - - // 2 segment away - assert.equal(fn(media, 0, 2, 4).mediaIndex, 0, '2 away 1 is correct'); - assert.equal(fn(media, 2.1, 3, 6).mediaIndex, 1, '2 away 2 is correct'); - assert.equal(fn(media, 4.1, 4, 8).mediaIndex, 2, '2 away 3 is correct'); - assert.equal(fn(media, 6.1, 5, 10).mediaIndex, 3, '2 away 4 is correct'); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 0, startingSegmentIndex: 1, startTime: 2}), + {segmentIndex: 0, startTime: 0, partIndex: null}, + '1 away 1 is correct' + ); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 2.1, startingSegmentIndex: 2, startTime: 4}), + {segmentIndex: 1, startTime: 2, partIndex: null}, + '1 away 2 is correct' + ); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 4.1, startingSegmentIndex: 3, startTime: 6}), + {segmentIndex: 2, startTime: 4, partIndex: null}, + '1 away 3 is correct' + ); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 6.1, startingSegmentIndex: 4, startTime: 8}), + {segmentIndex: 3, startTime: 6, partIndex: null}, + '1 away 4 is correct' + ); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 8.1, startingSegmentIndex: 5, startTime: 10}), + {segmentIndex: 4, startTime: 8, partIndex: null}, + '1 away 5 is correct' + ); + + // 2 segments away + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 0, startingSegmentIndex: 2, startTime: 4}), + {segmentIndex: 0, startTime: 0, partIndex: null}, + '2 away 1 is correct' + ); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 2.1, startingSegmentIndex: 3, startTime: 6}), + {segmentIndex: 1, startTime: 2, partIndex: null}, + '2 away 2 is correct' + ); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 4.1, startingSegmentIndex: 4, startTime: 8}), + {segmentIndex: 2, startTime: 4, partIndex: null}, + '2 away 3 is correct' + ); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 6.1, startingSegmentIndex: 5, startTime: 10}), + {segmentIndex: 3, startTime: 6, partIndex: null}, + '2 away 4 is correct' + ); // 3 segments away - assert.equal(fn(media, 0, 3, 6).mediaIndex, 0, '3 away 1 is correct'); - assert.equal(fn(media, 2.1, 4, 8).mediaIndex, 1, '3 away 2 is correct'); - assert.equal(fn(media, 4.1, 5, 10).mediaIndex, 2, '3 away 3 is correct'); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 0, startingSegmentIndex: 3, startTime: 6}), + {segmentIndex: 0, startTime: 0, partIndex: null}, + '3 away 1 is correct' + ); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 2.1, startingSegmentIndex: 4, startTime: 8}), + {segmentIndex: 1, startTime: 2, partIndex: null}, + '3 away 2 is correct' + ); + + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 4.1, startingSegmentIndex: 5, startTime: 10}), + {segmentIndex: 2, startTime: 4, partIndex: null}, + '3 away 3 is correct' + ); }); QUnit.test( @@ -1127,16 +1261,28 @@ QUnit.module('Playlist', function() { const media = loader.media(); - assert.equal( - Playlist.getMediaInfoForTime(media, 4, 0, 0).mediaIndex, 0, + this.defaults = { + playlist: media, + currentTime: 0, + startingSegmentIndex: 0, + startingPartIndex: null, + startTime: 0 + }; + + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 4}), + {segmentIndex: 0, startTime: 0, partIndex: null}, 'rounds down exact matches' ); - assert.equal( - Playlist.getMediaInfoForTime(media, 3.7, 0, 0).mediaIndex, 0, + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 3.7}), + {segmentIndex: 0, startTime: 0, partIndex: null}, 'rounds down' ); - assert.equal( - Playlist.getMediaInfoForTime(media, 4.5, 0, 0).mediaIndex, 1, + + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 4.5}), + {segmentIndex: 1, startTime: 4, partIndex: null}, 'rounds up at 0.5' ); } @@ -1161,65 +1307,117 @@ QUnit.module('Playlist', function() { const media = loader.media(); - assert.equal( - Playlist.getMediaInfoForTime(media, 45, 0, 150).mediaIndex, - 0, + this.defaults = { + playlist: media, + currentTime: 0, + startingSegmentIndex: 0, + startingPartIndex: null, + startTime: 0 + }; + + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 45, startTime: 150}), + {segmentIndex: 0, startTime: 45, partIndex: null}, 'expired content returns 0 for earliest segment available' ); - assert.equal( - Playlist.getMediaInfoForTime(media, 75, 0, 150).mediaIndex, - 0, + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 75, startTime: 150}), + {segmentIndex: 0, startTime: 75, partIndex: null}, 'expired content returns 0 for earliest segment available' ); - assert.equal( - Playlist.getMediaInfoForTime(media, 0, 0, 150).mediaIndex, - 0, + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 0, startTime: 150}), + {segmentIndex: 0, startTime: 0, partIndex: null}, 'time of 0 with no expired time returns first segment' ); - assert.equal( - Playlist.getMediaInfoForTime(media, 50 + 100, 0, 150).mediaIndex, - 0, + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 150, startTime: 150}), + {segmentIndex: 0, startTime: 150, partIndex: null}, 'calculates the earliest available position' ); - assert.equal( - Playlist.getMediaInfoForTime(media, 50 + 100 + 2, 0, 150).mediaIndex, - 0, + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 152, startTime: 150}), + {segmentIndex: 0, startTime: 150, partIndex: null}, 'calculates within the first segment' ); - assert.equal( - Playlist.getMediaInfoForTime(media, 50 + 100 + 2, 0, 150).mediaIndex, - 0, - 'calculates within the first segment' - ); - assert.equal( - Playlist.getMediaInfoForTime(media, 50 + 100 + 4, 0, 150).mediaIndex, - 0, + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 154, startTime: 150}), + {segmentIndex: 0, startTime: 150, partIndex: null}, 'calculates earlier segment on exact boundary match' ); - assert.equal( - Playlist.getMediaInfoForTime(media, 50 + 100 + 4.5, 0, 150).mediaIndex, - 1, + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 154.5, startTime: 150}), + {segmentIndex: 1, startTime: 154, partIndex: null}, 'calculates within the second segment' ); - assert.equal( - Playlist.getMediaInfoForTime(media, 50 + 100 + 6, 0, 150).mediaIndex, - 1, + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 156, startTime: 150}), + {segmentIndex: 1, startTime: 154, partIndex: null}, 'calculates within the second segment' ); - assert.equal( - Playlist.getMediaInfoForTime(media, 159, 0, 150).mediaIndex, - 1, + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 159, startTime: 150}), + {segmentIndex: 1, startTime: 154, partIndex: null}, 'returns last segment when time is equal to end of last segment' ); - assert.equal( - Playlist.getMediaInfoForTime(media, 160, 0, 150).mediaIndex, - 1, + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 160, startTime: 150}), + {segmentIndex: 1, startTime: 160, partIndex: null}, 'returns last segment when time is past end of last segment' ); } ); + QUnit.test('can return a partIndex', function(assert) { + this.fakeVhs.options_ = {experimentalLLHLS: true}; + const loader = new PlaylistLoader('media.m3u8', this.fakeVhs); + + loader.load(); + + this.requests.shift().respond( + 200, null, + '#EXTM3U\n' + + '#EXT-X-MEDIA-SEQUENCE:1001\n' + + '#EXTINF:4,\n' + + '1001.ts\n' + + '#EXTINF:5,\n' + + '1002.ts\n' + + '#EXT-X-PART:URI="1003.part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="1003.part2.ts",DURATION=1\n' + + '#EXT-X-PART:URI="1003.part3.ts",DURATION=1\n' + + '#EXT-X-PRELOAD-HINT:TYPE="PART",URI="1003.part4.ts"\n' + ); + + const media = loader.media(); + + this.defaults = { + playlist: media, + currentTime: 0, + startingSegmentIndex: 0, + startingPartIndex: null, + startTime: 0 + }; + + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 10, startTime: 0}), + {segmentIndex: 2, startTime: 9, partIndex: 0}, + 'returns expected part/segment' + ); + + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 11, startTime: 0}), + {segmentIndex: 2, startTime: 10, partIndex: 1}, + 'returns expected part/segment' + ); + + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 11, segmentIndex: -15}), + {segmentIndex: 2, startTime: 10, partIndex: 1}, + 'returns expected part/segment' + ); + }); + QUnit.test('liveEdgeDelay works as expected', function(assert) { const media = { endList: true, diff --git a/test/sync-controller.test.js b/test/sync-controller.test.js index ad15b7dc1..4650395c4 100644 --- a/test/sync-controller.test.js +++ b/test/sync-controller.test.js @@ -25,7 +25,7 @@ QUnit.test('returns correct sync point for VOD strategy', function(assert) { const vodStrategy = getStrategy('VOD'); let syncPoint = vodStrategy.run(this.syncController, playlist, duration, timeline); - assert.deepEqual(syncPoint, { time: 0, segmentIndex: 0 }, 'sync point found for vod'); + assert.deepEqual(syncPoint, { time: 0, segmentIndex: 0, partIndex: null }, 'sync point found for vod'); duration = Infinity; syncPoint = vodStrategy.run(this.syncController, playlist, duration, timeline); @@ -61,7 +61,8 @@ QUnit.test('returns correct sync point for ProgramDateTime strategy', function(a assert.deepEqual(syncPoint, { time: 10, - segmentIndex: 0 + segmentIndex: 0, + partIndex: null }, 'syncpoint found for ProgramDateTime set'); }); @@ -96,14 +97,16 @@ QUnit.test('ProgramDateTime strategy finds nearest segment for sync', function(a assert.deepEqual(syncPoint, { time: 160, - segmentIndex: 15 + segmentIndex: 15, + partIndex: null }, 'syncpoint found for ProgramDateTime set'); syncPoint = strategy.run(this.syncController, newPlaylist, duration, timeline, 0); assert.deepEqual(syncPoint, { time: 10, - segmentIndex: 0 + segmentIndex: 0, + partIndex: null }, 'syncpoint found for ProgramDateTime set at 0'); }); @@ -226,6 +229,40 @@ QUnit.test('uses separate date time to display time mapping for each timeline', ); }); +QUnit.test('ProgramDateTime strategy finds nearest llhls sync point', function(assert) { + const strategy = getStrategy('ProgramDateTime'); + const playlist = playlistWithDuration(200, {llhls: true}); + const timeline = 0; + const duration = Infinity; + let syncPoint; + + syncPoint = strategy.run(this.syncController, playlist, duration, timeline, 170); + + 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.setDateTimeMappingForStart(playlist); + + syncPoint = strategy.run(this.syncController, playlist, duration, timeline, 194); + + assert.deepEqual(syncPoint, { + time: 192, + segmentIndex: 19, + partIndex: 1 + }, 'syncpoint found for ProgramDateTime'); + + syncPoint = strategy.run(this.syncController, playlist, duration, timeline, 204); + + assert.deepEqual(syncPoint, { + time: 198, + segmentIndex: 19, + partIndex: 4 + }, 'syncpoint found for ProgramDateTime'); +}); + QUnit.test('returns correct sync point for Segment strategy', function(assert) { const strategy = getStrategy('Segment'); const playlist = { @@ -250,23 +287,75 @@ QUnit.test('returns correct sync point for Segment strategy', function(assert) { currentTimeline = 1; syncPoint = strategy.run(this.syncController, playlist, 80, currentTimeline, 30); assert.deepEqual( - syncPoint, { time: 20, segmentIndex: 3 }, + syncPoint, { time: 20, segmentIndex: 3, partIndex: null }, 'closest sync point found' ); syncPoint = strategy.run(this.syncController, playlist, 80, currentTimeline, 40); assert.deepEqual( - syncPoint, { time: 50, segmentIndex: 6 }, + syncPoint, { time: 50, segmentIndex: 6, partIndex: null }, 'closest sync point found' ); syncPoint = strategy.run(this.syncController, playlist, 80, currentTimeline, 50); assert.deepEqual( - syncPoint, { time: 50, segmentIndex: 6 }, + syncPoint, { time: 50, segmentIndex: 6, partIndex: null }, 'exact sync point found' ); }); +QUnit.test('returns correct sync point for llhls Segment strategy', function(assert) { + const strategy = getStrategy('Segment'); + const playlist = { + segments: [ + { timeline: 0 }, + { timeline: 0 }, + { timeline: 1, start: 10 }, + { timeline: 1, start: 20 }, + { timeline: 1 }, + { timeline: 1 }, + { timeline: 1, start: 50, parts: [ + {start: 50, duration: 1}, + {start: 51, duration: 1}, + {start: 52, duration: 1}, + {start: 53, duration: 1}, + {start: 54, duration: 1}, + {start: 55, duration: 1}, + {start: 56, duration: 1}, + {start: 57, duration: 1}, + {start: 58, duration: 1}, + {start: 59, duration: 1} + ]}, + { timeline: 1, start: 60, parts: [ + {start: 60, duration: 1}, + {start: 61, duration: 1}, + {start: 62, duration: 1}, + {start: 63, duration: 1}, + {start: 64, duration: 1}, + {start: 65, duration: 1}, + {start: 66, duration: 1}, + {start: 67, duration: 1}, + {start: 68, duration: 1}, + {start: 69, duration: 1} + ] } + ] + }; + const currentTimeline = 1; + + assert.deepEqual( + strategy.run(this.syncController, playlist, 80, currentTimeline, 55), + { time: 55, segmentIndex: 6, partIndex: 5 }, + 'exact sync point found' + ); + + assert.deepEqual( + strategy.run(this.syncController, playlist, 80, currentTimeline, 70), + { time: 69, segmentIndex: 7, partIndex: 9 }, + 'closest sync point found' + ); + +}); + QUnit.test('returns correct sync point for Discontinuity strategy', function(assert) { const strategy = getStrategy('Discontinuity'); const playlist = { @@ -299,7 +388,7 @@ QUnit.test('returns correct sync point for Discontinuity strategy', function(ass syncPoint = strategy.run(this.syncController, playlist, 100, currentTimeline, 55); assert.deepEqual( - syncPoint, { time: 40, segmentIndex: 2 }, + syncPoint, { time: 40, segmentIndex: 2, partIndex: null }, 'found sync point for timeline 3' ); @@ -311,7 +400,7 @@ QUnit.test('returns correct sync point for Discontinuity strategy', function(ass syncPoint = strategy.run(this.syncController, playlist, 100, currentTimeline, 90); assert.deepEqual( - syncPoint, { time: 70, segmentIndex: 5 }, + syncPoint, { time: 70, segmentIndex: 5, partIndex: null }, 'found sync point for timeline 4' ); }); @@ -329,7 +418,7 @@ QUnit.test('returns correct sync point for Playlist strategy', function(assert) syncPoint = strategy.run(this.syncController, playlist, 40, 0); assert.deepEqual( - syncPoint, { time: 10, segmentIndex: -2 }, + syncPoint, { time: 10, segmentIndex: -2, partIndex: null }, 'found sync point in playlist' ); });