Skip to content

Commit ff7c13c

Browse files
committed
Fix video thumbnail generation by using temp file instead of stdin pipe
ffmpeg was receiving video data via stdin (pipe:0), which is a non-seekable input. Some MP4/MOV files require seeking to read metadata (moov atom), causing 'partial file' and 'unspecified pixel format' errors.
1 parent 423c76c commit ff7c13c

1 file changed

Lines changed: 56 additions & 47 deletions

File tree

src/media.ts

Lines changed: 56 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { spawn } from "node:child_process";
22
import { readFileSync } from "node:fs";
3+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
4+
import { tmpdir } from "node:os";
35
import { join } from "node:path";
46
import { getLogger } from "@logtape/logtape";
57
import type { Sharp } from "sharp";
@@ -78,59 +80,66 @@ export function calculateThumbnailSize(
7880
export async function makeVideoScreenshot(
7981
videoData: Uint8Array,
8082
): Promise<Uint8Array> {
81-
const resultBuffer: Buffer = await new Promise((resolve, _) => {
82-
const process = spawn("ffmpeg", [
83-
"-i",
84-
"pipe:0",
85-
"-vframes",
86-
"1",
87-
"-f",
88-
"image2pipe",
89-
"pipe:1",
90-
]);
91-
const stdin = process.stdin;
92-
const stdout = process.stdout;
93-
const stderr = process.stderr;
94-
const chunks: Buffer[] = [];
95-
const stderrChunks: Buffer[] = [];
96-
if (!stdin || !stdout || !stderr) {
97-
logger.error(
98-
"Could not build pipes to ffmpeg, can't create a video screenshot",
99-
);
100-
logger.error("ffmpeg output: {stderr}", {
101-
stderr: Buffer.concat(stderrChunks).toString(),
102-
});
103-
resolve(defaultScreenshot);
104-
}
105-
stdout.on("data", (chunk) => {
106-
chunks.push(chunk);
107-
});
108-
stderr.on("data", (chunk) => {
109-
stderrChunks.push(chunk);
110-
});
111-
process.on("close", (code) => {
112-
if (code !== 0) {
113-
logger.error("ffmpeg returned a bad error code {code}", { code });
83+
const tmpDir = await mkdtemp(join(tmpdir(), "hollo-"));
84+
const inFile = join(tmpDir, "video");
85+
try {
86+
await writeFile(inFile, videoData);
87+
const resultBuffer: Buffer = await new Promise((resolve) => {
88+
const process = spawn("ffmpeg", [
89+
"-i",
90+
inFile,
91+
"-vframes",
92+
"1",
93+
"-f",
94+
"image2pipe",
95+
"pipe:1",
96+
]);
97+
const stdout = process.stdout;
98+
const stderr = process.stderr;
99+
const chunks: Buffer[] = [];
100+
const stderrChunks: Buffer[] = [];
101+
if (!stdout || !stderr) {
102+
logger.error(
103+
"Could not build pipes to ffmpeg, can't create a video screenshot",
104+
);
114105
logger.error("ffmpeg output: {stderr}", {
115106
stderr: Buffer.concat(stderrChunks).toString(),
116107
});
117108
resolve(defaultScreenshot);
109+
return;
118110
}
119-
resolve(Buffer.concat(chunks));
120-
});
121-
process.on("error", (error) => {
122-
logger.error("Could not run ffmpeg: {error}", { error });
123-
logger.error("ffmpeg output: {stderr}", {
124-
stderr: Buffer.concat(stderrChunks).toString(),
111+
stdout.on("data", (chunk) => {
112+
chunks.push(chunk);
113+
});
114+
stderr.on("data", (chunk) => {
115+
stderrChunks.push(chunk);
116+
});
117+
process.on("close", (code) => {
118+
if (code !== 0) {
119+
logger.error("ffmpeg returned a bad error code {code}", { code });
120+
logger.error("ffmpeg output: {stderr}", {
121+
stderr: Buffer.concat(stderrChunks).toString(),
122+
});
123+
resolve(defaultScreenshot);
124+
return;
125+
}
126+
resolve(Buffer.concat(chunks));
127+
});
128+
process.on("error", (error) => {
129+
logger.error("Could not run ffmpeg: {error}", { error });
130+
logger.error("ffmpeg output: {stderr}", {
131+
stderr: Buffer.concat(stderrChunks).toString(),
132+
});
133+
resolve(defaultScreenshot);
125134
});
126-
resolve(defaultScreenshot);
127135
});
128-
stdin.on("error", (_) => {
129-
// probably a EPIPE because ffmpeg does not consume the whole file; swallow it here
136+
return resultBuffer;
137+
} catch (error) {
138+
logger.error("Could not prepare temporary file for ffmpeg: {error}", {
139+
error,
130140
});
131-
132-
stdin.write(videoData);
133-
stdin.end();
134-
});
135-
return resultBuffer;
141+
return defaultScreenshot;
142+
} finally {
143+
await rm(tmpDir, { recursive: true, force: true });
144+
}
136145
}

0 commit comments

Comments
 (0)