UNPKG

rx-player

Version:
296 lines (295 loc) 12.3 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 { formatError } from "../../errors"; import features from "../../features"; import log from "../../log"; import Manifest from "../../manifest/classes"; import globalScope from "../../utils/global_scope"; import isNullOrUndefined from "../../utils/is_null_or_undefined"; import objectAssign from "../../utils/object_assign"; import request from "../../utils/request"; import { strToUtf8, utf8ToStr } from "../../utils/string_parsing"; export default function generateManifestParser(options) { const { referenceDateTime } = options; const serverTimeOffset = options.serverSyncInfos !== undefined ? options.serverSyncInfos.serverTimestamp - options.serverSyncInfos.clientTime : undefined; return function manifestParser(manifestData, parserOptions, onWarnings, cancelSignal, scheduleRequest) { var _a; const { responseData } = manifestData; const argClockOffset = parserOptions.externalClockOffset; const url = (_a = manifestData.url) !== null && _a !== void 0 ? _a : parserOptions.originalUrl; const externalClockOffset = serverTimeOffset !== null && serverTimeOffset !== void 0 ? serverTimeOffset : argClockOffset; const unsafelyBaseOnPreviousManifest = parserOptions.unsafeMode ? parserOptions.previousManifest : null; const dashParserOpts = { unsafelyBaseOnPreviousManifest, url, referenceDateTime, externalClockOffset, }; const parsers = features.dashParsers; if (parsers.wasm === null || parsers.wasm.status === "uninitialized" || parsers.wasm.status === "failure") { log.debug("dash", "WASM MPD Parser not initialized. Running JS one."); return runDefaultJsParser(); } else { const manifestAB = getManifestAsArrayBuffer(responseData); if (!doesXmlSeemsUtf8Encoded(manifestAB)) { log.info("dash", "MPD doesn't seem to be UTF-8-encoded. Running JS parser instead of the WASM one."); return runDefaultJsParser(); } if (parsers.wasm.status === "initialized") { log.debug("dash", "Running WASM MPD Parser."); const parsed = parsers.wasm.runWasmParser(manifestAB, dashParserOpts); return processMpdParserResponse(parsed); } else { log.debug("dash", "Awaiting WASM initialization before parsing the MPD."); const initProm = parsers.wasm.waitForInitialization().catch(() => { /* ignore errors, we will check the status later */ }); return initProm.then(() => { if (parsers.wasm === null || parsers.wasm.status !== "initialized") { log.warn("dash", "WASM MPD parser initialization failed. Running JS parser instead"); return runDefaultJsParser(); } log.debug("dash", "Running WASM MPD Parser."); const parsed = parsers.wasm.runWasmParser(manifestAB, dashParserOpts); return processMpdParserResponse(parsed); }); } } /** * Parse the MPD through the default JS-written parser (as opposed to the * WebAssembly one). * If it is not defined, throws. * @returns {Object|Promise.<Object>} */ function runDefaultJsParser() { if (parsers.js !== null) { const manifestStr = getManifestAsString(responseData); const parsedManifest = parsers.js(manifestStr, dashParserOpts); return processMpdParserResponse(parsedManifest); } else { throw new Error("No MPD parser is imported"); } } /** * Process return of one of the MPD parser. * If it asks for a resource, load it then continue. * @param {Object} parserResponse - Response returned from a MPD parser. * @returns {Object|Promise.<Object>} */ function processMpdParserResponse(parserResponse) { if (parserResponse.type === "done") { if (parserResponse.value.warnings.length > 0) { onWarnings(parserResponse.value.warnings); } if (cancelSignal.isCancelled()) { return Promise.reject(cancelSignal.cancellationError); } const manifest = new Manifest(parserResponse.value.parsed, options); return { manifest, url }; } const { value } = parserResponse; const externalResources = value.urls.map((resourceUrl) => { return scheduleRequest(() => { const defaultTimeout = config.getCurrent().DEFAULT_REQUEST_TIMEOUT; const defaultConnectionTimeout = config.getCurrent().DEFAULT_CONNECTION_TIMEOUT; return value.format === "string" ? request({ url: resourceUrl, responseType: "text", timeout: defaultTimeout, connectionTimeout: defaultConnectionTimeout, cancelSignal, }) : request({ url: resourceUrl, responseType: "arraybuffer", timeout: defaultTimeout, connectionTimeout: defaultConnectionTimeout, cancelSignal, }); }).then((res) => { if (value.format === "string") { if (typeof res.responseData !== "string") { throw new Error("External DASH resources should have been a string"); } return objectAssign(res, { responseData: { success: true, data: res.responseData, }, }); } else { if (!(res.responseData instanceof ArrayBuffer)) { throw new Error("External DASH resources should have been ArrayBuffers"); } return objectAssign(res, { responseData: { success: true, data: res.responseData, }, }); } }, (err) => { const error = formatError(err, { defaultCode: "PIPELINE_PARSE_ERROR", defaultReason: "An unknown error occured when parsing ressources.", }); return objectAssign({}, { size: undefined, requestDuration: undefined, responseData: { success: false, error, }, }); }); }); return Promise.all(externalResources).then((loadedResources) => { if (value.format === "string") { assertLoadedResourcesFormatString(loadedResources); return processMpdParserResponse(value.continue(loadedResources)); } else { assertLoadedResourcesFormatArrayBuffer(loadedResources); return processMpdParserResponse(value.continue(loadedResources)); } }); } }; } /** * Throw if the given input is not in the expected format. * Allows to enforce runtime type-checking as compile-time type-checking here is * difficult to enforce. * * @param loadedResource * @returns */ function assertLoadedResourcesFormatString(loadedResources) { if (0 /* __ENVIRONMENT__.CURRENT_ENV */ === 0 /* __ENVIRONMENT__.PRODUCTION */) { return; } loadedResources.forEach((loadedResource) => { const { responseData } = loadedResource; if (responseData.success && typeof responseData.data === "string") { return; } else if (!responseData.success) { return; } throw new Error("Invalid data given to the LoadedRessource"); }); } /** * Throw if the given input is not in the expected format. * Allows to enforce runtime type-checking as compile-time type-checking here is * difficult to enforce. * * @param loadedResource * @returns */ function assertLoadedResourcesFormatArrayBuffer(loadedResources) { if (0 /* __ENVIRONMENT__.CURRENT_ENV */ === 0 /* __ENVIRONMENT__.PRODUCTION */) { return; } loadedResources.forEach((loadedResource) => { const { responseData } = loadedResource; if (responseData.success && responseData.data instanceof ArrayBuffer) { return; } else if (!responseData.success) { return; } throw new Error("Invalid data given to the LoadedRessource"); }); } /** * Try to convert a Manifest from an unknown format to an array of nodes as * parsed by our XML DOM parser. * * Throws if the format cannot be converted. * @param {*} manifestSrc * @returns {Array.<Object | string>} */ function getManifestAsString(manifestSrc) { if (manifestSrc instanceof ArrayBuffer) { return utf8ToStr(new Uint8Array(manifestSrc)); } else if (typeof manifestSrc === "string") { return manifestSrc; } else if (!isNullOrUndefined(globalScope.Document) && manifestSrc instanceof globalScope.Document) { return manifestSrc.documentElement.outerHTML; } else { throw new Error("DASH Manifest Parser: Unrecognized Manifest format"); } } /** * Try to convert a Manifest from an unknown format to an `ArrayBuffer` format. * Throws if the format cannot be converted. * @param {*} manifestSrc * @returns {ArrayBuffer} */ function getManifestAsArrayBuffer(manifestSrc) { if (manifestSrc instanceof ArrayBuffer) { return manifestSrc; } else if (typeof manifestSrc === "string") { return strToUtf8(manifestSrc).buffer; } else if (!isNullOrUndefined(globalScope.Document) && manifestSrc instanceof globalScope.Document) { return strToUtf8(manifestSrc.documentElement.innerHTML).buffer; } else { throw new Error("DASH Manifest Parser: Unrecognized Manifest format"); } } /** * Returns true if the given XML appears to be encoded in UTF-8. * * For now, this function can return a lot of false positives, but it should * mostly work with real use cases. * @param {ArrayBuffer} xmlData * @returns {boolean} */ function doesXmlSeemsUtf8Encoded(xmlData) { const dv = new DataView(xmlData); if (dv.getUint16(0) === 0xefbb && dv.getUint8(2) === 0xbf) { // (UTF-8 BOM) return true; } else if (dv.getUint16(0) === 0xfeff || dv.getUint16(0) === 0xfffe) { // (UTF-16 BOM) return false; } // TODO check encoding from request mimeType and text declaration? // https://www.w3.org/TR/xml/#sec-TextDecl return true; }