remotion
Version:
Make videos programmatically
137 lines (136 loc) • 5.77 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.runEffectChain = exports.cleanupEffectChainState = exports.createEffectChainState = void 0;
const canvas_pool_js_1 = require("./canvas-pool.js");
const effect_internals_js_1 = require("./effect-internals.js");
const gpu_device_js_1 = require("./gpu-device.js");
const createEffectChainState = (width, height) => ({
pool: new canvas_pool_js_1.CanvasPool(width, height),
setupCache: new WeakMap(),
cleanupRegistry: [],
currentRunId: 0,
});
exports.createEffectChainState = createEffectChainState;
const cleanupEffectChainState = (state) => {
state.currentRunId++;
for (const entry of state.cleanupRegistry) {
entry.definition.cleanup(entry.state);
}
};
exports.cleanupEffectChainState = cleanupEffectChainState;
const ensureSetup = (state, def, target) => {
const widened = def;
if (state.setupCache.has(widened)) {
return state.setupCache.get(widened);
}
const setupState = def.setup(target);
state.setupCache.set(widened, setupState);
state.cleanupRegistry.push({ definition: widened, state: setupState });
return setupState;
};
// Runs the effect pipeline imperatively. Returns `true` if the pipeline
// completed and wrote to `output`, `false` if it was superseded by a newer
// run (caller should not act on a stale result).
const runEffectChain = async ({ state, source, effects, output, width, height, }) => {
var _a;
const runId = ++state.currentRunId;
const isCancelled = () => state.currentRunId !== runId;
// Bypass any effect with `disabled: true` before grouping by backend, so
// disabled effects don't create empty runs or force unnecessary backend
// transitions. The `disabled` flag is injected by `createEffect` and lives
// on `params` so it flows through code/drag override merging.
const enabledEffects = effects.filter((e) => !e.params.disabled);
const runs = (0, effect_internals_js_1.groupByBackend)(enabledEffects);
let currentImage = source;
let lastTarget = null;
if (runs.length === 0) {
// In-place pipeline (e.g. <HtmlInCanvas>: drawElementImage into the same
// surface, no further effects) — the bitmap is already on `output`.
if (source === output) {
return true;
}
const ctx = output.getContext('2d');
if (!ctx) {
throw new Error('Failed to acquire 2D context for output canvas');
}
ctx.clearRect(0, 0, width, height);
ctx.drawImage(currentImage, 0, 0, width, height);
return true;
}
let needsGpuDevice = false;
for (const run of runs) {
if (run.backend === 'webgpu') {
needsGpuDevice = true;
break;
}
}
const gpuDevice = needsGpuDevice ? await (0, gpu_device_js_1.getGpuDevice)() : null;
if (isCancelled()) {
return false;
}
// Raw component sources are 2D frame canvases (Gif, WrappedCanvas.canvas, …).
let flipWebGLSourceY = true;
for (let runIndex = 0; runIndex < runs.length; runIndex++) {
const run = runs[runIndex];
const [a, b] = state.pool.getPair(run.backend);
let dst = a;
for (const eff of run.effects) {
const def = eff.definition;
const setupState = ensureSetup(state, def, dst);
def.apply({
source: currentImage,
target: dst,
state: setupState,
params: eff.params,
width,
height,
gpuDevice,
flipSourceY: run.backend === 'webgl2' ? flipWebGLSourceY : false,
});
if (run.backend === 'webgl2') {
flipWebGLSourceY = false;
state.pool.assertContextNotLost(dst);
}
currentImage = dst;
dst = dst === a ? b : a;
}
lastTarget = (_a = currentImage) !== null && _a !== void 0 ? _a : lastTarget;
const nextRun = runs[runIndex + 1];
if (nextRun && nextRun.backend !== run.backend && lastTarget) {
// 2D → WebGL: pass the 2D canvas directly so `texImage2D` + `UNPACK_FLIP_Y`
// matches blur-only on a raw frame canvas. `createImageBitmap` here changes
// upload orientation and produced upside-down stacks (wave + blur).
if (run.backend === '2d' && nextRun.backend === 'webgl2') {
currentImage = lastTarget;
flipWebGLSourceY = true;
}
else {
// Other bridges use `createImageBitmap` rather than passing the canvas
// straight through. A direct `drawImage(webglCanvas)` in the next
// backend's first effect forces an implicit GPU readback / finish on the
// consuming context, which empirically blows the per-frame vsync budget and
// halves the paint rate. `createImageBitmap` pipelines the GPU work.
const bitmap = await createImageBitmap(lastTarget);
if (isCancelled()) {
bitmap.close();
return false;
}
currentImage = bitmap;
if (nextRun.backend === 'webgl2') {
flipWebGLSourceY = false;
}
}
}
}
if (!lastTarget) {
return true;
}
const outCtx = output.getContext('2d');
if (!outCtx) {
throw new Error('Failed to acquire 2D context for output canvas');
}
outCtx.clearRect(0, 0, width, height);
outCtx.drawImage(lastTarget, 0, 0, width, height);
return true;
};
exports.runEffectChain = runEffectChain;