refactor(render): share the render core and bearer check across routes

This commit is contained in:
tiennm99 committed 2026-10-01 11:43:42 +07:00
1 parent 2fd362dd01
commit 61d0e74709
4 files changed
+100 -69

No files matched your search

+7
View File
@@ -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}`;
+80
View File
@@ -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<string, unknown>} inputProps
* @property {string} filename
* @property {{codec: import('@remotion/renderer').Codec} & Partial<import('@remotion/renderer').RenderMediaOptions>} 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<RenderResult>}
*/
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);
}
};
+10 -64
View File
@@ -1,13 +1,8 @@
import {readFile} from 'node:fs/promises'; import {renderComposition} from './render-composition.js';
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 {import('../schemas/wheel-request.js').WheelRenderRequest} WheelRenderRequest * @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 * @property {number} timeoutInMilliseconds
*/ */
/**
* @typedef {object} RenderGifResult
* @property {Buffer} buffer
* @property {number} durationMs
* @property {number} byteLength
*/
/** /**
* @param {WheelRenderRequest} inputProps * @param {WheelRenderRequest} inputProps
* @param {RenderWheelGifOptions} options * @param {RenderWheelGifOptions} options
* @returns {Promise<RenderGifResult>} * @returns {Promise<RenderGifResult>}
*/ */
export const renderWheelGif = async (inputProps, options) => { export const renderWheelGif = (inputProps, options) =>
const startedAt = Date.now(); renderComposition({
const tempDir = await createRenderTempDir(); compositionId: 'WheelGif',
const outputLocation = path.join(tempDir, 'wheelofnames.gif'); filename: 'wheelofnames.gif',
const {cancel, cancelSignal} = makeCancelSignal(); inputProps: {...inputProps},
let timedOut = false; media: {codec: 'gif', imageFormat: 'png'},
const timeoutId = setTimeout(() => { timeoutInMilliseconds: options.timeoutInMilliseconds,
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);
}
};
+3 -5
View File
@@ -1,4 +1,5 @@
import {ZodError} from 'zod'; import {ZodError} from 'zod';
import {isAuthorized} from '../lib/bearer-auth.js';
import {isRenderTimeoutError} from '../lib/render-errors.js'; import {isRenderTimeoutError} from '../lib/render-errors.js';
import {pickWinnerIndex} from '../lib/winner.js'; import {pickWinnerIndex} from '../lib/winner.js';
import {parseWheelRequest} from '../schemas/wheel-request.js'; import {parseWheelRequest} from '../schemas/wheel-request.js';
@@ -30,11 +31,8 @@ const formatValidationError = (issues) => ({
*/ */
export const registerGifRoute = async (app, deps) => { export const registerGifRoute = async (app, deps) => {
app.post('/api/gif', async (request, reply) => { app.post('/api/gif', async (request, reply) => {
if (deps.config.apiToken) { if (!isAuthorized(request, deps.config.apiToken)) {
const expected = `Bearer ${deps.config.apiToken}`; return reply.code(401).send({error: 'unauthorized'});
if (request.headers.authorization !== expected) {
return reply.code(401).send({error: 'unauthorized'});
}
} }
let wheelRequest; let wheelRequest;