@effing/ffs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/motion.ts","../src/effect.ts","../src/ffmpeg.ts","../src/transition.ts","../src/render.ts","../src/fetch.ts","../src/cache.ts"],"sourcesContent":["import type { EffieMotion } from \"@effing/effie\";\n\n/**\n * Defines the building blocks for an FFmpeg motion expression.\n */\ntype MotionComponents = {\n initialX: string; // Expression for X before animation starts\n initialY: string; // Expression for Y before animation starts\n activeX: string; // Expression for X during animation (incorporates relative time)\n activeY: string; // Expression for Y during animation (incorporates relative time)\n finalX: string; // Expression for X after animation ends\n finalY: string; // Expression for Y after animation ends\n duration: number; // Duration of the animation effect itself\n};\n\nfunction getEasingExpression(\n tNormExpr: string,\n easingType: \"linear\" | \"ease-in\" | \"ease-out\" | \"ease-in-out\",\n): string {\n switch (easingType) {\n case \"ease-in\":\n // t^2\n return `pow(${tNormExpr},2)`;\n case \"ease-out\":\n // 1 - (1-t)^2\n return `(1-pow(1-(${tNormExpr}),2))`;\n case \"ease-in-out\":\n // t < 0.5 ? 2*t^2 : 1 - (-2*t + 2)^2 / 2\n return `if(lt(${tNormExpr},0.5),2*pow(${tNormExpr},2),1-pow(-2*(${tNormExpr})+2,2)/2)`;\n case \"linear\":\n default:\n // Default to linear if type is unknown or \"linear\"\n return `(${tNormExpr})`; // Ensure parentheses for safety if tNormExpr is complex\n }\n}\n\nfunction processSlideMotion(\n motion: Extract<EffieMotion, { type: \"slide\" }>,\n relativeTimeExpr: string,\n): MotionComponents {\n const duration = motion.duration ?? 1;\n const distance = motion.distance ?? 1;\n const reverse = motion.reverse ?? false;\n const easing = motion.easing ?? \"linear\"; // Default to linear easing\n\n // 1. Calculate normalized time (0 to 1 over the duration)\n // Assuming duration > 0\n const tNormExpr = `(${relativeTimeExpr})/${duration}`;\n\n // 2. Get the easing function expression applied to normalized time\n const easedProgressExpr = getEasingExpression(tNormExpr, easing);\n\n // 3. Determine the final time factor based on easing and direction (reverse)\n // - If reverse (slide out): Progress goes 0 -> 1 (eased)\n // - If not reverse (slide in): Progress goes 1 -> 0 (eased, so 1 - eased_progress)\n const finalTimeFactorExpr = reverse\n ? easedProgressExpr\n : `(1-(${easedProgressExpr}))`; // Parentheses around easedProgressExpr are crucial\n\n let activeX: string;\n let activeY: string;\n let initialX: string;\n let initialY: string;\n let finalX: string;\n let finalY: string;\n\n switch (motion.direction) {\n case \"left\": {\n const offsetXLeft = `${distance}*W`;\n activeX = `(${offsetXLeft})*${finalTimeFactorExpr}`;\n activeY = \"0\";\n initialX = reverse ? \"0\" : offsetXLeft;\n initialY = \"0\";\n finalX = reverse ? offsetXLeft : \"0\";\n finalY = \"0\";\n break;\n }\n case \"right\": {\n const offsetXRight = `-${distance}*W`;\n activeX = `(${offsetXRight})*${finalTimeFactorExpr}`;\n activeY = \"0\";\n initialX = reverse ? \"0\" : offsetXRight;\n initialY = \"0\";\n finalX = reverse ? offsetXRight : \"0\";\n finalY = \"0\";\n break;\n }\n case \"up\": {\n const offsetYUp = `${distance}*H`;\n activeX = \"0\";\n activeY = `(${offsetYUp})*${finalTimeFactorExpr}`;\n initialX = \"0\";\n initialY = reverse ? \"0\" : offsetYUp;\n finalX = \"0\";\n finalY = reverse ? offsetYUp : \"0\";\n break;\n }\n case \"down\": {\n const offsetYDown = `-${distance}*H`;\n activeX = \"0\";\n activeY = `(${offsetYDown})*${finalTimeFactorExpr}`;\n initialX = \"0\";\n initialY = reverse ? \"0\" : offsetYDown;\n finalX = \"0\";\n finalY = reverse ? offsetYDown : \"0\";\n break;\n }\n }\n\n return { initialX, initialY, activeX, activeY, finalX, finalY, duration };\n}\n\nfunction processBounceMotion(\n motion: Extract<EffieMotion, { type: \"bounce\" }>,\n relativeTimeExpr: string,\n): MotionComponents {\n const amplitude = motion.amplitude ?? 0.5;\n const duration = motion.duration ?? 1;\n const initialY = `-overlay_h*${amplitude}`;\n const finalY = \"0\";\n\n // Calculate the normalized time expression (ranging from 0 to 1 over the duration)\n // Note: Assumes duration > 0. FFmpeg might handle division by zero, but it's safer to ensure duration > 0.\n const tNormExpr = `(${relativeTimeExpr})/${duration}`;\n\n // Piecewise parabolic approximation using normalized time (tNormExpr)\n const activeBounceExpression =\n `if(lt(${tNormExpr},0.363636),${initialY}+overlay_h*${amplitude}*(7.5625*${tNormExpr}*${tNormExpr}),` +\n `if(lt(${tNormExpr},0.727273),${initialY}+overlay_h*${amplitude}*(7.5625*(${tNormExpr}-0.545455)*(${tNormExpr}-0.545455)+0.75),` +\n `if(lt(${tNormExpr},0.909091),${initialY}+overlay_h*${amplitude}*(7.5625*(${tNormExpr}-0.818182)*(${tNormExpr}-0.818182)+0.9375),` +\n `if(lt(${tNormExpr},0.954545),${initialY}+overlay_h*${amplitude}*(7.5625*(${tNormExpr}-0.954545)*(${tNormExpr}-0.954545)+0.984375),` +\n `${finalY}` + // Should settle to finalY as tNormExpr approaches 1\n `))))`;\n\n return {\n initialX: \"0\",\n initialY: initialY,\n activeX: \"0\",\n activeY: activeBounceExpression, // This expression now scales with duration\n finalX: \"0\",\n finalY: finalY,\n duration: duration, // Return the actual duration used\n };\n}\n\nfunction processShakeMotion(\n motion: Extract<EffieMotion, { type: \"shake\" }>,\n relativeTimeExpr: string,\n): MotionComponents {\n const intensity = motion.intensity ?? 10;\n const frequency = motion.frequency ?? 4;\n const duration = motion.duration ?? 1;\n\n const activeX = `${intensity}*sin(${relativeTimeExpr}*PI*${frequency})`;\n const activeY = `${intensity}*cos(${relativeTimeExpr}*PI*${frequency})`;\n\n return {\n initialX: \"0\",\n initialY: \"0\",\n activeX: activeX,\n activeY: activeY,\n finalX: \"0\",\n finalY: \"0\",\n duration: duration,\n };\n}\n\nexport function processMotion(delay: number, motion?: EffieMotion): string {\n if (!motion) return \"x=0:y=0\";\n\n const start = delay + (motion.start ?? 0);\n const relativeTimeExpr = `(t-${start})`;\n let components: MotionComponents;\n\n switch (motion.type) {\n case \"bounce\":\n components = processBounceMotion(motion, relativeTimeExpr);\n break;\n case \"shake\":\n components = processShakeMotion(motion, relativeTimeExpr);\n break;\n case \"slide\":\n components = processSlideMotion(motion, relativeTimeExpr);\n break;\n default:\n motion satisfies never;\n throw new Error(\n `Unsupported motion type: ${(motion as EffieMotion).type}`,\n );\n }\n\n const motionEndTime = start + components.duration;\n\n const xArg = `if(lt(t,${start}),${components.initialX},if(lt(t,${motionEndTime}),${components.activeX},${components.finalX}))`;\n const yArg = `if(lt(t,${start}),${components.initialY},if(lt(t,${motionEndTime}),${components.activeY},${components.finalY}))`;\n\n return `x='${xArg}':y='${yArg}'`;\n}\n","import type { EffieEffect } from \"@effing/effie\";\n\nfunction processFadeIn(\n effect: Extract<EffieEffect, { type: \"fade-in\" }>,\n _frameRate: number,\n _frameWidth: number,\n _frameHeight: number,\n): string {\n return `fade=t=in:st=${effect.start}:d=${effect.duration}:alpha=1`;\n}\n\nfunction processFadeOut(\n effect: Extract<EffieEffect, { type: \"fade-out\" }>,\n _frameRate: number,\n _frameWidth: number,\n _frameHeight: number,\n): string {\n return `fade=t=out:st=${effect.start}:d=${effect.duration}:alpha=1`;\n}\n\nfunction processSaturateIn(\n effect: Extract<EffieEffect, { type: \"saturate-in\" }>,\n _frameRate: number,\n _frameWidth: number,\n _frameHeight: number,\n): string {\n return `hue='s=max(0,min(1,(t-${effect.start})/${effect.duration}))'`;\n}\n\nfunction processSaturateOut(\n effect: Extract<EffieEffect, { type: \"saturate-out\" }>,\n _frameRate: number,\n _frameWidth: number,\n _frameHeight: number,\n): string {\n return `hue='s=max(0,min(1,(${effect.start + effect.duration}-t)/${effect.duration}))'`;\n}\n\nfunction processScroll(\n effect: Extract<EffieEffect, { type: \"scroll\" }>,\n frameRate: number,\n _frameWidth: number,\n _frameHeight: number,\n): string {\n const distance = effect.distance ?? 1;\n const scroll = distance / (1 + distance);\n const speed = scroll / (effect.duration * frameRate);\n switch (effect.direction) {\n case \"left\":\n return `scroll=h=${speed}`;\n case \"right\":\n return `scroll=hpos=${1 - scroll}:h=-${speed}`;\n case \"up\":\n return `scroll=v=${speed}`;\n case \"down\":\n return `scroll=vpos=${1 - scroll}:v=-${speed}`;\n }\n}\n\nfunction processEffect(\n effect: EffieEffect,\n frameRate: number,\n frameWidth: number,\n frameHeight: number,\n): string {\n switch (effect.type) {\n case \"fade-in\":\n return processFadeIn(effect, frameRate, frameWidth, frameHeight);\n case \"fade-out\":\n return processFadeOut(effect, frameRate, frameWidth, frameHeight);\n case \"saturate-in\":\n return processSaturateIn(effect, frameRate, frameWidth, frameHeight);\n case \"saturate-out\":\n return processSaturateOut(effect, frameRate, frameWidth, frameHeight);\n case \"scroll\":\n return processScroll(effect, frameRate, frameWidth, frameHeight);\n default:\n effect satisfies never;\n throw new Error(\n `Unsupported effect type: ${(effect as EffieEffect).type}`,\n );\n }\n}\n\nexport function processEffects(\n effects: EffieEffect[] | undefined,\n frameRate: number,\n frameWidth: number,\n frameHeight: number,\n): string {\n if (!effects || effects.length === 0) return \"\";\n\n const filters: string[] = [];\n\n for (const effect of effects) {\n const filter = processEffect(effect, frameRate, frameWidth, frameHeight);\n filters.push(filter);\n }\n\n return filters.join(\",\");\n}\n","import type { ChildProcess } from \"child_process\";\nimport { spawn } from \"child_process\";\nimport type { Readable } from \"stream\";\nimport { pipeline } from \"stream\";\nimport fs from \"fs/promises\";\nimport os from \"os\";\nimport path from \"path\";\nimport pathToFFmpeg from \"ffmpeg-static\";\nimport tar from \"tar-stream\";\nimport { createWriteStream } from \"fs\";\nimport { promisify } from \"util\";\n\nconst pump = promisify(pipeline);\n\n/**\n * Each input is represented by its index, its source, and the pre–arguments\n * that must appear immediately before its \"-i\" option.\n */\nexport type FFmpegInput = {\n index: number;\n source: string;\n preArgs: string[];\n type: \"image\" | \"video\" | \"audio\" | \"color\" | \"animation\";\n};\n\nexport class FFmpegCommand {\n globalArgs: string[];\n inputs: FFmpegInput[];\n filterComplex: string;\n outputArgs: string[];\n\n constructor(\n globalArgs: string[],\n inputs: FFmpegInput[],\n filterComplex: string,\n outputArgs: string[],\n ) {\n this.globalArgs = globalArgs;\n this.inputs = inputs;\n this.filterComplex = filterComplex;\n this.outputArgs = outputArgs;\n }\n\n buildArgs(inputResolver: (input: FFmpegInput) => string): string[] {\n const inputArgs: string[] = [];\n for (const input of this.inputs) {\n if (input.type === \"color\") {\n inputArgs.push(...input.preArgs);\n } else if (input.type === \"animation\") {\n inputArgs.push(\n ...input.preArgs,\n \"-i\",\n path.join(inputResolver(input), \"frame_%05d\"),\n );\n } else {\n inputArgs.push(...input.preArgs, \"-i\", inputResolver(input));\n }\n }\n const args = [\n ...this.globalArgs,\n ...inputArgs,\n \"-filter_complex\",\n this.filterComplex,\n ...this.outputArgs,\n ];\n return args;\n }\n}\n\nexport class FFmpegRunner {\n private command: FFmpegCommand;\n\n private ffmpegProc?: ChildProcess;\n\n constructor(command: FFmpegCommand) {\n this.command = command;\n }\n\n async run(\n sourceResolver: (input: {\n type: FFmpegInput[\"type\"];\n src: string;\n }) => Promise<Readable>,\n imageTransformer?: (imageStream: Readable) => Promise<Readable>,\n ): Promise<Readable> {\n const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), \"ffs-\"));\n const fileMapping = new Map<number, string>();\n // Cache for #reference sources to avoid duplicate fetches.\n // Uses promises to handle concurrent requests for the same source.\n const sourceCache = new Map<string, Promise<string>>();\n\n const fetchAndSaveSource = async (\n input: FFmpegInput,\n inputName: string,\n ): Promise<string> => {\n const stream = await sourceResolver({\n type: input.type,\n src: input.source,\n });\n\n if (input.type === \"animation\") {\n // we expect annie files for animations,\n // which are a TAR that needs to be extracted\n const extractionDir = path.join(tempDir, inputName);\n await fs.mkdir(extractionDir, { recursive: true });\n const extract = tar.extract();\n const extractPromise = new Promise<void>((resolve, reject) => {\n extract.on(\"entry\", async (header, stream, next) => {\n if (header.name.startsWith(\"frame_\")) {\n const transformedStream = imageTransformer\n ? await imageTransformer(stream)\n : stream;\n const outputPath = path.join(extractionDir, header.name);\n const writeStream = createWriteStream(outputPath);\n transformedStream.pipe(writeStream);\n writeStream.on(\"finish\", next);\n writeStream.on(\"error\", reject);\n }\n });\n extract.on(\"finish\", resolve);\n extract.on(\"error\", reject);\n });\n stream.pipe(extract);\n await extractPromise;\n return extractionDir;\n } else if (input.type === \"image\" && imageTransformer) {\n const tempFile = path.join(tempDir, inputName);\n const transformedStream = await imageTransformer(stream);\n const writeStream = createWriteStream(tempFile);\n transformedStream.on(\"error\", (e) => writeStream.destroy(e));\n await pump(transformedStream, writeStream);\n return tempFile;\n } else {\n const tempFile = path.join(tempDir, inputName);\n const writeStream = createWriteStream(tempFile);\n stream.on(\"error\", (e) => writeStream.destroy(e));\n await pump(stream, writeStream);\n return tempFile;\n }\n };\n\n await Promise.all(\n this.command.inputs.map(async (input) => {\n if (input.type === \"color\") return;\n\n const inputName = `ffmpeg_input_${input.index\n .toString()\n .padStart(3, \"0\")}`;\n\n // Only cache #reference sources (not data URLs or regular URLs to avoid\n // memory issues with large URL strings as cache keys)\n const shouldCache = input.source.startsWith(\"#\");\n\n if (shouldCache) {\n let fetchPromise = sourceCache.get(input.source);\n if (!fetchPromise) {\n fetchPromise = fetchAndSaveSource(input, inputName);\n sourceCache.set(input.source, fetchPromise);\n }\n const filePath = await fetchPromise;\n fileMapping.set(input.index, filePath);\n } else {\n const filePath = await fetchAndSaveSource(input, inputName);\n fileMapping.set(input.index, filePath);\n }\n }),\n );\n\n const finalArgs = this.command.buildArgs((input) => {\n const filePath = fileMapping.get(input.index);\n if (!filePath)\n throw new Error(`File for input index ${input.index} not found`);\n return filePath;\n });\n const ffmpegProc = spawn(process.env.FFMPEG ?? pathToFFmpeg!, finalArgs);\n ffmpegProc.stderr!.on(\"data\", (data) => {\n console.error(data.toString());\n });\n\n ffmpegProc.on(\"close\", async () => {\n try {\n await fs.rm(tempDir, { recursive: true, force: true });\n } catch (err) {\n console.error(\"Error removing temp directory:\", err);\n }\n });\n\n this.ffmpegProc = ffmpegProc;\n return ffmpegProc.stdout as Readable;\n }\n\n close(): void {\n if (this.ffmpegProc) {\n this.ffmpegProc.kill(\"SIGTERM\");\n this.ffmpegProc = undefined;\n }\n }\n}\n","import type { EffieTransition } from \"@effing/effie\";\n\nexport function processTransition(transition: EffieTransition): string {\n switch (transition.type) {\n case \"fade\": {\n if (\"through\" in transition) {\n // Fade through color: fadeblack, fadewhite, fadegrays\n return `fade${transition.through}`;\n }\n // Crossfade with easing\n const easing = transition.easing ?? \"linear\";\n return {\n linear: \"fade\",\n \"ease-in\": \"fadeslow\",\n \"ease-out\": \"fadefast\",\n }[easing];\n }\n case \"barn\": {\n // Barn door wipes: vertopen, vertclose, horzopen, horzclose\n const orientation = transition.orientation ?? \"horizontal\";\n const mode = transition.mode ?? \"open\";\n const prefix = orientation === \"vertical\" ? \"vert\" : \"horz\";\n return `${prefix}${mode}`;\n }\n case \"circle\": {\n // Circle wipes: circleopen, circleclose, circlecrop\n const mode = transition.mode ?? \"open\";\n return `circle${mode}`;\n }\n case \"wipe\":\n case \"slide\":\n case \"smooth\": {\n const direction = transition.direction ?? \"left\";\n return `${transition.type}${direction}`;\n }\n case \"slice\": {\n const direction = transition.direction ?? \"left\";\n const prefix = {\n left: \"hl\",\n right: \"hr\",\n up: \"vu\",\n down: \"vd\",\n }[direction];\n return `${prefix}${transition.type}`;\n }\n case \"zoom\": {\n return \"zoomin\";\n }\n case \"dissolve\":\n case \"pixelize\":\n case \"radial\":\n return transition.type;\n default:\n transition satisfies never;\n throw new Error(\n `Unsupported transition type: ${(transition as EffieTransition).type}`,\n );\n }\n}\n","import { Readable } from \"stream\";\nimport { createReadStream } from \"fs\";\nimport { processMotion } from \"./motion\";\nimport { processEffects } from \"./effect\";\nimport type { FFmpegInput } from \"./ffmpeg\";\nimport { FFmpegCommand, FFmpegRunner } from \"./ffmpeg\";\nimport { processTransition } from \"./transition\";\nimport type { EffieData, EffieSources, EffieWebUrl } from \"@effing/effie\";\nimport sharp from \"sharp\";\nimport { ffsFetch } from \"./fetch\";\nimport { fileURLToPath } from \"url\";\nimport { cacheKeys } from \"./cache\";\nimport type { CacheStorage } from \"./cache\";\n\nexport type EffieRendererOptions = {\n /**\n * Allow reading from local file paths.\n * WARNING: Only enable this for trusted internal operations.\n * Enabling this for user-provided data is a security risk.\n * @default false\n */\n allowLocalFiles?: boolean;\n /**\n * Cache storage instance for source lookups.\n * If not provided, a shared lazy-initialized cache will be used.\n */\n cacheStorage?: CacheStorage;\n};\n\nexport class EffieRenderer<U extends string = EffieWebUrl> {\n private effieData: EffieData<EffieSources<U>, U>;\n private ffmpegRunner?: FFmpegRunner;\n private allowLocalFiles: boolean;\n private cacheStorage?: CacheStorage;\n\n constructor(\n effieData: EffieData<EffieSources<U>, U>,\n options?: EffieRendererOptions,\n ) {\n this.effieData = effieData;\n this.allowLocalFiles = options?.allowLocalFiles ?? false;\n this.cacheStorage = options?.cacheStorage;\n }\n\n private async fetchSource(src: string): Promise<Readable> {\n if (src.startsWith(\"#\")) {\n const sourceName = src.slice(1);\n if (!(sourceName in this.effieData.sources!)) {\n throw new Error(`Named source \"${sourceName}\" not found`);\n }\n src = this.effieData.sources![sourceName];\n }\n\n // Handle data URLs (inline, no actual fetch or cache needed)\n if (src.startsWith(\"data:\")) {\n const commaIndex = src.indexOf(\",\");\n if (commaIndex === -1) {\n throw new Error(\"Invalid data URL\");\n }\n const meta = src.slice(5, commaIndex); // after \"data:\"\n const isBase64 = meta.endsWith(\";base64\");\n const data = src.slice(commaIndex + 1);\n const buffer = isBase64\n ? Buffer.from(data, \"base64\")\n : Buffer.from(decodeURIComponent(data));\n return Readable.from(buffer);\n }\n\n // Handle local file paths, if allowed\n if (src.startsWith(\"file:\")) {\n if (!this.allowLocalFiles) {\n throw new Error(\n \"Local file paths are not allowed. Use allowLocalFiles option for trusted operations.\",\n );\n }\n return createReadStream(fileURLToPath(src));\n }\n\n // If we have a cache, check the cache first\n if (this.cacheStorage) {\n const cachedStream = await this.cacheStorage.getStream(\n cacheKeys.source(src),\n );\n if (cachedStream) {\n return cachedStream;\n }\n }\n\n // Fetch from network\n const response = await ffsFetch(src, {\n headersTimeout: 10 * 60 * 1000, // 10 minutes\n bodyTimeout: 20 * 60 * 1000, // 20 minutes\n });\n if (!response.ok) {\n throw new Error(\n `Failed to fetch ${src}: ${response.status} ${response.statusText}`,\n );\n }\n if (!response.body) {\n throw new Error(`No body for ${src}`);\n }\n // Convert WHATWG ReadableStream to Node.js Readable\n return Readable.fromWeb(response.body);\n }\n\n private buildAudioFilter({\n duration,\n volume,\n fadeIn,\n fadeOut,\n }: {\n duration: number;\n volume?: number;\n fadeIn?: number;\n fadeOut?: number;\n }) {\n const filters = [];\n if (volume !== undefined) {\n filters.push(`volume=${volume}`);\n }\n if (fadeIn !== undefined) {\n filters.push(`afade=type=in:start_time=0:duration=${fadeIn}`);\n }\n if (fadeOut !== undefined) {\n filters.push(\n `afade=type=out:start_time=${duration - fadeOut}:duration=${fadeOut}`,\n );\n }\n return filters.length ? filters.join(\",\") : \"anull\";\n }\n\n private getFrameDimensions(scaleFactor: number) {\n // Round down to the nearest even number for H.264 compatibility\n return {\n frameWidth: Math.floor((this.effieData.width * scaleFactor) / 2) * 2,\n frameHeight: Math.floor((this.effieData.height * scaleFactor) / 2) * 2,\n };\n }\n\n /**\n * Builds an FFmpeg input for a background (global or segment).\n */\n private buildBackgroundInput(\n background: EffieData<EffieSources<U>, U>[\"background\"],\n inputIndex: number,\n frameWidth: number,\n frameHeight: number,\n ): FFmpegInput {\n if (background.type === \"image\") {\n return {\n index: inputIndex,\n source: background.source,\n preArgs: [\"-loop\", \"1\", \"-framerate\", this.effieData.fps.toString()],\n type: \"image\",\n };\n } else if (background.type === \"video\") {\n return {\n index: inputIndex,\n source: background.source,\n preArgs: [\"-stream_loop\", \"-1\"],\n type: \"video\",\n };\n }\n // Color background - use lavfi to generate\n return {\n index: inputIndex,\n source: \"\",\n preArgs: [\n \"-f\",\n \"lavfi\",\n \"-i\",\n `color=${background.color}:size=${frameWidth}x${frameHeight}:rate=${this.effieData.fps}`,\n ],\n type: \"color\",\n };\n }\n\n private buildOutputArgs(outputFilename: string): string[] {\n return [\n \"-map\",\n \"[outv]\",\n \"-map\",\n \"[outa]\",\n \"-c:v\",\n \"libx264\",\n \"-r\",\n this.effieData.fps.toString(),\n \"-pix_fmt\",\n \"yuv420p\",\n \"-preset\",\n \"fast\",\n \"-crf\",\n \"28\",\n \"-c:a\",\n \"aac\",\n \"-movflags\",\n \"frag_keyframe+empty_moov\",\n \"-f\",\n \"mp4\",\n outputFilename,\n ];\n }\n\n private buildLayerInput(\n layer: EffieData<EffieSources<U>, U>[\"segments\"][0][\"layers\"][0],\n duration: number,\n inputIndex: number,\n ): FFmpegInput {\n let preArgs: string[] = [];\n if (layer.type === \"image\") {\n preArgs = [\n \"-loop\",\n \"1\",\n \"-t\",\n duration.toString(),\n \"-framerate\",\n this.effieData.fps.toString(),\n ];\n } else if (layer.type === \"animation\") {\n preArgs = [\"-f\", \"image2\", \"-framerate\", this.effieData.fps.toString()];\n }\n return {\n index: inputIndex,\n source: layer.source,\n preArgs,\n type: layer.type,\n };\n }\n\n /**\n * Builds filter chain for all layers in a segment.\n * @param segment - The segment containing layers\n * @param bgLabel - Label for the background input (e.g., \"bg_seg0\" or \"bg_seg\")\n * @param labelPrefix - Prefix for generated labels (e.g., \"seg0_\" or \"\")\n * @param layerInputOffset - Starting input index for layers\n * @param frameWidth - Frame width for nullsrc\n * @param frameHeight - Frame height for nullsrc\n * @param outputLabel - Label for the final video output\n * @returns Array of filter parts to add to the filter chain\n */\n private buildLayerFilters(\n segment: EffieData<EffieSources<U>, U>[\"segments\"][0],\n bgLabel: string,\n labelPrefix: string,\n layerInputOffset: number,\n frameWidth: number,\n frameHeight: number,\n outputLabel: string,\n ): string[] {\n const filterParts: string[] = [];\n let currentVidLabel = bgLabel;\n\n for (let l = 0; l < segment.layers.length; l++) {\n const inputIdx = layerInputOffset + l;\n const layerLabel = `${labelPrefix}layer${l}`;\n const layer = segment.layers[l];\n const effectChain = layer.effects\n ? processEffects(\n layer.effects,\n this.effieData.fps,\n frameWidth,\n frameHeight,\n )\n : \"\";\n filterParts.push(\n `[${inputIdx}:v]trim=start=0:duration=${segment.duration},${\n effectChain ? effectChain + \",\" : \"\"\n }setsar=1,setpts=PTS-STARTPTS[${layerLabel}]`,\n );\n let overlayInputLabel = layerLabel;\n const delay = layer.delay ?? 0;\n if (delay > 0) {\n filterParts.push(\n `nullsrc=size=${frameWidth}x${frameHeight}:duration=${delay},setpts=PTS-STARTPTS[null_${layerLabel}]`,\n );\n filterParts.push(\n `[null_${layerLabel}][${layerLabel}]concat=n=2:v=1:a=0[delayed_${layerLabel}]`,\n );\n overlayInputLabel = `delayed_${layerLabel}`;\n }\n const overlayOutputLabel = `${labelPrefix}tmp${l}`;\n const offset = layer.motion ? processMotion(delay, layer.motion) : \"0:0\";\n const fromTime = layer.from ?? 0;\n const untilTime = layer.until ?? segment.duration;\n filterParts.push(\n `[${currentVidLabel}][${overlayInputLabel}]overlay=${offset}:enable='between(t,${fromTime},${untilTime})',fps=${this.effieData.fps}[${overlayOutputLabel}]`,\n );\n currentVidLabel = overlayOutputLabel;\n }\n filterParts.push(`[${currentVidLabel}]null[${outputLabel}]`);\n\n return filterParts;\n }\n\n /**\n * Applies xfade/concat transitions between video segments.\n * Modifies videoSegmentLabels in place to update labels after transitions.\n * @param filterParts - Array to append filter parts to\n * @param videoSegmentLabels - Array of video segment labels (modified in place)\n */\n private applyTransitions(\n filterParts: string[],\n videoSegmentLabels: string[],\n ): void {\n let transitionOffset = 0;\n this.effieData.segments.forEach((segment, i) => {\n if (i === 0) {\n transitionOffset = segment.duration;\n return;\n }\n const combineLabel = `[vid_com${i}]`;\n if (!segment.transition) {\n transitionOffset += segment.duration;\n filterParts.push(\n `${videoSegmentLabels[i - 1]}${\n videoSegmentLabels[i]\n }concat=n=2:v=1:a=0,fps=${this.effieData.fps}${combineLabel}`,\n );\n videoSegmentLabels[i] = combineLabel;\n return;\n }\n const transitionName = processTransition(segment.transition);\n const transitionDuration = segment.transition.duration;\n transitionOffset -= transitionDuration;\n filterParts.push(\n `${videoSegmentLabels[i - 1]}${\n videoSegmentLabels[i]\n }xfade=transition=${transitionName}:duration=${transitionDuration}:offset=${transitionOffset}${combineLabel}`,\n );\n videoSegmentLabels[i] = combineLabel;\n transitionOffset += segment.duration;\n });\n filterParts.push(`${videoSegmentLabels.at(-1)}null[outv]`);\n }\n\n /**\n * Applies general audio mixing: concats segment audio and mixes with global audio if present.\n * @param filterParts - Array to append filter parts to\n * @param audioSegmentLabels - Array of audio segment labels to concat\n * @param totalDuration - Total duration for audio trimming\n * @param generalAudioInputIndex - Input index for general audio (if present)\n */\n private applyGeneralAudio(\n filterParts: string[],\n audioSegmentLabels: string[],\n totalDuration: number,\n generalAudioInputIndex: number,\n ): void {\n if (this.effieData.audio) {\n const audioSeek = this.effieData.audio.seek ?? 0;\n const generalAudioFilter = this.buildAudioFilter({\n duration: totalDuration,\n volume: this.effieData.audio.volume,\n fadeIn: this.effieData.audio.fadeIn,\n fadeOut: this.effieData.audio.fadeOut,\n });\n filterParts.push(\n `[${generalAudioInputIndex}:a]atrim=start=${audioSeek}:duration=${totalDuration},${generalAudioFilter},asetpts=PTS-STARTPTS[general_audio]`,\n );\n filterParts.push(\n `${audioSegmentLabels.join(\"\")}concat=n=${\n this.effieData.segments.length\n }:v=0:a=1,atrim=start=0:duration=${totalDuration}[segments_audio]`,\n );\n filterParts.push(\n `[general_audio][segments_audio]amix=inputs=2:duration=longest[outa]`,\n );\n } else {\n filterParts.push(\n `${audioSegmentLabels.join(\"\")}concat=n=${\n this.effieData.segments.length\n }:v=0:a=1[outa]`,\n );\n }\n }\n\n private buildFFmpegCommand(\n outputFilename: string,\n scaleFactor: number = 1,\n ): FFmpegCommand {\n const globalArgs: string[] = [\"-y\", \"-loglevel\", \"error\"];\n const inputs: FFmpegInput[] = [];\n let inputIndex = 0;\n\n const { frameWidth, frameHeight } = this.getFrameDimensions(scaleFactor);\n const backgroundSeek =\n this.effieData.background.type === \"video\"\n ? (this.effieData.background.seek ?? 0)\n : 0;\n\n // Global background input:\n inputs.push(\n this.buildBackgroundInput(\n this.effieData.background,\n inputIndex,\n frameWidth,\n frameHeight,\n ),\n );\n const globalBgInputIdx = inputIndex;\n inputIndex++;\n\n // Segment background inputs:\n const segmentBgInputIndices: (number | null)[] = [];\n for (const segment of this.effieData.segments) {\n if (segment.background) {\n inputs.push(\n this.buildBackgroundInput(\n segment.background,\n inputIndex,\n frameWidth,\n frameHeight,\n ),\n );\n segmentBgInputIndices.push(inputIndex);\n inputIndex++;\n } else {\n segmentBgInputIndices.push(null);\n }\n }\n\n // Layer inputs:\n for (const segment of this.effieData.segments) {\n for (const layer of segment.layers) {\n inputs.push(this.buildLayerInput(layer, segment.duration, inputIndex));\n inputIndex++;\n }\n }\n\n // Audio inputs:\n for (const segment of this.effieData.segments) {\n if (segment.audio) {\n inputs.push({\n index: inputIndex,\n source: segment.audio.source,\n preArgs: [],\n type: \"audio\",\n });\n inputIndex++;\n }\n }\n\n // General audio input:\n if (this.effieData.audio) {\n inputs.push({\n index: inputIndex,\n source: this.effieData.audio.source,\n preArgs: [],\n type: \"audio\",\n });\n inputIndex++;\n }\n\n // Compute how many video inputs we have:\n const numSegmentBgInputs = segmentBgInputIndices.filter(\n (i) => i !== null,\n ).length;\n const numVideoInputs =\n 1 +\n numSegmentBgInputs +\n this.effieData.segments.reduce((sum, seg) => sum + seg.layers.length, 0);\n let audioCounter = 0;\n\n // Build filter_complex:\n let currentTime = 0;\n let layerInputOffset = 1 + numSegmentBgInputs; // Global background is input 0\n const filterParts: string[] = [];\n const videoSegmentLabels: string[] = [];\n const audioSegmentLabels: string[] = [];\n for (let segIdx = 0; segIdx < this.effieData.segments.length; segIdx++) {\n const segment = this.effieData.segments[segIdx];\n\n // Determine background for this segment (segment bg overrides global bg)\n const bgLabel = `bg_seg${segIdx}`;\n if (segment.background) {\n // Use segment background\n const segBgInputIdx = segmentBgInputIndices[segIdx]!;\n const segBgSeek =\n segment.background.type === \"video\"\n ? (segment.background.seek ?? 0)\n : 0;\n filterParts.push(\n `[${segBgInputIdx}:v]fps=${this.effieData.fps},scale=${frameWidth}x${frameHeight},trim=start=${segBgSeek}:duration=${segment.duration},setpts=PTS-STARTPTS[${bgLabel}]`,\n );\n } else {\n // Use global background\n filterParts.push(\n `[${globalBgInputIdx}:v]fps=${this.effieData.fps},scale=${frameWidth}x${frameHeight},trim=start=${backgroundSeek + currentTime}:duration=${segment.duration},setpts=PTS-STARTPTS[${bgLabel}]`,\n );\n }\n\n // Process layers\n const vidLabel = `vid_seg${segIdx}`;\n filterParts.push(\n ...this.buildLayerFilters(\n segment,\n bgLabel,\n `seg${segIdx}_`,\n layerInputOffset,\n frameWidth,\n frameHeight,\n vidLabel,\n ),\n );\n layerInputOffset += segment.layers.length;\n videoSegmentLabels.push(`[${vidLabel}]`);\n\n const nextSegment = this.effieData.segments[segIdx + 1];\n const transitionDuration = nextSegment?.transition?.duration ?? 0;\n // Ensure audio duration is always at least 0.001 seconds to avoid FFmpeg misbehavior\n const realDuration = Math.max(\n 0.001,\n segment.duration - transitionDuration,\n );\n\n // Process audio: use the corresponding audio input index if audio exists\n if (segment.audio) {\n // Audio inputs start after all video inputs\n const audioInputIndex = numVideoInputs + audioCounter;\n const audioFilter = this.buildAudioFilter({\n duration: realDuration,\n volume: segment.audio.volume,\n fadeIn: segment.audio.fadeIn,\n fadeOut: segment.audio.fadeOut,\n });\n filterParts.push(\n `[${audioInputIndex}:a]atrim=start=0:duration=${realDuration},${audioFilter},asetpts=PTS-STARTPTS[aud_seg${segIdx}]`,\n );\n audioCounter++;\n } else {\n filterParts.push(\n `anullsrc=r=44100:cl=stereo,atrim=start=0:duration=${realDuration},asetpts=PTS-STARTPTS[aud_seg${segIdx}]`,\n );\n }\n audioSegmentLabels.push(`[aud_seg${segIdx}]`);\n\n currentTime += realDuration;\n }\n\n // Add general audio if present\n this.applyGeneralAudio(\n filterParts,\n audioSegmentLabels,\n currentTime,\n numVideoInputs + audioCounter,\n );\n\n // Apply transitions between video segments\n this.applyTransitions(filterParts, videoSegmentLabels);\n\n const filterComplex = filterParts.join(\";\");\n const outputArgs = this.buildOutputArgs(outputFilename);\n\n return new FFmpegCommand(globalArgs, inputs, filterComplex, outputArgs);\n }\n\n private createImageTransformer(scaleFactor: number) {\n return async (imageStream: Readable): Promise<Readable> => {\n if (scaleFactor === 1) return imageStream;\n\n const sharpTransformer = sharp();\n imageStream.on(\"error\", (err) => {\n if (!sharpTransformer.destroyed) {\n sharpTransformer.destroy(err);\n }\n });\n sharpTransformer.on(\"error\", (err) => {\n if (!imageStream.destroyed) {\n imageStream.destroy(err);\n }\n });\n imageStream.pipe(sharpTransformer);\n try {\n const metadata = await sharpTransformer.metadata();\n const imageWidth = metadata.width ?? this.effieData.width;\n const imageHeight = metadata.height ?? this.effieData.height;\n return sharpTransformer.resize({\n width: Math.floor(imageWidth * scaleFactor),\n height: Math.floor(imageHeight * scaleFactor),\n });\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } catch (error: any) {\n if (!sharpTransformer.destroyed) {\n sharpTransformer.destroy(error);\n }\n throw error;\n }\n };\n }\n\n /**\n * Renders the effie data to a video stream.\n * @param scaleFactor - Scale factor for output dimensions\n */\n async render(scaleFactor = 1): Promise<Readable> {\n const ffmpegCommand = this.buildFFmpegCommand(\"-\", scaleFactor);\n this.ffmpegRunner = new FFmpegRunner(ffmpegCommand);\n return this.ffmpegRunner.run(\n async ({ src }) => this.fetchSource(src),\n this.createImageTransformer(scaleFactor),\n );\n }\n\n close(): void {\n if (this.ffmpegRunner) {\n this.ffmpegRunner.close();\n }\n }\n}\n","import { fetch, Agent, type Response, type BodyInit } from \"undici\";\n\n/**\n * Options for ffsFetch function\n */\nexport type FfsFetchOptions = {\n /** HTTP method */\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\" | \"HEAD\" | \"OPTIONS\";\n /** Request body */\n body?: BodyInit;\n /** Headers to send (merged with default User-Agent) */\n headers?: Record<string, string>;\n /** Timeout for receiving response headers in ms. @default 300000 (5 min) */\n headersTimeout?: number;\n /** Timeout between body data chunks in ms. 0 = no timeout. @default 300000 (5 min) */\n bodyTimeout?: number;\n};\n\n/**\n * Fetch with default User-Agent and configurable timeouts.\n *\n * @example\n * // Simple GET\n * const response = await ffsFetch(\"https://example.com/data.json\");\n *\n * @example\n * // Large file with infinite body timeout\n * const response = await ffsFetch(\"https://example.com/video.mp4\", {\n * bodyTimeout: 0,\n * });\n *\n * @example\n * // PUT upload\n * const response = await ffsFetch(\"https://s3.example.com/video.mp4\", {\n * method: \"PUT\",\n * body: videoBuffer,\n * bodyTimeout: 0,\n * headers: { \"Content-Type\": \"video/mp4\" },\n * });\n */\nexport async function ffsFetch(\n url: string,\n options?: FfsFetchOptions,\n): Promise<Response> {\n const {\n method,\n body,\n headers,\n headersTimeout = 300000, // 5 minutes\n bodyTimeout = 300000, // 5 minutes\n } = options ?? {};\n\n const agent = new Agent({ headersTimeout, bodyTimeout });\n\n return fetch(url, {\n method,\n body,\n headers: { \"User-Agent\": \"FFS (+https://effing.dev/ffs)\", ...headers },\n dispatcher: agent,\n });\n}\n","import {\n S3Client,\n PutObjectCommand,\n GetObjectCommand,\n HeadObjectCommand,\n DeleteObjectCommand,\n} from \"@aws-sdk/client-s3\";\nimport { Upload } from \"@aws-sdk/lib-storage\";\nimport fs from \"fs/promises\";\nimport { createReadStream, createWriteStream, existsSync } from \"fs\";\nimport { pipeline } from \"stream/promises\";\nimport path from \"path\";\nimport os from \"os\";\nimport crypto from \"crypto\";\nimport type { Readable } from \"stream\";\n\n/**\n * Cache storage interface\n */\nexport interface CacheStorage {\n /** Store a stream with the given key */\n put(key: string, stream: Readable): Promise<void>;\n /** Get a stream for the given key, or null if not found */\n getStream(key: string): Promise<Readable | null>;\n /** Check if a key exists */\n exists(key: string): Promise<boolean>;\n /** Check if multiple keys exist (batch operation) */\n existsMany(keys: string[]): Promise<Map<string, boolean>>;\n /** Delete a key */\n delete(key: string): Promise<void>;\n /** Store JSON data */\n putJson(key: string, data: object): Promise<void>;\n /** Get JSON data, or null if not found */\n getJson<T>(key: string): Promise<T | null>;\n /** Close and cleanup resources */\n close(): void;\n}\n\n/**\n * S3-compatible cache storage implementation\n */\nexport class S3CacheStorage implements CacheStorage {\n private client: S3Client;\n private bucket: string;\n private prefix: string;\n private ttlMs: number;\n\n constructor(options: {\n endpoint?: string;\n region?: string;\n bucket: string;\n prefix?: string;\n accessKeyId?: string;\n secretAccessKey?: string;\n ttlMs?: number;\n }) {\n this.client = new S3Client({\n endpoint: options.endpoint,\n region: options.region ?? \"auto\",\n credentials: options.accessKeyId\n ? {\n accessKeyId: options.accessKeyId,\n secretAccessKey: options.secretAccessKey!,\n }\n : undefined,\n forcePathStyle: !!options.endpoint,\n });\n this.bucket = options.bucket;\n this.prefix = options.prefix ?? \"\";\n this.ttlMs = options.ttlMs ?? 60 * 60 * 1000; // Default: 60 minutes\n }\n\n private getExpires(): Date {\n return new Date(Date.now() + this.ttlMs);\n }\n\n private getFullKey(key: string): string {\n return `${this.prefix}${key}`;\n }\n\n async put(key: string, stream: Readable): Promise<void> {\n const upload = new Upload({\n client: this.client,\n params: {\n Bucket: this.bucket,\n Key: this.getFullKey(key),\n Body: stream,\n Expires: this.getExpires(),\n },\n });\n await upload.done();\n }\n\n async getStream(key: string): Promise<Readable | null> {\n try {\n const response = await this.client.send(\n new GetObjectCommand({\n Bucket: this.bucket,\n Key: this.getFullKey(key),\n }),\n );\n return response.Body as Readable;\n } catch (err: unknown) {\n const error = err as {\n name?: string;\n $metadata?: { httpStatusCode?: number };\n };\n if (\n error.name === \"NoSuchKey\" ||\n error.$metadata?.httpStatusCode === 404\n ) {\n return null;\n }\n throw err;\n }\n }\n\n async exists(key: string): Promise<boolean> {\n try {\n await this.client.send(\n new HeadObjectCommand({\n Bucket: this.bucket,\n Key: this.getFullKey(key),\n }),\n );\n return true;\n } catch (err: unknown) {\n const error = err as {\n name?: string;\n $metadata?: { httpStatusCode?: number };\n };\n if (\n error.name === \"NotFound\" ||\n error.$metadata?.httpStatusCode === 404\n ) {\n return false;\n }\n throw err;\n }\n }\n\n async existsMany(keys: string[]): Promise<Map<string, boolean>> {\n const results = await Promise.all(\n keys.map(async (key) => [key, await this.exists(key)] as const),\n );\n return new Map(results);\n }\n\n async delete(key: string): Promise<void> {\n try {\n await this.client.send(\n new DeleteObjectCommand({\n Bucket: this.bucket,\n Key: this.getFullKey(key),\n }),\n );\n } catch (err: unknown) {\n const error = err as {\n name?: string;\n $metadata?: { httpStatusCode?: number };\n };\n if (\n error.name === \"NoSuchKey\" ||\n error.$metadata?.httpStatusCode === 404\n ) {\n return;\n }\n throw err;\n }\n }\n\n async putJson(key: string, data: object): Promise<void> {\n await this.client.send(\n new PutObjectCommand({\n Bucket: this.bucket,\n Key: this.getFullKey(key),\n Body: JSON.stringify(data),\n ContentType: \"application/json\",\n Expires: this.getExpires(),\n }),\n );\n }\n\n async getJson<T>(key: string): Promise<T | null> {\n try {\n const response = await this.client.send(\n new GetObjectCommand({\n Bucket: this.bucket,\n Key: this.getFullKey(key),\n }),\n );\n const body = await response.Body?.transformToString();\n if (!body) return null;\n return JSON.parse(body) as T;\n } catch (err: unknown) {\n const error = err as {\n name?: string;\n $metadata?: { httpStatusCode?: number };\n };\n if (\n error.name === \"NoSuchKey\" ||\n error.$metadata?.httpStatusCode === 404\n ) {\n return null;\n }\n throw err;\n }\n }\n\n close(): void {\n // nothing to do here\n }\n}\n\n/**\n * Local filesystem cache storage implementation\n */\nexport class LocalCacheStorage implements CacheStorage {\n private baseDir: string;\n private initialized = false;\n private cleanupInterval?: ReturnType<typeof setInterval>;\n private ttlMs: number;\n\n constructor(baseDir?: string, ttlMs: number = 60 * 60 * 1000) {\n this.baseDir = baseDir ?? path.join(os.tmpdir(), \"ffs-cache\");\n this.ttlMs = ttlMs;\n\n // Cleanup expired files every 5 minutes\n this.cleanupInterval = setInterval(() => {\n this.cleanupExpired().catch(console.error);\n }, 300_000);\n }\n\n /**\n * Remove files older than TTL\n */\n public async cleanupExpired(): Promise<void> {\n if (!this.initialized) return;\n\n const now = Date.now();\n await this.cleanupDir(this.baseDir, now);\n }\n\n private async cleanupDir(dir: string, now: number): Promise<void> {\n let entries;\n try {\n entries = await fs.readdir(dir, { withFileTypes: true });\n } catch {\n return; // Directory doesn't exist or can't be read\n }\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n\n if (entry.isDirectory()) {\n await this.cleanupDir(fullPath, now);\n // Remove empty directories\n try {\n await fs.rmdir(fullPath);\n } catch {\n // Directory not empty or other error, ignore\n }\n } else if (entry.isFile()) {\n try {\n const stat = await fs.stat(fullPath);\n if (now - stat.mtimeMs > this.ttlMs) {\n await fs.rm(fullPath, { force: true });\n }\n } catch {\n // File may have been deleted, ignore\n }\n }\n }\n }\n\n private async ensureDir(filePath: string): Promise<void> {\n await fs.mkdir(path.dirname(filePath), { recursive: true });\n this.initialized = true;\n }\n\n private filePath(key: string): string {\n return path.join(this.baseDir, key);\n }\n\n private tmpPathFor(finalPath: string): string {\n const rand = crypto.randomBytes(8).toString(\"hex\");\n // Keep tmp file in the same directory so rename stays atomic on POSIX filesystems.\n return `${finalPath}.tmp-${process.pid}-${rand}`;\n }\n\n async put(key: string, stream: Readable): Promise<void> {\n const fp = this.filePath(key);\n await this.ensureDir(fp);\n\n // Write to temp file, then rename for atomicity (no partial reads).\n const tmpPath = this.tmpPathFor(fp);\n try {\n const writeStream = createWriteStream(tmpPath);\n await pipeline(stream, writeStream);\n await fs.rename(tmpPath, fp);\n } catch (err) {\n await fs.rm(tmpPath, { force: true }).catch(() => {});\n throw err;\n }\n }\n\n async getStream(key: string): Promise<Readable | null> {\n const fp = this.filePath(key);\n if (!existsSync(fp)) return null;\n return createReadStream(fp);\n }\n\n async exists(key: string): Promise<boolean> {\n try {\n await fs.access(this.filePath(key));\n return true;\n } catch {\n return false;\n }\n }\n\n async existsMany(keys: string[]): Promise<Map<string, boolean>> {\n const results = await Promise.all(\n keys.map(async (key) => [key, await this.exists(key)] as const),\n );\n return new Map(results);\n }\n\n async delete(key: string): Promise<void> {\n await fs.rm(this.filePath(key), { force: true });\n }\n\n async putJson(key: string, data: object): Promise<void> {\n const fp = this.filePath(key);\n await this.ensureDir(fp);\n\n // Write to temp file, then rename for atomicity (no partial reads).\n const tmpPath = this.tmpPathFor(fp);\n try {\n await fs.writeFile(tmpPath, JSON.stringify(data));\n await fs.rename(tmpPath, fp);\n } catch (err) {\n await fs.rm(tmpPath, { force: true }).catch(() => {});\n throw err;\n }\n }\n\n async getJson<T>(key: string): Promise<T | null> {\n try {\n const content = await fs.readFile(this.filePath(key), \"utf-8\");\n return JSON.parse(content) as T;\n } catch {\n return null;\n }\n }\n\n close(): void {\n // Stop the cleanup interval\n if (this.cleanupInterval) {\n clearInterval(this.cleanupInterval);\n this.cleanupInterval = undefined;\n }\n }\n}\n\n/**\n * Create a cache storage instance based on environment variables.\n * Uses S3 if FFS_CACHE_BUCKET is set, otherwise uses local filesystem.\n */\nexport function createCacheStorage(): CacheStorage {\n // Parse TTL from env (default: 60 minutes)\n const ttlMs = process.env.FFS_CACHE_TTL_MS\n ? parseInt(process.env.FFS_CACHE_TTL_MS, 10)\n : 60 * 60 * 1000;\n\n if (process.env.FFS_CACHE_BUCKET) {\n return new S3CacheStorage({\n endpoint: process.env.FFS_CACHE_ENDPOINT,\n region: process.env.FFS_CACHE_REGION ?? \"auto\",\n bucket: process.env.FFS_CACHE_BUCKET,\n prefix: process.env.FFS_CACHE_PREFIX,\n accessKeyId: process.env.FFS_CACHE_ACCESS_KEY,\n secretAccessKey: process.env.FFS_CACHE_SECRET_KEY,\n ttlMs,\n });\n }\n\n return new LocalCacheStorage(process.env.FFS_CACHE_LOCAL_DIR, ttlMs);\n}\n\nexport function hashUrl(url: string): string {\n return crypto.createHash(\"sha256\").update(url).digest(\"hex\").slice(0, 16);\n}\n\nexport type SourceCacheKey = `sources/${string}`;\nexport type WarmupJobCacheKey = `jobs/warmup/${string}.json`;\nexport type RenderJobCacheKey = `jobs/render/${string}.json`;\n\n/**\n * Build the cache key for a source URL (hashing is handled internally).\n */\nexport function sourceCacheKey(url: string): SourceCacheKey {\n return `sources/${hashUrl(url)}`;\n}\n\nexport function warmupJobCacheKey(jobId: string): WarmupJobCacheKey {\n return `jobs/warmup/${jobId}.json`;\n}\n\nexport function renderJobCacheKey(jobId: string): RenderJobCacheKey {\n return `jobs/render/${jobId}.json`;\n}\n\n/**\n * Centralized cache key builders for known namespaces.\n * Prefer using these helpers over manual string interpolation.\n */\nexport const cacheKeys = {\n source: sourceCacheKey,\n warmupJob: warmupJobCacheKey,\n renderJob: renderJobCacheKey,\n} as const;\n"],"mappings":"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","names":["stream","createReadStream","fs","createWriteStream","pipeline","path","os","createReadStream"]}
@@ -0,0 +1,57 @@
1
+ import { C as CacheStorage } from '../cache-BUVFfGZF.js';
2
+ import { EffieData, EffieSources } from '@effing/effie';
3
+ import express from 'express';
4
+ import 'stream';
5
+
6
+ type UploadOptions = {
7
+ videoUrl: string;
8
+ coverUrl?: string;
9
+ };
10
+ type WarmupJob = {
11
+ sources: string[];
12
+ };
13
+ type RenderJob = {
14
+ effie: EffieData<EffieSources>;
15
+ scale: number;
16
+ upload?: UploadOptions;
17
+ createdAt: number;
18
+ };
19
+ type ServerContext = {
20
+ cacheStorage: CacheStorage;
21
+ baseUrl: string;
22
+ skipValidation: boolean;
23
+ cacheConcurrency: number;
24
+ };
25
+ type SSEEventSender = (event: string, data: object) => void;
26
+ /**
27
+ * Create the server context with configuration from environment variables
28
+ */
29
+ declare function createServerContext(): ServerContext;
30
+
31
+ /**
32
+ * POST /warmup - Create a warmup job
33
+ * Stores the source list in cache and returns a job ID for SSE streaming
34
+ */
35
+ declare function createWarmupJob(req: express.Request, res: express.Response, ctx: ServerContext): Promise<void>;
36
+ /**
37
+ * GET /warmup/:id - Stream warmup progress via SSE
38
+ * Fetches and caches sources, emitting progress events
39
+ */
40
+ declare function streamWarmupJob(req: express.Request, res: express.Response, ctx: ServerContext): Promise<void>;
41
+ /**
42
+ * POST /purge - Purge cached sources for an Effie composition
43
+ */
44
+ declare function purgeCache(req: express.Request, res: express.Response, ctx: ServerContext): Promise<void>;
45
+
46
+ /**
47
+ * POST /render - Create a render job
48
+ * Returns a job ID and URL for streaming the rendered video
49
+ */
50
+ declare function createRenderJob(req: express.Request, res: express.Response, ctx: ServerContext): Promise<void>;
51
+ /**
52
+ * GET /render/:id - Execute render job
53
+ * Streams video directly (no upload) or SSE progress events (with upload)
54
+ */
55
+ declare function streamRenderJob(req: express.Request, res: express.Response, ctx: ServerContext): Promise<void>;
56
+
57
+ export { type RenderJob, type SSEEventSender, type ServerContext, type UploadOptions, type WarmupJob, createRenderJob, createServerContext, createWarmupJob, purgeCache, streamRenderJob, streamWarmupJob };
@@ -0,0 +1,18 @@
1
+ import {
2
+ createRenderJob,
3
+ createServerContext,
4
+ createWarmupJob,
5
+ purgeCache,
6
+ streamRenderJob,
7
+ streamWarmupJob
8
+ } from "../chunk-LK5K4SQV.js";
9
+ import "../chunk-RNE6TKMF.js";
10
+ export {
11
+ createRenderJob,
12
+ createServerContext,
13
+ createWarmupJob,
14
+ purgeCache,
15
+ streamRenderJob,
16
+ streamWarmupJob
17
+ };
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,106 @@
1
+ import { Readable } from 'stream';
2
+ import { EffieWebUrl, EffieData, EffieSources, EffieMotion, EffieEffect, EffieTransition } from '@effing/effie';
3
+ import { C as CacheStorage } from './cache-BUVFfGZF.js';
4
+
5
+ type EffieRendererOptions = {
6
+ /**
7
+ * Allow reading from local file paths.
8
+ * WARNING: Only enable this for trusted internal operations.
9
+ * Enabling this for user-provided data is a security risk.
10
+ * @default false
11
+ */
12
+ allowLocalFiles?: boolean;
13
+ /**
14
+ * Cache storage instance for source lookups.
15
+ * If not provided, a shared lazy-initialized cache will be used.
16
+ */
17
+ cacheStorage?: CacheStorage;
18
+ };
19
+ declare class EffieRenderer<U extends string = EffieWebUrl> {
20
+ private effieData;
21
+ private ffmpegRunner?;
22
+ private allowLocalFiles;
23
+ private cacheStorage?;
24
+ constructor(effieData: EffieData<EffieSources<U>, U>, options?: EffieRendererOptions);
25
+ private fetchSource;
26
+ private buildAudioFilter;
27
+ private getFrameDimensions;
28
+ /**
29
+ * Builds an FFmpeg input for a background (global or segment).
30
+ */
31
+ private buildBackgroundInput;
32
+ private buildOutputArgs;
33
+ private buildLayerInput;
34
+ /**
35
+ * Builds filter chain for all layers in a segment.
36
+ * @param segment - The segment containing layers
37
+ * @param bgLabel - Label for the background input (e.g., "bg_seg0" or "bg_seg")
38
+ * @param labelPrefix - Prefix for generated labels (e.g., "seg0_" or "")
39
+ * @param layerInputOffset - Starting input index for layers
40
+ * @param frameWidth - Frame width for nullsrc
41
+ * @param frameHeight - Frame height for nullsrc
42
+ * @param outputLabel - Label for the final video output
43
+ * @returns Array of filter parts to add to the filter chain
44
+ */
45
+ private buildLayerFilters;
46
+ /**
47
+ * Applies xfade/concat transitions between video segments.
48
+ * Modifies videoSegmentLabels in place to update labels after transitions.
49
+ * @param filterParts - Array to append filter parts to
50
+ * @param videoSegmentLabels - Array of video segment labels (modified in place)
51
+ */
52
+ private applyTransitions;
53
+ /**
54
+ * Applies general audio mixing: concats segment audio and mixes with global audio if present.
55
+ * @param filterParts - Array to append filter parts to
56
+ * @param audioSegmentLabels - Array of audio segment labels to concat
57
+ * @param totalDuration - Total duration for audio trimming
58
+ * @param generalAudioInputIndex - Input index for general audio (if present)
59
+ */
60
+ private applyGeneralAudio;
61
+ private buildFFmpegCommand;
62
+ private createImageTransformer;
63
+ /**
64
+ * Renders the effie data to a video stream.
65
+ * @param scaleFactor - Scale factor for output dimensions
66
+ */
67
+ render(scaleFactor?: number): Promise<Readable>;
68
+ close(): void;
69
+ }
70
+
71
+ /**
72
+ * Each input is represented by its index, its source, and the pre–arguments
73
+ * that must appear immediately before its "-i" option.
74
+ */
75
+ type FFmpegInput = {
76
+ index: number;
77
+ source: string;
78
+ preArgs: string[];
79
+ type: "image" | "video" | "audio" | "color" | "animation";
80
+ };
81
+ declare class FFmpegCommand {
82
+ globalArgs: string[];
83
+ inputs: FFmpegInput[];
84
+ filterComplex: string;
85
+ outputArgs: string[];
86
+ constructor(globalArgs: string[], inputs: FFmpegInput[], filterComplex: string, outputArgs: string[]);
87
+ buildArgs(inputResolver: (input: FFmpegInput) => string): string[];
88
+ }
89
+ declare class FFmpegRunner {
90
+ private command;
91
+ private ffmpegProc?;
92
+ constructor(command: FFmpegCommand);
93
+ run(sourceResolver: (input: {
94
+ type: FFmpegInput["type"];
95
+ src: string;
96
+ }) => Promise<Readable>, imageTransformer?: (imageStream: Readable) => Promise<Readable>): Promise<Readable>;
97
+ close(): void;
98
+ }
99
+
100
+ declare function processMotion(delay: number, motion?: EffieMotion): string;
101
+
102
+ declare function processEffects(effects: EffieEffect[] | undefined, frameRate: number, frameWidth: number, frameHeight: number): string;
103
+
104
+ declare function processTransition(transition: EffieTransition): string;
105
+
106
+ export { EffieRenderer, type EffieRendererOptions, FFmpegCommand, type FFmpegInput, FFmpegRunner, processEffects, processMotion, processTransition };
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ import {
2
+ EffieRenderer,
3
+ FFmpegCommand,
4
+ FFmpegRunner,
5
+ processEffects,
6
+ processMotion,
7
+ processTransition
8
+ } from "./chunk-RNE6TKMF.js";
9
+ export {
10
+ EffieRenderer,
11
+ FFmpegCommand,
12
+ FFmpegRunner,
13
+ processEffects,
14
+ processMotion,
15
+ processTransition
16
+ };
17
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,5 @@
1
+ import express from 'express';
2
+
3
+ declare const app: express.Express;
4
+
5
+ export { app };