rx-player
Version:
Canal+ HTML5 Video Player
491 lines (490 loc) • 22.4 kB
JavaScript
"use strict";
/**
* Copyright 2015 CANAL+ Group
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __values = (this && this.__values) || function(o) {
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
if (m) return m.call(o);
if (o && typeof o.length === "number") return {
next: function () {
if (o && i >= o.length) o = void 0;
return { value: o && o[i++], done: !o };
}
};
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
};
Object.defineProperty(exports, "__esModule", { value: true });
var is_seeking_approximate_1 = require("../../../compat/is_seeking_approximate");
var config_1 = require("../../../config");
var errors_1 = require("../../../errors");
var log_1 = require("../../../log");
var manifest_1 = require("../../../manifest");
var event_emitter_1 = require("../../../utils/event_emitter");
var monotonic_timestamp_1 = require("../../../utils/monotonic_timestamp");
var ranges_1 = require("../../../utils/ranges");
var task_canceller_1 = require("../../../utils/task_canceller");
/**
* Work-around rounding errors with floating points by setting an acceptable,
* very short, deviation when checking equalities.
*/
var EPSILON = 1 / 60;
/**
* Monitor playback, trying to avoid stalling situation.
* If stopping the player to build buffer is needed, temporarily set the
* playback rate (i.e. speed) at `0` until enough buffer is available again.
*
* Emit "stalled" then "unstalled" respectively when an unavoidable stall is
* encountered and exited.
*/
var RebufferingController = /** @class */ (function (_super) {
__extends(RebufferingController, _super);
/**
* @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
*/
function RebufferingController(playbackObserver, manifest, speed) {
var _this = _super.call(this) || this;
_this._playbackObserver = playbackObserver;
_this._manifest = manifest;
_this._speed = speed;
_this._discontinuitiesStore = [];
_this._isStarted = false;
_this._canceller = new task_canceller_1.default();
return _this;
}
RebufferingController.prototype.start = function () {
var _this = this;
if (this._isStarted) {
return;
}
this._isStarted = true;
var playbackRateUpdater = new PlaybackRateUpdater(this._playbackObserver, this._speed);
this._canceller.signal.register(function () {
playbackRateUpdater.dispose();
});
this._playbackObserver.listen(function (observation) {
var discontinuitiesStore = _this._discontinuitiesStore;
var buffered = observation.buffered, position = observation.position, readyState = observation.readyState, rebuffering = observation.rebuffering, freezing = observation.freezing;
var _a = config_1.default.getCurrent(), BUFFER_DISCONTINUITY_THRESHOLD = _a.BUFFER_DISCONTINUITY_THRESHOLD, FREEZING_STALLED_DELAY = _a.FREEZING_STALLED_DELAY;
if (freezing !== null) {
var now = (0, monotonic_timestamp_1.default)();
if (now - freezing.timestamp > FREEZING_STALLED_DELAY) {
if (rebuffering === null) {
playbackRateUpdater.stopRebuffering();
}
else {
playbackRateUpdater.startRebuffering();
}
_this.trigger("stalled", "freezing");
return;
}
}
if (rebuffering === null) {
playbackRateUpdater.stopRebuffering();
if (readyState === 1) {
// With a readyState set to 1, we should still not be able to play:
// Return that we're stalled
var reason = void 0;
if (observation.seeking !== 0 /* SeekingState.None */) {
reason =
observation.seeking === 1 /* SeekingState.Internal */
? "internal-seek"
: "seeking";
}
else {
reason = "not-ready";
}
_this.trigger("stalled", reason);
return;
}
_this.trigger("unstalled", null);
return;
}
// We want to separate a stall situation when a seek is due to a seek done
// internally by the player to when its due to a regular user seek.
var stalledReason = rebuffering.reason === "seeking" &&
observation.seeking === 1 /* SeekingState.Internal */
? "internal-seek"
: rebuffering.reason;
if (position.isAwaitingFuturePosition()) {
playbackRateUpdater.stopRebuffering();
log_1.default.debug("Init", "let rebuffering happen as we're awaiting a future position", {
wantedPosition: position.getWanted(),
lastPolledPosition: position.getPolled(),
});
}
else {
playbackRateUpdater.startRebuffering();
}
if (_this._manifest === null ||
((0, is_seeking_approximate_1.default)() &&
// Don't handle discontinuities on devices with broken seeks before
// enough time have passed because seeking brings more risks to
// lead to a lengthy rebuffering-exiting process
(0, monotonic_timestamp_1.default)() - rebuffering.timestamp <= 1000)) {
_this.trigger("stalled", stalledReason);
return;
}
/** Position at which data is awaited. */
var stalledPosition = rebuffering.position;
/**
* We may still be in the process of waiting for a position to be seeked
* to. When calculating a potential position to e.g. skip over
* discontinuities, we should compare it to that "target" position if
* one, not the one we're currently playing.
*/
var targetTime = observation.position.isAwaitingFuturePosition()
? observation.position.getWanted()
: _this._playbackObserver.getCurrentTime();
if (stalledPosition !== null &&
stalledPosition !== undefined &&
_this._speed.getValue() > 0) {
var skippableDiscontinuity = findSeekableDiscontinuity(discontinuitiesStore, _this._manifest, stalledPosition);
if (skippableDiscontinuity !== null) {
var realSeekTime = skippableDiscontinuity + 0.001;
if (realSeekTime <= targetTime) {
log_1.default.info("Init", "position to seek already reached, no seeking", {
targetTime: targetTime,
realSeekTime: realSeekTime,
});
}
else {
log_1.default.warn("Init", "skippable discontinuity found in the stream", {
lastPolledPosition: position.getPolled(),
realSeekTime: realSeekTime,
});
_this._playbackObserver.setCurrentTime(realSeekTime);
_this.trigger("warning", generateDiscontinuityError(stalledPosition, realSeekTime));
return;
}
}
}
var positionBlockedAt = stalledPosition !== null && stalledPosition !== void 0 ? stalledPosition : position.getPolled();
// Is it a very short discontinuity in buffer ? -> Seek at the beginning of the
// next range
//
// Discontinuity check in case we are close a buffered range but still
// calculate a stalled state. This is useful for some
// implementation that might drop an injected segment, or in
// case of small discontinuity in the content.
var nextBufferRangeGap = (0, ranges_1.getNextBufferedTimeRangeGap)(buffered, positionBlockedAt);
if ((!(0, is_seeking_approximate_1.default)() ||
(0, monotonic_timestamp_1.default)() - rebuffering.timestamp > 1000) &&
_this._speed.getValue() > 0 &&
nextBufferRangeGap < BUFFER_DISCONTINUITY_THRESHOLD) {
var seekTo = positionBlockedAt + nextBufferRangeGap + EPSILON;
if (targetTime < seekTo) {
log_1.default.warn("Init", "discontinuity encountered inferior to the threshold", {
positionBlockedAt: positionBlockedAt,
seekTo: seekTo,
BUFFER_DISCONTINUITY_THRESHOLD: BUFFER_DISCONTINUITY_THRESHOLD,
});
_this._playbackObserver.setCurrentTime(seekTo);
_this.trigger("warning", generateDiscontinuityError(positionBlockedAt, seekTo));
return;
}
}
// Are we in a discontinuity between periods ? -> Seek at the beginning of the
// next period
for (var i = _this._manifest.periods.length - 2; i >= 0; i--) {
var period = _this._manifest.periods[i];
if (period.end !== undefined && period.end <= positionBlockedAt) {
if (_this._manifest.periods[i + 1].start > positionBlockedAt &&
_this._manifest.periods[i + 1].start > targetTime) {
var nextPeriod = _this._manifest.periods[i + 1];
_this._playbackObserver.setCurrentTime(nextPeriod.start);
_this.trigger("warning", generateDiscontinuityError(positionBlockedAt, nextPeriod.start));
return;
}
break;
}
}
_this.trigger("stalled", stalledReason);
}, { includeLastObservation: true, clearSignal: this._canceller.signal });
};
/**
* Update information on an upcoming discontinuity for a given buffer type and
* Period.
* Each new update for the same Period and type overwrites the previous one.
* @param {Object} evt
*/
RebufferingController.prototype.updateDiscontinuityInfo = function (evt) {
if (!this._isStarted) {
this.start();
}
var lastObservation = this._playbackObserver.getReference().getValue();
updateDiscontinuitiesStore(this._discontinuitiesStore, evt, lastObservation);
};
/**
* Function to call when a Stream is currently locked, i.e. we cannot load
* segments for the corresponding Period and buffer type until it is seeked
* to.
* @param {string} bufferType - Buffer type for which no segment will
* currently load.
* @param {Object} period - Period for which no segment will currently load.
*/
RebufferingController.prototype.onLockedStream = function (bufferType, period) {
var _a;
if (!this._isStarted) {
this.start();
}
var observation = this._playbackObserver.getReference().getValue();
if (observation.rebuffering === null ||
observation.paused ||
this._speed.getValue() <= 0 ||
(bufferType !== "audio" && bufferType !== "video")) {
return;
}
var loadedPos = observation.position.getWanted();
var rebufferingPos = (_a = observation.rebuffering.position) !== null && _a !== void 0 ? _a : loadedPos;
var lockedPeriodStart = period.start;
if (loadedPos < lockedPeriodStart &&
Math.abs(rebufferingPos - lockedPeriodStart) < 1) {
log_1.default.warn("Init", "rebuffering because of a future locked stream.\n" +
"Trying to unlock by seeking to the next Period");
this._playbackObserver.setCurrentTime(lockedPeriodStart + 0.001);
}
};
/**
* Stops the `RebufferingController` from montoring stalling situations,
* forever.
*/
RebufferingController.prototype.destroy = function () {
this._canceller.cancel();
};
return RebufferingController;
}(event_emitter_1.default));
exports.default = RebufferingController;
/**
* @param {Array.<Object>} discontinuitiesStore
* @param {Object} manifest
* @param {number} stalledPosition
* @returns {number|null}
*/
function findSeekableDiscontinuity(discontinuitiesStore, manifest, stalledPosition) {
var e_1, _a;
if (discontinuitiesStore.length === 0) {
return null;
}
var maxDiscontinuityEnd = null;
try {
for (var discontinuitiesStore_1 = __values(discontinuitiesStore), discontinuitiesStore_1_1 = discontinuitiesStore_1.next(); !discontinuitiesStore_1_1.done; discontinuitiesStore_1_1 = discontinuitiesStore_1.next()) {
var discontinuityInfo = discontinuitiesStore_1_1.value;
var period = discontinuityInfo.period;
if (period.start > stalledPosition) {
return maxDiscontinuityEnd;
}
var discontinuityEnd = void 0;
if (period.end === undefined || period.end > stalledPosition) {
var discontinuity = discontinuityInfo.discontinuity, position = discontinuityInfo.position;
var start = discontinuity.start, end = discontinuity.end;
var discontinuityLowerLimit = start !== null && start !== void 0 ? start : position;
if (stalledPosition >= discontinuityLowerLimit - EPSILON) {
if (end === null) {
var nextPeriod = (0, manifest_1.getPeriodAfter)(manifest, period);
if (nextPeriod !== null) {
discontinuityEnd = nextPeriod.start + EPSILON;
}
else {
log_1.default.warn("Init", "discontinuity at Period's end but no next Period", {
periodId: period.id,
});
}
}
else if (stalledPosition < end + EPSILON) {
discontinuityEnd = end + EPSILON;
}
}
if (discontinuityEnd !== undefined) {
log_1.default.info("Init", "discontinuity found", { stalledPosition: stalledPosition, discontinuityEnd: discontinuityEnd });
maxDiscontinuityEnd =
maxDiscontinuityEnd !== null && maxDiscontinuityEnd > discontinuityEnd
? maxDiscontinuityEnd
: discontinuityEnd;
}
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (discontinuitiesStore_1_1 && !discontinuitiesStore_1_1.done && (_a = discontinuitiesStore_1.return)) _a.call(discontinuitiesStore_1);
}
finally { if (e_1) throw e_1.error; }
}
return maxDiscontinuityEnd;
}
/**
* Return `true` if the given event indicates that a discontinuity is present.
* @param {Object} evt
* @returns {Array.<Object>}
*/
function eventContainsDiscontinuity(evt) {
return evt.discontinuity !== null;
}
/**
* Update the `discontinuitiesStore` Object with the given event information:
*
* - If that event indicates than no discontinuity is found for a Period
* and buffer type, remove a possible existing discontinuity for that
* combination.
*
* - If that event indicates that a discontinuity can be found for a Period
* and buffer type, replace previous occurences for that combination and
* store it in Period's chronological order in the Array.
* @param {Array.<Object>} discontinuitiesStore
* @param {Object} evt
* @param {Object} observation
* @returns {Array.<Object>}
*/
function updateDiscontinuitiesStore(discontinuitiesStore, evt, observation) {
var gcTime = Math.min(observation.position.getPolled(), observation.position.getWanted());
// First, perform clean-up of old discontinuities
while (discontinuitiesStore.length > 0 &&
discontinuitiesStore[0].period.end !== undefined &&
discontinuitiesStore[0].period.end + 10 < gcTime) {
discontinuitiesStore.shift();
}
var period = evt.period, bufferType = evt.bufferType;
if (bufferType !== "audio" && bufferType !== "video") {
return;
}
for (var i = 0; i < discontinuitiesStore.length; i++) {
if (discontinuitiesStore[i].period.id === period.id) {
if (discontinuitiesStore[i].bufferType === bufferType) {
if (!eventContainsDiscontinuity(evt)) {
discontinuitiesStore.splice(i, 1);
}
else {
discontinuitiesStore[i] = evt;
}
return;
}
}
else if (discontinuitiesStore[i].period.start > period.start) {
if (eventContainsDiscontinuity(evt)) {
discontinuitiesStore.splice(i, 0, evt);
}
return;
}
}
if (eventContainsDiscontinuity(evt)) {
discontinuitiesStore.push(evt);
}
return;
}
/**
* Generate error emitted when a discontinuity has been encountered.
* @param {number} stalledPosition
* @param {number} seekTo
* @returns {Error}
*/
function generateDiscontinuityError(stalledPosition, seekTo) {
return new errors_1.MediaError("DISCONTINUITY_ENCOUNTERED", "A discontinuity has been encountered at position " +
String(stalledPosition) +
", seeking at position " +
String(seekTo));
}
/**
* Manage playback speed, allowing to force a playback rate of `0` when
* rebuffering is wanted.
*
* Only one `PlaybackRateUpdater` should be created per HTMLMediaElement.
* Note that the `PlaybackRateUpdater` reacts to playback event and wanted
* speed change. You should call its `dispose` method once you don't need it
* anymore.
* @class PlaybackRateUpdater
*/
var PlaybackRateUpdater = /** @class */ (function () {
/**
* Create a new `PlaybackRateUpdater`.
* @param {Object} playbackObserver
* @param {Object} speed
*/
function PlaybackRateUpdater(playbackObserver, speed) {
this._speedUpdateCanceller = new task_canceller_1.default();
this._isRebuffering = false;
this._playbackObserver = playbackObserver;
this._isDisposed = false;
this._speed = speed;
this._updateSpeed();
}
/**
* Force the playback rate to `0`, to start a rebuffering phase.
*
* You can call `stopRebuffering` when you want the rebuffering phase to end.
*/
PlaybackRateUpdater.prototype.startRebuffering = function () {
if (this._isRebuffering || this._isDisposed) {
return;
}
this._isRebuffering = true;
this._speedUpdateCanceller.cancel();
log_1.default.info("Init", "Pause playback to build buffer");
this._playbackObserver.setPlaybackRate(0);
};
/**
* If in a rebuffering phase (during which the playback rate is forced to
* `0`), exit that phase to apply the wanted playback rate instead.
*
* Do nothing if not in a rebuffering phase.
*/
PlaybackRateUpdater.prototype.stopRebuffering = function () {
if (!this._isRebuffering || this._isDisposed) {
return;
}
this._isRebuffering = false;
this._speedUpdateCanceller = new task_canceller_1.default();
this._updateSpeed();
};
/**
* The `PlaybackRateUpdater` allocate resources to for example listen to
* wanted speed changes and react to it.
*
* Consequently, you should call the `dispose` method, when you don't want the
* `PlaybackRateUpdater` to have an effect anymore.
*/
PlaybackRateUpdater.prototype.dispose = function () {
this._speedUpdateCanceller.cancel();
this._isDisposed = true;
};
PlaybackRateUpdater.prototype._updateSpeed = function () {
var _this = this;
this._speed.onUpdate(function (lastSpeed) {
log_1.default.info("Init", "Resume playback speed", { newSpeed: lastSpeed });
_this._playbackObserver.setPlaybackRate(lastSpeed);
}, {
clearSignal: this._speedUpdateCanceller.signal,
emitCurrentValue: true,
});
};
return PlaybackRateUpdater;
}());