vargai 0.4.0-alpha55 → 0.4.0-alpha56
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/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import sharp from "sharp";
|
|
1
2
|
import { File } from "../../../file";
|
|
2
3
|
import type { StorageProvider } from "../../../storage/types";
|
|
3
4
|
import type {
|
|
@@ -122,7 +123,7 @@ export class RendiBackend implements FFmpegBackend {
|
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
async run(options: FFmpegRunOptions): Promise<FFmpegRunResult> {
|
|
125
|
-
|
|
126
|
+
let {
|
|
126
127
|
inputs,
|
|
127
128
|
filterComplex,
|
|
128
129
|
videoFilter,
|
|
@@ -131,11 +132,19 @@ export class RendiBackend implements FFmpegBackend {
|
|
|
131
132
|
verbose,
|
|
132
133
|
} = options;
|
|
133
134
|
|
|
135
|
+
// Synthetic-only commands (e.g. fill-color, gradient clips) produce a
|
|
136
|
+
// filterComplex that uses lavfi sources like `color=...` with zero file
|
|
137
|
+
// inputs. Rendi requires at least one input_file, so we upload a tiny
|
|
138
|
+
// 1×1 transparent PNG as a dummy input that ffmpeg silently ignores
|
|
139
|
+
// (the filterComplex never references [0:v]).
|
|
134
140
|
if (!inputs || inputs.length === 0) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
"
|
|
138
|
-
|
|
141
|
+
if (!filterComplex) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
"Rendi backend requires at least one input file or a filterComplex with synthetic sources.",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const dummyUrl = await this.getOrCreateDummyInput();
|
|
147
|
+
inputs = [dummyUrl];
|
|
139
148
|
}
|
|
140
149
|
|
|
141
150
|
const inputFiles: Record<string, string> = {};
|
|
@@ -283,6 +292,31 @@ export class RendiBackend implements FFmpegBackend {
|
|
|
283
292
|
return file.upload(this.storage);
|
|
284
293
|
}
|
|
285
294
|
|
|
295
|
+
/** Cached URL of the 1×1 dummy PNG so we upload it at most once per backend instance. */
|
|
296
|
+
private dummyInputUrl: string | null = null;
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Generate a 1×1 transparent PNG via sharp, upload it to storage, and cache
|
|
300
|
+
* the URL. Used as a placeholder input for Rendi when the ffmpeg command
|
|
301
|
+
* has only synthetic (lavfi) sources and no real file inputs.
|
|
302
|
+
*/
|
|
303
|
+
private async getOrCreateDummyInput(): Promise<string> {
|
|
304
|
+
if (this.dummyInputUrl) return this.dummyInputUrl;
|
|
305
|
+
const png = await sharp({
|
|
306
|
+
create: {
|
|
307
|
+
width: 1,
|
|
308
|
+
height: 1,
|
|
309
|
+
channels: 4,
|
|
310
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
311
|
+
},
|
|
312
|
+
})
|
|
313
|
+
.png()
|
|
314
|
+
.toBuffer();
|
|
315
|
+
const key = "internal/rendi-dummy-1x1.png";
|
|
316
|
+
this.dummyInputUrl = await this.storage.upload(png, key, "image/png");
|
|
317
|
+
return this.dummyInputUrl;
|
|
318
|
+
}
|
|
319
|
+
|
|
286
320
|
private buildCommandString(args: string[]): string {
|
|
287
321
|
return args
|
|
288
322
|
.map((arg) => {
|
|
@@ -10,9 +10,19 @@ const mockStorage: StorageProvider = {
|
|
|
10
10
|
},
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
/** Mock storage that accepts uploads and returns a predictable URL. */
|
|
14
|
+
const uploadableStorage: StorageProvider = {
|
|
15
|
+
async upload(_data: Uint8Array, key: string) {
|
|
16
|
+
return `https://mock-storage.test/${key}`;
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
13
20
|
describe("rendi backend validation", () => {
|
|
14
|
-
test("throws
|
|
15
|
-
const backend = createRendiBackend({
|
|
21
|
+
test("throws when inputs empty and no filterComplex", async () => {
|
|
22
|
+
const backend = createRendiBackend({
|
|
23
|
+
apiKey: "test-key",
|
|
24
|
+
storage: mockStorage,
|
|
25
|
+
});
|
|
16
26
|
|
|
17
27
|
await expect(
|
|
18
28
|
backend.run({
|
|
@@ -20,11 +30,16 @@ describe("rendi backend validation", () => {
|
|
|
20
30
|
outputArgs: ["-c:v", "libx264"],
|
|
21
31
|
outputPath: "output.mp4",
|
|
22
32
|
}),
|
|
23
|
-
).rejects.toThrow(
|
|
33
|
+
).rejects.toThrow(
|
|
34
|
+
"Rendi backend requires at least one input file or a filterComplex",
|
|
35
|
+
);
|
|
24
36
|
});
|
|
25
37
|
|
|
26
|
-
test("throws
|
|
27
|
-
const backend = createRendiBackend({
|
|
38
|
+
test("throws when inputs undefined and no filterComplex", async () => {
|
|
39
|
+
const backend = createRendiBackend({
|
|
40
|
+
apiKey: "test-key",
|
|
41
|
+
storage: mockStorage,
|
|
42
|
+
});
|
|
28
43
|
|
|
29
44
|
await expect(
|
|
30
45
|
backend.run({
|
|
@@ -32,7 +47,28 @@ describe("rendi backend validation", () => {
|
|
|
32
47
|
outputArgs: ["-c:v", "libx264"],
|
|
33
48
|
outputPath: "output.mp4",
|
|
34
49
|
}),
|
|
35
|
-
).rejects.toThrow(
|
|
50
|
+
).rejects.toThrow(
|
|
51
|
+
"Rendi backend requires at least one input file or a filterComplex",
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("generates dummy input when inputs empty but filterComplex present", async () => {
|
|
56
|
+
// The run() call will still fail at the Rendi API fetch (no real server),
|
|
57
|
+
// but it should NOT throw the "requires at least one input" error.
|
|
58
|
+
// It should get past the validation and fail at the network call.
|
|
59
|
+
const backend = createRendiBackend({
|
|
60
|
+
apiKey: "test-key",
|
|
61
|
+
storage: uploadableStorage,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await expect(
|
|
65
|
+
backend.run({
|
|
66
|
+
inputs: [],
|
|
67
|
+
filterComplex: "color=c=#1a1a2e:s=1080x1920:d=5:r=30[color0]",
|
|
68
|
+
outputArgs: ["-map", "[color0]", "-c:v", "libx264"],
|
|
69
|
+
outputPath: "output.mp4",
|
|
70
|
+
}),
|
|
71
|
+
).rejects.toThrow(/Rendi submit failed|fetch/);
|
|
36
72
|
});
|
|
37
73
|
});
|
|
38
74
|
|