Skip to content

Commit 12b789c

Browse files
committed
Merge pull request #333 from peterjeschke/bugfix/ffmpeg
Remove fluent-ffmpeg and directly invoke ffmpeg instead
2 parents 4c2e0a7 + 12a0ebf commit 12b789c

6 files changed

Lines changed: 89 additions & 63 deletions

File tree

CHANGES.md

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -98,24 +98,31 @@ To be released.
9898
`text/plain`.
9999

100100
- Implemented Mastodon 4.5.0 quote notification types (`quote` and
101-
`quoted_update`) for improved quote post interaction tracking.
102-
Users now receive notifications when their posts are quoted by others
103-
and when posts they've quoted are edited by the original authors.
104-
Key features include:
105-
106-
- Added `quote` notification type that triggers when someone quotes
107-
your post, with the notification showing the quote post itself.
108-
- Added `quoted_update` notification type that triggers when a post
109-
you quoted is edited, with the notification showing your quote post
110-
to provide context.
111-
- Both notification types are non-groupable, meaning each quote or edit
112-
generates an individual notification for better visibility.
113-
- Self-quotes (quoting your own posts) do not generate notifications
114-
to avoid unnecessary noise.
115-
- Existing quote posts are automatically backfilled with notifications
116-
during migration to ensure consistent notification history.
117-
- Added database index on `posts.quote_target_id` for improved query
118-
performance when looking up quote relationships.
101+
`quoted_update`) for improved quote post interaction tracking.
102+
Users now receive notifications when their posts are quoted by others
103+
and when posts they've quoted are edited by the original authors.
104+
Key features include:
105+
106+
- Added `quote` notification type that triggers when someone quotes
107+
your post, with the notification showing the quote post itself.
108+
- Added `quoted_update` notification type that triggers when a post
109+
you quoted is edited, with the notification showing your quote post
110+
to provide context.
111+
- Both notification types are non-groupable, meaning each quote or edit
112+
generates an individual notification for better visibility.
113+
- Self-quotes (quoting your own posts) do not generate notifications
114+
to avoid unnecessary noise.
115+
- Existing quote posts are automatically backfilled with notifications
116+
during migration to ensure consistent notification history.
117+
- Added database index on `posts.quote_target_id` for improved query
118+
performance when looking up quote relationships.
119+
120+
- Removed dependency on deprecated *fluent-ffmpeg* package and now invoke
121+
ffmpeg binary directly for video screenshot generation. This change
122+
improves reliability by preventing request failures when video screenshot
123+
generation encounters errors. On failure, a default screenshot (Hollo
124+
logo) is now returned instead of aborting the entire upload request, and
125+
ffmpeg error output is logged for debugging. [[#333] by Peter Jeschke]
119126

120127
[#94]: https://github.com/fedify-dev/hollo/issues/94
121128
[#312]: https://github.com/fedify-dev/hollo/issues/312
@@ -126,6 +133,7 @@ To be released.
126133
[#179]: https://github.com/fedify-dev/hollo/pull/179
127134
[#295]: https://github.com/fedify-dev/hollo/pull/295
128135
[#296]: https://github.com/fedify-dev/hollo/pull/296
136+
[#333]: https://github.com/fedify-dev/hollo/pull/333
129137

130138

131139
Version 0.6.19

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM docker.io/node:24.0-alpine
1+
FROM docker.io/node:24-alpine
22

33
LABEL org.opencontainers.image.title="Hollo"
44
LABEL org.opencontainers.image.description="Federated single-user \

assets/default-screenshot.png

5.42 KB
Loading

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
"drizzle-kit": "^0.31.8",
4545
"drizzle-orm": "^0.45.0",
4646
"es-toolkit": "^1.44.0",
47-
"fluent-ffmpeg": "^2.1.3",
4847
"flydrive": "^1.3.0",
4948
"hono": "^4.11.4",
5049
"iso-639-1": "^3.1.5",

pnpm-lock.yaml

Lines changed: 0 additions & 26 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/media.ts

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
import { mkdtemp, readFile, writeFile } from "node:fs/promises";
2-
import { tmpdir } from "node:os";
1+
import { spawn } from "node:child_process";
2+
import { readFileSync } from "node:fs";
33
import { join } from "node:path";
4-
import ffmpeg from "fluent-ffmpeg";
4+
import { getLogger } from "@logtape/logtape";
55
import type { Sharp } from "sharp";
66
import { drive } from "./storage";
77

8+
const logger = getLogger(["hollo", "media"]);
89
const DEFAULT_THUMBNAIL_AREA = 230_400;
10+
const defaultScreenshot = readFileSync(
11+
join(import.meta.dirname, "..", "assets", "default-screenshot.png"),
12+
);
913

1014
export interface Thumbnail {
1115
thumbnailUrl: string;
@@ -74,18 +78,59 @@ export function calculateThumbnailSize(
7478
export async function makeVideoScreenshot(
7579
videoData: Uint8Array,
7680
): Promise<Uint8Array> {
77-
const tmpDir = await mkdtemp(join(tmpdir(), "hollo-"));
78-
const inFile = join(tmpDir, "video");
79-
await writeFile(inFile, videoData);
80-
await new Promise((resolve) =>
81-
ffmpeg(inFile)
82-
.on("end", resolve)
83-
.screenshots({
84-
timestamps: [0],
85-
filename: "screenshot.png",
86-
folder: tmpDir,
87-
}),
88-
);
89-
const screenshot = await readFile(join(tmpDir, "screenshot.png"));
90-
return new Uint8Array(screenshot.buffer);
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 });
114+
logger.error("ffmpeg output: {stderr}", {
115+
stderr: Buffer.concat(stderrChunks).toString(),
116+
});
117+
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(),
125+
});
126+
resolve(defaultScreenshot);
127+
});
128+
stdin.on("error", (_) => {
129+
// probably a EPIPE because ffmpeg does not consume the whole file; swallow it here
130+
});
131+
132+
stdin.write(videoData);
133+
stdin.end();
134+
});
135+
return resultBuffer;
91136
}

0 commit comments

Comments
 (0)