diff --git a/src/compat/browser_detection.ts b/src/compat/browser_detection.ts index 87dd321829..4215b27d2f 100644 --- a/src/compat/browser_detection.ts +++ b/src/compat/browser_detection.ts @@ -60,6 +60,9 @@ let isWebOs2022 = false; /** `true` for Panasonic devices. */ let isPanasonic = false; +/** `true` for the PlayStation 5 game console. */ +let isPlayStation5 = false; + ((function findCurrentBrowser() : void { if (isNode) { return ; @@ -101,7 +104,9 @@ let isPanasonic = false; isSamsungBrowser = true; } - if (/Tizen/.test(navigator.userAgent)) { + if (navigator.userAgent.indexOf("PlayStation 5") !== -1) { + isPlayStation5 = true; + } else if (/Tizen/.test(navigator.userAgent)) { isTizen = true; // Inspired form: http://webostv.developer.lge.com/discover/specifications/web-engine/ @@ -136,6 +141,7 @@ export { isIEOrEdge, isFirefox, isPanasonic, + isPlayStation5, isSafariDesktop, isSafariMobile, isSamsungBrowser, diff --git a/src/compat/has_issues_with_high_media_source_duration.ts b/src/compat/has_issues_with_high_media_source_duration.ts new file mode 100644 index 0000000000..e3a4007289 --- /dev/null +++ b/src/compat/has_issues_with_high_media_source_duration.ts @@ -0,0 +1,27 @@ +import { isPlayStation5 } from "./browser_detection"; + +/** + * Some platforms have issues when the `MediaSource`'s `duration` property + * is set to a very high value (playback freezes) but not when setting it + * to `Infinity`, which is what the HTML spec as of now (2023-05-15) recommends + * for live contents. + * + * However setting the `MediaSource`'s `duration` property to `Infinity` seems + * more risky, considering all platforms we now support, than setting it at a + * relatively high ~2**32 value which is what we do generally. + * + * Moreover, setting it to `Infinity` require us to use another MSE API, + * `setLiveSeekableRange` to properly allow seeking. We're used to MSE issues so + * I'm not too confident of using another MSE API for all platforms directly. + * + * So this methods just return `true` based on a whitelist of platform for which + * it has been detected that high `duration` values cause issues but setting it + * to Infinity AND playing with `setLiveSeekableRange` does not. + * + * @returns {boolean} + */ +export default function hasIssuesWithHighMediaSourceDuration(): boolean { + // For now only seen on the Webkit present in the PlayStation 5, for which the + // alternative is known to work. + return isPlayStation5; +} diff --git a/src/core/init/media_source_content_initializer.ts b/src/core/init/media_source_content_initializer.ts index 914bf6c539..441fb98da5 100644 --- a/src/core/init/media_source_content_initializer.ts +++ b/src/core/init/media_source_content_initializer.ts @@ -664,7 +664,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer { this.trigger("activePeriodChanged", { period }); }); contentTimeBoundariesObserver.addEventListener("durationUpdate", (newDuration) => { - mediaSourceDurationUpdater.updateDuration(newDuration.duration, !newDuration.isEnd); + mediaSourceDurationUpdater.updateDuration(newDuration.duration, newDuration.isEnd); }); contentTimeBoundariesObserver.addEventListener("endOfStream", () => { if (endOfStreamCanceller === null) { @@ -683,7 +683,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer { }); const currentDuration = contentTimeBoundariesObserver.getCurrentDuration(); mediaSourceDurationUpdater.updateDuration(currentDuration.duration, - !currentDuration.isEnd); + currentDuration.isEnd); return contentTimeBoundariesObserver; } diff --git a/src/core/init/utils/media_source_duration_updater.ts b/src/core/init/utils/media_source_duration_updater.ts index c8ce07a89e..44b00fbc3a 100644 --- a/src/core/init/utils/media_source_duration_updater.ts +++ b/src/core/init/utils/media_source_duration_updater.ts @@ -19,6 +19,8 @@ import { onSourceEnded, onSourceClose, } from "../../../compat/event_listeners"; +/* eslint-disable-next-line max-len */ +import hasIssuesWithHighMediaSourceDuration from "../../../compat/has_issues_with_high_media_source_duration"; import log from "../../../log"; import createSharedReference, { IReadOnlySharedReference, @@ -65,7 +67,7 @@ export default class MediaSourceDurationUpdater { * which `duration` attribute should be set on the `MediaSource` associated * * @param {number} newDuration - * @param {boolean} addTimeMargin - If set to `true`, the current content is + * @param {boolean} isRealEndKnown - If set to `false`, 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 @@ -75,7 +77,7 @@ export default class MediaSourceDurationUpdater { */ public updateDuration( newDuration : number, - addTimeMargin : boolean + isRealEndKnown : boolean ) : void { if (this._currentMediaSourceDurationUpdateCanceller !== null) { this._currentMediaSourceDurationUpdateCanceller.cancel(); @@ -119,7 +121,7 @@ export default class MediaSourceDurationUpdater { recursivelyForceDurationUpdate(mediaSource, newDuration, - addTimeMargin, + isRealEndKnown, sourceBuffersUpdatingCanceller.signal); }, { clearSignal: msOpenStatusCanceller.signal, emitCurrentValue: true }); } @@ -146,26 +148,20 @@ export default class MediaSourceDurationUpdater { * * @param {MediaSource} mediaSource * @param {number} duration - * @param {boolean} addTimeMargin + * @param {boolean} isRealEndKnown * @returns {string} */ function setMediaSourceDuration( mediaSource: MediaSource, duration : number, - addTimeMargin : boolean + isRealEndKnown : boolean ) : MediaSourceDurationUpdateStatus { let newDuration = duration; - if (addTimeMargin) { - // Some targets poorly support setting a very high number for durations. - // 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); + if (!isRealEndKnown) { + newDuration = hasIssuesWithHighMediaSourceDuration() ? + Infinity : + getMaximumLiveSeekablePosition(duration); } let maxBufferedEnd : number = 0; @@ -198,6 +194,10 @@ function setMediaSourceDuration( try { log.info("Init: Updating duration", newDuration); mediaSource.duration = newDuration; + if (mediaSource.readyState === "open" && !isFinite(newDuration)) { + mediaSource.setLiveSeekableRange(0, + getMaximumLiveSeekablePosition(duration)); + } } catch (err) { log.warn("Duration Updater: Can't update duration on the MediaSource.", err instanceof Error ? err : ""); @@ -310,24 +310,36 @@ function createMediaSourceOpenReference( * * @param {MediaSource} mediaSource * @param {number} duration - * @param {boolean} addTimeMargin + * @param {boolean} isRealEndKnown * @param {Object} cancelSignal */ function recursivelyForceDurationUpdate( mediaSource : MediaSource, duration : number, - addTimeMargin : boolean, + isRealEndKnown : boolean, cancelSignal : CancellationSignal ) : void { - const res = setMediaSourceDuration(mediaSource, duration, addTimeMargin); + const res = setMediaSourceDuration(mediaSource, duration, isRealEndKnown); if (res === MediaSourceDurationUpdateStatus.Success) { return ; } const timeoutId = setTimeout(() => { unregisterClear(); - recursivelyForceDurationUpdate(mediaSource, duration, addTimeMargin, cancelSignal); + recursivelyForceDurationUpdate(mediaSource, duration, isRealEndKnown, cancelSignal); }, 2000); const unregisterClear = cancelSignal.register(() => { clearTimeout(timeoutId); }); } + +function getMaximumLiveSeekablePosition(contentLastPosition : number) : number { + // Some targets poorly support setting a very high number for seekable + // ranges. + // 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. + return Math.max(Math.pow(2, 32), contentLastPosition + YEAR_IN_SECONDS); +}