p2p-media-loader-core
Version:
P2P Media Loader core functionality
443 lines • 15.9 kB
JavaScript
import debug from "debug";
import { RequestError, } from "../types.js";
import * as Utils from "../utils/utils.js";
function mapSegmentWithStreamToSegment(segment) {
return {
runtimeId: segment.runtimeId,
externalId: segment.externalId,
url: segment.url,
byteRange: segment.byteRange,
startTime: segment.startTime,
endTime: segment.endTime,
};
}
export class Request {
constructor(segment, requestProcessQueueCallback, bandwidthCalculators, playback, playbackConfig, eventTarget) {
Object.defineProperty(this, "segment", {
enumerable: true,
configurable: true,
writable: true,
value: segment
});
Object.defineProperty(this, "requestProcessQueueCallback", {
enumerable: true,
configurable: true,
writable: true,
value: requestProcessQueueCallback
});
Object.defineProperty(this, "bandwidthCalculators", {
enumerable: true,
configurable: true,
writable: true,
value: bandwidthCalculators
});
Object.defineProperty(this, "playback", {
enumerable: true,
configurable: true,
writable: true,
value: playback
});
Object.defineProperty(this, "playbackConfig", {
enumerable: true,
configurable: true,
writable: true,
value: playbackConfig
});
Object.defineProperty(this, "currentAttempt", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_failedAttempts", {
enumerable: true,
configurable: true,
writable: true,
value: new FailedRequestAttempts()
});
Object.defineProperty(this, "finalData", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "bytes", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
Object.defineProperty(this, "_loadedBytes", {
enumerable: true,
configurable: true,
writable: true,
value: 0
});
Object.defineProperty(this, "_totalBytes", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_status", {
enumerable: true,
configurable: true,
writable: true,
value: "not-started"
});
Object.defineProperty(this, "progress", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "notReceivingBytesTimeout", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_abortRequestCallback", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_logger", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_isHandledByProcessQueue", {
enumerable: true,
configurable: true,
writable: true,
value: false
});
Object.defineProperty(this, "onSegmentError", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "onSegmentAbort", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "onSegmentStart", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "onSegmentLoaded", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "abortOnTimeout", {
enumerable: true,
configurable: true,
writable: true,
value: () => {
this.throwErrorIfNotLoadingStatus();
if (!this.currentAttempt)
return;
this.setStatus("failed");
const error = new RequestError("bytes-receiving-timeout");
this._abortRequestCallback?.(error);
this.logger(`${this.downloadSource} ${this.segment.externalId} failed ${error.type}`);
this._failedAttempts.add({
...this.currentAttempt,
error,
});
this.onSegmentError({
segment: mapSegmentWithStreamToSegment(this.segment),
error,
downloadSource: this.currentAttempt.downloadSource,
peerId: this.currentAttempt.downloadSource === "p2p"
? this.currentAttempt.peerId
: undefined,
streamType: this.segment.stream.type,
});
this.notReceivingBytesTimeout.clear();
this.manageBandwidthCalculatorsState("stop");
this.requestProcessQueueCallback();
}
});
Object.defineProperty(this, "abortOnError", {
enumerable: true,
configurable: true,
writable: true,
value: (error) => {
this.throwErrorIfNotLoadingStatus();
if (!this.currentAttempt)
return;
this.setStatus("failed");
this.logger(`${this.downloadSource} ${this.segment.externalId} failed ${error.type}`);
this._failedAttempts.add({
...this.currentAttempt,
error,
});
this.onSegmentError({
segment: mapSegmentWithStreamToSegment(this.segment),
error,
downloadSource: this.currentAttempt.downloadSource,
peerId: this.currentAttempt.downloadSource === "p2p"
? this.currentAttempt.peerId
: undefined,
streamType: this.segment.stream.type,
});
this.notReceivingBytesTimeout.clear();
this.manageBandwidthCalculatorsState("stop");
this.requestProcessQueueCallback();
}
});
Object.defineProperty(this, "completeOnSuccess", {
enumerable: true,
configurable: true,
writable: true,
value: () => {
this.throwErrorIfNotLoadingStatus();
if (!this.currentAttempt)
return;
this.manageBandwidthCalculatorsState("stop");
this.notReceivingBytesTimeout.clear();
this.setStatus("succeed");
this._totalBytes = this._loadedBytes;
this.onSegmentLoaded({
segmentUrl: this.segment.url,
bytesLength: this.data.byteLength,
downloadSource: this.currentAttempt.downloadSource,
peerId: this.currentAttempt.downloadSource === "p2p"
? this.currentAttempt.peerId
: undefined,
streamType: this.segment.stream.type,
});
this.logger(`${this.currentAttempt.downloadSource} ${this.segment.externalId} succeed`);
this.requestProcessQueueCallback();
}
});
Object.defineProperty(this, "addLoadedChunk", {
enumerable: true,
configurable: true,
writable: true,
value: (chunk) => {
this.throwErrorIfNotLoadingStatus();
if (!this.currentAttempt || !this.progress)
return;
this.notReceivingBytesTimeout.restart();
const { byteLength } = chunk;
const { all: allBC, http: httpBC } = this.bandwidthCalculators;
allBC.addBytes(byteLength);
if (this.currentAttempt.downloadSource === "http") {
httpBC.addBytes(byteLength);
}
this.bytes.push(chunk);
this.progress.lastLoadedChunkTimestamp = performance.now();
this.progress.loadedBytes += byteLength;
this._loadedBytes += byteLength;
}
});
Object.defineProperty(this, "firstBytesReceived", {
enumerable: true,
configurable: true,
writable: true,
value: () => {
this.throwErrorIfNotLoadingStatus();
this.notReceivingBytesTimeout.restart();
}
});
this.onSegmentError = eventTarget.getEventDispatcher("onSegmentError");
this.onSegmentAbort = eventTarget.getEventDispatcher("onSegmentAbort");
this.onSegmentStart = eventTarget.getEventDispatcher("onSegmentStart");
this.onSegmentLoaded = eventTarget.getEventDispatcher("onSegmentLoaded");
const { byteRange } = this.segment;
if (byteRange) {
const { end, start } = byteRange;
this._totalBytes = end - start + 1;
}
this.notReceivingBytesTimeout = new Timeout(this.abortOnTimeout);
const { type } = this.segment.stream;
this._logger = debug(`p2pml-core:request-${type}`);
}
clearLoadedBytes() {
this._loadedBytes = 0;
this.bytes = [];
this._totalBytes = undefined;
this.finalData = undefined;
}
get status() {
return this._status;
}
setStatus(status) {
this._status = status;
this._isHandledByProcessQueue = false;
}
get downloadSource() {
return this.currentAttempt?.downloadSource;
}
get loadedBytes() {
return this._loadedBytes;
}
get totalBytes() {
return this._totalBytes;
}
get data() {
if (!this.finalData)
this.finalData = Utils.joinChunks(this.bytes);
return this.finalData;
}
get failedAttempts() {
return this._failedAttempts;
}
get isHandledByProcessQueue() {
return this._isHandledByProcessQueue;
}
markHandledByProcessQueue() {
this._isHandledByProcessQueue = true;
}
setTotalBytes(value) {
if (this._totalBytes !== undefined) {
throw new Error("Request total bytes value is already set");
}
this._totalBytes = value;
}
start(requestData, controls) {
if (this._status === "succeed") {
throw new Error(`Request ${this.segment.externalId} has been already succeed.`);
}
if (this._status === "loading") {
throw new Error(`Request ${this.segment.externalId} has been already started.`);
}
this.setStatus("loading");
this.currentAttempt = { ...requestData };
this.progress = {
startFromByte: this._loadedBytes,
loadedBytes: 0,
startTimestamp: performance.now(),
};
this.manageBandwidthCalculatorsState("start");
const { notReceivingBytesTimeoutMs, abort } = controls;
this._abortRequestCallback = abort;
if (notReceivingBytesTimeoutMs !== undefined) {
this.notReceivingBytesTimeout.start(notReceivingBytesTimeoutMs);
}
this.logger(`${requestData.downloadSource} ${this.segment.externalId} started`);
this.onSegmentStart({
segment: mapSegmentWithStreamToSegment(this.segment),
downloadSource: requestData.downloadSource,
peerId: requestData.downloadSource === "p2p" ? requestData.peerId : undefined,
});
return {
firstBytesReceived: this.firstBytesReceived,
addLoadedChunk: this.addLoadedChunk,
completeOnSuccess: this.completeOnSuccess,
abortOnError: this.abortOnError,
};
}
abortFromProcessQueue() {
this.throwErrorIfNotLoadingStatus();
this.setStatus("aborted");
this.logger(`${this.currentAttempt?.downloadSource} ${this.segment.externalId} aborted`);
this._abortRequestCallback?.(new RequestError("abort"));
this.onSegmentAbort({
segment: mapSegmentWithStreamToSegment(this.segment),
downloadSource: this.currentAttempt?.downloadSource,
peerId: this.currentAttempt?.downloadSource === "p2p"
? this.currentAttempt.peerId
: undefined,
streamType: this.segment.stream.type,
});
this._abortRequestCallback = undefined;
this.manageBandwidthCalculatorsState("stop");
this.notReceivingBytesTimeout.clear();
}
throwErrorIfNotLoadingStatus() {
if (this._status !== "loading") {
throw new Error(`Request has been already ${this.status}.`);
}
}
logger(message) {
this._logger.color =
this.currentAttempt?.downloadSource === "http" ? "green" : "red";
this._logger(message);
this._logger.color = "";
}
manageBandwidthCalculatorsState(state) {
const { all, http } = this.bandwidthCalculators;
const method = state === "start" ? "startLoading" : "stopLoading";
if (this.currentAttempt?.downloadSource === "http")
http[method]();
all[method]();
}
}
class FailedRequestAttempts {
constructor() {
Object.defineProperty(this, "attempts", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
}
add(attempt) {
this.attempts.push(attempt);
}
get httpAttemptsCount() {
return this.attempts.reduce((sum, attempt) => (attempt.downloadSource === "http" ? sum + 1 : sum), 0);
}
get lastAttempt() {
return this.attempts[this.attempts.length - 1];
}
clear() {
this.attempts = [];
}
}
export class Timeout {
constructor(action) {
Object.defineProperty(this, "action", {
enumerable: true,
configurable: true,
writable: true,
value: action
});
Object.defineProperty(this, "timeoutId", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "ms", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
}
start(ms) {
if (this.timeoutId) {
throw new Error("Timeout is already started.");
}
this.ms = ms;
this.timeoutId = window.setTimeout(this.action, this.ms);
}
restart(ms) {
if (this.timeoutId)
clearTimeout(this.timeoutId);
if (ms)
this.ms = ms;
if (!this.ms)
return;
this.timeoutId = window.setTimeout(this.action, this.ms);
}
clear() {
clearTimeout(this.timeoutId);
this.timeoutId = undefined;
}
}
//# sourceMappingURL=request.js.map