UNPKG

@remotion/renderer

Version:

Render Remotion videos using Node.js or Bun

259 lines (258 loc) • 9.09 kB
"use strict"; /** * Copyright 2019 Google Inc. All rights reserved. * * 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. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.DOMWorld = void 0; const assert_1 = require("./assert"); const Errors_1 = require("./Errors"); const util_1 = require("./util"); class DOMWorld { #frame; #contextPromise = null; #contextResolveCallback = null; #detached = false; #waitTasks = new Set(); get _waitTasks() { return this.#waitTasks; } constructor(frame) { // Keep own reference to client because it might differ from the FrameManager's // client for OOP iframes. this.#frame = frame; this._setContext(null); } frame() { return this.#frame; } _setContext(context) { var _a; if (context) { (0, assert_1.assert)(this.#contextResolveCallback, 'Execution Context has already been set.'); (_a = this.#contextResolveCallback) === null || _a === void 0 ? void 0 : _a.call(null, context); this.#contextResolveCallback = null; for (const waitTask of this._waitTasks) { waitTask.rerun(); } } else { this.#contextPromise = new Promise((fulfill) => { this.#contextResolveCallback = fulfill; }); } } _hasContext() { return !this.#contextResolveCallback; } _detach() { this.#detached = true; for (const waitTask of this._waitTasks) { waitTask.terminate(new Error('waitForFunction failed: frame got detached.')); } } executionContext() { if (this.#detached) { throw new Error(`Execution context is not available in detached frame "${this.#frame.url()}" (are you trying to evaluate?)`); } if (this.#contextPromise === null) { throw new Error(`Execution content promise is missing`); } return this.#contextPromise; } async evaluateHandle(pageFunction, ...args) { const context = await this.executionContext(); return context.evaluateHandle(pageFunction, ...args); } async evaluate(pageFunction, ...args) { const context = await this.executionContext(); return context.evaluate(pageFunction, ...args); } waitForFunction({ browser, timeout, pageFunction, title, }) { return new WaitTask({ domWorld: this, predicateBody: pageFunction, title, timeout, args: [], browser, }); } } exports.DOMWorld = DOMWorld; const noop = () => undefined; class WaitTask { #domWorld; #timeout; #predicateBody; #args; #runCount = 0; #resolve = noop; #reject = noop; #timeoutTimer; #terminated = false; #browser; promise; constructor(options) { function getPredicateBody(predicateBody) { if ((0, util_1.isString)(predicateBody)) { return `return (${predicateBody});`; } return `return (${predicateBody})(...args);`; } this.#domWorld = options.domWorld; this.#timeout = options.timeout; this.#predicateBody = getPredicateBody(options.predicateBody); this.#args = options.args; this.#runCount = 0; this.#domWorld._waitTasks.add(this); this.promise = new Promise((resolve, reject) => { this.#resolve = resolve; this.#reject = reject; }); // Since page navigation requires us to re-install the pageScript, we should track // timeout on our end. if (options.timeout) { const timeoutError = new Errors_1.TimeoutError(`waiting for ${options.title} failed: timeout ${options.timeout}ms exceeded`); this.#timeoutTimer = setTimeout(() => { return this.#reject(timeoutError); }, options.timeout); } this.#browser = options.browser; this.#browser.on("closed" /* BrowserEmittedEvents.Closed */, this.onBrowserClose); this.#browser.on("closed-silent" /* BrowserEmittedEvents.ClosedSilent */, this.onBrowserCloseSilent); this.rerun(); } onBrowserClose = () => { return this.terminate(new Error('Browser was closed')); }; onBrowserCloseSilent = () => { return this.terminate(null); }; terminate(error) { this.#terminated = true; if (error) { this.#reject(error); } this.#cleanup(); } async rerun() { const runCount = ++this.#runCount; let success = null; let error = null; const context = await this.#domWorld.executionContext(); if (this.#terminated || runCount !== this.#runCount) { return; } if (this.#terminated || runCount !== this.#runCount) { return; } try { success = await context.evaluateHandle(waitForPredicatePageFunction, this.#predicateBody, this.#timeout, ...this.#args); } catch (error_) { error = error_; } if (this.#terminated || runCount !== this.#runCount) { if (success) { await success.dispose(); } return; } // Ignore timeouts in pageScript - we track timeouts ourselves. // If the frame's execution context has already changed, `frame.evaluate` will // throw an error - ignore this predicate run altogether. if (!error && (await this.#domWorld .evaluate((s) => { return !s; }, success) .catch(() => { return true; }))) { if (!success) { throw new Error('Assertion: result handle is not available'); } await success.dispose(); return; } if (error) { if (error.message.includes('TypeError: binding is not a function')) { return this.rerun(); } // When frame is detached the task should have been terminated by the DOMWorld. // This can fail if we were adding this task while the frame was detached, // so we terminate here instead. if (error.message.includes('Execution context is not available in detached frame')) { this.terminate(new Error('waitForFunction failed: frame got detached.')); return; } // When the page is navigated, the promise is rejected. // We will try again in the new execution context. if (error.message.includes('Execution context was destroyed')) { return; } // We could have tried to evaluate in a context which was already // destroyed. if (error.message.includes('Cannot find context with specified id')) { return; } this.#reject(error); } else { if (!success) { throw new Error('Assertion: result handle is not available'); } this.#resolve(success); } this.#cleanup(); } #cleanup() { if (this.#timeoutTimer !== undefined) { clearTimeout(this.#timeoutTimer); } this.#browser.off("closed" /* BrowserEmittedEvents.Closed */, this.onBrowserClose); this.#browser.off("closed-silent" /* BrowserEmittedEvents.ClosedSilent */, this.onBrowserCloseSilent); if (this.#domWorld._waitTasks.size > 100) { throw new Error('Leak detected: Too many WaitTasks'); } this.#domWorld._waitTasks.delete(this); } } function waitForPredicatePageFunction(predicateBody, timeout, ...args) { // eslint-disable-next-line no-new-func const predicate = new Function('...args', predicateBody); let timedOut = false; if (timeout) { setTimeout(() => { timedOut = true; }, timeout); } return new Promise((resolve) => { async function onRaf() { if (timedOut) { resolve(undefined); return; } const success = await predicate(...args); if (success) { resolve(success); } else { requestAnimationFrame(onRaf); } } onRaf(); }); }