/** * 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 hasWebassembly from "../../../../../compat/has_webassembly"; import log from "../../../../../log"; import { assertUnreachable } from "../../../../../utils/assert"; import globalScope from "../../../../../utils/global_scope"; import noop from "../../../../../utils/noop"; import type { IIrParserResponse, ILoadedXlinkData } from "../../common"; import parseMpdIr from "../../common"; import type { IMPDIntermediateRepresentation, IPeriodIntermediateRepresentation, } from "../../node_parser_types"; import type { IDashParserResponse, ILoadedResource, IMPDParserArguments, } from "../../parsers_types"; import { generateRootChildrenParser } from "./generators"; import { generateXLinkChildrenParser } from "./generators/XLink"; import ParsersStack from "./parsers_stack"; import type { AttributeName, TagName } from "./types"; import { CustomEventType } from "./types"; const MAX_READ_SIZE = 15e3; export default class DashWasmParser { /** * Current "status" of the DASH-WASM parser. * * Can be either: * - "uninitialized": Its `initialize` method hasn't been called yet. * - "initializing": The `DashWasmParser` is in the process of fetching * and instantiating the WebAssembly code. * - "initialized": The `DashWasmParser` is ready to parse. * - "failure": The `DashWasmParser` initialization failed. */ public status: "uninitialized" | "initializing" | "initialized" | "failure"; /** * Promise used to notify of the initialization status. * `null` when no initialization has happened yet. */ private _initProm: Promise | null; /** Abstraction simplifying the exploitation of the DASH-WASM parser's events. */ private _parsersStack: ParsersStack; /** * Created WebAssembly instance. * `null` when no WebAssembly instance is created. */ private _instance: WebAssembly.WebAssemblyInstantiatedSource | null; /** * Information about the data read by the DASH-WASM parser. * `null` if we're not parsing anything for the moment. */ private _mpdData: { /** * First not-yet read position in `mpd`, in bytes. * When the parser asks for new data, we start giving data from that point * on. */ cursor: number; /** * Complete data that needs to be parsed. * This is either the full MPD or xlinks. */ mpd: ArrayBufferLike; } | null; /** * Warnings event currently encountered during parsing. * Emptied when no parsing is taking place. */ private _warnings: Error[]; /** * Memory used by the WebAssembly instance. * `null` when no WebAssembly instance is created. */ private _linearMemory: WebAssembly.Memory | null; /** * `true` if we're currently parsing a MPD. * Used to explicitely forbid parsing a MPD when a parsing operation is * already pending, as the current logic cannot handle that. * * Note that this is not needed for now because parsing should happen * synchronously. * Still, this property was added to make it much more explicit, in case of * future improvements. */ private _isParsing: boolean; /** * Create a new `DashWasmParser`. */ constructor() { this._parsersStack = new ParsersStack(); this._instance = null; this._mpdData = null; this._linearMemory = null; this.status = "uninitialized"; this._initProm = null; this._warnings = []; this._isParsing = false; } /** * Returns Promise that will resolve when the initialization has ended (either * with success, in which cases the Promise resolves, either with failure, in * which case it rejects the corresponding error). * * This is actually the exact same Promise than the one returned by the first * `initialize` call. * * If that method was never called, returns a rejecting Promise. * @returns {Promise} */ public waitForInitialization(): Promise { return this._initProm ?? Promise.reject("No initialization performed yet."); } public async initialize(opts: IDashWasmParserOptions): Promise { if (this.status !== "uninitialized") { return Promise.reject(new Error("DashWasmParser already initialized.")); } else if (!this.isCompatible()) { this.status = "failure"; return Promise.reject(new Error("Target not compatible with WebAssembly.")); } this.status = "initializing"; const parsersStack = this._parsersStack; /** Re-used TextDecoder instance. */ const textDecoder = new TextDecoder(); // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; const imports = { env: { memoryBase: 0, tableBase: 0, memory: new WebAssembly.Memory({ initial: 10 }), table: new WebAssembly.Table({ initial: 1, element: "anyfunc" }), onTagOpen, onCustomEvent, onAttribute, readNext, onTagClose, }, }; let objectUrl: string | null = null; let fetchedWasm: Promise; if (typeof opts.wasmUrl === "string") { fetchedWasm = fetch(opts.wasmUrl); } else { objectUrl = URL.createObjectURL( new Blob([opts.wasmUrl], { type: "application/wasm" }), ); fetchedWasm = fetch(objectUrl); } const streamingProm = typeof WebAssembly.instantiateStreaming === "function" ? WebAssembly.instantiateStreaming(fetchedWasm, imports) : Promise.reject("`WebAssembly.instantiateStreaming` API not available"); this._initProm = streamingProm .catch(async (e) => { if (objectUrl !== null) { URL.revokeObjectURL(objectUrl); objectUrl = null; } log.warn( "dash", "Unable to call `instantiateStreaming` on WASM", e instanceof Error ? e : "", ); const res = await fetchedWasm; if (res.status < 200 || res.status >= 300) { throw new Error("WebAssembly request failed. status: " + String(res.status)); } const resAb = await res.arrayBuffer(); return WebAssembly.instantiate(resAb, imports); }) .then((instanceWasm) => { if (objectUrl !== null) { URL.revokeObjectURL(objectUrl); objectUrl = null; } this._instance = instanceWasm; // TODO better types? this._linearMemory = this._instance.instance.exports.memory as WebAssembly.Memory; this.status = "initialized"; }) .catch((err: Error) => { const message = err instanceof Error ? err.toString() : "Unknown error"; log.warn("dash", "Could not create DASH-WASM parser:", message); this.status = "failure"; throw err; }); return this._initProm; /** * Callback called when a new Element has been encountered by the WASM parser. * @param {number} tag - Identify the tag encountered (@see TagName) */ function onTagOpen(tag: TagName): void { // Call the active "childrenParser" return parsersStack.childrenParser(tag); } /** * Callback called when an open Element's ending tag has been encountered by * the WASM parser. * @param {number} tag - Identify the tag in question (@see TagName) */ function onTagClose(tag: TagName): void { // Only pop current parsers from the `parsersStack` if that tag was the // active one. return parsersStack.popIfCurrent(tag); } /** * Callback called each time a new Element's attribute is encountered by * the WASM parser. * * TODO Merge all attributes into the same callback with `onTagOpen`? I * tried but there's some difficulties if doing that. * * @param {number} attr - Identify the Attribute in question (@see TagName) * @param {number} ptr - Pointer to the first byte containing the * attribute's data in the WebAssembly's linear memory. * @param {number} len - Length of the attribute's value, in bytes. */ function onAttribute(attr: AttributeName, ptr: number, len: number): void { // Call the active "attributeParser" return parsersStack.attributeParser(attr, ptr, len); } /** * Callback called on the various "custom events" triggered by the WASM. * * @see CustomEventType * @param {number} evt - The type of the event * @param {number} ptr - Pointer to the first byte of the event's payload in * the WebAssembly's linear memory. * @param {number} len - Length of the payload, in bytes. */ function onCustomEvent(evt: CustomEventType, ptr: number, len: number): void { const linearMemory = self._linearMemory as WebAssembly.Memory; const arr = new Uint8Array(linearMemory.buffer, ptr, len); if (evt === CustomEventType.Error) { const decoded = textDecoder.decode(arr); log.warn("dash", "WASM Error Event:", decoded); self._warnings.push(new Error(decoded)); } else if (evt === CustomEventType.Log) { const decoded = textDecoder.decode(arr); log.warn("dash", "WASM Log Event:", decoded); } } /** * Callback called by the WebAssembly when it needs to read new data from * the MPD. * * @param {number} ptr - First byte offset, in the WebAssembly's linear * memory, where the MPD should be set (under an array of bytes form). * @param {number} wantedSize - Size of the data, in bytes, asked by the * WebAssembly parser. It might receive less depending on if there's less * data in the MPD or if it goes over the set maximum size it could read * at a time. * @returns {number} - Return the number of bytes effectively read and set * in WebAssembly's linear memory (at the `ptr` offset). */ function readNext(ptr: number, wantedSize: number): number { if (self._mpdData === null) { throw new Error("DashWasmParser Error: No MPD to read."); } const linearMemory = self._linearMemory as WebAssembly.Memory; const { mpd, cursor } = self._mpdData; const sizeToRead = Math.min(wantedSize, MAX_READ_SIZE, mpd.byteLength - cursor); const arr = new Uint8Array(linearMemory.buffer, ptr, sizeToRead); arr.set(new Uint8Array(mpd, cursor, sizeToRead)); self._mpdData.cursor += sizeToRead; return sizeToRead; } } /** * @param {Document} manifest - Original manifest as returned by the server * @param {Object} args * @returns {Object} */ public runWasmParser( mpd: ArrayBufferLike, args: IMPDParserArguments, ): IDashParserResponse | IDashParserResponse { const [mpdIR, warnings] = this._parseMpd(mpd); if (mpdIR === null) { throw new Error("DASH Parser: Unknown error while parsing the MPD"); } const ret = parseMpdIr(mpdIR, args, warnings); return this._processParserReturnValue(ret); } /** * Return `true` if the current plaform is compatible with WebAssembly and the * TextDecoder interface (for faster UTF-8 parsing), which are needed features * for the `DashWasmParser`. * @returns {boolean} */ public isCompatible(): boolean { return hasWebassembly && typeof globalScope.TextDecoder === "function"; } private _parseMpd( mpd: ArrayBufferLike, ): [IMPDIntermediateRepresentation | null, Error[]] { if (this._instance === null) { throw new Error("DashWasmParser not initialized"); } if (this._isParsing) { throw new Error("Parsing operation already pending."); } this._isParsing = true; this._mpdData = { mpd, cursor: 0 }; const rootObj: { mpd?: IMPDIntermediateRepresentation } = {}; const linearMemory = this._linearMemory as WebAssembly.Memory; const rootChildrenParser = generateRootChildrenParser( rootObj, linearMemory, this._parsersStack, mpd, ); this._parsersStack.pushParsers(null, rootChildrenParser, noop); this._warnings = []; try { // TODO better type this (this._instance.instance.exports.parse as () => void)(); } catch (err) { this._parsersStack.reset(); this._warnings = []; this._isParsing = false; throw err; } const parsed = rootObj.mpd ?? null; const warnings = this._warnings; this._parsersStack.reset(); this._warnings = []; this._isParsing = false; return [parsed, warnings]; } private _parseXlink( xlinkData: ArrayBuffer, ): [IPeriodIntermediateRepresentation[], Error[]] { if (this._instance === null) { throw new Error("DashWasmParser not initialized"); } if (this._isParsing) { throw new Error("Parsing operation already pending."); } this._isParsing = true; this._mpdData = { mpd: xlinkData, cursor: 0 }; const rootObj: { periods: IPeriodIntermediateRepresentation[] } = { periods: [], }; const linearMemory = this._linearMemory as WebAssembly.Memory; const xlinkParser = generateXLinkChildrenParser( rootObj, linearMemory, this._parsersStack, xlinkData, ); this._parsersStack.pushParsers(null, xlinkParser, noop); this._warnings = []; try { // TODO better type this (this._instance.instance.exports.parse as () => void)(); } catch (err) { this._parsersStack.reset(); this._warnings = []; this._isParsing = false; throw err; } const { periods } = rootObj; const warnings = this._warnings; this._parsersStack.reset(); this._warnings = []; this._isParsing = false; return [periods, warnings]; } /** * Handle `parseMpdIr` return values, asking for resources if they are needed * and pre-processing them before continuing parsing. * * @param {Object} initialRes * @returns {Object} */ private _processParserReturnValue( initialRes: IIrParserResponse, ): IDashParserResponse | IDashParserResponse { if (initialRes.type === "done") { return initialRes; } else if (initialRes.type === "needs-clock") { const continueParsingMPD = ( loadedClock: Array>, ): IDashParserResponse | IDashParserResponse => { if (loadedClock.length !== 1) { throw new Error("DASH parser: wrong number of loaded ressources."); } const newRet = initialRes.value.continue(loadedClock[0].responseData); return this._processParserReturnValue(newRet); }; return { type: "needs-resources", value: { urls: [initialRes.value.url], format: "string", continue: continueParsingMPD, }, }; } else if (initialRes.type === "needs-xlinks") { const continueParsingMPD = ( loadedXlinks: Array>, ): IDashParserResponse | IDashParserResponse => { const resourceInfos: ILoadedXlinkData[] = []; for (let i = 0; i < loadedXlinks.length; i++) { const { responseData: xlinkResp, receivedTime, sendingTime, url, } = loadedXlinks[i]; if (!xlinkResp.success) { throw xlinkResp.error; } const [periodsIr, periodsIRWarnings] = this._parseXlink(xlinkResp.data); resourceInfos.push({ url, receivedTime, sendingTime, parsed: periodsIr, warnings: periodsIRWarnings, }); } const newRet = initialRes.value.continue(resourceInfos); return this._processParserReturnValue(newRet); }; return { type: "needs-resources", value: { urls: initialRes.value.xlinksUrls, format: "arraybuffer", continue: continueParsingMPD, }, }; } else { assertUnreachable(initialRes); } } } /** Options needed when constructing the DASH-WASM parser. */ export interface IDashWasmParserOptions { wasmUrl: string | ArrayBuffer; }