rx-player
Version:
Canal+ HTML5 Video Player
296 lines (295 loc) • 12.3 kB
JavaScript
/**
* 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;
}