From a96fa9afd8e48a391ba9ca900930609f7dbc50c1 Mon Sep 17 00:00:00 2001 From: Peter Jeschke Date: Mon, 12 Jan 2026 22:09:33 +0100 Subject: [PATCH 1/2] Remove fluent-ffmpeg and directly invoke ffmpeg instead --- Dockerfile | 2 +- assets/default-screenshot.png | Bin 0 -> 5555 bytes package.json | 1 - pnpm-lock.yaml | 26 ------------ src/media.ts | 73 ++++++++++++++++++++++++++-------- 5 files changed, 57 insertions(+), 45 deletions(-) create mode 100644 assets/default-screenshot.png diff --git a/Dockerfile b/Dockerfile index b1648045..a2da2191 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/node:24.0-alpine +FROM docker.io/node:24-alpine LABEL org.opencontainers.image.title="Hollo" LABEL org.opencontainers.image.description="Federated single-user \ diff --git a/assets/default-screenshot.png b/assets/default-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..99b9954992f4dfe5b80667754683e64fb75e23e4 GIT binary patch literal 5555 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 y>DD}iG&N-6aX$Y1;oeH$k)+7~|CO=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true - async@0.2.10: - resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==} - axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -3109,11 +3103,6 @@ packages: resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} engines: {node: '>=8'} - fluent-ffmpeg@2.1.3: - resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==} - engines: {node: '>=18'} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - flydrive@1.3.0: resolution: {integrity: sha512-B0wsqrZR76d+J2ce6AxNcA1JDo4pViluaaFp5Fjso52zGY+s19uF8rKsQS8TJJsiyySKPI1b8K1w2j3RAUxd1Q==} engines: {node: '>=20.6.0'} @@ -4735,10 +4724,6 @@ packages: resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} engines: {node: '>=4'} - which@1.3.1: - resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} - hasBin: true - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -8167,8 +8152,6 @@ snapshots: - uploadthing - yaml - async@0.2.10: {} - axobject-query@4.1.0: {} bail@2.0.2: {} @@ -8718,11 +8701,6 @@ snapshots: flattie@1.1.1: {} - fluent-ffmpeg@2.1.3: - dependencies: - async: 0.2.10 - which: 1.3.1 - flydrive@1.3.0(@aws-sdk/client-s3@3.713.0)(@aws-sdk/s3-request-presigner@3.713.0): dependencies: '@humanwhocodes/retry': 0.4.3 @@ -10770,10 +10748,6 @@ snapshots: which-pm-runs@1.1.0: {} - which@1.3.1: - dependencies: - isexe: 2.0.0 - which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/src/media.ts b/src/media.ts index 793c5550..605e7cdf 100644 --- a/src/media.ts +++ b/src/media.ts @@ -1,11 +1,15 @@ -import { mkdtemp, readFile, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { spawn } from "node:child_process"; +import { readFileSync } from "node:fs"; import { join } from "node:path"; -import ffmpeg from "fluent-ffmpeg"; +import { getLogger } from "@logtape/logtape"; import type { Sharp } from "sharp"; import { drive } from "./storage"; +const logger = getLogger(["hollo", "media"]); const DEFAULT_THUMBNAIL_AREA = 230_400; +const defaultScreenshot = readFileSync( + join(import.meta.dirname, "..", "assets", "default-screenshot.png"), +); export interface Thumbnail { thumbnailUrl: string; @@ -74,18 +78,53 @@ export function calculateThumbnailSize( export async function makeVideoScreenshot( videoData: Uint8Array, ): Promise { - const tmpDir = await mkdtemp(join(tmpdir(), "hollo-")); - const inFile = join(tmpDir, "video"); - await writeFile(inFile, videoData); - await new Promise((resolve) => - ffmpeg(inFile) - .on("end", resolve) - .screenshots({ - timestamps: [0], - filename: "screenshot.png", - folder: tmpDir, - }), - ); - const screenshot = await readFile(join(tmpDir, "screenshot.png")); - return new Uint8Array(screenshot.buffer); + const resultBuffer: Buffer = await new Promise((resolve, _) => { + const process = spawn("ffmpeg", [ + "-i", + "pipe:0", + "-vframes", + "1", + "-f", + "image2pipe", + "pipe:1", + ]); + const stdin = process.stdin; + const stdout = process.stdout; + const stderr = process.stderr; + const chunks: Buffer[] = []; + const stderrChunks: Buffer[] = []; + if (!stdin || !stdout || !stderr) { + logger.error( + "Could not build pipes to ffmpeg, can't create a video screenshot", + ); + logger.error("ffmpeg output: {stderr}", { stderr: Buffer.concat(stderrChunks).toString() }); + resolve(defaultScreenshot); + } + stdout.on("data", (chunk) => { + chunks.push(chunk); + }); + stderr.on("data", (chunk) => { + stderrChunks.push(chunk); + }); + process.on("close", (code) => { + if (code !== 0) { + logger.error("ffmpeg returned a bad error code {code}", { code }); + logger.error("ffmpeg output: {stderr}", { stderr: Buffer.concat(stderrChunks).toString() }); + resolve(defaultScreenshot); + } + resolve(Buffer.concat(chunks)); + }); + process.on("error", (error) => { + logger.error("Could not run ffmpeg: {error}", { error }); + logger.error("ffmpeg output: {stderr}", { stderr: Buffer.concat(stderrChunks).toString() }); + resolve(defaultScreenshot); + }); + stdin.on("error", (_) => { + // probably a EPIPE because ffmpeg does not consume the whole file; swallow it here + }); + + stdin.write(videoData); + stdin.end(); + }); + return resultBuffer; } From 16cba1a6991f3431dffba59b78987d87c0d98af9 Mon Sep 17 00:00:00 2001 From: Peter Jeschke Date: Thu, 22 Jan 2026 19:43:23 +0100 Subject: [PATCH 2/2] Fix formatting --- src/media.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/media.ts b/src/media.ts index 605e7cdf..51b19484 100644 --- a/src/media.ts +++ b/src/media.ts @@ -97,7 +97,9 @@ export async function makeVideoScreenshot( logger.error( "Could not build pipes to ffmpeg, can't create a video screenshot", ); - logger.error("ffmpeg output: {stderr}", { stderr: Buffer.concat(stderrChunks).toString() }); + logger.error("ffmpeg output: {stderr}", { + stderr: Buffer.concat(stderrChunks).toString(), + }); resolve(defaultScreenshot); } stdout.on("data", (chunk) => { @@ -109,14 +111,18 @@ export async function makeVideoScreenshot( process.on("close", (code) => { if (code !== 0) { logger.error("ffmpeg returned a bad error code {code}", { code }); - logger.error("ffmpeg output: {stderr}", { stderr: Buffer.concat(stderrChunks).toString() }); + logger.error("ffmpeg output: {stderr}", { + stderr: Buffer.concat(stderrChunks).toString(), + }); resolve(defaultScreenshot); } resolve(Buffer.concat(chunks)); }); process.on("error", (error) => { logger.error("Could not run ffmpeg: {error}", { error }); - logger.error("ffmpeg output: {stderr}", { stderr: Buffer.concat(stderrChunks).toString() }); + logger.error("ffmpeg output: {stderr}", { + stderr: Buffer.concat(stderrChunks).toString(), + }); resolve(defaultScreenshot); }); stdin.on("error", (_) => {