Skip to content

Commit c3470c9

Browse files
committed
Merge tag '0.7.7'
Hollo 0.7.7
2 parents 46e5d50 + 1b98906 commit c3470c9

File tree

2 files changed

+75
-47
lines changed

2 files changed

+75
-47
lines changed

CHANGES.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,19 @@ To be released.
8686
[Fedify debugger]: https://fedify.dev/manual/debug
8787

8888

89+
Version 0.7.7
90+
-------------
91+
92+
Released on March 13, 2026.
93+
94+
- Fixed video thumbnail generation failing for some MP4/MOV files by writing
95+
the video data to a temporary file instead of piping it via stdin (`pipe:0`),
96+
which does not support seeking. [[#397], [#398] by NTSK]
97+
98+
[#397]: https://github.com/fedify-dev/hollo/issues/397
99+
[#398]: https://github.com/fedify-dev/hollo/pull/398
100+
101+
89102
Version 0.7.6
90103
-------------
91104

src/media.ts

Lines changed: 62 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,72 @@ 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(),
83+
let tmpDir: string | undefined;
84+
try {
85+
tmpDir = await mkdtemp(join(tmpdir(), "hollo-"));
86+
const inFile = join(tmpDir, "video");
87+
await writeFile(inFile, videoData);
88+
const resultBuffer: Buffer = await new Promise((resolve) => {
89+
const process = spawn("ffmpeg", [
90+
"-i",
91+
inFile,
92+
"-vframes",
93+
"1",
94+
"-f",
95+
"image2pipe",
96+
"pipe:1",
97+
]);
98+
const stdout = process.stdout;
99+
const stderr = process.stderr;
100+
const chunks: Buffer[] = [];
101+
const stderrChunks: Buffer[] = [];
102+
if (!stdout || !stderr) {
103+
logger.error(
104+
"Could not build pipes to ffmpeg, can't create a video screenshot",
105+
);
106+
resolve(defaultScreenshot);
107+
return;
108+
}
109+
stdout.on("data", (chunk) => {
110+
chunks.push(chunk);
102111
});
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 });
112+
stderr.on("data", (chunk) => {
113+
stderrChunks.push(chunk);
114+
});
115+
process.on("close", (code) => {
116+
if (code !== 0) {
117+
logger.error("ffmpeg returned a bad error code {code}", { code });
118+
logger.error("ffmpeg output: {stderr}", {
119+
stderr: Buffer.concat(stderrChunks).toString(),
120+
});
121+
resolve(defaultScreenshot);
122+
return;
123+
}
124+
resolve(Buffer.concat(chunks));
125+
});
126+
process.on("error", (error) => {
127+
logger.error("Could not run ffmpeg: {error}", { error });
114128
logger.error("ffmpeg output: {stderr}", {
115129
stderr: Buffer.concat(stderrChunks).toString(),
116130
});
117131
resolve(defaultScreenshot);
118-
}
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(),
125132
});
126-
resolve(defaultScreenshot);
127133
});
128-
stdin.on("error", (_) => {
129-
// probably a EPIPE because ffmpeg does not consume the whole file; swallow it here
134+
return resultBuffer;
135+
} catch (error) {
136+
logger.error("Could not prepare temporary file for ffmpeg: {error}", {
137+
error,
130138
});
131-
132-
stdin.write(videoData);
133-
stdin.end();
134-
});
135-
return resultBuffer;
139+
return defaultScreenshot;
140+
} finally {
141+
if (tmpDir) {
142+
try {
143+
await rm(tmpDir, { recursive: true, force: true });
144+
} catch (cleanupError) {
145+
logger.warn("Failed to clean up temporary directory: {error}", {
146+
error: cleanupError,
147+
});
148+
}
149+
}
150+
}
136151
}

0 commit comments

Comments
 (0)