UNPKG

rx-player

Version:
412 lines (382 loc) 14 kB
/** * 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"); }, }, }; }