p2p-media-loader-shaka
Version:
P2P Media Loader Shaka Player integration
144 lines (128 loc) • 4.23 kB
text/typescript
import type shaka from "shaka-player/dist/shaka-player.compiled.d.ts";
import * as Utils from "./stream-utils.js";
import { StreamInfo, Shaka, Stream } from "./types.js";
import {
Core,
CoreRequestError,
SegmentResponse,
EngineCallbacks,
} from "p2p-media-loader-core";
type LoadingHandlerParams = Parameters<shaka.extern.SchemePlugin>;
type Response = shaka.extern.Response;
type LoadingHandlerResult = shaka.extern.IAbortableOperation<Response>;
export class Loader {
private loadArgs!: LoadingHandlerParams;
constructor(
private readonly shaka: Shaka,
private readonly core: Core<Stream>,
readonly streamInfo: StreamInfo,
) {}
private defaultLoad() {
const fetchPlugin = this.shaka.net.HttpFetchPlugin;
return fetchPlugin.parse(...this.loadArgs);
}
load(...args: LoadingHandlerParams): LoadingHandlerResult {
this.loadArgs = args;
const { RequestType } = this.shaka.net.NetworkingEngine;
const [url, request, requestType] = args;
if (requestType === RequestType.SEGMENT) {
return this.loadSegment(url, request);
}
const loading = this.defaultLoad() as LoadingHandlerResult;
if (requestType === RequestType.MANIFEST) {
this.handleManifestLoading(loading.promise).catch(() => undefined);
}
return loading;
}
private async handleManifestLoading(loadingPromise: Promise<Response>) {
if (!this.streamInfo.manifestResponseUrl) {
// loading main manifest either HLS or DASH
const response = await loadingPromise;
this.setManifestResponseUrl(response.uri);
}
}
private loadSegment(
segmentUrl: string,
originalRequest: shaka.extern.Request,
): LoadingHandlerResult {
const byteRangeString = originalRequest.headers.Range;
const segmentRuntimeId = Utils.getSegmentRuntimeId(
segmentUrl,
byteRangeString,
);
const isSegmentDownloadableByP2PCore =
this.core.isSegmentLoadable(segmentRuntimeId);
if (
!this.core.hasSegment(segmentRuntimeId) ||
!isSegmentDownloadableByP2PCore
) {
return this.defaultLoad() as LoadingHandlerResult;
}
const loadSegment = async (): Promise<Response> => {
const { request, callbacks } = getSegmentRequest();
void this.core.loadSegment(segmentRuntimeId, callbacks);
try {
const { data, bandwidth } = await request;
return {
data,
headers: {},
originalRequest,
uri: segmentUrl,
originalUri: segmentUrl,
timeMs: getLoadingDurationBasedOnBandwidth(
bandwidth,
data.byteLength,
),
};
} catch (error) {
// TODO: throw Shaka Errors
if (error instanceof CoreRequestError) {
const { Error: ShakaError } = this.shaka.util;
if (error.type === "aborted") {
throw new ShakaError(
ShakaError.Severity.RECOVERABLE,
ShakaError.Category.NETWORK,
this.shaka.util.Error.Code.OPERATION_ABORTED,
);
}
}
throw error;
}
};
return new this.shaka.util.AbortableOperation(loadSegment(), () => {
this.core.abortSegmentLoading(segmentRuntimeId);
return Promise.resolve();
});
}
private setManifestResponseUrl(responseUrl: string) {
this.streamInfo.manifestResponseUrl = responseUrl;
this.core.setManifestResponseUrl(responseUrl);
}
}
function getLoadingDurationBasedOnBandwidth(
bandwidth: number,
bytesLoaded: number,
) {
const bits = bytesLoaded * 8;
return bandwidth > 0 ? Math.round(bits / bandwidth) * 1000 : 0;
}
function getSegmentRequest(): {
callbacks: EngineCallbacks;
request: Promise<SegmentResponse>;
} {
let onSuccess: (value: SegmentResponse) => void;
let onError: (reason?: unknown) => void;
const request = new Promise<SegmentResponse>((resolve, reject) => {
onSuccess = resolve;
onError = reject;
});
return {
request,
callbacks: {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
onSuccess: onSuccess!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
onError: onError!,
},
};
}