From 61d0e74709fb55a7c9d87c898bdb31640740591a Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Thu, 1 Oct 2026 11:43:42 +0700 Subject: [PATCH] refactor(render): share the render core and bearer check across routes --- renderer/src/lib/bearer-auth.js | 7 ++ renderer/src/render/render-composition.js | 80 +++++++++++++++++++++++ renderer/src/render/render-gif.js | 74 +++------------------ renderer/src/routes/gif.js | 8 +-- 4 files changed, 100 insertions(+), 69 deletions(-) create mode 100644 renderer/src/lib/bearer-auth.js create mode 100644 renderer/src/render/render-composition.js diff --git a/renderer/src/lib/bearer-auth.js b/renderer/src/lib/bearer-auth.js new file mode 100644 index 0000000..454b92f --- /dev/null +++ b/renderer/src/lib/bearer-auth.js @@ -0,0 +1,7 @@ +/** + * @param {import('fastify').FastifyRequest} request + * @param {string | undefined} apiToken + * @returns {boolean} true when no token is configured or the request carries it. + */ +export const isAuthorized = (request, apiToken) => + !apiToken || request.headers.authorization === `Bearer ${apiToken}`; diff --git a/renderer/src/render/render-composition.js b/renderer/src/render/render-composition.js new file mode 100644 index 0000000..ea7d803 --- /dev/null +++ b/renderer/src/render/render-composition.js @@ -0,0 +1,80 @@ +import {readFile} from 'node:fs/promises'; +import path from 'node:path'; +import {makeCancelSignal, renderMedia, selectComposition} from '@remotion/renderer'; +import {minRenderTimeoutMs} from '../config.js'; +import {RenderTimeoutError} from '../lib/render-errors.js'; +import {cleanupTempDir, createRenderTempDir} from '../lib/tmp-files.js'; +import {getRemotionServeUrl} from './remotion-bundle.js'; + +/** + * @typedef {object} RenderCompositionOptions + * @property {string} compositionId + * @property {Record} inputProps + * @property {string} filename + * @property {{codec: import('@remotion/renderer').Codec} & Partial} media Codec and encoder settings. + * @property {number} timeoutInMilliseconds + */ + +/** + * @typedef {object} RenderResult + * @property {Buffer} buffer + * @property {number} durationMs + * @property {number} byteLength + */ + +/** + * Renders one composition from the shared bundle into memory, cancelling the + * render once the total timeout elapses. + * + * @param {RenderCompositionOptions} options + * @returns {Promise} + */ +export const renderComposition = async (options) => { + const startedAt = Date.now(); + const tempDir = await createRenderTempDir(); + const outputLocation = path.join(tempDir, options.filename); + const {cancel, cancelSignal} = makeCancelSignal(); + let timedOut = false; + const timeoutId = setTimeout(() => { + timedOut = true; + cancel(); + }, options.timeoutInMilliseconds); + const rendererTimeout = Math.max(minRenderTimeoutMs, options.timeoutInMilliseconds); + + try { + const serveUrl = await getRemotionServeUrl(); + const composition = await selectComposition({ + id: options.compositionId, + inputProps: options.inputProps, + serveUrl, + timeoutInMilliseconds: rendererTimeout, + }); + + await renderMedia({ + ...options.media, + cancelSignal, + composition, + inputProps: options.inputProps, + outputLocation, + overwrite: true, + serveUrl, + timeoutInMilliseconds: rendererTimeout, + }); + + const buffer = await readFile(outputLocation); + return { + buffer, + byteLength: buffer.byteLength, + durationMs: Date.now() - startedAt, + }; + } catch (error) { + if (timedOut || Date.now() - startedAt >= options.timeoutInMilliseconds) { + throw new RenderTimeoutError(options.timeoutInMilliseconds, {cause: error}); + } + + throw error; + } finally { + clearTimeout(timeoutId); + await cleanupTempDir(tempDir); + } +}; diff --git a/renderer/src/render/render-gif.js b/renderer/src/render/render-gif.js index cb89a8c..62dff40 100644 --- a/renderer/src/render/render-gif.js +++ b/renderer/src/render/render-gif.js @@ -1,13 +1,8 @@ -import {readFile} from 'node:fs/promises'; -import path from 'node:path'; -import {makeCancelSignal, renderMedia, selectComposition} from '@remotion/renderer'; -import {minRenderTimeoutMs} from '../config.js'; -import {RenderTimeoutError} from '../lib/render-errors.js'; -import {cleanupTempDir, createRenderTempDir} from '../lib/tmp-files.js'; -import {getRemotionServeUrl} from './remotion-bundle.js'; +import {renderComposition} from './render-composition.js'; /** * @typedef {import('../schemas/wheel-request.js').WheelRenderRequest} WheelRenderRequest + * @typedef {import('./render-composition.js').RenderResult} RenderGifResult */ /** @@ -15,65 +10,16 @@ import {getRemotionServeUrl} from './remotion-bundle.js'; * @property {number} timeoutInMilliseconds */ -/** - * @typedef {object} RenderGifResult - * @property {Buffer} buffer - * @property {number} durationMs - * @property {number} byteLength - */ - /** * @param {WheelRenderRequest} inputProps * @param {RenderWheelGifOptions} options * @returns {Promise} */ -export const renderWheelGif = async (inputProps, options) => { - const startedAt = Date.now(); - const tempDir = await createRenderTempDir(); - const outputLocation = path.join(tempDir, 'wheelofnames.gif'); - const {cancel, cancelSignal} = makeCancelSignal(); - let timedOut = false; - const timeoutId = setTimeout(() => { - timedOut = true; - cancel(); - }, options.timeoutInMilliseconds); - const rendererTimeout = Math.max(minRenderTimeoutMs, options.timeoutInMilliseconds); - - try { - const serveUrl = await getRemotionServeUrl(); - const composition = await selectComposition({ - id: 'WheelGif', - inputProps, - serveUrl, - timeoutInMilliseconds: rendererTimeout, - }); - - await renderMedia({ - cancelSignal, - codec: 'gif', - composition, - imageFormat: 'png', - inputProps, - outputLocation, - overwrite: true, - serveUrl, - timeoutInMilliseconds: rendererTimeout, - }); - - const buffer = await readFile(outputLocation); - return { - buffer, - byteLength: buffer.byteLength, - durationMs: Date.now() - startedAt, - }; - } catch (error) { - if (timedOut || Date.now() - startedAt >= options.timeoutInMilliseconds) { - throw new RenderTimeoutError(options.timeoutInMilliseconds, {cause: error}); - } - - throw error; - } finally { - clearTimeout(timeoutId); - await cleanupTempDir(tempDir); - } -}; +export const renderWheelGif = (inputProps, options) => + renderComposition({ + compositionId: 'WheelGif', + filename: 'wheelofnames.gif', + inputProps: {...inputProps}, + media: {codec: 'gif', imageFormat: 'png'}, + timeoutInMilliseconds: options.timeoutInMilliseconds, + }); diff --git a/renderer/src/routes/gif.js b/renderer/src/routes/gif.js index 50dba82..438e9fb 100644 --- a/renderer/src/routes/gif.js +++ b/renderer/src/routes/gif.js @@ -1,4 +1,5 @@ import {ZodError} from 'zod'; +import {isAuthorized} from '../lib/bearer-auth.js'; import {isRenderTimeoutError} from '../lib/render-errors.js'; import {pickWinnerIndex} from '../lib/winner.js'; import {parseWheelRequest} from '../schemas/wheel-request.js'; @@ -30,11 +31,8 @@ const formatValidationError = (issues) => ({ */ export const registerGifRoute = async (app, deps) => { app.post('/api/gif', async (request, reply) => { - if (deps.config.apiToken) { - const expected = `Bearer ${deps.config.apiToken}`; - if (request.headers.authorization !== expected) { - return reply.code(401).send({error: 'unauthorized'}); - } + if (!isAuthorized(request, deps.config.apiToken)) { + return reply.code(401).send({error: 'unauthorized'}); } let wheelRequest;