diff --git a/demo/full/scripts/modules/player/events.ts b/demo/full/scripts/modules/player/events.ts index 8ba4071632..83aed41c83 100644 --- a/demo/full/scripts/modules/player/events.ts +++ b/demo/full/scripts/modules/player/events.ts @@ -47,7 +47,13 @@ function linkPlayerEventsToState( stopPositionUpdates(); // use an interval for current position - positionUpdatesInterval = window.setInterval(() => { + positionUpdatesInterval = window.setInterval( + updatePositionInfo, + POSITION_UPDATES_INTERVAL); + + updatePositionInfo(); + + function updatePositionInfo() { const position = player.getPosition(); const duration = player.getVideoDuration(); const videoTrack = player.getVideoTrack(); @@ -68,7 +74,7 @@ function linkPlayerEventsToState( videoTrack.trickModeTracks !== undefined && videoTrack.trickModeTracks.length > 0, }); - }, POSITION_UPDATES_INTERVAL); + } } function stopPositionUpdates() { diff --git a/src/core/init/media_source_content_initializer.ts b/src/core/init/media_source_content_initializer.ts index 1bfa50c164..914bf6c539 100644 --- a/src/core/init/media_source_content_initializer.ts +++ b/src/core/init/media_source_content_initializer.ts @@ -68,7 +68,7 @@ import getInitialTime, { import getLoadedReference from "./utils/get_loaded_reference"; import performInitialSeekAndPlay from "./utils/initial_seek_and_play"; import initializeContentDecryption from "./utils/initialize_content_decryption"; -import MediaDurationUpdater from "./utils/media_duration_updater"; +import MediaSourceDurationUpdater from "./utils/media_source_duration_updater"; import RebufferingController from "./utils/rebuffering_controller"; import streamEventsEmitter from "./utils/stream_events_emitter"; import listenToMediaError from "./utils/throw_on_media_error"; @@ -644,9 +644,9 @@ export default class MediaSourceContentInitializer extends ContentInitializer { cancelSignal : CancellationSignal ) : ContentTimeBoundariesObserver { /** Maintains the MediaSource's duration up-to-date with the Manifest */ - const mediaDurationUpdater = new MediaDurationUpdater(manifest, mediaSource); + const mediaSourceDurationUpdater = new MediaSourceDurationUpdater(mediaSource); cancelSignal.register(() => { - mediaDurationUpdater.stop(); + mediaSourceDurationUpdater.stopUpdating(); }); /** Allows to cancel a pending `end-of-stream` operation. */ let endOfStreamCanceller : TaskCanceller | null = null; @@ -664,7 +664,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer { this.trigger("activePeriodChanged", { period }); }); contentTimeBoundariesObserver.addEventListener("durationUpdate", (newDuration) => { - mediaDurationUpdater.updateKnownDuration(newDuration); + mediaSourceDurationUpdater.updateDuration(newDuration.duration, !newDuration.isEnd); }); contentTimeBoundariesObserver.addEventListener("endOfStream", () => { if (endOfStreamCanceller === null) { @@ -681,6 +681,9 @@ export default class MediaSourceContentInitializer extends ContentInitializer { endOfStreamCanceller = null; } }); + const currentDuration = contentTimeBoundariesObserver.getCurrentDuration(); + mediaSourceDurationUpdater.updateDuration(currentDuration.duration, + !currentDuration.isEnd); return contentTimeBoundariesObserver; } diff --git a/src/core/init/utils/content_time_boundaries_observer.ts b/src/core/init/utils/content_time_boundaries_observer.ts index 09a43f0adc..922e990d59 100644 --- a/src/core/init/utils/content_time_boundaries_observer.ts +++ b/src/core/init/utils/content_time_boundaries_observer.ts @@ -111,18 +111,20 @@ export default class ContentTimeBoundariesObserver }, { includeLastObservation: true, clearSignal: cancelSignal }); manifest.addEventListener("manifestUpdate", () => { - this.trigger("durationUpdate", getManifestDuration()); + this.trigger("durationUpdate", this._getManifestDuration()); if (cancelSignal.isCancelled()) { return; } this._checkEndOfStream(); }, cancelSignal); + } - function getManifestDuration() : number | undefined { - return manifest.isDynamic ? - maximumPositionCalculator.getMaximumAvailablePosition() : - maximumPositionCalculator.getEndingPosition(); - } + /** + * Returns an estimate of the current duration of the content. + * @returns {Object} + */ + public getCurrentDuration() : IDurationItem { + return this._getManifestDuration(); } /** @@ -154,9 +156,12 @@ export default class ContentTimeBoundariesObserver this._maximumPositionCalculator .updateLastVideoAdaptation(adaptation); } - const newDuration = this._manifest.isDynamic ? - this._maximumPositionCalculator.getMaximumAvailablePosition() : - this._maximumPositionCalculator.getEndingPosition(); + const endingPosition = this._maximumPositionCalculator.getEndingPosition(); + const newDuration = endingPosition !== undefined ? + { isEnd: true, + duration: endingPosition } : + { isEnd: false, + duration: this._maximumPositionCalculator.getMaximumAvailablePosition() }; this.trigger("durationUpdate", newDuration); } } @@ -306,6 +311,15 @@ export default class ContentTimeBoundariesObserver } } + private _getManifestDuration() : IDurationItem { + const endingPosition = this._maximumPositionCalculator.getEndingPosition(); + return endingPosition !== undefined ? + { isEnd: true, + duration: endingPosition } : + { isEnd: false, + duration: this._maximumPositionCalculator.getMaximumAvailablePosition() }; + } + private _lazilyCreateActiveStreamInfo(bufferType : IBufferType) : IActiveStreamsInfo { let streamInfo = this._activeStreams.get(bufferType); if (streamInfo === undefined) { @@ -334,6 +348,28 @@ export default class ContentTimeBoundariesObserver } } +export interface IDurationItem { + /** + * The new maximum known position (note that this is the ending position + * currently known of the current content, it might be superior to the last + * position at which segments are available and it might also evolve over + * time), in seconds. + */ + duration : number; + /** + * If `true`, the communicated `duration` is the actual end of the content. + * It may still be updated due to a track change or to add precision, but it + * is still a (rough) estimate of the maximum position that content should + * have. + * + * If `false`, this is the currently known maximum position associated to + * the content, but the content is still evolving (typically, new media + * segments are still being generated) and as such it can still have a + * longer duration in the future. + */ + isEnd : boolean; +} + /** * Events triggered by a `ContentTimeBoundariesObserver` where the keys are the * event names and the value is the payload of those events. @@ -347,7 +383,7 @@ export interface IContentTimeBoundariesObserverEvent { * Triggered when the duration of the currently-playing content became known * or changed. */ - durationUpdate : number | undefined; + durationUpdate : IDurationItem; /** * Triggered when the last possible chronological segment for all types of * buffers has either been pushed or is being pushed to the corresponding diff --git a/src/core/init/utils/media_duration_updater.ts b/src/core/init/utils/media_source_duration_updater.ts similarity index 60% rename from src/core/init/utils/media_duration_updater.ts rename to src/core/init/utils/media_source_duration_updater.ts index 11039e066b..c8ce07a89e 100644 --- a/src/core/init/utils/media_duration_updater.ts +++ b/src/core/init/utils/media_source_duration_updater.ts @@ -20,10 +20,8 @@ import { onSourceClose, } from "../../../compat/event_listeners"; import log from "../../../log"; -import Manifest from "../../../manifest"; import createSharedReference, { IReadOnlySharedReference, - ISharedReference, } from "../../../utils/reference"; import TaskCanceller, { CancellationSignal, @@ -33,124 +31,108 @@ import TaskCanceller, { const YEAR_IN_SECONDS = 365 * 24 * 3600; /** - * Keep the MediaSource's duration up-to-date with what is being played. - * @class MediaDurationUpdater + * Keep the MediaSource's `duration` attribute up-to-date with the duration of + * the content played on it. + * @class MediaSourceDurationUpdater */ -export default class MediaDurationUpdater { - private _canceller : TaskCanceller; +export default class MediaSourceDurationUpdater { + /** + * `MediaSource` on which we're going to update the `duration` attribute. + */ + private _mediaSource : MediaSource; /** - * The last known audio Adaptation (i.e. track) chosen for the last Period. - * Useful to determinate the duration of the current content. - * `undefined` if the audio track for the last Period has never been known yet. - * `null` if there are no chosen audio Adaptation. + * Abort the current duration-setting logic. + * `null` if no such logic is pending. */ - private _currentKnownDuration : ISharedReference; + private _currentMediaSourceDurationUpdateCanceller : TaskCanceller | null; /** - * Create a new `MediaDurationUpdater` that will keep the given MediaSource's - * duration as soon as possible. - * This duration will be updated until the `stop` method is called. - * @param {Object} manifest - The Manifest currently played. - * For another content, you will have to create another `MediaDurationUpdater`. + * Create a new `MediaSourceDurationUpdater`, * @param {MediaSource} mediaSource - The MediaSource on which the content is - * pushed. + * played. */ - constructor(manifest : Manifest, mediaSource : MediaSource) { - const canceller = new TaskCanceller(); - const currentKnownDuration = createSharedReference(undefined, canceller.signal); + constructor(mediaSource : MediaSource) { + this._mediaSource = mediaSource; + this._currentMediaSourceDurationUpdateCanceller = null; + } - this._canceller = canceller; - this._currentKnownDuration = currentKnownDuration; + /** + * Indicate to the `MediaSourceDurationUpdater` the currently known duration + * of the content. + * + * The `MediaSourceDurationUpdater` will then use that value to determine + * which `duration` attribute should be set on the `MediaSource` associated + * + * @param {number} newDuration + * @param {boolean} addTimeMargin - If set to `true`, the current content is + * a dynamic content (it might evolve in the future) and the `newDuration` + * communicated might be greater still. In effect the + * `MediaSourceDurationUpdater` will actually set a much higher value to the + * `MediaSource`'s duration to prevent being annoyed by the HTML-related + * side-effects of having a too low duration (such as the impossibility to + * seek over that value). + */ + public updateDuration( + newDuration : number, + addTimeMargin : boolean + ) : void { + if (this._currentMediaSourceDurationUpdateCanceller !== null) { + this._currentMediaSourceDurationUpdateCanceller.cancel(); + } + this._currentMediaSourceDurationUpdateCanceller = new TaskCanceller(); + const mediaSource = this._mediaSource; + const currentSignal = this._currentMediaSourceDurationUpdateCanceller.signal; const isMediaSourceOpened = createMediaSourceOpenReference(mediaSource, - this._canceller.signal); - - /** TaskCanceller triggered each time the MediaSource open status changes. */ - let msUpdateCanceller = new TaskCanceller(); - msUpdateCanceller.linkToSignal(this._canceller.signal); + currentSignal); + /** TaskCanceller triggered each time the MediaSource switches to and from "open". */ + let msOpenStatusCanceller = new TaskCanceller(); + msOpenStatusCanceller.linkToSignal(currentSignal); isMediaSourceOpened.onUpdate(onMediaSourceOpenedStatusChanged, { emitCurrentValue: true, - clearSignal: this._canceller.signal }); - + clearSignal: currentSignal }); function onMediaSourceOpenedStatusChanged() { - msUpdateCanceller.cancel(); + msOpenStatusCanceller.cancel(); if (!isMediaSourceOpened.getValue()) { return; } - msUpdateCanceller = new TaskCanceller(); - msUpdateCanceller.linkToSignal(canceller.signal); - - /** TaskCanceller triggered each time the content's duration may have changed */ - let durationChangeCanceller = new TaskCanceller(); - durationChangeCanceller.linkToSignal(msUpdateCanceller.signal); - - const reSetDuration = () => { - durationChangeCanceller.cancel(); - durationChangeCanceller = new TaskCanceller(); - durationChangeCanceller.linkToSignal(msUpdateCanceller.signal); - onDurationMayHaveChanged(durationChangeCanceller.signal); - }; - - currentKnownDuration.onUpdate(reSetDuration, - { emitCurrentValue: false, - clearSignal: msUpdateCanceller.signal }); - - manifest.addEventListener("manifestUpdate", - reSetDuration, - msUpdateCanceller.signal); - - onDurationMayHaveChanged(durationChangeCanceller.signal); - } - - function onDurationMayHaveChanged(cancelSignal : CancellationSignal) { + msOpenStatusCanceller = new TaskCanceller(); + msOpenStatusCanceller.linkToSignal(currentSignal); const areSourceBuffersUpdating = createSourceBuffersUpdatingReference( mediaSource.sourceBuffers, - cancelSignal + msOpenStatusCanceller.signal ); - /** TaskCanceller triggered each time SourceBuffers' updating status changes */ let sourceBuffersUpdatingCanceller = new TaskCanceller(); - sourceBuffersUpdatingCanceller.linkToSignal(cancelSignal); + sourceBuffersUpdatingCanceller.linkToSignal(msOpenStatusCanceller.signal); + return areSourceBuffersUpdating.onUpdate((areUpdating) => { sourceBuffersUpdatingCanceller.cancel(); sourceBuffersUpdatingCanceller = new TaskCanceller(); - sourceBuffersUpdatingCanceller.linkToSignal(cancelSignal); + sourceBuffersUpdatingCanceller.linkToSignal(msOpenStatusCanceller.signal); if (areUpdating) { return; } + recursivelyForceDurationUpdate(mediaSource, - manifest, - currentKnownDuration.getValue(), - cancelSignal); - }, { clearSignal: cancelSignal, emitCurrentValue: true }); + newDuration, + addTimeMargin, + sourceBuffersUpdatingCanceller.signal); + }, { clearSignal: msOpenStatusCanceller.signal, emitCurrentValue: true }); } } /** - * By default, the `MediaDurationUpdater` only set a safe estimate for the - * MediaSource's duration. - * A more precize duration can be set by communicating to it a more precize - * media duration through `updateKnownDuration`. - * If the duration becomes unknown, `undefined` can be given to it so the - * `MediaDurationUpdater` goes back to a safe estimate. - * @param {number | undefined} newDuration - */ - public updateKnownDuration( - newDuration : number | undefined - ) : void { - this._currentKnownDuration.setValueIfChanged(newDuration); - } - - /** - * Stop the `MediaDurationUpdater` from updating and free its resources. - * Once stopped, it is not possible to start it again, beside creating another - * `MediaDurationUpdater`. + * Abort the last duration-setting operation and free its resources. */ - public stop() { - this._canceller.cancel(); + public stopUpdating() { + if (this._currentMediaSourceDurationUpdateCanceller !== null) { + this._currentMediaSourceDurationUpdateCanceller.cancel(); + this._currentMediaSourceDurationUpdateCanceller = null; + } } } @@ -163,33 +145,27 @@ export default class MediaDurationUpdater { * - `null` if it hasn'nt been updated * * @param {MediaSource} mediaSource - * @param {Object} manifest + * @param {number} duration + * @param {boolean} addTimeMargin * @returns {string} */ function setMediaSourceDuration( mediaSource: MediaSource, - manifest: Manifest, - knownDuration : number | undefined + duration : number, + addTimeMargin : boolean ) : MediaSourceDurationUpdateStatus { - let newDuration = knownDuration; - - if (newDuration === undefined) { - if (manifest.isDynamic) { - newDuration = manifest.getLivePosition() ?? - manifest.getMaximumSafePosition(); - } else { - newDuration = manifest.getMaximumSafePosition(); - } - } + let newDuration = duration; - if (manifest.isDynamic) { + if (addTimeMargin) { // Some targets poorly support setting a very high number for durations. - // Yet, in dynamic contents, we would prefer setting a value as high as possible - // to still be able to seek anywhere we want to (even ahead of the Manifest if - // we want to). As such, we put it at a safe default value of 2^32 excepted - // when the maximum position is already relatively close to that value, where - // we authorize exceptionally going over it. - newDuration = Math.max(Math.pow(2, 32), newDuration + YEAR_IN_SECONDS); + // Yet, in contents whose end is not yet known (e.g. live contents), we + // would prefer setting a value as high as possible to still be able to + // seek anywhere we want to (even ahead of the Manifest if we want to). + // As such, we put it at a safe default value of 2^32 excepted when the + // maximum position is already relatively close to that value, where we + // authorize exceptionally going over it. + newDuration = Math.max(Math.pow(2, 32), + newDuration + YEAR_IN_SECONDS); } let maxBufferedEnd : number = 0; @@ -328,28 +304,28 @@ function createMediaSourceOpenReference( /** * Immediately tries to set the MediaSource's duration to the most appropriate - * one according to the Manifest and duration given. + * one. * * If it fails, wait 2 seconds and retries. * * @param {MediaSource} mediaSource - * @param {Object} manifest - * @param {number|undefined} duration + * @param {number} duration + * @param {boolean} addTimeMargin * @param {Object} cancelSignal */ function recursivelyForceDurationUpdate( mediaSource : MediaSource, - manifest : Manifest, - duration : number | undefined, + duration : number, + addTimeMargin : boolean, cancelSignal : CancellationSignal ) : void { - const res = setMediaSourceDuration(mediaSource, manifest, duration); + const res = setMediaSourceDuration(mediaSource, duration, addTimeMargin); if (res === MediaSourceDurationUpdateStatus.Success) { return ; } const timeoutId = setTimeout(() => { unregisterClear(); - recursivelyForceDurationUpdate(mediaSource, manifest, duration, cancelSignal); + recursivelyForceDurationUpdate(mediaSource, duration, addTimeMargin, cancelSignal); }, 2000); const unregisterClear = cancelSignal.register(() => { clearTimeout(timeoutId);