diff --git a/src/segment-loader.js b/src/segment-loader.js index a75f6773f..d4a0bcb1d 100644 --- a/src/segment-loader.js +++ b/src/segment-loader.js @@ -11,7 +11,7 @@ import segmentTransmuxer from './segment-transmuxer'; import { TIME_FUDGE_FACTOR, timeUntilRebuffer as timeUntilRebuffer_ } from './ranges'; import { minRebufferMaxBandwidthSelector } from './playlist-selectors'; import logger from './util/logger'; -import { concatSegments } from './util/segment'; +import {compactSegmentUrlDescription, concatSegments} from './util/segment'; import { createCaptionsTrackIfNotExists, addCaptionData, @@ -678,6 +678,18 @@ export default class SegmentLoader extends videojs.EventTarget { } } + /** + * TODO: Current sync controller consists of many hls-specific strategies + * media sequence sync is also hls-specific, and we would like to be protocol-agnostic on this level + * this should be a part of the sync-controller and sync controller should expect different strategy list based on the protocol. + * + * @return {MediaSequenceSync|null} + * @private + */ + get mediaSequenceSync_() { + return this.syncController_.getMediaSequenceSync(this.loaderType_); + } + createTransmuxer_() { return segmentTransmuxer.createTransmuxer({ remux: false, @@ -1034,8 +1046,14 @@ export default class SegmentLoader extends videojs.EventTarget { } this.logger_(`playlist update [${oldId} => ${newPlaylist.id || newPlaylist.uri}]`); - this.syncController_.updateMediaSequenceMap(newPlaylist, this.currentTime_(), this.loaderType_); + if (this.mediaSequenceSync_) { + this.mediaSequenceSync_.update(newPlaylist, this.currentTime_()); + this.logger_(`Playlist update: +currentTime: ${this.currentTime_()} +bufferedEnd: ${lastBufferedEnd(this.buffered_())} +`, this.mediaSequenceSync_.diagnostics); + } // in VOD, this is always a rendition switch (or we updated our syncInfo above) // in LIVE, we always want to update with new playlists (including refreshes) this.trigger('syncinfoupdate'); @@ -1200,6 +1218,9 @@ export default class SegmentLoader extends videojs.EventTarget { */ resetLoader() { this.fetchAtBuffer_ = false; + if (this.mediaSequenceSync_) { + this.mediaSequenceSync_.resetAppendedStatus(); + } this.resyncLoader(); } @@ -1216,7 +1237,11 @@ export default class SegmentLoader extends videojs.EventTarget { this.partIndex = null; this.syncPoint_ = null; this.isPendingTimestampOffset_ = false; - this.shouldForceTimestampOffsetAfterResync_ = true; + // this is mainly to sync timing-info when switching between renditions with and without timestamp-rollover, + // so we don't want it for DASH + if (this.sourceType_ === 'hls') { + this.shouldForceTimestampOffsetAfterResync_ = true; + } this.callQueue_ = []; this.loadQueue_ = []; this.metadataQueue_.id3 = []; @@ -1452,18 +1477,50 @@ export default class SegmentLoader extends videojs.EventTarget { next.mediaIndex = this.mediaIndex + 1; } } else { - // Find the segment containing the end of the buffer or current time. - const {segmentIndex, startTime, partIndex} = Playlist.getMediaInfoForTime({ - exactManifestTimings: this.exactManifestTimings, - playlist: this.playlist_, - currentTime: this.fetchAtBuffer_ ? bufferedEnd : this.currentTime_(), - startingPartIndex: this.syncPoint_.partIndex, - startingSegmentIndex: this.syncPoint_.segmentIndex, - startTime: this.syncPoint_.time - }); + let segmentIndex; let partIndex; let startTime; + const targetTime = this.fetchAtBuffer_ ? bufferedEnd : this.currentTime_(); + + if (this.mediaSequenceSync_) { + this.logger_(`chooseNextRequest_ request after Quality Switch: +For TargetTime: ${targetTime}. +CurrentTime: ${this.currentTime_()} +BufferedEnd: ${bufferedEnd} +Fetch At Buffer: ${this.fetchAtBuffer_} +`, this.mediaSequenceSync_.diagnostics); + } - next.getMediaInfoForTime = this.fetchAtBuffer_ ? - `bufferedEnd ${bufferedEnd}` : `currentTime ${this.currentTime_()}`; + if (this.mediaSequenceSync_ && this.mediaSequenceSync_.isReliable) { + const syncInfo = this.getSyncInfoFromMediaSequenceSync_(targetTime); + + if (!syncInfo) { + this.logger_('chooseNextRequest_ - no sync info found using media sequence sync'); + // no match + return null; + } + + this.logger_(`chooseNextRequest_ mediaSequence syncInfo (${syncInfo.start} --> ${syncInfo.end})`); + + segmentIndex = syncInfo.segmentIndex; + partIndex = syncInfo.partIndex; + startTime = syncInfo.start; + } else { + this.logger_('chooseNextRequest_ - fallback to a regular segment selection algorithm, based on a syncPoint.'); + // fallback + const mediaInfoForTime = Playlist.getMediaInfoForTime({ + exactManifestTimings: this.exactManifestTimings, + playlist: this.playlist_, + currentTime: targetTime, + startingPartIndex: this.syncPoint_.partIndex, + startingSegmentIndex: this.syncPoint_.segmentIndex, + startTime: this.syncPoint_.time + }); + + segmentIndex = mediaInfoForTime.segmentIndex; + partIndex = mediaInfoForTime.partIndex; + startTime = mediaInfoForTime.startTime; + } + + next.getMediaInfoForTime = this.fetchAtBuffer_ ? `bufferedEnd ${targetTime}` : `currentTime ${targetTime}`; next.mediaIndex = segmentIndex; next.startOfSegment = startTime; next.partIndex = partIndex; @@ -1536,6 +1593,47 @@ export default class SegmentLoader extends videojs.EventTarget { return this.generateSegmentInfo_(next); } + getSyncInfoFromMediaSequenceSync_(targetTime) { + if (!this.mediaSequenceSync_) { + return null; + } + + // we should pull the target time to the least available time if we drop out of sync for any reason + const finalTargetTime = Math.max(targetTime, this.mediaSequenceSync_.start); + + if (targetTime !== finalTargetTime) { + this.logger_(`getSyncInfoFromMediaSequenceSync_. Pulled target time from ${targetTime} to ${finalTargetTime}`); + } + + const mediaSequenceSyncInfo = this.mediaSequenceSync_.getSyncInfoForTime(finalTargetTime); + + if (!mediaSequenceSyncInfo) { + // no match at all + return null; + } + + if (!mediaSequenceSyncInfo.isAppended) { + // has a perfect match + return mediaSequenceSyncInfo; + } + + // has match, but segment was already appended. + // attempt to auto-advance to the nearest next segment: + const nextMediaSequenceSyncInfo = this.mediaSequenceSync_.getSyncInfoForTime(mediaSequenceSyncInfo.end); + + if (!nextMediaSequenceSyncInfo) { + // no match at all + return null; + } + + if (nextMediaSequenceSyncInfo.isAppended) { + this.logger_('getSyncInfoFromMediaSequenceSync_: We encounter unexpected scenario where next media sequence sync info is also appended!'); + } + + // got match with the nearest next segment + return nextMediaSequenceSyncInfo; + } + generateSegmentInfo_(options) { const { independent, @@ -2493,7 +2591,9 @@ export default class SegmentLoader extends videojs.EventTarget { segmentInfo.timeline > 0; const isEndOfTimeline = isEndOfStream || (isWalkingForward && isDiscontinuity); - this.logger_(`Requesting ${segmentInfoString(segmentInfo)}`); + this.logger_(`Requesting +${compactSegmentUrlDescription(segmentInfo.uri)} +${segmentInfoString(segmentInfo)}`); // If there's an init segment associated with this segment, but it is not cached (identified by a lack of bytes), // then this init segment has never been seen before and should be appended. @@ -3020,6 +3120,14 @@ export default class SegmentLoader extends videojs.EventTarget { const segmentInfo = this.pendingSegment_; + if (segmentInfo.part && segmentInfo.part.syncInfo) { + // low-latency flow + segmentInfo.part.syncInfo.markAppended(); + } else if (segmentInfo.segment.syncInfo) { + // normal flow + segmentInfo.segment.syncInfo.markAppended(); + } + // Now that the end of the segment has been reached, we can set the end time. It's // best to wait until all appends are done so we're sure that the primary media is // finished (and we have its end time). diff --git a/src/source-updater.js b/src/source-updater.js index f179e9727..48c1d889c 100644 --- a/src/source-updater.js +++ b/src/source-updater.js @@ -9,7 +9,7 @@ import {getMimeForCodec} from '@videojs/vhs-utils/es/codecs.js'; import window from 'global/window'; import toTitleCase from './util/to-title-case.js'; import { QUOTA_EXCEEDED_ERR } from './error-codes'; -import {createTimeRanges} from './util/vjs-compat'; +import {createTimeRanges, bufferedRangesToString} from './util/vjs-compat'; const bufferTypes = [ 'video', @@ -314,6 +314,11 @@ const onUpdateend = (type, sourceUpdater) => (e) => { // updateend events on source buffers. This does not appear to be in the spec. As such, // if we encounter an updateend without a corresponding pending action from our queue // for that source buffer type, process the next action. + const bufferedRangesForType = sourceUpdater[`${type}Buffered`](); + const descriptiveString = bufferedRangesToString(bufferedRangesForType); + + sourceUpdater.logger_(`received "updateend" event for ${type} Source Buffer: `, descriptiveString); + if (sourceUpdater.queuePending[type]) { const doneFn = sourceUpdater.queuePending[type].doneFn; diff --git a/src/sync-controller.js b/src/sync-controller.js index a2da9719a..46bce919a 100644 --- a/src/sync-controller.js +++ b/src/sync-controller.js @@ -5,6 +5,7 @@ import {sumDurations, getPartsAndSegments} from './playlist'; import videojs from 'video.js'; import logger from './util/logger'; +import MediaSequenceSync from './util/media-sequence-sync'; // The maximum gap allowed between two media sequence tags when trying to // synchronize expired playlist segments. @@ -44,71 +45,27 @@ export const syncPointStrategies = [ * @param {string} type */ run: (syncController, playlist, duration, currentTimeline, currentTime, type) => { - if (!type) { - return null; - } - - const mediaSequenceMap = syncController.getMediaSequenceMap(type); + const mediaSequenceSync = syncController.getMediaSequenceSync(type); - if (!mediaSequenceMap || mediaSequenceMap.size === 0) { + if (!mediaSequenceSync) { return null; } - if (playlist.mediaSequence === undefined || !Array.isArray(playlist.segments) || !playlist.segments.length) { + if (!mediaSequenceSync.isReliable) { return null; } - let currentMediaSequence = playlist.mediaSequence; - let segmentIndex = 0; - - for (const segment of playlist.segments) { - const range = mediaSequenceMap.get(currentMediaSequence); - - if (!range) { - // unexpected case - // we expect this playlist to be the same playlist in the map - // just break from the loop and move forward to the next strategy - break; - } - - if (currentTime >= range.start && currentTime < range.end) { - // we found segment - - if (Array.isArray(segment.parts) && segment.parts.length) { - let currentPartStart = range.start; - let partIndex = 0; - - for (const part of segment.parts) { - const start = currentPartStart; - const end = start + part.duration; - - if (currentTime >= start && currentTime < end) { - return { - time: range.start, - segmentIndex, - partIndex - }; - } + const syncInfo = mediaSequenceSync.getSyncInfoForTime(currentTime); - partIndex++; - currentPartStart = end; - } - } - - // no parts found, return sync point for segment - return { - time: range.start, - segmentIndex, - partIndex: null - }; - } - - segmentIndex++; - currentMediaSequence++; + if (!syncInfo) { + return null; } - // we didn't find any segments for provided current time - return null; + return { + time: syncInfo.start, + partIndex: syncInfo.partIndex, + segmentIndex: syncInfo.segmentIndex + }; } }, // Stategy "ProgramDateTime": We have a program-date-time tag in this playlist @@ -272,78 +229,25 @@ export default class SyncController extends videojs.EventTarget { this.timelines = []; this.discontinuities = []; this.timelineToDatetimeMappings = {}; - - /** - * @type {Map>} - * @private - */ - this.mediaSequenceStorage_ = new Map(); - + // TODO: this map should be only available for HLS. Since only HLS has MediaSequence. + // For some reason this map helps with syncing between quality switch for MPEG-DASH as well. + // Moreover if we disable this map for MPEG-DASH - quality switch will be broken. + // MPEG-DASH should have its own separate sync strategy + this.mediaSequenceStorage_ = { + main: new MediaSequenceSync(), + audio: new MediaSequenceSync(), + vtt: new MediaSequenceSync() + }; this.logger_ = logger('SyncController'); } /** - * Get media sequence map by type * - * @param {string} type - segment loader type - * @return {Map | undefined} + * @param {string} loaderType + * @return {MediaSequenceSync|null} */ - getMediaSequenceMap(type) { - return this.mediaSequenceStorage_.get(type); - } - - /** - * Update Media Sequence Map -> - * - * @param {Object} playlist - parsed playlist - * @param {number} currentTime - current player's time - * @param {string} type - segment loader type - * @return {void} - */ - updateMediaSequenceMap(playlist, currentTime, type) { - // we should not process this playlist if it does not have mediaSequence or segments - if (playlist.mediaSequence === undefined || !Array.isArray(playlist.segments) || !playlist.segments.length) { - return; - } - - const currentMap = this.getMediaSequenceMap(type); - const result = new Map(); - - let currentMediaSequence = playlist.mediaSequence; - let currentBaseTime; - - if (!currentMap) { - // first playlist setup: - currentBaseTime = 0; - } else if (currentMap.has(playlist.mediaSequence)) { - // further playlists setup: - currentBaseTime = currentMap.get(playlist.mediaSequence).start; - } else { - // it seems like we have a gap between playlists, use current time as a fallback: - this.logger_(`MediaSequence sync for ${type} segment loader - received a gap between playlists. -Fallback base time to: ${currentTime}. -Received media sequence: ${currentMediaSequence}. -Current map: `, currentMap); - currentBaseTime = currentTime; - } - - this.logger_(`MediaSequence sync for ${type} segment loader. -Received media sequence: ${currentMediaSequence}. -base time is ${currentBaseTime} -Current map: `, currentMap); - - playlist.segments.forEach((segment) => { - const start = currentBaseTime; - const end = start + segment.duration; - const range = { start, end }; - - result.set(currentMediaSequence, range); - - currentMediaSequence++; - currentBaseTime = end; - }); - - this.mediaSequenceStorage_.set(type, result); + getMediaSequenceSync(loaderType) { + return this.mediaSequenceStorage_[loaderType] || null; } /** @@ -436,8 +340,7 @@ Current map: `, currentMap); playlist, duration, playlist.discontinuitySequence, - 0, - 'main' + 0 ); // Without sync-points, there is not enough information to determine the expired time diff --git a/src/util/media-sequence-sync.js b/src/util/media-sequence-sync.js new file mode 100644 index 000000000..41797cea6 --- /dev/null +++ b/src/util/media-sequence-sync.js @@ -0,0 +1,246 @@ +import {compactSegmentUrlDescription} from './segment'; + +class SyncInfo { + /** + * @param {number} start - media sequence start + * @param {number} end - media sequence end + * @param {number} segmentIndex - index for associated segment + * @param {number|null} [partIndex] - index for associated part + * @param {boolean} [appended] - appended indicator + * + */ + constructor({start, end, segmentIndex, partIndex = null, appended = false}) { + this.start_ = start; + this.end_ = end; + this.segmentIndex_ = segmentIndex; + this.partIndex_ = partIndex; + this.appended_ = appended; + } + + isInRange(targetTime) { + return targetTime >= this.start && targetTime < this.end; + } + + markAppended() { + this.appended_ = true; + } + + resetAppendedStatus() { + this.appended_ = false; + } + + get isAppended() { + return this.appended_; + } + + get start() { + return this.start_; + } + + get end() { + return this.end_; + } + + get segmentIndex() { + return this.segmentIndex_; + } + + get partIndex() { + return this.partIndex_; + } +} + +class SyncInfoData { + /** + * + * @param {SyncInfo} segmentSyncInfo - sync info for a given segment + * @param {Array} [partsSyncInfo] - sync infos for a list of parts for a given segment + */ + constructor(segmentSyncInfo, partsSyncInfo = []) { + this.segmentSyncInfo_ = segmentSyncInfo; + this.partsSyncInfo_ = partsSyncInfo; + } + + get segmentSyncInfo() { + return this.segmentSyncInfo_; + } + + get partsSyncInfo() { + return this.partsSyncInfo_; + } + + get hasPartsSyncInfo() { + return this.partsSyncInfo_.length > 0; + } + + resetAppendStatus() { + this.segmentSyncInfo_.resetAppendedStatus(); + this.partsSyncInfo_.forEach((partSyncInfo) => partSyncInfo.resetAppendedStatus()); + } +} + +export default class MediaSequenceSync { + constructor() { + /** + * @type {Map} + * @private + */ + this.storage_ = new Map(); + this.diagnostics_ = ''; + this.isReliable_ = false; + this.start_ = -Infinity; + this.end_ = Infinity; + } + + get start() { + return this.start_; + } + + get end() { + return this.end_; + } + + get diagnostics() { + return this.diagnostics_; + } + + get isReliable() { + return this.isReliable_; + } + + resetAppendedStatus() { + this.storage_.forEach((syncInfoData) => syncInfoData.resetAppendStatus()); + } + + /** + * update sync storage + * + * @param {Object} playlist + * @param {number} currentTime + * + * @return {void} + */ + update(playlist, currentTime) { + const { mediaSequence, segments } = playlist; + + this.isReliable_ = this.isReliablePlaylist_(mediaSequence, segments); + + if (!this.isReliable_) { + return; + } + + return this.updateStorage_( + segments, + mediaSequence, + this.calculateBaseTime_(mediaSequence, currentTime) + ); + } + + /** + * @param {number} targetTime + * @return {SyncInfo|null} + */ + getSyncInfoForTime(targetTime) { + for (const { segmentSyncInfo, partsSyncInfo } of this.storage_.values()) { + // Normal segment flow: + if (!partsSyncInfo.length) { + if (segmentSyncInfo.isInRange(targetTime)) { + return segmentSyncInfo; + } + } else { + // Low latency flow: + for (const partSyncInfo of partsSyncInfo) { + if (partSyncInfo.isInRange(targetTime)) { + return partSyncInfo; + } + } + } + } + + return null; + } + + updateStorage_(segments, startingMediaSequence, startingTime) { + const newStorage = new Map(); + let newDiagnostics = '\n'; + + let currentStart = startingTime; + let currentMediaSequence = startingMediaSequence; + + this.start_ = currentStart; + + segments.forEach((segment, segmentIndex) => { + const prevSyncInfoData = this.storage_.get(currentMediaSequence); + + const segmentStart = currentStart; + const segmentEnd = segmentStart + segment.duration; + const segmentIsAppended = Boolean(prevSyncInfoData && + prevSyncInfoData.segmentSyncInfo && + prevSyncInfoData.segmentSyncInfo.isAppended); + + const segmentSyncInfo = new SyncInfo({ + start: segmentStart, + end: segmentEnd, + appended: segmentIsAppended, + segmentIndex + }); + + segment.syncInfo = segmentSyncInfo; + + let currentPartStart = currentStart; + + const partsSyncInfo = (segment.parts || []).map((part, partIndex) => { + const partStart = currentPartStart; + const partEnd = currentPartStart + part.duration; + const partIsAppended = Boolean(prevSyncInfoData && + prevSyncInfoData.partsSyncInfo && + prevSyncInfoData.partsSyncInfo[partIndex] && + prevSyncInfoData.partsSyncInfo[partIndex].isAppended); + + const partSyncInfo = new SyncInfo({ + start: partStart, + end: partEnd, + appended: partIsAppended, + segmentIndex, + partIndex + }); + + currentPartStart = partEnd; + newDiagnostics += `Media Sequence: ${currentMediaSequence}.${partIndex} | Range: ${partStart} --> ${partEnd} | Appended: ${partIsAppended}\n`; + part.syncInfo = partSyncInfo; + + return partSyncInfo; + }); + + newStorage.set(currentMediaSequence, new SyncInfoData(segmentSyncInfo, partsSyncInfo)); + newDiagnostics += `${compactSegmentUrlDescription(segment.resolvedUri)} | Media Sequence: ${currentMediaSequence} | Range: ${segmentStart} --> ${segmentEnd} | Appended: ${segmentIsAppended}\n`; + + currentMediaSequence++; + currentStart = segmentEnd; + }); + + this.end_ = currentStart; + this.storage_ = newStorage; + this.diagnostics_ = newDiagnostics; + } + + calculateBaseTime_(mediaSequence, fallback) { + if (!this.storage_.size) { + // Initial setup flow. + return 0; + } + + if (this.storage_.has(mediaSequence)) { + // Normal flow. + return this.storage_.get(mediaSequence).segmentSyncInfo.start; + } + + // Fallback flow. + // There is a gap between last recorded playlist and a new one received. + return fallback; + } + + isReliablePlaylist_(mediaSequence, segments) { + return mediaSequence !== undefined && mediaSequence !== null && Array.isArray(segments) && segments.length; + } +} diff --git a/src/util/segment.js b/src/util/segment.js index c9adbe942..269cde4d0 100644 --- a/src/util/segment.js +++ b/src/util/segment.js @@ -21,3 +21,24 @@ export const concatSegments = (segmentObj) => { return tempBuffer; }; + +/** + * Example: + * https://host.com/path1/path2/path3/segment.ts?arg1=val1 + * --> + * path3/segment.ts + * + * @param resolvedUri + * @return {string} + */ +export function compactSegmentUrlDescription(resolvedUri) { + try { + return new URL(resolvedUri) + .pathname + .split('/') + .slice(-2) + .join('/'); + } catch (e) { + return ''; + } +} diff --git a/src/util/vjs-compat.js b/src/util/vjs-compat.js index d51a4b963..d798fc3c2 100644 --- a/src/util/vjs-compat.js +++ b/src/util/vjs-compat.js @@ -24,3 +24,27 @@ export function createTimeRanges(...args) { return fn.apply(context, args); } + +/** + * Converts provided buffered ranges to a descriptive string + * + * @param {TimeRanges} buffered - received buffered time ranges + * + * @return {string} - descriptive string + */ +export function bufferedRangesToString(buffered) { + if (buffered.length === 0) { + return 'Buffered Ranges are empty'; + } + + let bufferedRangesStr = 'Buffered Ranges: \n'; + + for (let i = 0; i < buffered.length; i++) { + const start = buffered.start(i); + const end = buffered.end(i); + + bufferedRangesStr += `${start} --> ${end}. Duration (${end - start})\n`; + } + + return bufferedRangesStr; +} diff --git a/src/vtt-segment-loader.js b/src/vtt-segment-loader.js index 820d3f1a9..2af32fe30 100644 --- a/src/vtt-segment-loader.js +++ b/src/vtt-segment-loader.js @@ -37,8 +37,6 @@ export default class VTTSegmentLoader extends SegmentLoader { this.subtitlesTrack_ = null; - this.loaderType_ = 'subtitle'; - this.featuresNativeTextTracks_ = settings.featuresNativeTextTracks; this.loadVttJs = settings.loadVttJs; diff --git a/test/loader-common.js b/test/loader-common.js index 29845f289..fa8081b8c 100644 --- a/test/loader-common.js +++ b/test/loader-common.js @@ -835,11 +835,11 @@ export const LoaderCommonFactory = ({ const segmentInfo = loader.pendingSegment_; - assert.equal(segmentInfo.mediaIndex, 3, 'segmentInfo.mediaIndex starts at 3'); + assert.equal(segmentInfo.mediaIndex, 4, 'segmentInfo.mediaIndex starts at 4'); assert.equal( this.requests[0].url, - '3.ts', - 'requesting the segment at mediaIndex 3' + '4.ts', + 'requesting the segment at mediaIndex 4' ); // Update the playlist shifting the mediaSequence by 2 which will result @@ -849,8 +849,8 @@ export const LoaderCommonFactory = ({ endList: false })); - assert.equal(segmentInfo.mediaIndex, 1, 'segmentInfo.mediaIndex is updated to 1'); - expectedLoaderIndex = 1; + assert.equal(segmentInfo.mediaIndex, 2, 'segmentInfo.mediaIndex is updated to 2'); + expectedLoaderIndex = 2; standardXHRResponse(this.requests.shift(), testData()); diff --git a/test/playback.test.js b/test/playback.test.js index 07a7215eb..ef2835549 100644 --- a/test/playback.test.js +++ b/test/playback.test.js @@ -333,7 +333,7 @@ if (!videojs.browser.IS_FIREFOX) { // Firefox sometimes won't loop if seeking directly to the duration, or to too close // to the duration (e.g., 10ms from duration). 100ms seems to work. - player.currentTime(player.duration() - 0.5); + player.currentTime(player.duration() - 1); }); player.src({ diff --git a/test/playlist-controller.test.js b/test/playlist-controller.test.js index d0d1b1a9b..85a45edb1 100644 --- a/test/playlist-controller.test.js +++ b/test/playlist-controller.test.js @@ -2273,8 +2273,8 @@ QUnit.test('does not get stuck in a loop due to inconsistent network/caching', f assert.notOk(media1ResolvedPlaylist.excludeUntil, 'media1 not excluded'); assert.equal( segmentRequest.uri.substring(segmentRequest.uri.length - 4), - '0.ts', - 'requested first segment' + '1.ts', + 'requested second segment' ); // needs a timeout for early abort to occur (we skip the function otherwise, since no @@ -2298,8 +2298,8 @@ QUnit.test('does not get stuck in a loop due to inconsistent network/caching', f assert.ok(media1ResolvedPlaylist.excludeUntil, 'excluded media1'); assert.equal( segmentRequest.uri.substring(segmentRequest.uri.length - 4), - '0.ts', - 'requested first segment' + '1.ts', + 'requested second segment' ); // remove aborted request @@ -2321,8 +2321,8 @@ QUnit.test('does not get stuck in a loop due to inconsistent network/caching', f assert.equal(mediaChanges.length, 2, 'did not change media'); assert.equal( segmentRequest.uri.substring(segmentRequest.uri.length - 4), - '1.ts', - 'requested second segment' + '2.ts', + 'requested third segment' ); // 1ms for the cached segment response @@ -2342,8 +2342,8 @@ QUnit.test('does not get stuck in a loop due to inconsistent network/caching', f segmentRequest = this.requests[0]; assert.equal( segmentRequest.uri.substring(segmentRequest.uri.length - 4), - '2.ts', - 'requested third segment' + '3.ts', + 'requested 4th segment' ); assert.equal(this.env.log.warn.callCount, 1, 'logged a warning'); diff --git a/test/source-updater.test.js b/test/source-updater.test.js index 4ca5d57f2..b79fc2150 100644 --- a/test/source-updater.test.js +++ b/test/source-updater.test.js @@ -216,14 +216,14 @@ QUnit.test('verifies that sourcebuffer is in source buffers list before attempti assert.deepEqual(actionCalls, { audioAbort: 1, audioAppendBuffer: 1, - audioBuffered: 8, + audioBuffered: 12, audioChangeType: 1, audioRemove: 1, audioRemoveSourceBuffer: 1, audioTimestampOffset: 1, videoAbort: 1, videoAppendBuffer: 1, - videoBuffered: 8, + videoBuffered: 12, videoChangeType: 1, videoRemove: 1, videoRemoveSourceBuffer: 1,