rx-player
Version:
Canal+ HTML5 Video Player
412 lines (382 loc) • 14 kB
text/typescript
/**
* 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 config from "../../config";
import features from "../../features";
import type { IMetaPlaylistPrivateInfos, ISegment } from "../../manifest";
import Manifest from "../../manifest/classes";
import type { IParserResponse as IMPLParserResponse } from "../../parsers/manifest/metaplaylist";
import parseMetaPlaylist from "../../parsers/manifest/metaplaylist";
import type { ICdnMetadata, IParsedManifest } from "../../parsers/manifest/types";
import type { IPlayerError } from "../../public_types";
import isNullOrUndefined from "../../utils/is_null_or_undefined";
import objectAssign from "../../utils/object_assign";
import type { CancellationSignal } from "../../utils/task_canceller";
import type {
IChunkTimeInfo,
ILoadedAudioVideoSegmentFormat,
ILoadedTextSegmentFormat,
IManifestParserOptions,
IManifestParserRequestScheduler,
IManifestParserResult,
IRequestedData,
ISegmentContext,
ISegmentLoaderCallbacks,
ISegmentLoaderOptions,
ISegmentLoaderResultChunkedComplete,
ISegmentLoaderResultSegmentCreated,
ISegmentLoaderResultSegmentLoaded,
ISegmentParserParsedInitChunk,
ISegmentParserParsedMediaChunk,
ITextTrackSegmentData,
ITransportOptions,
ITransportPipelines,
} from "../types";
import generateManifestLoader from "./manifest_loader";
/**
* Get base - real - content from an offseted metaplaylist content.
* @param {Object} mplContext
* @returns {Object}
*/
function getOriginalContext(mplContext: ISegmentContext): ISegmentContext {
const { segment } = mplContext;
if (segment.privateInfos?.metaplaylistInfos === undefined) {
throw new Error("MetaPlaylist: missing private infos");
}
const { isLive, periodStart, periodEnd, manifestPublishTime } =
segment.privateInfos.metaplaylistInfos;
const { originalSegment } = segment.privateInfos.metaplaylistInfos;
return {
segment: originalSegment,
type: mplContext.type,
language: mplContext.language,
mimeType: mplContext.mimeType,
codecs: mplContext.codecs,
isLive,
periodStart,
periodEnd,
manifestPublishTime,
};
}
/**
* @param {Object} transports
* @param {string} transportName
* @param {Object} options
* @returns {Object}
*/
function getTransportPipelines(
transports: Partial<Record<string, ITransportPipelines>>,
transportName: string,
options: ITransportOptions,
): ITransportPipelines {
const initialTransport = transports[transportName];
if (initialTransport !== undefined) {
return initialTransport;
}
const feature = features.transports[transportName];
if (feature === undefined) {
throw new Error(`MetaPlaylist: Unknown transport ${transportName}.`);
}
const transport = feature(options);
transports[transportName] = transport;
return transport;
}
/**
* @param {Object} segment
* @returns {Object}
*/
function getMetaPlaylistPrivateInfos(segment: ISegment): IMetaPlaylistPrivateInfos {
const { privateInfos } = segment;
if (privateInfos?.metaplaylistInfos === undefined) {
throw new Error("MetaPlaylist: Undefined transport for content for metaplaylist.");
}
return privateInfos.metaplaylistInfos;
}
export default function (options: ITransportOptions): ITransportPipelines {
const transports: Partial<Record<string, ITransportPipelines>> = {};
const manifestLoader = generateManifestLoader({
customManifestLoader: options.manifestLoader,
});
// remove some options that we might not want to apply to the
// other streaming protocols used here
const otherTransportOptions = objectAssign({}, options, {
manifestLoader: undefined,
});
const manifestPipeline = {
loadManifest: manifestLoader,
parseManifest(
manifestData: IRequestedData<unknown>,
parserOptions: IManifestParserOptions,
onWarnings: (warnings: Error[]) => void,
cancelSignal: CancellationSignal,
scheduleRequest: IManifestParserRequestScheduler,
): Promise<IManifestParserResult> {
const url = manifestData.url ?? parserOptions.originalUrl;
const { responseData } = manifestData;
const mplParserOptions = {
url,
serverSyncInfos: options.serverSyncInfos,
};
const parsed = parseMetaPlaylist(responseData, mplParserOptions);
return handleParsedResult(parsed);
function handleParsedResult(
parsedResult: IMPLParserResponse<IParsedManifest>,
): Promise<IManifestParserResult> {
if (parsedResult.type === "done") {
const warnings: IPlayerError[] = [];
const manifest = new Manifest(parsedResult.value, options, warnings);
return Promise.resolve({ manifest, warnings });
}
const parsedValue = parsedResult.value;
const loaderProms = parsedValue.ressources.map((resource) => {
const transport = getTransportPipelines(
transports,
resource.transportType,
otherTransportOptions,
);
return scheduleRequest(loadSubManifest).then((data) =>
transport.manifest.parseManifest(
data,
{ ...parserOptions, originalUrl: resource.url },
onWarnings,
cancelSignal,
scheduleRequest,
),
);
function loadSubManifest() {
/*
* Whether a ManifestLoader's timeout should be relied on here
* is ambiguous.
*/
const manOpts = {
timeout: config.getCurrent().DEFAULT_REQUEST_TIMEOUT,
connectionTimeout: config.getCurrent().DEFAULT_CONNECTION_TIMEOUT,
cmcdPayload: undefined,
};
return transport.manifest.loadManifest(resource.url, manOpts, cancelSignal);
}
});
return Promise.all(loaderProms).then((parsedReqs) => {
const loadedRessources = parsedReqs.map((e) => e.manifest);
return handleParsedResult(parsedResult.value.continue(loadedRessources));
});
}
},
};
/**
* @param {Object} segment
* @returns {Object}
*/
function getTransportPipelinesFromSegment(segment: ISegment): ITransportPipelines {
const { transportType } = getMetaPlaylistPrivateInfos(segment);
return getTransportPipelines(transports, transportType, otherTransportOptions);
}
/**
* @param {number} contentOffset
* @param {number|undefined} contentEnd
* @param {Object} segmentResponse
* @returns {Object}
*/
function offsetTimeInfos(
contentOffset: number,
contentEnd: number | undefined,
segmentResponse: ISegmentParserParsedMediaChunk<unknown>,
): {
chunkInfos: IChunkTimeInfo | null;
chunkOffset: number;
appendWindow: [number | undefined, number | undefined];
} {
const offsetedSegmentOffset = segmentResponse.chunkOffset + contentOffset;
if (isNullOrUndefined(segmentResponse.chunkData)) {
return {
chunkInfos: segmentResponse.chunkInfos,
chunkOffset: offsetedSegmentOffset,
appendWindow: [undefined, undefined],
};
}
// clone chunkInfos
const { chunkInfos, appendWindow } = segmentResponse;
const offsetedChunkInfos = chunkInfos === null ? null : objectAssign({}, chunkInfos);
if (offsetedChunkInfos !== null) {
offsetedChunkInfos.time += contentOffset;
}
const offsetedWindowStart =
appendWindow[0] !== undefined
? Math.max(appendWindow[0] + contentOffset, contentOffset)
: contentOffset;
let offsetedWindowEnd: number | undefined;
if (appendWindow[1] !== undefined) {
offsetedWindowEnd =
contentEnd !== undefined
? Math.min(appendWindow[1] + contentOffset, contentEnd)
: appendWindow[1] + contentOffset;
} else if (contentEnd !== undefined) {
offsetedWindowEnd = contentEnd;
}
return {
chunkInfos: offsetedChunkInfos,
chunkOffset: offsetedSegmentOffset,
appendWindow: [offsetedWindowStart, offsetedWindowEnd],
};
}
const audioPipeline = {
loadSegment(
wantedCdn: ICdnMetadata | null,
context: ISegmentContext,
loaderOptions: ISegmentLoaderOptions,
cancelToken: CancellationSignal,
callbacks: ISegmentLoaderCallbacks<ILoadedAudioVideoSegmentFormat>,
): Promise<
| ISegmentLoaderResultSegmentLoaded<ILoadedAudioVideoSegmentFormat>
| ISegmentLoaderResultSegmentCreated<ILoadedAudioVideoSegmentFormat>
| ISegmentLoaderResultChunkedComplete
> {
const { segment } = context;
const { audio } = getTransportPipelinesFromSegment(segment);
const ogContext = getOriginalContext(context);
return audio.loadSegment(
wantedCdn,
ogContext,
loaderOptions,
cancelToken,
callbacks,
);
},
parseSegment(
loadedSegment: {
data: ILoadedAudioVideoSegmentFormat;
isChunked: boolean;
},
context: ISegmentContext,
initTimescale: number | undefined,
):
| ISegmentParserParsedInitChunk<ArrayBuffer | Uint8Array | null>
| ISegmentParserParsedMediaChunk<ArrayBuffer | Uint8Array | null> {
const { segment } = context;
const { contentStart, contentEnd } = getMetaPlaylistPrivateInfos(segment);
const { audio } = getTransportPipelinesFromSegment(segment);
const ogContext = getOriginalContext(context);
const parsed = audio.parseSegment(loadedSegment, ogContext, initTimescale);
if (parsed.segmentType === "init") {
return parsed;
}
const timeInfos = offsetTimeInfos(contentStart, contentEnd, parsed);
return objectAssign({}, parsed, timeInfos);
},
};
const videoPipeline = {
loadSegment(
wantedCdn: ICdnMetadata | null,
context: ISegmentContext,
loaderOptions: ISegmentLoaderOptions,
cancelToken: CancellationSignal,
callbacks: ISegmentLoaderCallbacks<ILoadedAudioVideoSegmentFormat>,
): Promise<
| ISegmentLoaderResultSegmentLoaded<ILoadedAudioVideoSegmentFormat>
| ISegmentLoaderResultSegmentCreated<ILoadedAudioVideoSegmentFormat>
| ISegmentLoaderResultChunkedComplete
> {
const { segment } = context;
const { video } = getTransportPipelinesFromSegment(segment);
const ogContext = getOriginalContext(context);
return video.loadSegment(
wantedCdn,
ogContext,
loaderOptions,
cancelToken,
callbacks,
);
},
parseSegment(
loadedSegment: {
data: ILoadedAudioVideoSegmentFormat;
isChunked: boolean;
},
context: ISegmentContext,
initTimescale: number | undefined,
):
| ISegmentParserParsedInitChunk<ArrayBuffer | Uint8Array | null>
| ISegmentParserParsedMediaChunk<ArrayBuffer | Uint8Array | null> {
const { segment } = context;
const { contentStart, contentEnd } = getMetaPlaylistPrivateInfos(segment);
const { video } = getTransportPipelinesFromSegment(segment);
const ogContext = getOriginalContext(context);
const parsed = video.parseSegment(loadedSegment, ogContext, initTimescale);
if (parsed.segmentType === "init") {
return parsed;
}
const timeInfos = offsetTimeInfos(contentStart, contentEnd, parsed);
return objectAssign({}, parsed, timeInfos);
},
};
const textTrackPipeline = {
loadSegment(
wantedCdn: ICdnMetadata | null,
context: ISegmentContext,
loaderOptions: ISegmentLoaderOptions,
cancelToken: CancellationSignal,
callbacks: ISegmentLoaderCallbacks<ILoadedTextSegmentFormat>,
): Promise<
| ISegmentLoaderResultSegmentLoaded<ILoadedTextSegmentFormat>
| ISegmentLoaderResultSegmentCreated<ILoadedTextSegmentFormat>
| ISegmentLoaderResultChunkedComplete
> {
const { segment } = context;
const { text } = getTransportPipelinesFromSegment(segment);
const ogContext = getOriginalContext(context);
return text.loadSegment(
wantedCdn,
ogContext,
loaderOptions,
cancelToken,
callbacks,
);
},
parseSegment(
loadedSegment: { data: ILoadedTextSegmentFormat; isChunked: boolean },
context: ISegmentContext,
initTimescale: number | undefined,
):
| ISegmentParserParsedInitChunk<ITextTrackSegmentData | null>
| ISegmentParserParsedMediaChunk<ITextTrackSegmentData> {
const { segment } = context;
const { contentStart, contentEnd } = getMetaPlaylistPrivateInfos(segment);
const { text } = getTransportPipelinesFromSegment(segment);
const ogContext = getOriginalContext(context);
const parsed = text.parseSegment(loadedSegment, ogContext, initTimescale);
if (parsed.segmentType === "init") {
return parsed;
}
const timeInfos = offsetTimeInfos(contentStart, contentEnd, parsed);
return objectAssign({}, parsed, timeInfos);
},
};
return {
transportName: "metaplaylist",
manifest: manifestPipeline,
audio: audioPipeline,
video: videoPipeline,
text: textTrackPipeline,
thumbnails: {
loadThumbnail: () =>
Promise.reject(
new Error("Thumbnail tracks aren't implemented with MetaPlaylist"),
),
parseThumbnail: () => {
throw new Error("Thumbnail tracks aren't implemented with MetaPlaylist");
},
},
};
}