Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -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 \
Expand Down
Binary file added assets/default-screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
"drizzle-kit": "^0.31.8",
"drizzle-orm": "^0.45.0",
"es-toolkit": "^1.40.0",
"fluent-ffmpeg": "^2.1.3",
"flydrive": "^1.3.0",
"hono": "^4.11.4",
"iso-639-1": "^3.1.5",
Expand Down
26 changes: 0 additions & 26 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

73 changes: 56 additions & 17 deletions src/media.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { mkdtemp, readFile, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { spawn } from "node:child_process";

Check failure on line 1 in src/media.ts

View workflow job for this annotation

GitHub Actions / check

format

File content differs from formatting output
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;
Expand Down Expand Up @@ -74,18 +78,53 @@
export async function makeVideoScreenshot(
videoData: Uint8Array,
): Promise<Uint8Array> {
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;
}
Loading