@canvas-commons/ffmpeg 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/lib/client/index.d.ts +7 -0
- package/lib/client/index.d.ts.map +1 -0
- package/lib/client/index.js +127 -0
- package/lib/client/index.js.map +1 -0
- package/lib/server/index.d.ts +7 -0
- package/lib/server/index.d.ts.map +1 -0
- package/lib/server/index.js +263 -0
- package/lib/server/index.js.map +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 motion-canvas
|
|
4
|
+
Copyright (c) 2025 canvas-commons
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../client/index.ts"],"mappings":""}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { BoolMetaField, EventDispatcher, NumberMetaField, ObjectMetaField, makePlugin } from "@canvas-commons/core";
|
|
2
|
+
//#region client/FFmpegExporterClient.ts
|
|
3
|
+
const EXPORT_FRAME_LIMIT = 256;
|
|
4
|
+
const EXPORT_RETRY_DELAY = 1e3;
|
|
5
|
+
/**
|
|
6
|
+
* FFmpeg video exporter.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* Most of the export logic is handled on the server. This class communicates
|
|
10
|
+
* with the FFmpegBridge through a WebSocket connection which lets it invoke
|
|
11
|
+
* methods on the FFmpegExporterServer class.
|
|
12
|
+
*
|
|
13
|
+
* For example, calling the following method:
|
|
14
|
+
* ```ts
|
|
15
|
+
* async this.invoke('process', 7);
|
|
16
|
+
* ```
|
|
17
|
+
* Will invoke the `process` method on the FFmpegExporterServer class with `7`
|
|
18
|
+
* as the argument. The result of the method will be returned as a Promise.
|
|
19
|
+
*
|
|
20
|
+
* Before any methods can be invoked, the FFmpegExporterServer class must be
|
|
21
|
+
* initialized by invoking `start`.
|
|
22
|
+
*/
|
|
23
|
+
var FFmpegExporterClient = class FFmpegExporterClient {
|
|
24
|
+
project;
|
|
25
|
+
settings;
|
|
26
|
+
static id = "@canvas-commons/ffmpeg";
|
|
27
|
+
static displayName = "Video (FFmpeg)";
|
|
28
|
+
static meta(project) {
|
|
29
|
+
return new ObjectMetaField(this.displayName, {
|
|
30
|
+
fastStart: new BoolMetaField("fast start", true),
|
|
31
|
+
includeAudio: new BoolMetaField("include audio", true).disable(!project.audio),
|
|
32
|
+
audioSampleRate: new NumberMetaField("audio sample rate", 48e3)
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
static async create(project, settings) {
|
|
36
|
+
return new FFmpegExporterClient(project, settings);
|
|
37
|
+
}
|
|
38
|
+
static response = new EventDispatcher();
|
|
39
|
+
static {
|
|
40
|
+
if (import.meta.hot) import.meta.hot.on(`canvas-commons/ffmpeg-ack`, (response) => this.response.dispatch(response));
|
|
41
|
+
}
|
|
42
|
+
concurrentFrames = 0;
|
|
43
|
+
error = false;
|
|
44
|
+
constructor(project, settings) {
|
|
45
|
+
this.project = project;
|
|
46
|
+
this.settings = settings;
|
|
47
|
+
}
|
|
48
|
+
async start(sounds, duration) {
|
|
49
|
+
const options = this.settings.exporter.options;
|
|
50
|
+
await this.invoke("start", {
|
|
51
|
+
...this.settings,
|
|
52
|
+
...options,
|
|
53
|
+
audio: this.project.audio,
|
|
54
|
+
audioOffset: this.project.meta.shared.audioOffset.get() - this.settings.range[0],
|
|
55
|
+
sounds,
|
|
56
|
+
duration
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async handleFrame(canvas, _frame, _sceneFrame, _sceneName, _signal, context) {
|
|
60
|
+
while (this.concurrentFrames >= EXPORT_FRAME_LIMIT) await new Promise((resolve) => setTimeout(resolve, EXPORT_RETRY_DELAY));
|
|
61
|
+
if (this.error) throw this.error;
|
|
62
|
+
const data = context.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
63
|
+
this.concurrentFrames++;
|
|
64
|
+
this.invoke("handleFrame", data, "octet-stream").then(() => {
|
|
65
|
+
this.concurrentFrames--;
|
|
66
|
+
}).catch((error) => {
|
|
67
|
+
this.error = error;
|
|
68
|
+
this.concurrentFrames--;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async stop(result) {
|
|
72
|
+
while (this.concurrentFrames >= EXPORT_FRAME_LIMIT) await new Promise((resolve) => setTimeout(resolve, EXPORT_RETRY_DELAY));
|
|
73
|
+
if (this.error) throw this.error;
|
|
74
|
+
await this.invoke("end", result);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Remotely invoke a method on the server and wait for a response.
|
|
78
|
+
*
|
|
79
|
+
* @param method - The method name to execute on the server.
|
|
80
|
+
* @param data - The data that will be passed as an argument to the method.
|
|
81
|
+
* Should be serializable.
|
|
82
|
+
* @param strategy - How the data should be sent to the server.
|
|
83
|
+
*/
|
|
84
|
+
invoke(method, data, strategy = "ws") {
|
|
85
|
+
if (import.meta.hot) return new Promise((resolve, reject) => {
|
|
86
|
+
const handle = (response) => {
|
|
87
|
+
if (response.method !== method) return;
|
|
88
|
+
FFmpegExporterClient.response.unsubscribe(handle);
|
|
89
|
+
if (response.status === "success") resolve(response.data);
|
|
90
|
+
else reject({
|
|
91
|
+
message: "An error occurred while exporting the video.",
|
|
92
|
+
remarks: `Method: ${method}<br>Server error: ${response.message}`,
|
|
93
|
+
object: data
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
FFmpegExporterClient.response.subscribe(handle);
|
|
97
|
+
switch (strategy) {
|
|
98
|
+
case "ws":
|
|
99
|
+
import.meta.hot.send("canvas-commons/ffmpeg", {
|
|
100
|
+
method,
|
|
101
|
+
data
|
|
102
|
+
});
|
|
103
|
+
break;
|
|
104
|
+
case "octet-stream":
|
|
105
|
+
fetch(`/ffmpeg/${method}`, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
body: data,
|
|
108
|
+
headers: { "Content-Type": "application/octet-stream" }
|
|
109
|
+
}).catch(reject);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
else throw new Error("FFmpegExporter can only be used locally.");
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region client/index.ts
|
|
118
|
+
var client_default = makePlugin({
|
|
119
|
+
name: "ffmpeg-plugin",
|
|
120
|
+
exporters() {
|
|
121
|
+
return [FFmpegExporterClient];
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
//#endregion
|
|
125
|
+
export { client_default as default };
|
|
126
|
+
|
|
127
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../client/FFmpegExporterClient.ts","../../client/index.ts"],"sourcesContent":["import type {\n Exporter,\n MetaField,\n Project,\n RendererResult,\n RendererSettings,\n Sound,\n} from '@canvas-commons/core';\nimport {\n BoolMetaField,\n EventDispatcher,\n NumberMetaField,\n ObjectMetaField,\n ValueOf,\n} from '@canvas-commons/core';\n\ntype ServerResponse =\n | {\n status: 'success';\n method: string;\n data: unknown;\n }\n | {\n status: 'error';\n method: string;\n message?: string;\n };\n\ntype FFmpegExporterOptions = ValueOf<\n ReturnType<typeof FFmpegExporterClient.meta>\n>;\n\ntype InvokeStrategy = 'ws' | 'octet-stream';\n\nconst EXPORT_FRAME_LIMIT = 256;\nconst EXPORT_RETRY_DELAY = 1000;\n\n/**\n * FFmpeg video exporter.\n *\n * @remarks\n * Most of the export logic is handled on the server. This class communicates\n * with the FFmpegBridge through a WebSocket connection which lets it invoke\n * methods on the FFmpegExporterServer class.\n *\n * For example, calling the following method:\n * ```ts\n * async this.invoke('process', 7);\n * ```\n * Will invoke the `process` method on the FFmpegExporterServer class with `7`\n * as the argument. The result of the method will be returned as a Promise.\n *\n * Before any methods can be invoked, the FFmpegExporterServer class must be\n * initialized by invoking `start`.\n */\nexport class FFmpegExporterClient implements Exporter {\n public static readonly id = '@canvas-commons/ffmpeg';\n public static readonly displayName = 'Video (FFmpeg)';\n\n public static meta(project: Project): MetaField<any> {\n return new ObjectMetaField(this.displayName, {\n fastStart: new BoolMetaField('fast start', true),\n includeAudio: new BoolMetaField('include audio', true).disable(\n !project.audio,\n ),\n audioSampleRate: new NumberMetaField('audio sample rate', 48000),\n });\n }\n\n public static async create(project: Project, settings: RendererSettings) {\n return new FFmpegExporterClient(project, settings);\n }\n\n private static readonly response = new EventDispatcher<ServerResponse>();\n\n static {\n if (import.meta.hot) {\n import.meta.hot.on(\n `canvas-commons/ffmpeg-ack`,\n (response: ServerResponse) => this.response.dispatch(response),\n );\n }\n }\n\n private concurrentFrames = 0;\n private error: unknown = false;\n\n public constructor(\n private readonly project: Project,\n private readonly settings: RendererSettings,\n ) {}\n\n public async start(sounds: Sound[], duration: number): Promise<void> {\n const options = this.settings.exporter.options as FFmpegExporterOptions;\n await this.invoke('start', {\n ...this.settings,\n ...options,\n audio: this.project.audio,\n audioOffset:\n this.project.meta.shared.audioOffset.get() - this.settings.range[0],\n sounds,\n duration,\n });\n }\n\n public async handleFrame(\n canvas: HTMLCanvasElement,\n _frame: number,\n _sceneFrame: number,\n _sceneName: string,\n _signal: AbortSignal,\n context: CanvasRenderingContext2D,\n ): Promise<void> {\n while (this.concurrentFrames >= EXPORT_FRAME_LIMIT) {\n await new Promise(resolve => setTimeout(resolve, EXPORT_RETRY_DELAY));\n }\n\n if (this.error) {\n throw this.error;\n }\n\n const data = context.getImageData(0, 0, canvas.width, canvas.height).data;\n this.concurrentFrames++;\n this.invoke('handleFrame', data, 'octet-stream')\n .then(() => {\n this.concurrentFrames--;\n })\n .catch(error => {\n this.error = error;\n this.concurrentFrames--;\n });\n }\n\n public async stop(result: RendererResult): Promise<void> {\n while (this.concurrentFrames >= EXPORT_FRAME_LIMIT) {\n await new Promise(resolve => setTimeout(resolve, EXPORT_RETRY_DELAY));\n }\n\n if (this.error) {\n throw this.error;\n }\n\n await this.invoke('end', result);\n }\n\n /**\n * Remotely invoke a method on the server and wait for a response.\n *\n * @param method - The method name to execute on the server.\n * @param data - The data that will be passed as an argument to the method.\n * Should be serializable.\n * @param strategy - How the data should be sent to the server.\n */\n private invoke<TResponse = unknown, TData = unknown>(\n method: string,\n data: TData,\n strategy: InvokeStrategy = 'ws',\n ): Promise<TResponse> {\n if (import.meta.hot) {\n return new Promise((resolve, reject) => {\n const handle = (response: ServerResponse) => {\n if (response.method !== method) {\n return;\n }\n\n FFmpegExporterClient.response.unsubscribe(handle);\n if (response.status === 'success') {\n resolve(response.data as TResponse);\n } else {\n reject({\n message: 'An error occurred while exporting the video.',\n remarks: `Method: ${method}<br>Server error: ${response.message}`,\n object: data,\n });\n }\n };\n FFmpegExporterClient.response.subscribe(handle);\n switch (strategy) {\n case 'ws':\n import.meta.hot!.send('canvas-commons/ffmpeg', {method, data});\n break;\n case 'octet-stream':\n fetch(`/ffmpeg/${method}`, {\n method: 'POST',\n body: data as ArrayBuffer,\n // eslint-disable-next-line @typescript-eslint/naming-convention\n headers: {'Content-Type': 'application/octet-stream'},\n }).catch(reject);\n break;\n }\n });\n } else {\n throw new Error('FFmpegExporter can only be used locally.');\n }\n }\n}\n","import type {ExporterClass} from '@canvas-commons/core';\nimport {makePlugin} from '@canvas-commons/core';\nimport {FFmpegExporterClient} from './FFmpegExporterClient';\n\nexport default makePlugin({\n name: 'ffmpeg-plugin',\n exporters(): ExporterClass[] {\n return [FFmpegExporterClient];\n },\n});\n"],"mappings":";;AAkCA,MAAM,qBAAqB;AAC3B,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;AAoB3B,IAAa,uBAAb,MAAa,qBAAyC;CAiCjC;CACA;CAjCnB,OAAuB,KAAK;CAC5B,OAAuB,cAAc;CAErC,OAAc,KAAK,SAAkC;EACnD,OAAO,IAAI,gBAAgB,KAAK,aAAa;GAC3C,WAAW,IAAI,cAAc,cAAc,IAAI;GAC/C,cAAc,IAAI,cAAc,iBAAiB,IAAI,EAAE,QACrD,CAAC,QAAQ,KACX;GACA,iBAAiB,IAAI,gBAAgB,qBAAqB,IAAK;EACjE,CAAC;CACH;CAEA,aAAoB,OAAO,SAAkB,UAA4B;EACvE,OAAO,IAAI,qBAAqB,SAAS,QAAQ;CACnD;CAEA,OAAwB,WAAW,IAAI,gBAAgC;CAEvE;EACE,IAAI,OAAO,KAAK,KACd,OAAO,KAAK,IAAI,GACd,8BACC,aAA6B,KAAK,SAAS,SAAS,QAAQ,CAC/D;CAEJ;CAEA,mBAA2B;CAC3B,QAAyB;CAEzB,YACE,SACA,UACA;EAFiB,KAAA,UAAA;EACA,KAAA,WAAA;CAChB;CAEH,MAAa,MAAM,QAAiB,UAAiC;EACnE,MAAM,UAAU,KAAK,SAAS,SAAS;EACvC,MAAM,KAAK,OAAO,SAAS;GACzB,GAAG,KAAK;GACR,GAAG;GACH,OAAO,KAAK,QAAQ;GACpB,aACE,KAAK,QAAQ,KAAK,OAAO,YAAY,IAAI,IAAI,KAAK,SAAS,MAAM;GACnE;GACA;EACF,CAAC;CACH;CAEA,MAAa,YACX,QACA,QACA,aACA,YACA,SACA,SACe;EACf,OAAO,KAAK,oBAAoB,oBAC9B,MAAM,IAAI,SAAQ,YAAW,WAAW,SAAS,kBAAkB,CAAC;EAGtE,IAAI,KAAK,OACP,MAAM,KAAK;EAGb,MAAM,OAAO,QAAQ,aAAa,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM,EAAE;EACrE,KAAK;EACL,KAAK,OAAO,eAAe,MAAM,cAAc,EAC5C,WAAW;GACV,KAAK;EACP,CAAC,EACA,OAAM,UAAS;GACd,KAAK,QAAQ;GACb,KAAK;EACP,CAAC;CACL;CAEA,MAAa,KAAK,QAAuC;EACvD,OAAO,KAAK,oBAAoB,oBAC9B,MAAM,IAAI,SAAQ,YAAW,WAAW,SAAS,kBAAkB,CAAC;EAGtE,IAAI,KAAK,OACP,MAAM,KAAK;EAGb,MAAM,KAAK,OAAO,OAAO,MAAM;CACjC;;;;;;;;;CAUA,OACE,QACA,MACA,WAA2B,MACP;EACpB,IAAI,OAAO,KAAK,KACd,OAAO,IAAI,SAAS,SAAS,WAAW;GACtC,MAAM,UAAU,aAA6B;IAC3C,IAAI,SAAS,WAAW,QACtB;IAGF,qBAAqB,SAAS,YAAY,MAAM;IAChD,IAAI,SAAS,WAAW,WACtB,QAAQ,SAAS,IAAiB;SAElC,OAAO;KACL,SAAS;KACT,SAAS,WAAW,OAAO,oBAAoB,SAAS;KACxD,QAAQ;IACV,CAAC;GAEL;GACA,qBAAqB,SAAS,UAAU,MAAM;GAC9C,QAAQ,UAAR;IACE,KAAK;KACH,OAAO,KAAK,IAAK,KAAK,yBAAyB;MAAC;MAAQ;KAAI,CAAC;KAC7D;IACF,KAAK;KACH,MAAM,WAAW,UAAU;MACzB,QAAQ;MACR,MAAM;MAEN,SAAS,EAAC,gBAAgB,2BAA0B;KACtD,CAAC,EAAE,MAAM,MAAM;KACf;GACJ;EACF,CAAC;OAED,MAAM,IAAI,MAAM,0CAA0C;CAE9D;AACF;;;AC/LA,IAAA,iBAAe,WAAW;CACxB,MAAM;CACN,YAA6B;EAC3B,OAAO,CAAC,oBAAoB;CAC9B;AACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../server/index.ts"],"mappings":";;;cAIqC,QAAA,QAGlB,MAAM"}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { PLUGIN_OPTIONS } from "@canvas-commons/vite-plugin";
|
|
2
|
+
import { ffmpegPath, ffprobePath } from "ffmpeg-ffprobe-static";
|
|
3
|
+
import ffmpeg from "fluent-ffmpeg";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import { Readable } from "stream";
|
|
7
|
+
//#region server/ImageStream.ts
|
|
8
|
+
var ImageStream = class extends Readable {
|
|
9
|
+
size;
|
|
10
|
+
queue = [];
|
|
11
|
+
constructor(size) {
|
|
12
|
+
super();
|
|
13
|
+
this.size = size;
|
|
14
|
+
}
|
|
15
|
+
async pushImage(readable) {
|
|
16
|
+
if (readable) {
|
|
17
|
+
const length = this.size.x * this.size.y * 4;
|
|
18
|
+
const item = {
|
|
19
|
+
type: "frame",
|
|
20
|
+
array: new Uint8Array(length),
|
|
21
|
+
finished: false
|
|
22
|
+
};
|
|
23
|
+
this.queue.push(item);
|
|
24
|
+
let pointer = 0;
|
|
25
|
+
readable.on("data", (chunk) => {
|
|
26
|
+
item.array.set(chunk, pointer);
|
|
27
|
+
pointer += chunk.length;
|
|
28
|
+
});
|
|
29
|
+
await new Promise((resolve, reject) => {
|
|
30
|
+
readable.on("end", resolve).on("error", reject);
|
|
31
|
+
});
|
|
32
|
+
item.finished = true;
|
|
33
|
+
} else this.queue.push({ type: "end" });
|
|
34
|
+
this._read();
|
|
35
|
+
}
|
|
36
|
+
_read() {
|
|
37
|
+
while (this.queue.length > 0) {
|
|
38
|
+
const item = this.queue[0];
|
|
39
|
+
if (item.type === "end") {
|
|
40
|
+
this.queue = [];
|
|
41
|
+
this.push(null);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (!item.finished) return;
|
|
45
|
+
this.queue.shift();
|
|
46
|
+
this.push(item.array);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region server/FFmpegExporterServer.ts
|
|
52
|
+
ffmpeg.setFfmpegPath(ffmpegPath);
|
|
53
|
+
ffmpeg.setFfprobePath(ffprobePath);
|
|
54
|
+
function formatFilters(filters) {
|
|
55
|
+
return filters.map((f) => {
|
|
56
|
+
let options = [];
|
|
57
|
+
if (typeof f.options === "string") options = [f.options];
|
|
58
|
+
else if (f.options.constructor === Array) options = f.options;
|
|
59
|
+
else options = Object.entries(f.options).filter(([, v]) => v !== void 0).map(([k, v]) => `${k}=${v}`);
|
|
60
|
+
return `${f.filter}=${options.join(":")}`;
|
|
61
|
+
}).join(",");
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The server-side implementation of the FFmpeg video exporter.
|
|
65
|
+
*/
|
|
66
|
+
var FFmpegExporterServer = class {
|
|
67
|
+
config;
|
|
68
|
+
stream;
|
|
69
|
+
command;
|
|
70
|
+
promise;
|
|
71
|
+
constructor(settings, config) {
|
|
72
|
+
this.config = config;
|
|
73
|
+
const size = {
|
|
74
|
+
x: Math.round(settings.size.x * settings.resolutionScale),
|
|
75
|
+
y: Math.round(settings.size.y * settings.resolutionScale)
|
|
76
|
+
};
|
|
77
|
+
this.stream = new ImageStream(size);
|
|
78
|
+
this.command = ffmpeg();
|
|
79
|
+
this.command.input(this.stream).inputFormat("rawvideo").inputOptions([
|
|
80
|
+
"-pix_fmt rgba",
|
|
81
|
+
"-s:v",
|
|
82
|
+
`${size.x}x${size.y}`
|
|
83
|
+
]).inputFps(settings.fps);
|
|
84
|
+
const sounds = [...settings.sounds];
|
|
85
|
+
if (settings.audio && settings.includeAudio) sounds.push({
|
|
86
|
+
audio: settings.audio,
|
|
87
|
+
realPlaybackRate: 1,
|
|
88
|
+
offset: settings.audioOffset ?? 0
|
|
89
|
+
});
|
|
90
|
+
const filterSpec = [];
|
|
91
|
+
const streams = [];
|
|
92
|
+
for (let i = 0; i < sounds.length; i++) {
|
|
93
|
+
const sound = sounds[i];
|
|
94
|
+
this.command.input(sound.audio.slice(1));
|
|
95
|
+
let trimmed = sound.start ?? 0;
|
|
96
|
+
if (sound.offset < 0) trimmed -= sound.offset * sound.realPlaybackRate;
|
|
97
|
+
if (trimmed !== 0) this.command.inputOptions(`-ss ${trimmed}`);
|
|
98
|
+
const filters = [];
|
|
99
|
+
if (sound.end !== void 0) filters.push({
|
|
100
|
+
filter: "atrim",
|
|
101
|
+
options: { end: sound.end - trimmed }
|
|
102
|
+
});
|
|
103
|
+
filters.push({
|
|
104
|
+
filter: "aresample",
|
|
105
|
+
options: settings.audioSampleRate.toString()
|
|
106
|
+
});
|
|
107
|
+
if (sound.gain) filters.push({
|
|
108
|
+
filter: "volume",
|
|
109
|
+
options: { volume: `${sound.gain}dB` }
|
|
110
|
+
});
|
|
111
|
+
if (sound.realPlaybackRate !== 1) {
|
|
112
|
+
const rate = Math.round(settings.audioSampleRate * sound.realPlaybackRate);
|
|
113
|
+
filters.push({
|
|
114
|
+
filter: "asetrate",
|
|
115
|
+
options: { r: rate }
|
|
116
|
+
});
|
|
117
|
+
filters.push({
|
|
118
|
+
filter: "aresample",
|
|
119
|
+
options: settings.audioSampleRate.toString()
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (sound.offset > 0) {
|
|
123
|
+
const delay = Math.round(sound.offset * 1e3);
|
|
124
|
+
filters.push({
|
|
125
|
+
filter: "adelay",
|
|
126
|
+
options: {
|
|
127
|
+
delays: delay,
|
|
128
|
+
all: 1
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (filters.length > 0) {
|
|
133
|
+
filterSpec.push({
|
|
134
|
+
inputs: `${i + 1}:a`,
|
|
135
|
+
filter: formatFilters(filters),
|
|
136
|
+
outputs: `a${i + 1}`
|
|
137
|
+
});
|
|
138
|
+
streams.push(`a${i + 1}`);
|
|
139
|
+
} else streams.push(`${i + 1}:a`);
|
|
140
|
+
}
|
|
141
|
+
if (sounds.length > 0) {
|
|
142
|
+
this.command.complexFilter([...filterSpec, {
|
|
143
|
+
filter: "amix",
|
|
144
|
+
options: {
|
|
145
|
+
inputs: sounds.length,
|
|
146
|
+
dropout_transition: 0,
|
|
147
|
+
normalize: 0
|
|
148
|
+
},
|
|
149
|
+
inputs: streams,
|
|
150
|
+
outputs: "a"
|
|
151
|
+
}]);
|
|
152
|
+
this.command.outputOptions(["-map 0:v", "-map [a]"]);
|
|
153
|
+
}
|
|
154
|
+
this.command.output(path.join(this.config.output, `${settings.name}.mp4`)).outputOptions(["-pix_fmt yuv420p", `-t ${settings.duration / settings.fps}`]).outputFps(settings.fps).size(`${size.x}x${size.y}`);
|
|
155
|
+
if (settings.fastStart) this.command.outputOptions(["-movflags +faststart"]);
|
|
156
|
+
this.promise = new Promise((resolve, reject) => {
|
|
157
|
+
this.command.on("end", () => resolve()).on("error", reject);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async start() {
|
|
161
|
+
if (!fs.existsSync(this.config.output)) await fs.promises.mkdir(this.config.output, { recursive: true });
|
|
162
|
+
this.command.on("stderr", console.error);
|
|
163
|
+
this.command.run();
|
|
164
|
+
}
|
|
165
|
+
async handleFrame(req) {
|
|
166
|
+
await this.stream.pushImage(req);
|
|
167
|
+
}
|
|
168
|
+
async end(result) {
|
|
169
|
+
this.stream.pushImage(null);
|
|
170
|
+
if (result === 1) try {
|
|
171
|
+
this.command.kill("SIGKILL");
|
|
172
|
+
await this.promise;
|
|
173
|
+
} catch (_) {}
|
|
174
|
+
else await this.promise;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region server/FFmpegBridge.ts
|
|
179
|
+
/**
|
|
180
|
+
* A simple bridge between the FFmpegExporterServer and FFmpegExporterClient.
|
|
181
|
+
*
|
|
182
|
+
* @remarks
|
|
183
|
+
* This class lets the client exporter invoke methods on the server and receive
|
|
184
|
+
* responses using a simple Promise-based API.
|
|
185
|
+
*/
|
|
186
|
+
var FFmpegBridge = class {
|
|
187
|
+
server;
|
|
188
|
+
config;
|
|
189
|
+
process = null;
|
|
190
|
+
constructor(server, config) {
|
|
191
|
+
this.server = server;
|
|
192
|
+
this.config = config;
|
|
193
|
+
server.ws.on("canvas-commons/ffmpeg", this.handleMessage);
|
|
194
|
+
server.middlewares.use("/ffmpeg", this.handleRequest);
|
|
195
|
+
}
|
|
196
|
+
handleRequest = async (req, res) => {
|
|
197
|
+
res.end();
|
|
198
|
+
await this.handleMessage({
|
|
199
|
+
method: req.url.slice(1),
|
|
200
|
+
data: req
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
handleMessage = async ({ method, data }) => {
|
|
204
|
+
if (method === "start") {
|
|
205
|
+
try {
|
|
206
|
+
this.process = new FFmpegExporterServer(data, this.config);
|
|
207
|
+
this.respondSuccess(method, await this.process.start());
|
|
208
|
+
} catch (e) {
|
|
209
|
+
this.respondError(method, e?.message);
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (!this.process) {
|
|
214
|
+
this.respondError(method, "The exporting process has not been started.");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (!(method in this.process)) {
|
|
218
|
+
this.respondError(method, `Unknown method: "${method}".`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
this.respondSuccess(method, await this.process[method](data));
|
|
223
|
+
} catch (e) {
|
|
224
|
+
this.respondError(method, e?.message);
|
|
225
|
+
}
|
|
226
|
+
if (method === "end") this.process = null;
|
|
227
|
+
};
|
|
228
|
+
respondSuccess(method, data = {}) {
|
|
229
|
+
this.server.ws.send("canvas-commons/ffmpeg-ack", {
|
|
230
|
+
status: "success",
|
|
231
|
+
method,
|
|
232
|
+
data
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
respondError(method, message = "Unknown error.") {
|
|
236
|
+
this.server.ws.send("canvas-commons/ffmpeg-ack", {
|
|
237
|
+
status: "error",
|
|
238
|
+
method,
|
|
239
|
+
message
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
//#endregion
|
|
244
|
+
//#region server/index.ts
|
|
245
|
+
var server_default = () => {
|
|
246
|
+
let config;
|
|
247
|
+
return {
|
|
248
|
+
name: "canvas-commons/ffmpeg",
|
|
249
|
+
[PLUGIN_OPTIONS]: {
|
|
250
|
+
entryPoint: "@canvas-commons/ffmpeg/client",
|
|
251
|
+
async config(value) {
|
|
252
|
+
config = value;
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
configureServer(server) {
|
|
256
|
+
new FFmpegBridge(server, config);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
};
|
|
260
|
+
//#endregion
|
|
261
|
+
export { server_default as default };
|
|
262
|
+
|
|
263
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../server/ImageStream.ts","../../server/FFmpegExporterServer.ts","../../server/FFmpegBridge.ts","../../server/index.ts"],"sourcesContent":["import {Readable} from 'stream';\n\ntype QueueItem =\n | {\n type: 'frame';\n array: Uint8Array;\n finished: boolean;\n }\n | {\n type: 'end';\n };\n\nexport class ImageStream extends Readable {\n private queue: QueueItem[] = [];\n\n public constructor(private size: {x: number; y: number}) {\n super();\n }\n\n public async pushImage(readable: Readable | null) {\n if (readable) {\n const length = this.size.x * this.size.y * 4;\n const item: QueueItem = {\n type: 'frame',\n array: new Uint8Array(length),\n finished: false,\n };\n this.queue.push(item);\n\n let pointer = 0;\n readable.on('data', (chunk: Uint8Array) => {\n item.array.set(chunk, pointer);\n pointer += chunk.length;\n });\n\n await new Promise((resolve, reject) => {\n readable.on('end', resolve).on('error', reject);\n });\n\n item.finished = true;\n } else {\n this.queue.push({type: 'end'});\n }\n\n this._read();\n }\n\n // eslint-disable-next-line @typescript-eslint/naming-convention\n public override _read() {\n while (this.queue.length > 0) {\n const item = this.queue[0];\n if (item.type === 'end') {\n this.queue = [];\n this.push(null);\n return;\n }\n\n if (!item.finished) {\n return;\n }\n\n this.queue.shift();\n this.push(item.array);\n }\n }\n}\n","import type {\n RendererResult,\n RendererSettings,\n Sound,\n} from '@canvas-commons/core';\nimport type {PluginConfig} from '@canvas-commons/vite-plugin';\nimport {ffmpegPath, ffprobePath} from 'ffmpeg-ffprobe-static';\nimport type {AudioVideoFilter, FilterSpecification} from 'fluent-ffmpeg';\nimport ffmpeg from 'fluent-ffmpeg';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {Readable} from 'stream';\nimport {ImageStream} from './ImageStream';\n\nffmpeg.setFfmpegPath(ffmpegPath!);\nffmpeg.setFfprobePath(ffprobePath!);\n\nexport interface FFmpegExporterSettings extends RendererSettings {\n audio?: string;\n audioOffset?: number;\n\n sounds: Sound[];\n duration: number;\n\n fastStart: boolean;\n includeAudio: boolean;\n audioSampleRate: number;\n}\n\nfunction formatFilters(filters: AudioVideoFilter[]): string {\n return filters\n .map(f => {\n let options: string[] = [];\n if (typeof f.options === 'string') {\n options = [f.options];\n } else if (f.options.constructor === Array) {\n options = f.options;\n } else {\n options = Object.entries(f.options)\n .filter(([, v]) => v !== undefined)\n .map(([k, v]) => `${k}=${v}`);\n }\n return `${f.filter}=${options.join(':')}`;\n })\n .join(',');\n}\n\n/**\n * The server-side implementation of the FFmpeg video exporter.\n */\nexport class FFmpegExporterServer {\n private readonly stream: ImageStream;\n private readonly command: ffmpeg.FfmpegCommand;\n private readonly promise: Promise<void>;\n\n public constructor(\n settings: FFmpegExporterSettings,\n private readonly config: PluginConfig,\n ) {\n const size = {\n x: Math.round(settings.size.x * settings.resolutionScale),\n y: Math.round(settings.size.y * settings.resolutionScale),\n };\n this.stream = new ImageStream(size);\n this.command = ffmpeg();\n\n // Input image sequence\n this.command\n .input(this.stream)\n .inputFormat('rawvideo')\n .inputOptions(['-pix_fmt rgba', '-s:v', `${size.x}x${size.y}`])\n .inputFps(settings.fps);\n\n // Input audio\n const sounds = [...settings.sounds];\n if (settings.audio && settings.includeAudio) {\n sounds.push({\n audio: settings.audio,\n realPlaybackRate: 1,\n offset: settings.audioOffset ?? 0,\n });\n }\n\n const filterSpec: FilterSpecification[] = [];\n const streams: string[] = [];\n\n for (let i = 0; i < sounds.length; i++) {\n const sound = sounds[i];\n this.command.input(sound.audio.slice(1));\n\n let trimmed = sound.start ?? 0;\n if (sound.offset < 0) {\n trimmed -= sound.offset * sound.realPlaybackRate;\n }\n\n if (trimmed !== 0) {\n this.command.inputOptions(`-ss ${trimmed}`);\n }\n\n const filters: AudioVideoFilter[] = [];\n if (sound.end !== undefined) {\n filters.push({\n filter: 'atrim',\n options: {end: sound.end - trimmed},\n });\n }\n\n filters.push({\n filter: 'aresample',\n options: settings.audioSampleRate.toString(),\n });\n\n if (sound.gain) {\n filters.push({\n filter: 'volume',\n options: {volume: `${sound.gain}dB`},\n });\n }\n\n if (sound.realPlaybackRate !== 1) {\n const rate = Math.round(\n settings.audioSampleRate * sound.realPlaybackRate,\n );\n filters.push({\n filter: 'asetrate',\n options: {r: rate},\n });\n filters.push({\n filter: 'aresample',\n options: settings.audioSampleRate.toString(),\n });\n }\n\n if (sound.offset > 0) {\n const delay = Math.round(sound.offset * 1000);\n filters.push({\n filter: 'adelay',\n options: {delays: delay, all: 1},\n });\n }\n\n if (filters.length > 0) {\n filterSpec.push({\n inputs: `${i + 1}:a`,\n filter: formatFilters(filters),\n outputs: `a${i + 1}`,\n });\n streams.push(`a${i + 1}`);\n } else {\n streams.push(`${i + 1}:a`);\n }\n }\n\n if (sounds.length > 0) {\n this.command.complexFilter([\n ...filterSpec,\n {\n filter: 'amix',\n // eslint-disable-next-line @typescript-eslint/naming-convention\n options: {inputs: sounds.length, dropout_transition: 0, normalize: 0},\n inputs: streams,\n outputs: 'a',\n },\n ]);\n this.command.outputOptions(['-map 0:v', '-map [a]']);\n }\n\n // Output settings\n this.command\n .output(path.join(this.config.output, `${settings.name}.mp4`))\n .outputOptions([\n '-pix_fmt yuv420p',\n `-t ${settings.duration / settings.fps}`,\n ])\n .outputFps(settings.fps)\n .size(`${size.x}x${size.y}`);\n if (settings.fastStart) {\n this.command.outputOptions(['-movflags +faststart']);\n }\n\n this.promise = new Promise<void>((resolve, reject) => {\n this.command.on('end', () => resolve()).on('error', reject);\n });\n }\n\n public async start() {\n if (!fs.existsSync(this.config.output)) {\n await fs.promises.mkdir(this.config.output, {recursive: true});\n }\n this.command.on('stderr', console.error);\n this.command.run();\n }\n\n public async handleFrame(req: Readable) {\n await this.stream.pushImage(req);\n }\n\n public async end(result: RendererResult) {\n this.stream.pushImage(null);\n if (result === 1) {\n try {\n this.command.kill('SIGKILL');\n await this.promise;\n } catch (_) {\n // do nothing\n }\n } else {\n await this.promise;\n }\n }\n}\n","import {PluginConfig} from '@canvas-commons/vite-plugin';\nimport {ServerResponse} from 'node:http';\nimport {Connect, ViteDevServer} from 'vite';\nimport {\n FFmpegExporterServer,\n FFmpegExporterSettings,\n} from './FFmpegExporterServer';\n\ninterface BrowserRequest {\n method: string;\n data: unknown;\n}\n\n/**\n * A simple bridge between the FFmpegExporterServer and FFmpegExporterClient.\n *\n * @remarks\n * This class lets the client exporter invoke methods on the server and receive\n * responses using a simple Promise-based API.\n */\nexport class FFmpegBridge {\n private process: FFmpegExporterServer | null = null;\n\n public constructor(\n private readonly server: ViteDevServer,\n private readonly config: PluginConfig,\n ) {\n server.ws.on('canvas-commons/ffmpeg', this.handleMessage);\n server.middlewares.use('/ffmpeg', this.handleRequest);\n }\n\n private handleRequest = async (\n req: Connect.IncomingMessage,\n res: ServerResponse,\n ) => {\n res.end();\n await this.handleMessage({\n method: req.url!.slice(1),\n data: req,\n });\n };\n\n private handleMessage = async ({method, data}: BrowserRequest) => {\n if (method === 'start') {\n try {\n this.process = new FFmpegExporterServer(\n data as FFmpegExporterSettings,\n this.config,\n );\n this.respondSuccess(method, await this.process.start());\n } catch (e: any) {\n this.respondError(method, e?.message);\n }\n return;\n }\n\n if (!this.process) {\n this.respondError(method, 'The exporting process has not been started.');\n return;\n }\n\n if (!(method in this.process)) {\n this.respondError(method, `Unknown method: \"${method}\".`);\n return;\n }\n\n try {\n this.respondSuccess(method, await (this.process as any)[method](data));\n } catch (e: any) {\n this.respondError(method, e?.message);\n }\n\n if (method === 'end') {\n this.process = null;\n }\n };\n\n private respondSuccess(method: string, data: any = {}) {\n this.server.ws.send('canvas-commons/ffmpeg-ack', {\n status: 'success',\n method,\n data,\n });\n }\n\n private respondError(method: string, message = 'Unknown error.') {\n this.server.ws.send('canvas-commons/ffmpeg-ack', {\n status: 'error',\n method,\n message,\n });\n }\n}\n","import {\n Plugin,\n PLUGIN_OPTIONS,\n PluginConfig,\n} from '@canvas-commons/vite-plugin';\nimport {FFmpegBridge} from './FFmpegBridge';\n\nexport default (): Plugin => {\n let config: PluginConfig;\n return {\n name: 'canvas-commons/ffmpeg',\n [PLUGIN_OPTIONS]: {\n entryPoint: '@canvas-commons/ffmpeg/client',\n async config(value) {\n config = value;\n },\n },\n configureServer(server) {\n new FFmpegBridge(server, config);\n },\n };\n};\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@canvas-commons/ffmpeg",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "An FFmpeg video exporter for Canvas Commons",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib/server/index.js",
|
|
7
|
+
"types": "./lib/server/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/server/index.d.ts",
|
|
11
|
+
"default": "./lib/server/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./server": {
|
|
14
|
+
"types": "./lib/server/index.d.ts",
|
|
15
|
+
"default": "./lib/server/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./client": {
|
|
18
|
+
"types": "./lib/client/index.d.ts",
|
|
19
|
+
"default": "./lib/client/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20.19.0"
|
|
25
|
+
},
|
|
26
|
+
"author": "canvas-commons",
|
|
27
|
+
"homepage": "https://canvascommons.io/",
|
|
28
|
+
"bugs": "https://github.com/canvas-commons/canvas-commons/issues",
|
|
29
|
+
"license": "GPLv3",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/canvas-commons/canvas-commons.git",
|
|
33
|
+
"directory": "packages/ffmpeg"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"lib"
|
|
37
|
+
],
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/fluent-ffmpeg": "^2.1.21"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"ffmpeg-ffprobe-static": "^6.1.1-rc.5",
|
|
43
|
+
"fluent-ffmpeg": "^2.1.3",
|
|
44
|
+
"@canvas-commons/core": "^0.2.0",
|
|
45
|
+
"@canvas-commons/vite-plugin": "^0.2.0"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"dev": "tsdown --watch",
|
|
49
|
+
"build": "tsdown",
|
|
50
|
+
"lint:pkg": "publint --strict && attw --pack . --profile esm-only --ignore-rules internal-resolution-error"
|
|
51
|
+
}
|
|
52
|
+
}
|