p2p-media-loader-core
Version:
P2P Media Loader core functionality
193 lines • 7.83 kB
JavaScript
import { RequestError, } from "./types.js";
export class HttpRequestExecutor {
constructor(request, httpConfig, eventTarget) {
Object.defineProperty(this, "request", {
enumerable: true,
configurable: true,
writable: true,
value: request
});
Object.defineProperty(this, "httpConfig", {
enumerable: true,
configurable: true,
writable: true,
value: httpConfig
});
Object.defineProperty(this, "requestControls", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "abortController", {
enumerable: true,
configurable: true,
writable: true,
value: new AbortController()
});
Object.defineProperty(this, "expectedBytesLength", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "requestByteRange", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "onChunkDownloaded", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
this.onChunkDownloaded =
eventTarget.getEventDispatcher("onChunkDownloaded");
const { byteRange } = this.request.segment;
if (byteRange)
this.requestByteRange = { ...byteRange };
if (request.loadedBytes !== 0) {
this.requestByteRange = this.requestByteRange ?? { start: 0 };
this.requestByteRange.start =
this.requestByteRange.start + request.loadedBytes;
}
if (this.request.totalBytes) {
this.expectedBytesLength =
this.request.totalBytes - this.request.loadedBytes;
}
this.requestControls = this.request.start({ downloadSource: "http" }, {
abort: () => this.abortController.abort("abort"),
notReceivingBytesTimeoutMs: this.httpConfig.httpNotReceivingBytesTimeoutMs,
});
void this.fetch();
}
async fetch() {
const { segment } = this.request;
try {
let request = await this.httpConfig.httpRequestSetup?.(segment.url, segment.byteRange, this.abortController.signal, this.requestByteRange);
if (!request) {
const headers = new Headers(this.requestByteRange
? {
Range: `bytes=${this.requestByteRange.start}-${this.requestByteRange.end ?? ""}`,
}
: undefined);
request = new Request(segment.url, {
headers,
signal: this.abortController.signal,
});
}
if (this.abortController.signal.aborted) {
throw new DOMException("Request aborted before request fetch", "AbortError");
}
const response = await window.fetch(request);
this.handleResponseHeaders(response);
if (!response.body)
return;
const { requestControls } = this;
requestControls.firstBytesReceived();
const reader = response.body.getReader();
for await (const chunk of readStream(reader)) {
this.requestControls.addLoadedChunk(chunk);
this.onChunkDownloaded(chunk.byteLength, "http");
}
const isValid = (await this.httpConfig.validateHTTPSegment?.(segment.url, segment.byteRange, this.request.data)) ?? true;
if (!isValid) {
this.request.clearLoadedBytes();
throw new RequestError("http-segment-validation-failed");
}
requestControls.completeOnSuccess();
}
catch (error) {
this.handleError(error);
}
}
handleResponseHeaders(response) {
if (!response.ok) {
if (response.status === 406) {
this.request.clearLoadedBytes();
throw new RequestError("http-bytes-mismatch", response.statusText);
}
else {
throw new RequestError("http-error", response.statusText);
}
}
const { requestByteRange } = this;
if (requestByteRange) {
if (response.status === 200) {
if (this.request.segment.byteRange) {
throw new RequestError("http-unexpected-status-code");
}
else {
this.request.clearLoadedBytes();
}
}
else {
if (response.status !== 206) {
throw new RequestError("http-unexpected-status-code", response.statusText);
}
const contentLengthHeader = response.headers.get("Content-Length");
if (contentLengthHeader &&
this.expectedBytesLength !== undefined &&
this.expectedBytesLength !== +contentLengthHeader) {
this.request.clearLoadedBytes();
throw new RequestError("http-bytes-mismatch", response.statusText);
}
const contentRangeHeader = response.headers.get("Content-Range");
const contentRange = contentRangeHeader
? parseContentRangeHeader(contentRangeHeader)
: undefined;
if (contentRange) {
const { from, to } = contentRange;
const responseExpectedBytesLength = to !== undefined && from !== undefined ? to - from + 1 : undefined;
if ((responseExpectedBytesLength !== undefined &&
this.expectedBytesLength !== responseExpectedBytesLength) ||
(from !== undefined && requestByteRange.start !== from) ||
(to !== undefined &&
requestByteRange.end !== undefined &&
requestByteRange.end !== to)) {
this.request.clearLoadedBytes();
throw new RequestError("http-bytes-mismatch", response.statusText);
}
}
}
}
if (response.status === 200 && this.request.totalBytes === undefined) {
const contentLengthHeader = response.headers.get("Content-Length");
if (contentLengthHeader)
this.request.setTotalBytes(+contentLengthHeader);
}
}
handleError(error) {
if (error instanceof Error) {
if (error.name !== "abort")
return;
const httpLoaderError = error instanceof RequestError
? error
: new RequestError("http-error", error.message);
this.requestControls.abortOnError(httpLoaderError);
}
}
}
async function* readStream(reader) {
while (true) {
const { done, value } = await reader.read();
if (done)
break;
yield value;
}
}
const rangeHeaderRegex = /^bytes (?:(?:(\d+)|)-(?:(\d+)|)|\*)\/(?:(\d+)|\*)$/;
function parseContentRangeHeader(headerValue) {
const match = rangeHeaderRegex.exec(headerValue.trim());
if (!match)
return;
const [, from, to, total] = match;
return {
from: from ? parseInt(from) : undefined,
to: to ? parseInt(to) : undefined,
total: total ? parseInt(total) : undefined,
};
}
//# sourceMappingURL=http-loader.js.map