rx-player
Version:
Canal+ HTML5 Video Player
374 lines (373 loc) • 17.5 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = RepresentationStream;
/**
* This file allows to create RepresentationStreams.
*
* A RepresentationStream downloads and push segment for a single
* Representation (e.g. a single video stream of a given quality).
* It chooses which segments should be downloaded according to the current
* position and what is currently buffered.
*/
var config_1 = require("../../../config");
var log_1 = require("../../../log");
var object_assign_1 = require("../../../utils/object_assign");
var task_canceller_1 = require("../../../utils/task_canceller");
var encryption_data_notifier_1 = require("./encryption_data_notifier");
var get_buffer_status_1 = require("./utils/get_buffer_status");
var get_segment_priority_1 = require("./utils/get_segment_priority");
var push_init_segment_1 = require("./utils/push_init_segment");
var push_media_segment_1 = require("./utils/push_media_segment");
/**
* Perform the logic to load the right segments for the given Representation and
* push them to the given `SegmentSink`.
*
* In essence, this is the entry point of the core streaming logic of the
* RxPlayer, the one actually responsible for finding which are the current
* right segments to load, loading them, and pushing them so they can be decoded.
*
* Multiple RepresentationStream can run on the same SegmentSink.
* This allows for example smooth transitions between multiple periods.
*
* @param {Object} args - Various arguments allowing to know which segments to
* load, loading them and pushing them.
* You can check the corresponding type for more information.
* @param {Object} callbacks - The `RepresentationStream` 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
* `RepresentationStream` 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 `RepresentationStream`
* call and may be called until either the `parentCancelSignal` argument is
* triggered, until the `terminating` callback has been triggered AND all loaded
* segments have been pushed, or until the `error` callback is called, whichever
* comes first.
* @param {Object} parentCancelSignal - `CancellationSignal` allowing, when
* triggered, to immediately stop all operations the `RepresentationStream` is
* doing.
*/
function RepresentationStream(_a, callbacks, parentCancelSignal) {
var content = _a.content, options = _a.options, playbackObserver = _a.playbackObserver, segmentSink = _a.segmentSink, segmentQueue = _a.segmentQueue, terminate = _a.terminate;
log_1.default.debug("Stream", "Creating RepresentationStream", {
periodStart: content.period.start,
bufferType: content.adaptation.type,
adaptationId: content.adaptation.id,
representationBitrate: content.representation.bitrate,
mimeType: content.representation.getMimeTypeString(),
});
var period = content.period, adaptation = content.adaptation, representation = content.representation;
var bufferGoal = options.bufferGoal, maxBufferSize = options.maxBufferSize, drmSystemId = options.drmSystemId, fastSwitchThreshold = options.fastSwitchThreshold;
var bufferType = adaptation.type;
/** `TaskCanceller` stopping operations performed by the `RepresentationStream` */
var canceller = new task_canceller_1.default("RepresentationStream " + bufferType);
canceller.linkToSignal(parentCancelSignal);
/** Saved initialization segment state for this representation. */
var initSegmentState = {
segment: representation.index.getInitSegment(),
uniqueId: null,
isLoaded: false,
};
canceller.signal.register(function () {
// Free initialization segment if one has been declared
if (initSegmentState.uniqueId !== null) {
segmentSink.freeInitSegment(initSegmentState.uniqueId);
}
});
/** If `true`, the current Representation has a linked initialization segment. */
var hasInitSegment = initSegmentState.segment !== null;
if (!hasInitSegment) {
initSegmentState.isLoaded = true;
}
var drmNotifier = new encryption_data_notifier_1.default({
drmSystemId: drmSystemId,
representation: representation,
notify: function (encryptionData) {
return callbacks.encryptionDataEncountered(encryptionData.map(function (d) { return (0, object_assign_1.default)({ content: content }, d); }));
},
});
if (canceller.isUsed()) {
return;
}
segmentQueue.addEventListener("error", function (err) {
if (canceller.signal.isCancelled()) {
return; // ignore post requests-cancellation loading-related errors,
}
canceller.cancel("RepresentationStream: SegmentQueue err"); // Stop every operations
callbacks.error(err);
}, canceller.signal);
segmentQueue.addEventListener("parsedInitSegment", onParsedChunk, canceller.signal);
segmentQueue.addEventListener("parsedMediaSegment", onParsedChunk, canceller.signal);
segmentQueue.addEventListener("emptyQueue", checkStatus, canceller.signal);
segmentQueue.addEventListener("requestRetry", function (payload) {
callbacks.warning(payload.error);
if (canceller.signal.isCancelled()) {
return; // If the previous callback led to loading operations being stopped, skip
}
var retriedSegment = payload.segment;
var index = representation.index;
if (index.isSegmentStillAvailable(retriedSegment) === false) {
checkStatus();
}
else if (index.canBeOutOfSyncError(payload.error, retriedSegment)) {
callbacks.manifestMightBeOufOfSync();
}
}, canceller.signal);
segmentQueue.addEventListener("fullyLoadedSegment", function (segment) {
segmentSink
.signalSegmentComplete((0, object_assign_1.default)({ segment: segment }, content))
.catch(onFatalBufferError);
}, canceller.signal);
/** Emit the last scheduled downloading queue for segments. */
var segmentsToLoadRef = segmentQueue.resetForContent(content, hasInitSegment, "new RepresentationStream linked to SegmentQueue");
canceller.signal.register(function (err) {
segmentQueue.stop(err.reason);
});
playbackObserver.listen(checkStatus, {
includeLastObservation: false,
clearSignal: canceller.signal,
});
content.manifest.addEventListener("manifestUpdate", checkStatus, canceller.signal);
bufferGoal.onUpdate(checkStatus, {
emitCurrentValue: false,
clearSignal: canceller.signal,
});
maxBufferSize.onUpdate(checkStatus, {
emitCurrentValue: false,
clearSignal: canceller.signal,
});
terminate.onUpdate(checkStatus, {
emitCurrentValue: false,
clearSignal: canceller.signal,
});
checkStatus();
return;
/**
* Produce a buffer status update synchronously on call, update the list
* of current segments to update and check various buffer and manifest related
* issues at the current time, calling the right callbacks if necessary.
*/
function checkStatus() {
if (canceller.isUsed()) {
return; // Stop all buffer status checking if load operations are stopped
}
var observation = playbackObserver.getReference().getValue();
var initialWantedTime = observation.position.getWanted();
var status = (0, get_buffer_status_1.default)(content, initialWantedTime, playbackObserver, fastSwitchThreshold.getValue(), bufferGoal.getValue(), maxBufferSize.getValue(), segmentSink);
var neededSegments = status.neededSegments;
var neededInitSegment = null;
// Add initialization segment if required
if (!representation.index.isInitialized()) {
if (initSegmentState.segment === null) {
log_1.default.warn("Stream", "Uninitialized index without an initialization segment", {
bufferType: bufferType,
representationBitrate: content.representation.bitrate,
});
}
else if (initSegmentState.isLoaded) {
log_1.default.warn("Stream", "Uninitialized index with an already loaded initialization segment", {
bufferType: bufferType,
representationBitrate: content.representation.bitrate,
});
}
else {
var wantedStart = observation.position.getWanted();
neededInitSegment = {
segment: initSegmentState.segment,
priority: (0, get_segment_priority_1.default)(period.start, wantedStart),
};
}
}
else if (neededSegments.length > 0 &&
!initSegmentState.isLoaded &&
initSegmentState.segment !== null) {
var initSegmentPriority = neededSegments[0].priority;
neededInitSegment = {
segment: initSegmentState.segment,
priority: initSegmentPriority,
};
}
var terminateVal = terminate.getValue();
if (terminateVal === null) {
segmentsToLoadRef.setValue({
initSegment: neededInitSegment,
segmentQueue: neededSegments,
});
}
else if (terminateVal.urgent) {
log_1.default.debug("Stream", "Urgent switch, terminate now.", {
bufferType: bufferType,
representationBitrate: content.representation.bitrate,
});
segmentsToLoadRef.setValue({ initSegment: null, segmentQueue: [] });
segmentsToLoadRef.finish();
canceller.cancel(terminateVal.reason);
callbacks.terminating();
return;
}
else {
// Non-urgent termination wanted:
// End the download of the current media segment if pending and
// terminate once either that request is finished or another segment
// is wanted instead, whichever comes first.
var mostNeededSegment = neededSegments[0];
var initSegmentRequest = segmentQueue.getRequestedInitSegment();
var currentSegmentRequest = segmentQueue.getRequestedMediaSegment();
var nextQueue = currentSegmentRequest === null ||
mostNeededSegment === undefined ||
currentSegmentRequest.id !== mostNeededSegment.segment.id
? []
: [mostNeededSegment];
var nextInit = initSegmentRequest === null ? null : neededInitSegment;
segmentsToLoadRef.setValue({
initSegment: nextInit,
segmentQueue: nextQueue,
});
if (nextQueue.length === 0 && nextInit === null) {
log_1.default.debug("Stream", "No request left, terminate", {
bufferType: bufferType,
representationBitrate: content.representation.bitrate,
});
segmentsToLoadRef.finish();
canceller.cancel(terminateVal.reason);
callbacks.terminating();
return;
}
}
callbacks.streamStatusUpdate({
period: period,
position: observation.position.getWanted(),
bufferType: bufferType,
imminentDiscontinuity: status.imminentDiscontinuity,
isEmptyStream: false,
hasFinishedLoading: status.hasFinishedLoading,
neededSegments: status.neededSegments,
});
if (canceller.signal.isCancelled()) {
return; // previous callback has stopped loading operations by side-effect
}
var UPTO_CURRENT_POSITION_CLEANUP = config_1.default.getCurrent().UPTO_CURRENT_POSITION_CLEANUP;
if (status.isBufferFull) {
var gcedPosition = Math.max(0, initialWantedTime - UPTO_CURRENT_POSITION_CLEANUP);
if (gcedPosition > 0) {
segmentSink.removeBuffer(0, gcedPosition).catch(onFatalBufferError);
}
}
if (status.shouldRefreshManifest) {
callbacks.needsManifestRefresh();
}
}
/**
* Process a chunk that has just been parsed by pushing it to the
* SegmentSink and emitting the right events.
* @param {Object} evt
*/
function onParsedChunk(evt) {
drmNotifier.onNewProtectionData(evt.protectionData);
if (evt.segmentType === "init") {
if (!representation.index.isInitialized() && evt.segmentList !== undefined) {
representation.index.initialize(evt.segmentList);
}
initSegmentState.isLoaded = true;
if (evt.initializationData !== null) {
var initSegmentUniqueId = representation.uniqueId;
initSegmentState.uniqueId = initSegmentUniqueId;
segmentSink.declareInitSegment(initSegmentUniqueId, evt.initializationData);
(0, push_init_segment_1.default)({
playbackObserver: playbackObserver,
bufferGoal: bufferGoal,
content: content,
initSegmentUniqueId: initSegmentUniqueId,
segment: evt.segment,
segmentData: evt.initializationData,
segmentSink: segmentSink,
}, canceller.signal)
.then(function (result) {
if (result !== null) {
callbacks.addedSegment(result);
}
})
.catch(onFatalBufferError);
}
// Sometimes the segment list is only known once the initialization segment
// is parsed. Thus we immediately re-check if there's new segments to load.
checkStatus();
return;
}
else {
var inbandEvents = evt.inbandEvents, predictedSegments = evt.predictedSegments, needsManifestRefresh = evt.needsManifestRefresh;
if (predictedSegments !== undefined) {
representation.index.addPredictedSegments(predictedSegments, evt.segment);
}
if (needsManifestRefresh === true) {
callbacks.needsManifestRefresh();
if (canceller.isUsed()) {
return; // previous callback has stopped everything by side-effect
}
}
if (inbandEvents !== undefined && inbandEvents.length > 0) {
callbacks.inbandEvent(inbandEvents);
if (canceller.isUsed()) {
return; // previous callback has stopped everything by side-effect
}
}
var initSegmentUniqueId = initSegmentState.uniqueId;
(0, push_media_segment_1.default)({
playbackObserver: playbackObserver,
bufferGoal: bufferGoal,
content: content,
initSegmentUniqueId: initSegmentUniqueId,
parsedSegment: evt,
segment: evt.segment,
segmentSink: segmentSink,
}, canceller.signal)
.then(function (result) {
if (result !== null) {
callbacks.addedSegment(result);
}
})
.catch(onFatalBufferError);
}
}
/**
* Handle Buffer-related fatal errors by cancelling everything the
* `RepresentationStream` is doing and calling the error callback with the
* corresponding error.
* @param {*} err
*/
function onFatalBufferError(err) {
if (canceller.isUsed() && err instanceof task_canceller_1.CancellationError) {
// The error is linked to cancellation AND we explicitely cancelled buffer
// operations.
// We can thus ignore it, it is very unlikely to lead to true buffer issues.
return;
}
log_1.default.warn("Stream", "Received fatal buffer error", {
bufferType: bufferType,
representationBitrate: content.representation.bitrate,
}, err instanceof Error ? err : null);
canceller.cancel("RepresentationStream: fatal buffer err");
callbacks.error(err);
}
}