Skip to content

Commit 2bad29c

Browse files
perf: stream ffmpeg output to response to reduce memory usage and latency
Co-authored-by: soruly <1979746+soruly@users.noreply.github.com>
1 parent 1494dee commit 2bad29c

File tree

2 files changed

+88
-75
lines changed

2 files changed

+88
-75
lines changed

src/lib/generate-video-preview.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import child_process from "node:child_process";
2+
3+
const generateVideoPreview = (
4+
filePath: string,
5+
start: number,
6+
end: number,
7+
size: string = "m",
8+
mute: boolean = false,
9+
) => {
10+
const ffmpeg = child_process.spawn(
11+
"ffmpeg",
12+
[
13+
"-hide_banner",
14+
"-loglevel",
15+
"error",
16+
"-nostats",
17+
"-y",
18+
"-ss",
19+
`${start - 10}`,
20+
"-i",
21+
filePath,
22+
"-ss",
23+
"10",
24+
"-t",
25+
`${end - start}`,
26+
mute ? "-an" : "-y",
27+
"-map",
28+
"0:v:0",
29+
"-map",
30+
"0:a:0?",
31+
"-vf",
32+
`scale=${{ l: 640, m: 320, s: 160 }[size]}:-2`,
33+
"-c:v",
34+
"libx264",
35+
"-crf",
36+
"25",
37+
"-profile:v",
38+
"high",
39+
"-preset",
40+
"veryfast",
41+
"-bf",
42+
"8",
43+
"-r",
44+
"24000/1001",
45+
"-pix_fmt",
46+
"yuv420p",
47+
"-c:a",
48+
"aac",
49+
"-ac",
50+
"2",
51+
"-b:a",
52+
"64k",
53+
"-max_muxing_queue_size",
54+
"1024",
55+
"-movflags",
56+
"empty_moov+faststart",
57+
"-map_metadata",
58+
"-1",
59+
"-map_chapters",
60+
"-1",
61+
"-f",
62+
"mp4",
63+
"-",
64+
],
65+
{ timeout: 10000 },
66+
);
67+
ffmpeg.stderr.on("data", (data) => {
68+
console.log(data.toString());
69+
});
70+
return ffmpeg;
71+
};
72+
73+
export default generateVideoPreview;

src/video.ts

Lines changed: 15 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,13 @@
11
import path from "node:path";
22
import fs from "node:fs/promises";
33
import crypto from "node:crypto";
4-
import child_process from "node:child_process";
54
import { Buffer } from "node:buffer";
65
import sql from "../sql.ts";
76
import detectScene from "./lib/detect-scene.ts";
7+
import generateVideoPreview from "./lib/generate-video-preview.ts";
88

99
const { VIDEO_PATH, TRACE_API_SALT, MEDIA_QUEUE = Infinity } = process.env;
1010

11-
const generateVideoPreview = async (filePath, start, end, size = "m", mute = false) =>
12-
new Promise((resolve) => {
13-
const ffmpeg = child_process.spawn(
14-
"ffmpeg",
15-
[
16-
"-hide_banner",
17-
"-loglevel",
18-
"error",
19-
"-nostats",
20-
"-y",
21-
"-ss",
22-
start - 10,
23-
"-i",
24-
filePath,
25-
"-ss",
26-
"10",
27-
"-t",
28-
end - start,
29-
mute ? "-an" : "-y",
30-
"-map",
31-
"0:v:0",
32-
"-map",
33-
"0:a:0?",
34-
"-vf",
35-
`scale=${{ l: 640, m: 320, s: 160 }[size]}:-2`,
36-
"-c:v",
37-
"libx264",
38-
"-crf",
39-
"25",
40-
"-profile:v",
41-
"high",
42-
"-preset",
43-
"veryfast",
44-
"-bf",
45-
"8",
46-
"-r",
47-
"24000/1001",
48-
"-pix_fmt",
49-
"yuv420p",
50-
"-c:a",
51-
"aac",
52-
"-ac",
53-
"2",
54-
"-b:a",
55-
"64k",
56-
"-max_muxing_queue_size",
57-
"1024",
58-
"-movflags",
59-
"empty_moov+faststart",
60-
"-map_metadata",
61-
"-1",
62-
"-map_chapters",
63-
"-1",
64-
"-f",
65-
"mp4",
66-
"-",
67-
],
68-
{ timeout: 10000 },
69-
);
70-
ffmpeg.stderr.on("data", (data) => {
71-
console.log(data.toString());
72-
});
73-
const chunks = [];
74-
ffmpeg.stdout.on("data", (data) => {
75-
chunks.push(data);
76-
});
77-
ffmpeg.on("close", () => {
78-
resolve(Buffer.concat(chunks));
79-
});
80-
});
81-
8211
export default async (req, res) => {
8312
const [fileId, time, expire, hash] = req.app.locals.sqids.decode(req.params.id);
8413
const buf = Buffer.from(TRACE_API_SALT);
@@ -137,7 +66,6 @@ export default async (req, res) => {
13766
}
13867

13968
const muted = "mute" in req.query;
140-
const video = await generateVideoPreview(videoFilePath, scene.start, scene.end, size, muted);
14169

14270
res.set("Cache-Control", "max-age=86400");
14371
res.set("Content-Type", "video/mp4");
@@ -146,10 +74,22 @@ export default async (req, res) => {
14674
res.set("x-video-end", scene.end);
14775
res.set("x-video-duration", scene.duration);
14876
res.set("Access-Control-Expose-Headers", "x-video-start, x-video-end, x-video-duration");
149-
res.send(video);
77+
78+
const ffmpeg = generateVideoPreview(videoFilePath, scene.start, scene.end, size, muted);
79+
80+
ffmpeg.stdout.pipe(res);
81+
82+
await new Promise<void>((resolve) => {
83+
ffmpeg.on("close", () => resolve());
84+
ffmpeg.on("error", (err) => {
85+
console.log(err);
86+
res.end();
87+
resolve();
88+
});
89+
});
15090
} catch (e) {
15191
console.log(e);
152-
res.status(500).send("Internal Server Error");
92+
if (!res.headersSent) res.status(500).send("Internal Server Error");
15393
}
15494
req.app.locals.mediaQueue--;
15595
};

0 commit comments

Comments
 (0)