diff --git a/src/core/init/directfile_content_initializer.ts b/src/core/init/directfile_content_initializer.ts index 660c8c1792a..e95a903a044 100644 --- a/src/core/init/directfile_content_initializer.ts +++ b/src/core/init/directfile_content_initializer.ts @@ -118,6 +118,7 @@ export default class DirectFileContentInitializer extends ContentInitializer { * events when it cannot, as well as "unstalled" events when it get out of one. */ const rebufferingController = new RebufferingController(playbackObserver, + null, null, speed); rebufferingController.addEventListener("stalled", (evt) => diff --git a/src/core/init/media_source_content_initializer.ts b/src/core/init/media_source_content_initializer.ts index 8024bde3a02..97c49b02063 100644 --- a/src/core/init/media_source_content_initializer.ts +++ b/src/core/init/media_source_content_initializer.ts @@ -448,9 +448,20 @@ export default class MediaSourceContentInitializer extends ContentInitializer { const rebufferingController = this._createRebufferingController(playbackObserver, manifest, + segmentBuffersStore, speed, cancelSignal); - + rebufferingController.addEventListener("needsReload", () => { + // NOTE couldn't both be always calculated at event destination? + // Maybe there are exceptions? + const position = initialSeekPerformed.getValue() ? + playbackObserver.getCurrentTime() : + initialTime; + const autoplay = initialPlayPerformed.getValue() ? + !playbackObserver.getIsPaused() : + autoPlay; + onReloadOrder({ position, autoPlay: autoplay }); + }, cancelSignal); const contentTimeBoundariesObserver = this ._createContentTimeBoundariesObserver(manifest, mediaSource, @@ -754,11 +765,13 @@ export default class MediaSourceContentInitializer extends ContentInitializer { private _createRebufferingController( playbackObserver : PlaybackObserver, manifest : Manifest, + segmentBuffersStore : SegmentBuffersStore, speed : IReadOnlySharedReference, cancelSignal : CancellationSignal ) : RebufferingController { const rebufferingController = new RebufferingController(playbackObserver, manifest, + segmentBuffersStore, speed); // Bubble-up events rebufferingController.addEventListener("stalled", diff --git a/src/core/init/utils/rebuffering_controller.ts b/src/core/init/utils/rebuffering_controller.ts index f1753b8c2b5..84b120a9c08 100644 --- a/src/core/init/utils/rebuffering_controller.ts +++ b/src/core/init/utils/rebuffering_controller.ts @@ -30,7 +30,7 @@ import { IPlaybackObservation, PlaybackObserver, } from "../../api"; -import { IBufferType } from "../../segment_buffers"; +import SegmentBuffersStore, { IBufferType } from "../../segment_buffers"; import { IStallingSituation } from "../types"; @@ -54,6 +54,7 @@ export default class RebufferingController /** Emit the current playback conditions */ private _playbackObserver : PlaybackObserver; private _manifest : Manifest | null; + private _segmentBuffersStore : SegmentBuffersStore | null; private _speed : IReadOnlySharedReference; private _isStarted : boolean; @@ -65,6 +66,18 @@ export default class RebufferingController private _canceller : TaskCanceller; + /** + * If set to something else than `null`, this is the DOMHighResTimestamp as + * outputed by `performance.now()` when playback begin to seem to not start + * despite having decipherable data in the buffer(s). + * + * If enough time in that condition is spent, special considerations are + * taken at which point `_currentFreezeTimestamp` is reset to `null`. + * + * It is also reset to `null` when and if there is no such issue anymore. + */ + private _currentFreezeTimestamp : number | null; + /** * @param {object} playbackObserver - emit the current playback conditions. * @param {Object} manifest - The Manifest of the currently-played content. @@ -72,16 +85,19 @@ export default class RebufferingController */ constructor( playbackObserver : PlaybackObserver, - manifest: Manifest | null, + manifest : Manifest | null, + segmentBuffersStore : SegmentBuffersStore | null, speed : IReadOnlySharedReference ) { super(); this._playbackObserver = playbackObserver; this._manifest = manifest; + this._segmentBuffersStore = segmentBuffersStore; this._speed = speed; this._discontinuitiesStore = []; this._isStarted = false; this._canceller = new TaskCanceller(); + this._currentFreezeTimestamp = null; } public start() : void { @@ -154,6 +170,10 @@ export default class RebufferingController Math.max(observation.pendingInternalSeek ?? 0, observation.position) : null; + if (this._checkDecipherabilityFreeze(observation)) { + return ; + } + if (freezing !== null) { const now = performance.now(); @@ -215,7 +235,7 @@ export default class RebufferingController this.trigger("stalled", stalledReason); return ; } else { - log.warn("Init: ignored stall for too long, checking discontinuity", + log.warn("Init: ignored stall for too long, considering it", now - ignoredStallTimeStamp); } } @@ -358,6 +378,96 @@ export default class RebufferingController public destroy() : void { this._canceller.cancel(); } + + /** + * Support of contents with DRM on all the platforms out there is a pain in + * the *ss considering all the DRM-related bugs there are. + * + * We found out a frequent issue which is to be unable to play despite having + * all the decryption keys to play what is currently buffered. + * When this happens, re-creating the buffers from scratch, with a reload, is + * usually sufficient to unlock the situation. + * + * Although we prefer providing more targeted fixes or telling to platform + * developpers to fix their implementation, it's not always possible. + * We thus resorted to developping an heuristic which detects such situation + * and reload in that case. + * + * @param {Object} observation - The last playback observation produced, it + * has to be recent (just triggered for example). + * @returns {boolean} - Returns `true` if it seems to be such kind of + * decipherability freeze, in which case this method already performed the + * right handling steps. + */ + private _checkDecipherabilityFreeze( + observation : IPlaybackObservation + ): boolean { + const { readyState, + rebuffering, + freezing } = observation; + const bufferGap = observation.bufferGap !== undefined && + isFinite(observation.bufferGap) ? observation.bufferGap : + 0; + if ( + this._segmentBuffersStore === null || + bufferGap < 6 || + (rebuffering === null && freezing === null) || + readyState > 1 + ) { + this._currentFreezeTimestamp = null; + return false; + } + + const now = performance.now(); + if (this._currentFreezeTimestamp === null) { + this._currentFreezeTimestamp = now; + } + const rebufferingForTooLong = + rebuffering !== null && now - rebuffering.timestamp > 4000; + const frozenForTooLong = + freezing !== null && now - freezing.timestamp > 4000; + + if ( + (rebufferingForTooLong || frozenForTooLong) && + performance.now() - this._currentFreezeTimestamp > 4000 + ) { + const statusAudio = this._segmentBuffersStore.getStatus("audio"); + const statusVideo = this._segmentBuffersStore.getStatus("video"); + let hasOnlyDecipherableSegments = true; + let isClear = true; + for (const status of [statusAudio, statusVideo]) { + if (status.type === "initialized") { + for (const segment of status.value.getInventory()) { + const { representation } = segment.infos; + if (representation.decipherable === false) { + log.warn( + "Init: we have undecipherable segments left in the buffer, reloading" + ); + this._currentFreezeTimestamp = null; + this.trigger("needsReload", null); + return true; + } else if (representation.contentProtections !== undefined) { + isClear = false; + if (representation.decipherable !== true) { + hasOnlyDecipherableSegments = false; + } + } + } + } + } + + if (!isClear && hasOnlyDecipherableSegments) { + log.warn( + "Init: we are frozen despite only having decipherable " + + "segments left in the buffer, reloading" + ); + this._currentFreezeTimestamp = null; + this.trigger("needsReload", null); + return true; + } + } + return false; + } } /** @@ -581,6 +691,7 @@ class PlaybackRateUpdater { export interface IRebufferingControllerEvent { stalled : IStallingSituation; unstalled : null; + needsReload : null; warning : IPlayerError; }