-
Notifications
You must be signed in to change notification settings - Fork 4
/
FFmpegExporterServer.ts
92 lines (81 loc) · 2.54 KB
/
FFmpegExporterServer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import {ImageStream} from './ImageStream';
import type {PluginConfig} from '@motion-canvas/vite-plugin/lib/plugins';
import type {
RendererSettings,
RendererResult,
} from '@motion-canvas/core/lib/app';
import * as ffmpeg from 'fluent-ffmpeg';
import {path as ffmpegPath} from '@ffmpeg-installer/ffmpeg';
import {path as ffprobePath} from '@ffprobe-installer/ffprobe';
import * as fs from 'fs';
import * as path from 'path';
ffmpeg.setFfmpegPath(ffmpegPath);
ffmpeg.setFfprobePath(ffprobePath);
export interface FFmpegExporterSettings extends RendererSettings {
audio?: string;
audioOffset?: number;
fastStart: boolean;
includeAudio: boolean;
}
/**
* The server-side implementation of the FFmpeg video exporter.
*/
export class FFmpegExporterServer {
private readonly stream: ImageStream;
private readonly command: ffmpeg.FfmpegCommand;
private readonly promise: Promise<void>;
public constructor(
settings: FFmpegExporterSettings,
private readonly config: PluginConfig,
) {
this.stream = new ImageStream();
this.command = ffmpeg();
// Input image sequence
this.command
.input(this.stream)
.inputFormat('image2pipe')
.inputFps(settings.fps);
// Input audio file
if (settings.includeAudio && settings.audio) {
this.command
.input((settings.audio as string).slice(1))
// FIXME Offset only works for negative values.
.inputOptions([`-itsoffset ${settings.audioOffset ?? 0}`]);
}
// Output settings
this.command
.output(path.join(this.config.output, `${settings.name}.mp4`))
.outputOptions(['-pix_fmt yuv420p', '-shortest'])
.outputFps(settings.fps)
.size(`${settings.size.x}x${settings.size.y}`);
if (settings.fastStart) {
this.command.outputOptions(['-movflags +faststart']);
}
this.promise = new Promise<void>((resolve, reject) => {
this.command.on('end', resolve).on('error', reject);
});
}
public async start() {
if (!fs.existsSync(this.config.output)) {
await fs.promises.mkdir(this.config.output, {recursive: true});
}
this.command.run();
}
public async handleFrame({data}: {data: string}) {
const base64Data = data.slice(data.indexOf(',') + 1);
this.stream.pushImage(Buffer.from(base64Data, 'base64'));
}
public async end(result: RendererResult) {
this.stream.pushImage(null);
if (result === 1) {
try {
this.command.kill('SIGKILL');
await this.promise;
} catch (_) {
// do nothing
}
} else {
await this.promise;
}
}
}