rx-player
Version:
Canal+ HTML5 Video Player
426 lines (425 loc) • 20.7 kB
JavaScript
;
/**
* 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 __());
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
var log_1 = require("../../../log");
var assert_1 = require("../../../utils/assert");
var event_emitter_1 = require("../../../utils/event_emitter");
var noop_1 = require("../../../utils/noop");
var object_assign_1 = require("../../../utils/object_assign");
var reference_1 = require("../../../utils/reference");
var task_canceller_1 = require("../../../utils/task_canceller");
/**
* Class scheduling segment downloads as a FIFO queue.
*/
var SegmentQueue = /** @class */ (function (_super) {
__extends(SegmentQueue, _super);
/**
* Create a new `SegmentQueue`.
*
* @param {Object} segmentFetcher - Interface to facilitate the download of
* segments.
* @param {Object} isMediaSegmentQueueInterrupted - Reference to a boolean indicating
* if the media segment queue is interrupted.
*/
function SegmentQueue(segmentFetcher, isMediaSegmentQueueInterrupted) {
var _this = _super.call(this) || this;
_this._segmentFetcher = segmentFetcher;
_this._currentContentInfo = null;
_this.isMediaSegmentQueueInterrupted = isMediaSegmentQueueInterrupted;
return _this;
}
/**
* Returns the initialization segment currently being requested.
* Returns `null` if no initialization segment request is pending.
* @returns {Object | null}
*/
SegmentQueue.prototype.getRequestedInitSegment = function () {
var _a, _b, _c;
return (_c = (_b = (_a = this._currentContentInfo) === null || _a === void 0 ? void 0 : _a.initSegmentRequest) === null || _b === void 0 ? void 0 : _b.segment) !== null && _c !== void 0 ? _c : null;
};
/**
* Returns the media segment currently being requested.
* Returns `null` if no media segment request is pending.
* @returns {Object | null}
*/
SegmentQueue.prototype.getRequestedMediaSegment = function () {
var _a, _b, _c;
return (_c = (_b = (_a = this._currentContentInfo) === null || _a === void 0 ? void 0 : _a.mediaSegmentRequest) === null || _b === void 0 ? void 0 : _b.segment) !== null && _c !== void 0 ? _c : null;
};
/**
* Return an object allowing to schedule segment requests linked to the given
* content.
* The `SegmentQueue` will emit events as it loads and parses initialization
* and media segments.
*
* Calling this method resets all previous queues that were previously started
* on the same instance.
*
* @param {Object} content - The context of the Representation you want to
* load segments for.
* @param {boolean} hasInitSegment - Declare that an initialization segment
* will need to be downloaded.
*
* A `SegmentQueue` ALWAYS wait for the initialization segment to be
* loaded and parsed before parsing a media segment.
*
* In cases where no initialization segment exist, this would lead to the
* `SegmentQueue` waiting indefinitely for it.
*
* By setting that value to `false`, you anounce to the `SegmentQueue`
* that it should not wait for an initialization segment before parsing a
* media segment.
* @returns {Object} - `SharedReference` on which the queue of segment for
* that content can be communicated and updated. See type for more
* information.
*/
SegmentQueue.prototype.resetForContent = function (content, hasInitSegment) {
var _this = this;
var _a;
(_a = this._currentContentInfo) === null || _a === void 0 ? void 0 : _a.currentCanceller.cancel();
var downloadQueue = new reference_1.default({
initSegment: null,
segmentQueue: [],
});
var currentCanceller = new task_canceller_1.default();
currentCanceller.signal.register(function () {
downloadQueue.finish();
});
var currentContentInfo = {
content: content,
downloadQueue: downloadQueue,
initSegmentInfoRef: hasInitSegment
? new reference_1.default(undefined)
: new reference_1.default(null),
currentCanceller: currentCanceller,
initSegmentRequest: null,
mediaSegmentRequest: null,
mediaSegmentAwaitingInitMetadata: null,
};
this._currentContentInfo = currentContentInfo;
this.isMediaSegmentQueueInterrupted.onUpdate(function (val) {
if (!val) {
log_1.default.debug("SQ: Media segment can be loaded again, restarting queue.", content.adaptation.type);
_this._restartMediaSegmentDownloadingQueue(currentContentInfo);
}
}, { clearSignal: currentCanceller.signal });
// Listen for asked media segments
downloadQueue.onUpdate(function (queue) {
var segmentQueue = queue.segmentQueue;
if (segmentQueue.length > 0 &&
segmentQueue[0].segment.id ===
currentContentInfo.mediaSegmentAwaitingInitMetadata) {
// The most needed segment is still the same one, and there's no need to
// update its priority as the request already ended, just quit.
return;
}
var currentSegmentRequest = currentContentInfo.mediaSegmentRequest;
if (segmentQueue.length === 0) {
if (currentSegmentRequest === null) {
// There's nothing to load but there's already no request pending.
return;
}
log_1.default.debug("SQ: no more media segment to request. Cancelling queue.", content.adaptation.type);
_this._restartMediaSegmentDownloadingQueue(currentContentInfo);
return;
}
else if (currentSegmentRequest === null) {
// There's no request although there are needed segments: start requests
log_1.default.debug("SQ: Media segments now need to be requested. Starting queue.", content.adaptation.type, segmentQueue.length);
_this._restartMediaSegmentDownloadingQueue(currentContentInfo);
return;
}
else {
var nextItem = segmentQueue[0];
if (currentSegmentRequest.segment.id !== nextItem.segment.id) {
// The most important request if for another segment, request it
log_1.default.debug("SQ: Next media segment changed, cancelling previous", content.adaptation.type);
_this._restartMediaSegmentDownloadingQueue(currentContentInfo);
return;
}
if (currentSegmentRequest.priority !== nextItem.priority) {
// The priority of the most important request has changed, update it
log_1.default.debug("SQ: Priority of next media segment changed, updating", content.adaptation.type, currentSegmentRequest.priority, nextItem.priority);
_this._segmentFetcher.updatePriority(currentSegmentRequest.request, nextItem.priority);
}
return;
}
}, { emitCurrentValue: true, clearSignal: currentCanceller.signal });
// Listen for asked init segment
downloadQueue.onUpdate(function (next) {
var _a;
var initSegmentRequest = currentContentInfo.initSegmentRequest;
if (next.initSegment !== null && initSegmentRequest !== null) {
if (next.initSegment.priority !== initSegmentRequest.priority) {
_this._segmentFetcher.updatePriority(initSegmentRequest.request, next.initSegment.priority);
}
return;
}
else if (((_a = next.initSegment) === null || _a === void 0 ? void 0 : _a.segment.id) === (initSegmentRequest === null || initSegmentRequest === void 0 ? void 0 : initSegmentRequest.segment.id)) {
return;
}
if (next.initSegment === null) {
log_1.default.debug("SQ: no more init segment to request. Cancelling queue.", content.adaptation.type);
}
_this._restartInitSegmentDownloadingQueue(currentContentInfo, next.initSegment);
}, { emitCurrentValue: true, clearSignal: currentCanceller.signal });
return downloadQueue;
};
/**
* Stop the currently-active `SegmentQueue`.
*
* Do nothing if no queue is active.
*/
SegmentQueue.prototype.stop = function () {
var _a;
(_a = this._currentContentInfo) === null || _a === void 0 ? void 0 : _a.currentCanceller.cancel();
this._currentContentInfo = null;
};
/**
* Internal logic performing media segment requests.
*/
SegmentQueue.prototype._restartMediaSegmentDownloadingQueue = function (contentInfo) {
var _this = this;
if (contentInfo.mediaSegmentRequest !== null) {
contentInfo.mediaSegmentRequest.canceller.cancel();
}
var downloadQueue = contentInfo.downloadQueue, content = contentInfo.content, initSegmentInfoRef = contentInfo.initSegmentInfoRef, currentCanceller = contentInfo.currentCanceller;
var recursivelyRequestSegments = function () {
var _a;
if (_this.isMediaSegmentQueueInterrupted.getValue()) {
log_1.default.debug("SQ: Segment fetching postponed because it cannot stream now.");
return;
}
var segmentQueue = downloadQueue.getValue().segmentQueue;
var startingSegment = segmentQueue[0];
if (currentCanceller !== null && currentCanceller.isUsed()) {
contentInfo.mediaSegmentRequest = null;
return;
}
if (startingSegment === undefined) {
contentInfo.mediaSegmentRequest = null;
_this.trigger("emptyQueue", null);
return;
}
var canceller = new task_canceller_1.default();
var unlinkCanceller = currentCanceller === null
? noop_1.default
: canceller.linkToSignal(currentCanceller.signal);
var segment = startingSegment.segment, priority = startingSegment.priority;
var context = (0, object_assign_1.default)({ segment: segment, nextSegment: (_a = segmentQueue[1]) === null || _a === void 0 ? void 0 : _a.segment }, content);
/**
* If `true` , the current task has either errored, finished, or was
* cancelled.
*/
var isComplete = false;
/**
* If true, we're currently waiting for the initialization segment to be
* parsed before parsing a received chunk.
*/
var isWaitingOnInitSegment = false;
canceller.signal.register(function () {
contentInfo.mediaSegmentRequest = null;
if (isComplete) {
return;
}
if (contentInfo.mediaSegmentAwaitingInitMetadata === segment.id) {
contentInfo.mediaSegmentAwaitingInitMetadata = null;
}
isComplete = true;
isWaitingOnInitSegment = false;
});
var emitChunk = function (parsed) {
(0, assert_1.default)(parsed.segmentType === "media", "Should have loaded a media segment.");
_this.trigger("parsedMediaSegment", (0, object_assign_1.default)({}, parsed, { segment: segment }));
};
var continueToNextSegment = function () {
var lastQueue = downloadQueue.getValue().segmentQueue;
if (lastQueue.length === 0) {
isComplete = true;
_this.trigger("emptyQueue", null);
return;
}
else if (lastQueue[0].segment.id === segment.id) {
lastQueue.shift();
}
isComplete = true;
recursivelyRequestSegments();
};
/** Scheduled actual segment request. */
var request = _this._segmentFetcher.createRequest(context, priority, {
/**
* Callback called when the request has to be retried.
* @param {Error} error
*/
onRetry: function (error) {
_this.trigger("requestRetry", { segment: segment, error: error });
},
/**
* Callback called when the request has to be interrupted and
* restarted later.
*/
beforeInterrupted: function () {
log_1.default.info("SQ: segment request interrupted temporarly.", segment.id, segment.time);
},
/**
* Callback called when a decodable chunk of the segment is available.
* @param {Function} parse - Function allowing to parse the segment.
*/
onChunk: function (parse) {
var initTimescale = initSegmentInfoRef.getValue();
if (initTimescale !== undefined) {
emitChunk(parse(initTimescale !== null && initTimescale !== void 0 ? initTimescale : undefined));
}
else {
isWaitingOnInitSegment = true;
// We could also technically call `waitUntilDefined` in both cases,
// but I found it globally clearer to segregate the two cases,
// especially to always have a meaningful `isWaitingOnInitSegment`
// boolean which is a very important variable.
initSegmentInfoRef.waitUntilDefined(function (actualTimescale) {
emitChunk(parse(actualTimescale !== null && actualTimescale !== void 0 ? actualTimescale : undefined));
}, { clearSignal: canceller.signal });
}
},
/** Callback called after all chunks have been sent. */
onAllChunksReceived: function () {
if (!isWaitingOnInitSegment) {
_this.trigger("fullyLoadedSegment", segment);
}
else {
contentInfo.mediaSegmentAwaitingInitMetadata = segment.id;
initSegmentInfoRef.waitUntilDefined(function () {
contentInfo.mediaSegmentAwaitingInitMetadata = null;
isWaitingOnInitSegment = false;
_this.trigger("fullyLoadedSegment", segment);
}, { clearSignal: canceller.signal });
}
},
/**
* Callback called right after the request ended but before the next
* requests are scheduled. It is used to schedule the next segment.
*/
beforeEnded: function () {
unlinkCanceller();
contentInfo.mediaSegmentRequest = null;
if (isWaitingOnInitSegment) {
initSegmentInfoRef.waitUntilDefined(continueToNextSegment, {
clearSignal: canceller.signal,
});
}
else {
continueToNextSegment();
}
},
}, canceller.signal);
request.catch(function (error) {
unlinkCanceller();
if (!isComplete) {
isComplete = true;
_this.stop();
_this.trigger("error", error);
}
});
contentInfo.mediaSegmentRequest = { segment: segment, priority: priority, request: request, canceller: canceller };
};
recursivelyRequestSegments();
};
/**
* Internal logic performing initialization segment requests.
* @param {Object} contentInfo
* @param {Object} queuedInitSegment
*/
SegmentQueue.prototype._restartInitSegmentDownloadingQueue = function (contentInfo, queuedInitSegment) {
var _this = this;
var content = contentInfo.content, initSegmentInfoRef = contentInfo.initSegmentInfoRef;
if (contentInfo.initSegmentRequest !== null) {
contentInfo.initSegmentRequest.canceller.cancel();
}
if (queuedInitSegment === null) {
return;
}
var canceller = new task_canceller_1.default();
var unlinkCanceller = contentInfo.currentCanceller === null
? noop_1.default
: canceller.linkToSignal(contentInfo.currentCanceller.signal);
var segment = queuedInitSegment.segment, priority = queuedInitSegment.priority;
var context = (0, object_assign_1.default)({ segment: segment, nextSegment: undefined }, content);
/**
* If `true` , the current task has either errored, finished, or was
* cancelled.
*/
var isComplete = false;
var request = this._segmentFetcher.createRequest(context, priority, {
onRetry: function (err) {
_this.trigger("requestRetry", { segment: segment, error: err });
},
beforeInterrupted: function () {
log_1.default.info("SQ: init segment request interrupted temporarly.", segment.id);
},
beforeEnded: function () {
unlinkCanceller();
contentInfo.initSegmentRequest = null;
isComplete = true;
},
onChunk: function (parse) {
var _a;
var parsed = parse(undefined);
(0, assert_1.default)(parsed.segmentType === "init", "Should have loaded an init segment.");
_this.trigger("parsedInitSegment", (0, object_assign_1.default)({}, parsed, { segment: segment }));
if (parsed.segmentType === "init") {
initSegmentInfoRef.setValue((_a = parsed.initTimescale) !== null && _a !== void 0 ? _a : null);
}
},
onAllChunksReceived: function () {
_this.trigger("fullyLoadedSegment", segment);
},
}, canceller.signal);
request.catch(function (error) {
unlinkCanceller();
if (!isComplete) {
isComplete = true;
_this.stop();
_this.trigger("error", error);
}
});
canceller.signal.register(function () {
contentInfo.initSegmentRequest = null;
if (isComplete) {
return;
}
isComplete = true;
});
contentInfo.initSegmentRequest = { segment: segment, priority: priority, request: request, canceller: canceller };
};
return SegmentQueue;
}(event_emitter_1.default));
exports.default = SegmentQueue;