rx-player
Version:
Canal+ HTML5 Video Player
546 lines (545 loc) • 29.6 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 __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
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 });
exports.default = PeriodStream;
var config_1 = require("../../../config");
var errors_1 = require("../../../errors");
var log_1 = require("../../../log");
var manifest_1 = require("../../../manifest");
var array_find_1 = require("../../../utils/array_find");
var object_assign_1 = require("../../../utils/object_assign");
var queue_microtask_1 = require("../../../utils/queue_microtask");
var ranges_1 = require("../../../utils/ranges");
var reference_1 = require("../../../utils/reference");
var task_canceller_1 = require("../../../utils/task_canceller");
var segment_sinks_1 = require("../../segment_sinks");
var adaptation_1 = require("../adaptation");
var get_adaptation_switch_strategy_1 = require("./utils/get_adaptation_switch_strategy");
/**
* Create a single PeriodStream:
* - Lazily create (or reuse) a SegmentSink for the given type.
* - Create a Stream linked to an Adaptation each time it changes, to
* download and append the corresponding segments to the SegmentSink.
* - Announce when the Stream is full or is awaiting new Segments through
* events
*
* @param {Object} args - Various arguments allowing the `PeriodStream` to
* determine which Adaptation and which Representation to choose, as well as
* which segments to load from it.
* You can check the corresponding type for more information.
* @param {Object} callbacks - The `PeriodStream` relies on a system of
* callbacks that it will call on various events.
*
* Depending on the event, the caller may be supposed to perform actions to
* react upon some of them.
*
* This approach is taken instead of a more classical EventEmitter pattern to:
* - Allow callbacks to be called synchronously after the
* `AdaptationStream` is called.
* - Simplify bubbling events up, by just passing through callbacks
* - Force the caller to explicitely handle or not the different events.
*
* Callbacks may start being called immediately after the `AdaptationStream`
* call and may be called until either the `parentCancelSignal` argument is
* triggered, or until the `error` callback is called, whichever comes first.
* @param {Object} parentCancelSignal - `CancellationSignal` allowing, when
* triggered, to immediately stop all operations the `PeriodStream` is
* doing.
*/
function PeriodStream(_a, callbacks, parentCancelSignal) {
var _this = this;
var bufferType = _a.bufferType, content = _a.content, garbageCollectors = _a.garbageCollectors, playbackObserver = _a.playbackObserver, representationEstimator = _a.representationEstimator, segmentQueueCreator = _a.segmentQueueCreator, segmentSinksStore = _a.segmentSinksStore, options = _a.options, wantedBufferAhead = _a.wantedBufferAhead, maxVideoBufferSize = _a.maxVideoBufferSize;
var manifest = content.manifest, period = content.period;
/**
* Emits the chosen Adaptation and Representations for the current type.
* `null` when no Adaptation is chosen (e.g. no subtitles)
* `undefined` at the beginning (it can be ignored.).
*/
var adaptationRef = new reference_1.default(undefined, parentCancelSignal);
callbacks.periodStreamReady({
type: bufferType,
manifest: manifest,
period: period,
adaptationRef: adaptationRef,
});
if (parentCancelSignal.isCancelled()) {
return;
}
var currentStreamCanceller;
var isFirstAdaptationSwitch = true;
adaptationRef.onUpdate(function (choice) {
// As an IIFE to profit from async/await while respecting onUpdate's signature
(function () { return __awaiter(_this, void 0, void 0, function () {
var streamCanceller, segmentSinkStatus, periodEnd, adaptations, adaptation, DELTA_POSITION_AFTER_RELOAD, relativePosHasBeenDefaulted, relativePosAfterSwitch, representations, segmentSink, strategy, _a, _b, _c, start, end, e_1_1;
var e_1, _d;
var _e;
return __generator(this, function (_f) {
switch (_f.label) {
case 0:
if (choice === undefined) {
return [2 /*return*/];
}
streamCanceller = new task_canceller_1.default();
streamCanceller.linkToSignal(parentCancelSignal);
currentStreamCanceller === null || currentStreamCanceller === void 0 ? void 0 : currentStreamCanceller.cancel(); // Cancel previously created stream if one
currentStreamCanceller = streamCanceller;
if (!(choice === null)) return [3 /*break*/, 7];
// Current type is disabled for that Period
log_1.default.info("Stream", "Set no Adaptation", {
periodStart: period.start,
bufferType: bufferType,
});
segmentSinkStatus = segmentSinksStore.getStatus(bufferType);
if (!(segmentSinkStatus.type === "initialized")) return [3 /*break*/, 5];
log_1.default.info("Stream", "Clearing previous SegmentSink", {
periodStart: period.start,
bufferType: bufferType,
});
if (!segment_sinks_1.default.isNative(bufferType)) return [3 /*break*/, 1];
return [2 /*return*/, askForMediaSourceReload(0, true, streamCanceller.signal)];
case 1:
periodEnd = (_e = period.end) !== null && _e !== void 0 ? _e : Infinity;
if (!(period.start > periodEnd)) return [3 /*break*/, 2];
log_1.default.warn("Stream", "Can't free buffer: period's start is after its end", {
periodStart: period.start,
periodEnd: periodEnd,
bufferType: bufferType,
});
return [3 /*break*/, 4];
case 2: return [4 /*yield*/, segmentSinkStatus.value.removeBuffer(period.start, periodEnd)];
case 3:
_f.sent();
if (streamCanceller.isUsed()) {
return [2 /*return*/]; // The stream has been cancelled
}
_f.label = 4;
case 4: return [3 /*break*/, 6];
case 5:
if (segmentSinkStatus.type === "uninitialized") {
segmentSinksStore.disableSegmentSink(bufferType);
if (streamCanceller.isUsed()) {
return [2 /*return*/]; // The stream has been cancelled
}
}
_f.label = 6;
case 6:
callbacks.adaptationChange({
type: bufferType,
adaptation: null,
period: period,
});
if (streamCanceller.isUsed()) {
return [2 /*return*/]; // Previous call has provoken Stream cancellation by side-effect
}
return [2 /*return*/, createEmptyAdaptationStream(playbackObserver, wantedBufferAhead, bufferType, { period: period }, callbacks, streamCanceller.signal)];
case 7:
adaptations = period.adaptations[bufferType];
adaptation = (0, array_find_1.default)(adaptations !== null && adaptations !== void 0 ? adaptations : [], function (a) { return a.id === choice.adaptationId; });
if (adaptation === undefined) {
currentStreamCanceller.cancel();
log_1.default.warn("Stream", "Unfound chosen Adaptation choice", {
adaptationId: choice.adaptationId,
});
return [2 /*return*/];
}
DELTA_POSITION_AFTER_RELOAD = config_1.default.getCurrent().DELTA_POSITION_AFTER_RELOAD;
relativePosHasBeenDefaulted = false;
if (isFirstAdaptationSwitch) {
relativePosAfterSwitch = 0;
}
else if (choice.relativeResumingPosition !== undefined) {
relativePosAfterSwitch = choice.relativeResumingPosition;
}
else {
relativePosHasBeenDefaulted = true;
switch (bufferType) {
case "audio":
relativePosAfterSwitch = DELTA_POSITION_AFTER_RELOAD.trackSwitch.audio;
break;
case "video":
relativePosAfterSwitch = DELTA_POSITION_AFTER_RELOAD.trackSwitch.video;
break;
default:
relativePosAfterSwitch = DELTA_POSITION_AFTER_RELOAD.trackSwitch.other;
break;
}
}
isFirstAdaptationSwitch = false;
if (segment_sinks_1.default.isNative(bufferType) &&
segmentSinksStore.getStatus(bufferType).type === "disabled") {
return [2 /*return*/, askForMediaSourceReload(relativePosAfterSwitch, true, streamCanceller.signal)];
}
// Reload if the Adaptation disappears from the manifest
manifest.addEventListener("manifestUpdate", function (updates) {
var e_2, _a, e_3, _b;
try {
// If current period has been unexpectedly removed, ask to reload
for (var _c = __values(updates.updatedPeriods), _d = _c.next(); !_d.done; _d = _c.next()) {
var element = _d.value;
if (element.period.id === period.id) {
try {
for (var _e = (e_3 = void 0, __values(element.result.removedAdaptations)), _f = _e.next(); !_f.done; _f = _e.next()) {
var adap = _f.value;
if (adap.id === adaptation.id) {
return askForMediaSourceReload(relativePosAfterSwitch, true, streamCanceller.signal);
}
}
}
catch (e_3_1) { e_3 = { error: e_3_1 }; }
finally {
try {
if (_f && !_f.done && (_b = _e.return)) _b.call(_e);
}
finally { if (e_3) throw e_3.error; }
}
}
else if (element.period.start > period.start) {
break;
}
}
}
catch (e_2_1) { e_2 = { error: e_2_1 }; }
finally {
try {
if (_d && !_d.done && (_a = _c.return)) _a.call(_c);
}
finally { if (e_2) throw e_2.error; }
}
}, currentStreamCanceller.signal);
representations = choice.representations;
log_1.default.info("Stream", "Updating adaptation", {
bufferType: adaptation.type,
periodStart: period.start,
adaptationId: adaptation.id,
});
callbacks.adaptationChange({ type: bufferType, adaptation: adaptation, period: period });
if (streamCanceller.isUsed()) {
return [2 /*return*/]; // Previous call has provoken cancellation by side-effect
}
segmentSink = createOrReuseSegmentSink(segmentSinksStore, bufferType, adaptation);
strategy = (0, get_adaptation_switch_strategy_1.default)(segmentSink, period, adaptation, choice.switchingMode, playbackObserver, options);
if (strategy.type === "needs-reload") {
return [2 /*return*/, askForMediaSourceReload(relativePosAfterSwitch, true, streamCanceller.signal)];
}
return [4 /*yield*/, segmentSinksStore.waitForUsableBuffers(streamCanceller.signal)];
case 8:
_f.sent();
if (streamCanceller.isUsed()) {
return [2 /*return*/]; // The Stream has since been cancelled
}
if (!(strategy.type === "flush-buffer" || strategy.type === "clean-buffer")) return [3 /*break*/, 17];
_f.label = 9;
case 9:
_f.trys.push([9, 14, 15, 16]);
_a = __values(strategy.value), _b = _a.next();
_f.label = 10;
case 10:
if (!!_b.done) return [3 /*break*/, 13];
_c = _b.value, start = _c.start, end = _c.end;
return [4 /*yield*/, segmentSink.removeBuffer(start, end)];
case 11:
_f.sent();
if (streamCanceller.isUsed()) {
return [2 /*return*/]; // The Stream has since been cancelled
}
_f.label = 12;
case 12:
_b = _a.next();
return [3 /*break*/, 10];
case 13: return [3 /*break*/, 16];
case 14:
e_1_1 = _f.sent();
e_1 = { error: e_1_1 };
return [3 /*break*/, 16];
case 15:
try {
if (_b && !_b.done && (_d = _a.return)) _d.call(_a);
}
finally { if (e_1) throw e_1.error; }
return [7 /*endfinally*/];
case 16:
if (strategy.type === "flush-buffer") {
// The seek to `relativePosAfterSwitch` is only performed if strategy.type
// is "flush-buffer" because there should be no interuption when playing in
// with `clean-buffer` strategy
callbacks.needsBufferFlush({
relativeResumingPosition: relativePosAfterSwitch,
relativePosHasBeenDefaulted: relativePosHasBeenDefaulted,
});
if (streamCanceller.isUsed()) {
return [2 /*return*/]; // Previous callback cancelled the Stream by side-effect
}
}
_f.label = 17;
case 17:
garbageCollectors.get(segmentSink)(streamCanceller.signal);
createAdaptationStream(adaptation, representations, segmentSink, streamCanceller.signal);
return [2 /*return*/];
}
});
}); })().catch(function (err) {
if (err instanceof task_canceller_1.CancellationError) {
return;
}
currentStreamCanceller === null || currentStreamCanceller === void 0 ? void 0 : currentStreamCanceller.cancel();
callbacks.error(err);
});
}, { clearSignal: parentCancelSignal, emitCurrentValue: true });
/**
* @param {Object} adaptation
* @param {Object} representations
* @param {Object} segmentSink
* @param {Object} cancelSignal
*/
function createAdaptationStream(adaptation, representations, segmentSink, cancelSignal) {
var adaptationPlaybackObserver = createAdaptationStreamPlaybackObserver(playbackObserver, adaptation.type);
(0, adaptation_1.default)({
content: { manifest: manifest, period: period, adaptation: adaptation, representations: representations },
options: options,
playbackObserver: adaptationPlaybackObserver,
representationEstimator: representationEstimator,
segmentSink: segmentSink,
segmentQueueCreator: segmentQueueCreator,
wantedBufferAhead: wantedBufferAhead,
maxVideoBufferSize: maxVideoBufferSize,
}, __assign(__assign({}, callbacks), { error: onAdaptationStreamError }), cancelSignal);
function onAdaptationStreamError(error) {
// Stream linked to a non-native media buffer should not impact the
// stability of the player. ie: if a text buffer sends an error, we want
// to continue playing without any subtitles
if (!segment_sinks_1.default.isNative(bufferType)) {
log_1.default.error("Stream", "".concat(bufferType, " Stream crashed. Aborting it."), error instanceof Error ? error : "");
segmentSinksStore.disposeSegmentSink(bufferType);
var formattedError = (0, errors_1.formatError)(error, {
defaultCode: "NONE",
defaultReason: "Unknown `AdaptationStream` error",
});
callbacks.warning(formattedError);
if (cancelSignal.isCancelled()) {
return; // Previous callback cancelled the Stream by side-effect
}
return createEmptyAdaptationStream(playbackObserver, wantedBufferAhead, bufferType, { period: period }, callbacks, cancelSignal);
}
log_1.default.error("Stream", "".concat(bufferType, " Stream crashed. Stopping playback."), error instanceof Error ? error : "");
callbacks.error(error);
}
}
/**
* Regularly ask to reload the MediaSource on each playback observation
* performed by the playback observer.
*
* @param {number} timeOffset - Relative position, compared to the current
* playhead, at which we should restart playback after reloading.
* For example `-2` will reload 2 seconds before the current position.
* @param {boolean} stayInPeriod - If `true`, we will control that the position
* we reload at, after applying `timeOffset`, is still part of the Period
* `period`.
*
* If it isn't we will re-calculate that reloaded position to be:
* - either the Period's start if the calculated position is before the
* Period's start.
* - either the Period'end start if the calculated position is after the
* Period's end.
* @param {Object} cancelSignal
*/
function askForMediaSourceReload(timeOffset, stayInPeriod, cancelSignal) {
// We begin by scheduling a micro-task to reduce the possibility of race
// conditions where `askForMediaSourceReload` would be called synchronously before
// the next observation (which may reflect very different playback conditions)
// is actually received.
// It can happen when `askForMediaSourceReload` is called as a side-effect of
// the same event that triggers the playback observation to be emitted.
(0, queue_microtask_1.default)(function () {
playbackObserver.listen(function () {
if (cancelSignal.isCancelled()) {
return;
}
callbacks.waitingMediaSourceReload({
bufferType: bufferType,
period: period,
timeOffset: timeOffset,
stayInPeriod: stayInPeriod,
});
}, { includeLastObservation: true, clearSignal: cancelSignal });
});
}
}
/**
* @param {string} bufferType
* @param {Object} adaptation
* @returns {Object}
*/
function createOrReuseSegmentSink(segmentSinksStore, bufferType, adaptation) {
var segmentSinkStatus = segmentSinksStore.getStatus(bufferType);
if (segmentSinkStatus.type === "initialized") {
log_1.default.info("Stream", "Reusing a previous SegmentSink for the type", { bufferType: bufferType });
return segmentSinkStatus.value;
}
var codec = getFirstDeclaredMimeType(adaptation);
return segmentSinksStore.createSegmentSink(bufferType, codec);
}
/**
* Get mime-type string of the first representation declared in the given
* adaptation.
* @param {Adaptation} adaptation
* @returns {string}
*/
function getFirstDeclaredMimeType(adaptation) {
var representations = adaptation.representations.filter(function (r) { return r.isPlayable() !== false; });
if (representations.length > 0) {
return representations[0].getMimeTypeString();
}
else if (adaptation.representations.length > 0) {
return adaptation.representations[0].getMimeTypeString();
}
else {
var noRepErr = new errors_1.MediaError("NO_PLAYABLE_REPRESENTATION", "No Representation in the chosen " + adaptation.type + " Adaptation can be played", { tracks: [(0, manifest_1.toTaggedTrack)(adaptation)] });
throw noRepErr;
}
}
/**
* Create AdaptationStream's version of a playback observer.
* @param {Object} initialPlaybackObserver
* @param {string} trackType
* @returns {Object}
*/
function createAdaptationStreamPlaybackObserver(initialPlaybackObserver, trackType) {
return initialPlaybackObserver.deriveReadOnlyObserver(function transform(observationRef, cancellationSignal) {
var newRef = new reference_1.default(constructAdaptationStreamPlaybackObservation(), cancellationSignal);
observationRef.onUpdate(emitAdaptationStreamPlaybackObservation, {
clearSignal: cancellationSignal,
emitCurrentValue: false,
});
return newRef;
function constructAdaptationStreamPlaybackObservation() {
var baseObservation = observationRef.getValue();
var buffered = baseObservation.buffered[trackType];
var bufferGap = buffered !== null
? (0, ranges_1.getLeftSizeOfRange)(buffered, baseObservation.position.getWanted())
: 0;
return (0, object_assign_1.default)({}, baseObservation, { bufferGap: bufferGap, buffered: buffered });
}
function emitAdaptationStreamPlaybackObservation() {
newRef.setValue(constructAdaptationStreamPlaybackObservation());
}
});
}
/**
* Create empty AdaptationStream, linked to a Period.
* This AdaptationStream will never download any segment and just emit a "full"
* event when reaching the end.
* @param {Object} playbackObserver
* @param {Object} wantedBufferAhead
* @param {string} bufferType
* @param {Object} content
* @param {Object} callbacks
* @param {Object} cancelSignal
*/
function createEmptyAdaptationStream(playbackObserver, wantedBufferAhead, bufferType, content, callbacks, cancelSignal) {
var period = content.period;
var hasFinishedLoading = false;
wantedBufferAhead.onUpdate(sendStatus, {
emitCurrentValue: false,
clearSignal: cancelSignal,
});
playbackObserver.listen(sendStatus, {
includeLastObservation: false,
clearSignal: cancelSignal,
});
sendStatus();
function sendStatus() {
var observation = playbackObserver.getReference().getValue();
var wba = wantedBufferAhead.getValue();
var position = observation.position.getWanted();
if (period.end !== undefined && position + wba >= period.end) {
log_1.default.debug("Stream", 'full "empty" AdaptationStream', {
bufferType: bufferType,
periodEnd: period.end,
position: position,
wantedBufferAhead: wba,
});
hasFinishedLoading = true;
}
callbacks.streamStatusUpdate({
period: period,
bufferType: bufferType,
imminentDiscontinuity: null,
position: position,
isEmptyStream: true,
hasFinishedLoading: hasFinishedLoading,
neededSegments: [],
});
}
}