Skip to content

Commit

Permalink
Merge pull request #1236 from canalplus/misc/decipherability-reload-v4
Browse files Browse the repository at this point in the history
DRM: Reload when playback is unexpectedly frozen with encrypted but only decipherable data in the buffer
  • Loading branch information
peaBerberian committed Jul 4, 2023
2 parents ed373d7 + 1d32519 commit a0f15b3
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 4 deletions.
1 change: 1 addition & 0 deletions src/core/init/directfile_content_initializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
15 changes: 14 additions & 1 deletion src/core/init/media_source_content_initializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -754,11 +765,13 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
private _createRebufferingController(
playbackObserver : PlaybackObserver,
manifest : Manifest,
segmentBuffersStore : SegmentBuffersStore,
speed : IReadOnlySharedReference<number>,
cancelSignal : CancellationSignal
) : RebufferingController {
const rebufferingController = new RebufferingController(playbackObserver,
manifest,
segmentBuffersStore,
speed);
// Bubble-up events
rebufferingController.addEventListener("stalled",
Expand Down
117 changes: 114 additions & 3 deletions src/core/init/utils/rebuffering_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
IPlaybackObservation,
PlaybackObserver,
} from "../../api";
import { IBufferType } from "../../segment_buffers";
import SegmentBuffersStore, { IBufferType } from "../../segment_buffers";
import { IStallingSituation } from "../types";


Expand All @@ -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<number>;
private _isStarted : boolean;

Expand All @@ -65,23 +66,38 @@ 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.
* @param {Object} speed - The last speed set by the user
*/
constructor(
playbackObserver : PlaybackObserver,
manifest: Manifest | null,
manifest : Manifest | null,
segmentBuffersStore : SegmentBuffersStore | null,
speed : IReadOnlySharedReference<number>
) {
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 {
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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;
}
}

/**
Expand Down Expand Up @@ -581,6 +691,7 @@ class PlaybackRateUpdater {
export interface IRebufferingControllerEvent {
stalled : IStallingSituation;
unstalled : null;
needsReload : null;
warning : IPlayerError;
}

Expand Down

0 comments on commit a0f15b3

Please sign in to comment.