Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/compat/browser_detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ;
Expand Down Expand Up @@ -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/
Expand Down Expand Up @@ -136,6 +141,7 @@ export {
isIEOrEdge,
isFirefox,
isPanasonic,
isPlayStation5,
isSafariDesktop,
isSafariMobile,
isSamsungBrowser,
Expand Down
27 changes: 27 additions & 0 deletions src/compat/has_issues_with_high_media_source_duration.ts
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 2 additions & 2 deletions src/core/init/media_source_content_initializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -683,7 +683,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
});
const currentDuration = contentTimeBoundariesObserver.getCurrentDuration();
mediaSourceDurationUpdater.updateDuration(currentDuration.duration,
!currentDuration.isEnd);
currentDuration.isEnd);
return contentTimeBoundariesObserver;
}

Expand Down
50 changes: 31 additions & 19 deletions src/core/init/utils/media_source_duration_updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -75,7 +77,7 @@ export default class MediaSourceDurationUpdater {
*/
public updateDuration(
newDuration : number,
addTimeMargin : boolean
isRealEndKnown : boolean
) : void {
if (this._currentMediaSourceDurationUpdateCanceller !== null) {
this._currentMediaSourceDurationUpdateCanceller.cancel();
Expand Down Expand Up @@ -119,7 +121,7 @@ export default class MediaSourceDurationUpdater {

recursivelyForceDurationUpdate(mediaSource,
newDuration,
addTimeMargin,
isRealEndKnown,
sourceBuffersUpdatingCanceller.signal);
}, { clearSignal: msOpenStatusCanceller.signal, emitCurrentValue: true });
}
Expand All @@ -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;
Expand Down Expand Up @@ -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 : "");
Expand Down Expand Up @@ -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);
}