rx-player
Version:
Canal+ HTML5 Video Player
441 lines (414 loc) • 14.4 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 log from "../../log";
import Manifest from "../../manifest/classes";
import { getMDAT } from "../../parsers/containers/isobmff";
import type { ICdnMetadata } from "../../parsers/manifest";
import createSmoothManifestParser from "../../parsers/manifest/smooth";
import type { IPlayerError } from "../../public_types";
import request from "../../utils/request";
import { strToUtf8, utf8ToStr } from "../../utils/string_parsing";
import type { CancellationSignal } from "../../utils/task_canceller";
import type {
IChunkTimeInfo,
ILoadedAudioVideoSegmentFormat,
ILoadedTextSegmentFormat,
IManifestParserOptions,
IManifestParserResult,
IRequestedData,
ISegmentContext,
ISegmentLoaderCallbacks,
ISegmentLoaderOptions,
ISegmentLoaderResultSegmentCreated,
ISegmentLoaderResultSegmentLoaded,
ISegmentParserParsedInitChunk,
ISegmentParserParsedMediaChunk,
ITextTrackSegmentData,
ITransportOptions,
ITransportPipelines,
} from "../types";
import addQueryString from "../utils/add_query_string";
import checkISOBMFFIntegrity from "../utils/check_isobmff_integrity";
import generateManifestLoader from "../utils/generate_manifest_loader";
import extractTimingsInfos from "./extract_timings_infos";
import isMP4EmbeddedTrack from "./is_mp4_embedded_track";
import { patchSegment } from "./isobmff";
import generateSegmentLoader from "./segment_loader";
import { constructSegmentUrl } from "./utils";
export default function (transportOptions: ITransportOptions): ITransportPipelines {
const smoothManifestParser = createSmoothManifestParser(transportOptions);
const segmentLoader = generateSegmentLoader(transportOptions);
const manifestLoaderOptions = {
customManifestLoader: transportOptions.manifestLoader,
};
const manifestLoader = generateManifestLoader(manifestLoaderOptions, "text", null);
const manifestPipeline = {
loadManifest: manifestLoader,
parseManifest(
manifestData: IRequestedData<unknown>,
parserOptions: IManifestParserOptions,
): IManifestParserResult {
const url = manifestData.url ?? parserOptions.originalUrl;
const { receivedTime: manifestReceivedTime, responseData } = manifestData;
const documentData =
typeof responseData === "string"
? new DOMParser().parseFromString(responseData, "text/xml")
: (responseData as Document); // TODO find a way to check if Document?
const parserResult = smoothManifestParser(documentData, url, manifestReceivedTime);
const warnings: IPlayerError[] = [];
const manifest = new Manifest(
parserResult,
{
representationFilter: transportOptions.representationFilter,
},
warnings,
);
return { manifest, url, warnings };
},
};
/**
* Export functions allowing to load and parse audio and video smooth
* segments.
*/
const audioVideoPipeline = {
/**
* Load a Smooth audio/video segment.
* @param {Object|null} wantedCdn
* @param {Object} context
* @param {Object} loaderOptions
* @param {Object} cancelSignal
* @param {Object} callbacks
* @returns {Promise}
*/
loadSegment(
wantedCdn: ICdnMetadata | null,
context: ISegmentContext,
loaderOptions: ISegmentLoaderOptions,
cancelSignal: CancellationSignal,
callbacks: ISegmentLoaderCallbacks<ILoadedAudioVideoSegmentFormat>,
): Promise<
| ISegmentLoaderResultSegmentLoaded<ILoadedAudioVideoSegmentFormat>
| ISegmentLoaderResultSegmentCreated<ILoadedAudioVideoSegmentFormat>
> {
const url = constructSegmentUrl(wantedCdn, context.segment);
return segmentLoader(url, context, loaderOptions, cancelSignal, callbacks);
},
parseSegment(
loadedSegment: {
data: ArrayBuffer | Uint8Array | null;
isChunked: boolean;
},
context: ISegmentContext,
initTimescale: number | undefined,
):
| ISegmentParserParsedInitChunk<ArrayBuffer | Uint8Array | null>
| ISegmentParserParsedMediaChunk<ArrayBuffer | Uint8Array | null> {
const { segment } = context;
const { data, isChunked } = loadedSegment;
if (data === null) {
if (segment.isInit) {
return {
segmentType: "init",
initializationData: null,
initializationDataSize: 0,
protectionData: [],
initTimescale: undefined,
};
}
return {
segmentType: "media",
chunkData: null,
chunkInfos: null,
chunkOffset: 0,
chunkSize: 0,
protectionData: [],
appendWindow: [undefined, undefined],
};
}
const responseBuffer = data instanceof Uint8Array ? data : new Uint8Array(data);
if (segment.isInit) {
const timescale = segment.privateInfos?.smoothInitSegment?.timescale;
return {
segmentType: "init",
initializationData: data,
initializationDataSize: data.byteLength,
// smooth init segments are crafted by hand.
// Their timescale is the one from the manifest.
initTimescale: timescale,
protectionData: [],
};
}
const timingInfos =
initTimescale !== undefined
? extractTimingsInfos(
responseBuffer,
isChunked,
initTimescale,
segment,
context.isLive,
)
: null;
if (
timingInfos === null ||
timingInfos.chunkInfos === null ||
timingInfos.scaledSegmentTime === undefined
) {
throw new Error("Smooth Segment without time information");
}
const { nextSegments, chunkInfos, scaledSegmentTime } = timingInfos;
const chunkData = patchSegment(responseBuffer, scaledSegmentTime);
const predictedSegments = nextSegments.length > 0 ? nextSegments : undefined;
return {
segmentType: "media",
chunkData,
chunkInfos,
chunkOffset: 0,
chunkSize: chunkData.length,
protectionData: [],
predictedSegments,
appendWindow: [undefined, undefined],
};
},
};
const textTrackPipeline = {
loadSegment(
wantedCdn: ICdnMetadata | null,
context: ISegmentContext,
loaderOptions: ISegmentLoaderOptions,
cancelSignal: CancellationSignal,
callbacks: ISegmentLoaderCallbacks<ILoadedTextSegmentFormat>,
): Promise<
| ISegmentLoaderResultSegmentLoaded<ILoadedTextSegmentFormat>
| ISegmentLoaderResultSegmentCreated<ILoadedTextSegmentFormat>
> {
const { segment } = context;
const url = constructSegmentUrl(wantedCdn, segment);
if (segment.isInit || url === null) {
return Promise.resolve({
resultType: "segment-created",
resultData: null,
});
}
const isMP4 = isMP4EmbeddedTrack(context.mimeType);
if (!isMP4) {
return request({
url:
loaderOptions.cmcdPayload?.type === "query"
? addQueryString(url, loaderOptions.cmcdPayload.value)
: url,
headers:
loaderOptions.cmcdPayload?.type === "headers"
? loaderOptions.cmcdPayload.value
: undefined,
responseType: "text",
timeout: loaderOptions.timeout,
connectionTimeout: loaderOptions.connectionTimeout,
cancelSignal,
onProgress: callbacks.onProgress,
}).then((data) => ({
resultType: "segment-loaded" as const,
resultData: data,
}));
} else {
return request({
url:
loaderOptions.cmcdPayload?.type === "query"
? addQueryString(url, loaderOptions.cmcdPayload.value)
: url,
headers:
loaderOptions.cmcdPayload?.type === "headers"
? loaderOptions.cmcdPayload.value
: undefined,
responseType: "arraybuffer",
timeout: loaderOptions.timeout,
connectionTimeout: loaderOptions.connectionTimeout,
cancelSignal,
onProgress: callbacks.onProgress,
}).then((data) => {
if (transportOptions.checkMediaSegmentIntegrity !== true) {
return { resultType: "segment-loaded" as const, resultData: data };
}
const dataU8 = new Uint8Array(data.responseData);
checkISOBMFFIntegrity(dataU8, context.segment.isInit);
return {
resultType: "segment-loaded" as const,
resultData: { ...data, responseData: dataU8 },
};
});
}
},
parseSegment(
loadedSegment: {
data: ArrayBuffer | Uint8Array | string | null;
isChunked: boolean;
},
context: ISegmentContext,
initTimescale: number | undefined,
):
| ISegmentParserParsedInitChunk<null>
| ISegmentParserParsedMediaChunk<ITextTrackSegmentData | null> {
const { segment, language, mimeType = "", codecs = "" } = context;
const isMP4 = isMP4EmbeddedTrack(context.mimeType);
const { data, isChunked } = loadedSegment;
let chunkSize: number | undefined;
if (segment.isInit) {
// text init segment has no use in HSS
return {
segmentType: "init",
initializationData: null,
initializationDataSize: 0,
protectionData: [],
initTimescale: undefined,
};
}
if (data === null) {
return {
segmentType: "media",
chunkData: null,
chunkInfos: null,
chunkOffset: 0,
chunkSize: 0,
protectionData: [],
appendWindow: [undefined, undefined],
};
}
let nextSegments;
let chunkInfos: IChunkTimeInfo | null = null;
let segmentStart: number | undefined;
let segmentEnd: number | undefined;
let _sdData: string;
let _sdType: string | undefined;
if (isMP4) {
let chunkBytes: Uint8Array;
if (typeof data === "string") {
chunkBytes = strToUtf8(data);
} else {
chunkBytes = data instanceof Uint8Array ? data : new Uint8Array(data);
}
chunkSize = chunkBytes.length;
const timingInfos =
initTimescale !== undefined
? extractTimingsInfos(
chunkBytes,
isChunked,
initTimescale,
segment,
context.isLive,
)
: null;
nextSegments = timingInfos?.nextSegments;
chunkInfos = timingInfos?.chunkInfos ?? null;
if (chunkInfos === null) {
if (isChunked) {
log.warn("Smooth: Unavailable time data for current text track.");
} else {
segmentStart = segment.time;
segmentEnd = segment.end;
}
} else {
segmentStart = chunkInfos.time;
segmentEnd =
chunkInfos.duration !== undefined
? chunkInfos.time + chunkInfos.duration
: segment.end;
}
const lcCodec = codecs.toLowerCase();
if (
mimeType === "application/ttml+xml+mp4" ||
lcCodec === "stpp" ||
lcCodec === "stpp.ttml" ||
lcCodec === "stpp.ttml.im1t"
) {
_sdType = "ttml";
} else if (lcCodec === "wvtt") {
_sdType = "vtt";
} else {
throw new Error(`could not find a text-track parser for the type ${mimeType}`);
}
const mdat = getMDAT(chunkBytes);
_sdData = mdat === null ? "" : utf8ToStr(mdat);
} else {
// not MP4
segmentStart = segment.time;
segmentEnd = segment.end;
let chunkString: string;
if (typeof data !== "string") {
const bytesData = data instanceof Uint8Array ? data : new Uint8Array(data);
chunkSize = bytesData.length;
chunkString = utf8ToStr(bytesData);
} else {
chunkString = data;
}
switch (mimeType) {
case "application/x-sami":
case "application/smil": // TODO SMIL should be its own format, no?
_sdType = "sami";
break;
case "application/ttml+xml":
_sdType = "ttml";
break;
case "text/vtt":
_sdType = "vtt";
break;
}
if (_sdType === undefined) {
const lcCodec = codecs.toLowerCase();
if (lcCodec === "srt") {
_sdType = "srt";
} else {
throw new Error(
`could not find a text-track parser for the type ${mimeType}`,
);
}
}
_sdData = chunkString;
}
const predictedSegments =
Array.isArray(nextSegments) && nextSegments.length > 0 ? nextSegments : undefined;
const chunkOffset = segmentStart ?? 0;
return {
segmentType: "media",
chunkData: {
type: _sdType,
data: _sdData,
start: segmentStart,
end: segmentEnd,
language,
},
chunkSize,
chunkInfos,
chunkOffset,
protectionData: [],
predictedSegments,
appendWindow: [undefined, undefined],
};
},
};
return {
transportName: "smooth",
manifest: manifestPipeline,
audio: audioVideoPipeline,
video: audioVideoPipeline,
text: textTrackPipeline,
thumbnails: {
loadThumbnail: () =>
Promise.reject(new Error("Thumbnail tracks aren't implemented with smooth")),
parseThumbnail: () => {
throw new Error("Thumbnail tracks aren't implemented with smooth");
},
},
};
}