rx-player
Version:
Canal+ HTML5 Video Player
402 lines (401 loc) • 18.6 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.
*/
import log from "../../../log";
import assert from "../../../utils/assert";
import EventEmitter from "../../../utils/event_emitter";
import noop from "../../../utils/noop";
import objectAssign from "../../../utils/object_assign";
import SharedReference from "../../../utils/reference";
import TaskCanceller from "../../../utils/task_canceller";
/**
* Class scheduling segment downloads as a FIFO queue.
*/
export default class SegmentQueue extends EventEmitter {
/**
* 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.
*/
constructor(segmentFetcher, isMediaSegmentQueueInterrupted) {
super();
this._segmentFetcher = segmentFetcher;
this._currentContentInfo = null;
this.isMediaSegmentQueueInterrupted = isMediaSegmentQueueInterrupted;
}
/**
* Returns the initialization segment currently being requested.
* Returns `null` if no initialization segment request is pending.
* @returns {Object | null}
*/
getRequestedInitSegment() {
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}
*/
getRequestedMediaSegment() {
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.
*/
resetForContent(content, hasInitSegment) {
var _a;
(_a = this._currentContentInfo) === null || _a === void 0 ? void 0 : _a.currentCanceller.cancel();
const downloadQueue = new SharedReference({
initSegment: null,
segmentQueue: [],
});
const currentCanceller = new TaskCanceller();
currentCanceller.signal.register(() => {
downloadQueue.finish();
});
const currentContentInfo = {
content,
downloadQueue,
initSegmentInfoRef: hasInitSegment
? new SharedReference(undefined)
: new SharedReference(null),
currentCanceller,
initSegmentRequest: null,
mediaSegmentRequest: null,
mediaSegmentAwaitingInitMetadata: null,
};
this._currentContentInfo = currentContentInfo;
this.isMediaSegmentQueueInterrupted.onUpdate((val) => {
if (!val) {
log.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((queue) => {
const { segmentQueue } = queue;
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;
}
const currentSegmentRequest = currentContentInfo.mediaSegmentRequest;
if (segmentQueue.length === 0) {
if (currentSegmentRequest === null) {
// There's nothing to load but there's already no request pending.
return;
}
log.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.debug("SQ: Media segments now need to be requested. Starting queue.", content.adaptation.type, segmentQueue.length);
this._restartMediaSegmentDownloadingQueue(currentContentInfo);
return;
}
else {
const nextItem = segmentQueue[0];
if (currentSegmentRequest.segment.id !== nextItem.segment.id) {
// The most important request if for another segment, request it
log.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.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((next) => {
var _a;
const 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.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.
*/
stop() {
var _a;
(_a = this._currentContentInfo) === null || _a === void 0 ? void 0 : _a.currentCanceller.cancel();
this._currentContentInfo = null;
}
/**
* Internal logic performing media segment requests.
*/
_restartMediaSegmentDownloadingQueue(contentInfo) {
if (contentInfo.mediaSegmentRequest !== null) {
contentInfo.mediaSegmentRequest.canceller.cancel();
}
const { downloadQueue, content, initSegmentInfoRef, currentCanceller } = contentInfo;
const recursivelyRequestSegments = () => {
var _a;
if (this.isMediaSegmentQueueInterrupted.getValue()) {
log.debug("SQ: Segment fetching postponed because it cannot stream now.");
return;
}
const { segmentQueue } = downloadQueue.getValue();
const startingSegment = segmentQueue[0];
if (currentCanceller !== null && currentCanceller.isUsed()) {
contentInfo.mediaSegmentRequest = null;
return;
}
if (startingSegment === undefined) {
contentInfo.mediaSegmentRequest = null;
this.trigger("emptyQueue", null);
return;
}
const canceller = new TaskCanceller();
const unlinkCanceller = currentCanceller === null
? noop
: canceller.linkToSignal(currentCanceller.signal);
const { segment, priority } = startingSegment;
const context = objectAssign({ 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.
*/
let isComplete = false;
/**
* If true, we're currently waiting for the initialization segment to be
* parsed before parsing a received chunk.
*/
let isWaitingOnInitSegment = false;
canceller.signal.register(() => {
contentInfo.mediaSegmentRequest = null;
if (isComplete) {
return;
}
if (contentInfo.mediaSegmentAwaitingInitMetadata === segment.id) {
contentInfo.mediaSegmentAwaitingInitMetadata = null;
}
isComplete = true;
isWaitingOnInitSegment = false;
});
const emitChunk = (parsed) => {
assert(parsed.segmentType === "media", "Should have loaded a media segment.");
this.trigger("parsedMediaSegment", objectAssign({}, parsed, { segment }));
};
const continueToNextSegment = () => {
const 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. */
const request = this._segmentFetcher.createRequest(context, priority, {
/**
* Callback called when the request has to be retried.
* @param {Error} error
*/
onRetry: (error) => {
this.trigger("requestRetry", { segment, error });
},
/**
* Callback called when the request has to be interrupted and
* restarted later.
*/
beforeInterrupted() {
log.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: (parse) => {
const 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((actualTimescale) => {
emitChunk(parse(actualTimescale !== null && actualTimescale !== void 0 ? actualTimescale : undefined));
}, { clearSignal: canceller.signal });
}
},
/** Callback called after all chunks have been sent. */
onAllChunksReceived: () => {
if (!isWaitingOnInitSegment) {
this.trigger("fullyLoadedSegment", segment);
}
else {
contentInfo.mediaSegmentAwaitingInitMetadata = segment.id;
initSegmentInfoRef.waitUntilDefined(() => {
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: () => {
unlinkCanceller();
contentInfo.mediaSegmentRequest = null;
if (isWaitingOnInitSegment) {
initSegmentInfoRef.waitUntilDefined(continueToNextSegment, {
clearSignal: canceller.signal,
});
}
else {
continueToNextSegment();
}
},
}, canceller.signal);
request.catch((error) => {
unlinkCanceller();
if (!isComplete) {
isComplete = true;
this.stop();
this.trigger("error", error);
}
});
contentInfo.mediaSegmentRequest = { segment, priority, request, canceller };
};
recursivelyRequestSegments();
}
/**
* Internal logic performing initialization segment requests.
* @param {Object} contentInfo
* @param {Object} queuedInitSegment
*/
_restartInitSegmentDownloadingQueue(contentInfo, queuedInitSegment) {
const { content, initSegmentInfoRef } = contentInfo;
if (contentInfo.initSegmentRequest !== null) {
contentInfo.initSegmentRequest.canceller.cancel();
}
if (queuedInitSegment === null) {
return;
}
const canceller = new TaskCanceller();
const unlinkCanceller = contentInfo.currentCanceller === null
? noop
: canceller.linkToSignal(contentInfo.currentCanceller.signal);
const { segment, priority } = queuedInitSegment;
const context = objectAssign({ segment, nextSegment: undefined }, content);
/**
* If `true` , the current task has either errored, finished, or was
* cancelled.
*/
let isComplete = false;
const request = this._segmentFetcher.createRequest(context, priority, {
onRetry: (err) => {
this.trigger("requestRetry", { segment, error: err });
},
beforeInterrupted: () => {
log.info("SQ: init segment request interrupted temporarly.", segment.id);
},
beforeEnded: () => {
unlinkCanceller();
contentInfo.initSegmentRequest = null;
isComplete = true;
},
onChunk: (parse) => {
var _a;
const parsed = parse(undefined);
assert(parsed.segmentType === "init", "Should have loaded an init segment.");
this.trigger("parsedInitSegment", objectAssign({}, parsed, { segment }));
if (parsed.segmentType === "init") {
initSegmentInfoRef.setValue((_a = parsed.initTimescale) !== null && _a !== void 0 ? _a : null);
}
},
onAllChunksReceived: () => {
this.trigger("fullyLoadedSegment", segment);
},
}, canceller.signal);
request.catch((error) => {
unlinkCanceller();
if (!isComplete) {
isComplete = true;
this.stop();
this.trigger("error", error);
}
});
canceller.signal.register(() => {
contentInfo.initSegmentRequest = null;
if (isComplete) {
return;
}
isComplete = true;
});
contentInfo.initSegmentRequest = { segment, priority, request, canceller };
}
}