@remotion/renderer 4.0.538 → 4.0.539
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/dist/assets/read-file.js +1 -2
- package/dist/esm/client.mjs +23 -4
- package/dist/esm/index.mjs +87 -88
- package/dist/merge-audio-track.d.ts +1 -1
- package/dist/merge-audio-track.js +24 -23
- package/dist/options/experimental-client-side-rendering.d.ts +16 -0
- package/dist/options/experimental-client-side-rendering.js +30 -0
- package/dist/options/experimental-sequence-activity-limit.d.ts +19 -0
- package/dist/options/experimental-sequence-activity-limit.js +40 -0
- package/dist/options/experimental-sequence-activity.d.ts +19 -0
- package/dist/options/experimental-sequence-activity.js +38 -0
- package/dist/options/experimental-tracks.d.ts +19 -0
- package/dist/options/experimental-tracks.js +43 -0
- package/dist/options/is-production.js +1 -1
- package/dist/options/license-key.js +1 -1
- package/dist/prestitcher-memory-usage.d.ts +13 -0
- package/dist/prestitcher-memory-usage.js +33 -0
- package/dist/render-partitions.d.ts +7 -0
- package/dist/render-partitions.js +50 -0
- package/dist/serve-handler/range-parser.js +1 -1
- package/dist/serve-static.js +4 -1
- package/package.json +13 -13
package/dist/assets/read-file.js
CHANGED
|
@@ -46,8 +46,7 @@ const readFile = async (url, redirectsSoFar = 0) => {
|
|
|
46
46
|
if (!response.headers.location) {
|
|
47
47
|
throw new Error(`Received a status code ${response.statusCode} but no "Location" header while calling ${response.headers.location}`);
|
|
48
48
|
}
|
|
49
|
-
const
|
|
50
|
-
const redirectUrl = new URL(response.headers.location, origin).toString();
|
|
49
|
+
const redirectUrl = new URL(response.headers.location, url).toString();
|
|
51
50
|
request.destroy();
|
|
52
51
|
response.destroy();
|
|
53
52
|
return (0, exports.readFile)(redirectUrl, redirectsSoFar + 1);
|
package/dist/esm/client.mjs
CHANGED
|
@@ -3291,12 +3291,11 @@ var isProductionOption = {
|
|
|
3291
3291
|
/* @__PURE__ */ jsx47("code", {
|
|
3292
3292
|
children: "false"
|
|
3293
3293
|
}),
|
|
3294
|
-
"
|
|
3295
|
-
" ",
|
|
3294
|
+
" for a development render so it is not counted as a billable render on remotion.pro. Defaults to ",
|
|
3296
3295
|
/* @__PURE__ */ jsx47("code", {
|
|
3297
|
-
children: "
|
|
3296
|
+
children: "true"
|
|
3298
3297
|
}),
|
|
3299
|
-
"."
|
|
3298
|
+
". Only has an effect when a license key is provided."
|
|
3300
3299
|
]
|
|
3301
3300
|
}),
|
|
3302
3301
|
ssrName: "isProduction",
|
|
@@ -3498,6 +3497,26 @@ var licenseKeyOption = {
|
|
|
3498
3497
|
/* @__PURE__ */ jsx51("code", {
|
|
3499
3498
|
children: "@remotion/licensing"
|
|
3500
3499
|
}),
|
|
3500
|
+
". For ",
|
|
3501
|
+
/* @__PURE__ */ jsx51("code", {
|
|
3502
|
+
children: "npx remotion render"
|
|
3503
|
+
}),
|
|
3504
|
+
" and",
|
|
3505
|
+
" ",
|
|
3506
|
+
/* @__PURE__ */ jsx51("code", {
|
|
3507
|
+
children: "npx remotion still"
|
|
3508
|
+
}),
|
|
3509
|
+
", pass a private API key starting with",
|
|
3510
|
+
" ",
|
|
3511
|
+
/* @__PURE__ */ jsx51("code", {
|
|
3512
|
+
children: "rm_sec_"
|
|
3513
|
+
}),
|
|
3514
|
+
" from the License keys page on",
|
|
3515
|
+
" ",
|
|
3516
|
+
/* @__PURE__ */ jsx51("a", {
|
|
3517
|
+
href: "https://www.remotion.pro/dashboard",
|
|
3518
|
+
children: "remotion.pro"
|
|
3519
|
+
}),
|
|
3501
3520
|
"."
|
|
3502
3521
|
]
|
|
3503
3522
|
}),
|
package/dist/esm/index.mjs
CHANGED
|
@@ -1155,8 +1155,7 @@ var readFile = async (url, redirectsSoFar = 0) => {
|
|
|
1155
1155
|
if (!response.headers.location) {
|
|
1156
1156
|
throw new Error(`Received a status code ${response.statusCode} but no "Location" header while calling ${response.headers.location}`);
|
|
1157
1157
|
}
|
|
1158
|
-
const
|
|
1159
|
-
const redirectUrl = new URL(response.headers.location, origin).toString();
|
|
1158
|
+
const redirectUrl = new URL(response.headers.location, url).toString();
|
|
1160
1159
|
request.destroy();
|
|
1161
1160
|
response.destroy();
|
|
1162
1161
|
return readFile(redirectUrl, redirectsSoFar + 1);
|
|
@@ -17698,6 +17697,69 @@ var makeRange = (from, to) => {
|
|
|
17698
17697
|
});
|
|
17699
17698
|
};
|
|
17700
17699
|
|
|
17700
|
+
// src/port-config.ts
|
|
17701
|
+
import os9 from "os";
|
|
17702
|
+
var cached = null;
|
|
17703
|
+
var getPortConfig = (preferIpv4) => {
|
|
17704
|
+
if (cached) {
|
|
17705
|
+
return cached;
|
|
17706
|
+
}
|
|
17707
|
+
const networkInterfaces = os9.networkInterfaces();
|
|
17708
|
+
const flattened = flattenNetworkInterfaces(networkInterfaces);
|
|
17709
|
+
const host = getHostToBind(flattened, preferIpv4);
|
|
17710
|
+
const hostsToTry = getHostsToTry(flattened);
|
|
17711
|
+
const response = { host, hostsToTry };
|
|
17712
|
+
cached = response;
|
|
17713
|
+
return response;
|
|
17714
|
+
};
|
|
17715
|
+
var getHostToBind = (flattened, preferIpv4) => {
|
|
17716
|
+
if (preferIpv4 || !isIpV6Supported(flattened)) {
|
|
17717
|
+
return "0.0.0.0";
|
|
17718
|
+
}
|
|
17719
|
+
return "::";
|
|
17720
|
+
};
|
|
17721
|
+
var getHostsToTry = (flattened) => {
|
|
17722
|
+
return [
|
|
17723
|
+
hasIPv6LoopbackAddress(flattened) ? "::1" : null,
|
|
17724
|
+
hasIpv4LoopbackAddress(flattened) ? "127.0.0.1" : null,
|
|
17725
|
+
isIpV6Supported(flattened) ? "::" : null,
|
|
17726
|
+
"0.0.0.0"
|
|
17727
|
+
].filter(truthy);
|
|
17728
|
+
};
|
|
17729
|
+
var flattenNetworkInterfaces = (networkInterfaces) => {
|
|
17730
|
+
const result = [];
|
|
17731
|
+
for (const iface in networkInterfaces) {
|
|
17732
|
+
for (const configuration of networkInterfaces[iface]) {
|
|
17733
|
+
result.push(configuration);
|
|
17734
|
+
}
|
|
17735
|
+
}
|
|
17736
|
+
return result;
|
|
17737
|
+
};
|
|
17738
|
+
var isIpV6Supported = (flattened) => {
|
|
17739
|
+
for (const configuration of flattened) {
|
|
17740
|
+
if (configuration.family === "IPv6" && !configuration.internal) {
|
|
17741
|
+
return true;
|
|
17742
|
+
}
|
|
17743
|
+
}
|
|
17744
|
+
return false;
|
|
17745
|
+
};
|
|
17746
|
+
var hasIPv6LoopbackAddress = (flattened) => {
|
|
17747
|
+
for (const configuration of flattened) {
|
|
17748
|
+
if (configuration.family === "IPv6" && configuration.internal && configuration.address === "::1") {
|
|
17749
|
+
return true;
|
|
17750
|
+
}
|
|
17751
|
+
}
|
|
17752
|
+
return false;
|
|
17753
|
+
};
|
|
17754
|
+
var hasIpv4LoopbackAddress = (flattened) => {
|
|
17755
|
+
for (const configuration of flattened) {
|
|
17756
|
+
if (configuration.family === "IPv4" && configuration.internal && configuration.address === "127.0.0.1") {
|
|
17757
|
+
return true;
|
|
17758
|
+
}
|
|
17759
|
+
}
|
|
17760
|
+
return false;
|
|
17761
|
+
};
|
|
17762
|
+
|
|
17701
17763
|
// src/serve-handler/index.ts
|
|
17702
17764
|
import { createReadStream, promises as promises3 } from "node:fs";
|
|
17703
17765
|
import path19 from "node:path";
|
|
@@ -17743,7 +17805,7 @@ var rangeParser = (size, str) => {
|
|
|
17743
17805
|
let start = parseInt(range[0], 10);
|
|
17744
17806
|
let end = parseInt(range[1], 10);
|
|
17745
17807
|
if (isNaN(start)) {
|
|
17746
|
-
start = size - end;
|
|
17808
|
+
start = Math.max(size - end, 0);
|
|
17747
17809
|
end = size - 1;
|
|
17748
17810
|
} else if (isNaN(end)) {
|
|
17749
17811
|
end = size - 1;
|
|
@@ -17985,6 +18047,7 @@ var serveStatic = async (path, options2) => {
|
|
|
17985
18047
|
let selectedPort = null;
|
|
17986
18048
|
const maxTries = 10;
|
|
17987
18049
|
const host = "127.0.0.1";
|
|
18050
|
+
const { hostsToTry } = getPortConfig(options2.forceIPv4);
|
|
17988
18051
|
for (let i = 0;i < maxTries; i++) {
|
|
17989
18052
|
let unlock = () => {};
|
|
17990
18053
|
try {
|
|
@@ -17993,7 +18056,7 @@ var serveStatic = async (path, options2) => {
|
|
|
17993
18056
|
desiredPort: options2?.port ?? undefined,
|
|
17994
18057
|
from: 3000,
|
|
17995
18058
|
to: 3100,
|
|
17996
|
-
hostsToTry
|
|
18059
|
+
hostsToTry
|
|
17997
18060
|
}).then(({ port, unlockPort }) => {
|
|
17998
18061
|
unlock = unlockPort;
|
|
17999
18062
|
server.listen({ port, host });
|
|
@@ -19496,69 +19559,6 @@ var validateSelectedPixelFormatAndCodecCombination = (pixelFormat, codec) => {
|
|
|
19496
19559
|
}
|
|
19497
19560
|
};
|
|
19498
19561
|
|
|
19499
|
-
// src/port-config.ts
|
|
19500
|
-
import os9 from "os";
|
|
19501
|
-
var cached = null;
|
|
19502
|
-
var getPortConfig = (preferIpv4) => {
|
|
19503
|
-
if (cached) {
|
|
19504
|
-
return cached;
|
|
19505
|
-
}
|
|
19506
|
-
const networkInterfaces = os9.networkInterfaces();
|
|
19507
|
-
const flattened = flattenNetworkInterfaces(networkInterfaces);
|
|
19508
|
-
const host = getHostToBind(flattened, preferIpv4);
|
|
19509
|
-
const hostsToTry = getHostsToTry(flattened);
|
|
19510
|
-
const response = { host, hostsToTry };
|
|
19511
|
-
cached = response;
|
|
19512
|
-
return response;
|
|
19513
|
-
};
|
|
19514
|
-
var getHostToBind = (flattened, preferIpv4) => {
|
|
19515
|
-
if (preferIpv4 || !isIpV6Supported(flattened)) {
|
|
19516
|
-
return "0.0.0.0";
|
|
19517
|
-
}
|
|
19518
|
-
return "::";
|
|
19519
|
-
};
|
|
19520
|
-
var getHostsToTry = (flattened) => {
|
|
19521
|
-
return [
|
|
19522
|
-
hasIPv6LoopbackAddress(flattened) ? "::1" : null,
|
|
19523
|
-
hasIpv4LoopbackAddress(flattened) ? "127.0.0.1" : null,
|
|
19524
|
-
isIpV6Supported(flattened) ? "::" : null,
|
|
19525
|
-
"0.0.0.0"
|
|
19526
|
-
].filter(truthy);
|
|
19527
|
-
};
|
|
19528
|
-
var flattenNetworkInterfaces = (networkInterfaces) => {
|
|
19529
|
-
const result = [];
|
|
19530
|
-
for (const iface in networkInterfaces) {
|
|
19531
|
-
for (const configuration of networkInterfaces[iface]) {
|
|
19532
|
-
result.push(configuration);
|
|
19533
|
-
}
|
|
19534
|
-
}
|
|
19535
|
-
return result;
|
|
19536
|
-
};
|
|
19537
|
-
var isIpV6Supported = (flattened) => {
|
|
19538
|
-
for (const configuration of flattened) {
|
|
19539
|
-
if (configuration.family === "IPv6" && !configuration.internal) {
|
|
19540
|
-
return true;
|
|
19541
|
-
}
|
|
19542
|
-
}
|
|
19543
|
-
return false;
|
|
19544
|
-
};
|
|
19545
|
-
var hasIPv6LoopbackAddress = (flattened) => {
|
|
19546
|
-
for (const configuration of flattened) {
|
|
19547
|
-
if (configuration.family === "IPv6" && configuration.internal && configuration.address === "::1") {
|
|
19548
|
-
return true;
|
|
19549
|
-
}
|
|
19550
|
-
}
|
|
19551
|
-
return false;
|
|
19552
|
-
};
|
|
19553
|
-
var hasIpv4LoopbackAddress = (flattened) => {
|
|
19554
|
-
for (const configuration of flattened) {
|
|
19555
|
-
if (configuration.family === "IPv4" && configuration.internal && configuration.address === "127.0.0.1") {
|
|
19556
|
-
return true;
|
|
19557
|
-
}
|
|
19558
|
-
}
|
|
19559
|
-
return false;
|
|
19560
|
-
};
|
|
19561
|
-
|
|
19562
19562
|
// src/probe-shared-memory-capture.ts
|
|
19563
19563
|
import fs16 from "node:fs";
|
|
19564
19564
|
import os10 from "node:os";
|
|
@@ -23816,7 +23816,8 @@ var createSilentAudio = async ({
|
|
|
23816
23816
|
};
|
|
23817
23817
|
|
|
23818
23818
|
// src/merge-audio-track.ts
|
|
23819
|
-
var
|
|
23819
|
+
var limit2 = pLimit(3);
|
|
23820
|
+
var mergeAudioTrack = async ({
|
|
23820
23821
|
outName,
|
|
23821
23822
|
files,
|
|
23822
23823
|
downloadMap,
|
|
@@ -23831,7 +23832,7 @@ var mergeAudioTrackUnlimited = async ({
|
|
|
23831
23832
|
sampleRate
|
|
23832
23833
|
}) => {
|
|
23833
23834
|
if (files.length === 0) {
|
|
23834
|
-
await createSilentAudio
|
|
23835
|
+
await limit2(createSilentAudio, {
|
|
23835
23836
|
outName,
|
|
23836
23837
|
chunkLengthInSeconds,
|
|
23837
23838
|
indent,
|
|
@@ -23920,29 +23921,27 @@ var mergeAudioTrackUnlimited = async ({
|
|
|
23920
23921
|
["-y", outName]
|
|
23921
23922
|
].filter(truthy).flat(2);
|
|
23922
23923
|
Log.verbose({ indent, logLevel }, `Merging audio tracks`, "Command:", `ffmpeg ${args.join(" ")}`, "Complex filter script:", complexFilter);
|
|
23923
|
-
|
|
23924
|
-
|
|
23925
|
-
|
|
23926
|
-
|
|
23927
|
-
|
|
23928
|
-
|
|
23929
|
-
|
|
23930
|
-
|
|
23931
|
-
|
|
23932
|
-
|
|
23933
|
-
|
|
23934
|
-
|
|
23935
|
-
|
|
23936
|
-
|
|
23924
|
+
await limit2(async () => {
|
|
23925
|
+
const task = callFf({
|
|
23926
|
+
bin: "ffmpeg",
|
|
23927
|
+
args,
|
|
23928
|
+
indent,
|
|
23929
|
+
logLevel,
|
|
23930
|
+
binariesDirectory,
|
|
23931
|
+
cancelSignal
|
|
23932
|
+
});
|
|
23933
|
+
task.stderr?.on("data", (data) => {
|
|
23934
|
+
const utf8 = data.toString("utf8");
|
|
23935
|
+
const parsed = parseFfmpegProgress(utf8, fps);
|
|
23936
|
+
if (parsed !== undefined) {
|
|
23937
|
+
onProgress(parsed / (chunkLengthInSeconds * fps));
|
|
23938
|
+
}
|
|
23939
|
+
});
|
|
23940
|
+
await task;
|
|
23937
23941
|
});
|
|
23938
|
-
await task;
|
|
23939
23942
|
onProgress(1);
|
|
23940
23943
|
cleanup();
|
|
23941
23944
|
};
|
|
23942
|
-
var limit2 = pLimit(3);
|
|
23943
|
-
var mergeAudioTrack = (options2) => {
|
|
23944
|
-
return limit2(mergeAudioTrackUnlimited, options2);
|
|
23945
|
-
};
|
|
23946
23945
|
|
|
23947
23946
|
// src/merge-inline-audio-tracks.ts
|
|
23948
23947
|
import path27 from "node:path";
|
|
@@ -16,5 +16,5 @@ type Options = {
|
|
|
16
16
|
chunkLengthInSeconds: number;
|
|
17
17
|
sampleRate: number;
|
|
18
18
|
};
|
|
19
|
-
export declare const mergeAudioTrack: (
|
|
19
|
+
export declare const mergeAudioTrack: ({ outName, files, downloadMap, remotionRoot, indent, logLevel, binariesDirectory, cancelSignal, onProgress, fps, chunkLengthInSeconds, sampleRate, }: Options) => Promise<void>;
|
|
20
20
|
export {};
|
|
@@ -16,10 +16,13 @@ const p_limit_1 = require("./p-limit");
|
|
|
16
16
|
const parse_ffmpeg_progress_1 = require("./parse-ffmpeg-progress");
|
|
17
17
|
const tmp_dir_1 = require("./tmp-dir");
|
|
18
18
|
const truthy_1 = require("./truthy");
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// Limits how many FFmpeg processes mix audio at once. Only the FFmpeg calls
|
|
20
|
+
// take a slot: a merge that waits for its chunks to be merged first holds none,
|
|
21
|
+
// so concurrent renders with many audio tracks can't block each other.
|
|
22
|
+
const limit = (0, p_limit_1.pLimit)(3);
|
|
23
|
+
const mergeAudioTrack = async ({ outName, files, downloadMap, remotionRoot, indent, logLevel, binariesDirectory, cancelSignal, onProgress, fps, chunkLengthInSeconds, sampleRate, }) => {
|
|
21
24
|
if (files.length === 0) {
|
|
22
|
-
await (
|
|
25
|
+
await limit(create_silent_audio_1.createSilentAudio, {
|
|
23
26
|
outName,
|
|
24
27
|
chunkLengthInSeconds,
|
|
25
28
|
indent,
|
|
@@ -109,28 +112,26 @@ const mergeAudioTrackUnlimited = async ({ outName, files, downloadMap, remotionR
|
|
|
109
112
|
.filter(truthy_1.truthy)
|
|
110
113
|
.flat(2);
|
|
111
114
|
logger_1.Log.verbose({ indent, logLevel }, `Merging audio tracks`, 'Command:', `ffmpeg ${args.join(' ')}`, 'Complex filter script:', complexFilter);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
115
|
+
await limit(async () => {
|
|
116
|
+
var _a;
|
|
117
|
+
const task = (0, call_ffmpeg_1.callFf)({
|
|
118
|
+
bin: 'ffmpeg',
|
|
119
|
+
args,
|
|
120
|
+
indent,
|
|
121
|
+
logLevel,
|
|
122
|
+
binariesDirectory,
|
|
123
|
+
cancelSignal,
|
|
124
|
+
});
|
|
125
|
+
(_a = task.stderr) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
|
|
126
|
+
const utf8 = data.toString('utf8');
|
|
127
|
+
const parsed = (0, parse_ffmpeg_progress_1.parseFfmpegProgress)(utf8, fps);
|
|
128
|
+
if (parsed !== undefined) {
|
|
129
|
+
onProgress(parsed / (chunkLengthInSeconds * fps));
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
await task;
|
|
126
133
|
});
|
|
127
|
-
await task;
|
|
128
134
|
onProgress(1);
|
|
129
135
|
cleanup();
|
|
130
136
|
};
|
|
131
|
-
// Must be at least 3 because recursively called twice in mergeAudioTrack
|
|
132
|
-
const limit = (0, p_limit_1.pLimit)(3);
|
|
133
|
-
const mergeAudioTrack = (options) => {
|
|
134
|
-
return limit(mergeAudioTrackUnlimited, options);
|
|
135
|
-
};
|
|
136
137
|
exports.mergeAudioTrack = mergeAudioTrack;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const experimentalClientSideRenderingOption: {
|
|
2
|
+
name: string;
|
|
3
|
+
cliFlag: "enable-experimental-client-side-rendering";
|
|
4
|
+
description: () => import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
ssrName: null;
|
|
6
|
+
docLink: string;
|
|
7
|
+
type: boolean;
|
|
8
|
+
getValue: ({ commandLine }: {
|
|
9
|
+
commandLine: Record<string, unknown>;
|
|
10
|
+
}) => {
|
|
11
|
+
value: boolean;
|
|
12
|
+
source: string;
|
|
13
|
+
};
|
|
14
|
+
setConfig(value: boolean): void;
|
|
15
|
+
id: "enable-experimental-client-side-rendering";
|
|
16
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.experimentalClientSideRenderingOption = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
let experimentalClientSideRenderingEnabled = false;
|
|
6
|
+
const cliFlag = 'enable-experimental-client-side-rendering';
|
|
7
|
+
exports.experimentalClientSideRenderingOption = {
|
|
8
|
+
name: 'Enable Experimental Client-Side Rendering',
|
|
9
|
+
cliFlag,
|
|
10
|
+
description: () => (jsx_runtime_1.jsx(jsx_runtime_1.Fragment, { children: "Enable WIP client-side rendering in the Remotion Studio. See https://www.remotion.dev/docs/client-side-rendering/ for notes." })),
|
|
11
|
+
ssrName: null,
|
|
12
|
+
docLink: 'https://www.remotion.dev/docs/client-side-rendering',
|
|
13
|
+
type: false,
|
|
14
|
+
getValue: ({ commandLine }) => {
|
|
15
|
+
if (commandLine[cliFlag] !== null) {
|
|
16
|
+
return {
|
|
17
|
+
value: commandLine[cliFlag],
|
|
18
|
+
source: 'cli',
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
value: experimentalClientSideRenderingEnabled,
|
|
23
|
+
source: 'config',
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
setConfig(value) {
|
|
27
|
+
experimentalClientSideRenderingEnabled = value;
|
|
28
|
+
},
|
|
29
|
+
id: cliFlag,
|
|
30
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const experimentalSequenceActivityLimitOption: {
|
|
2
|
+
name: string;
|
|
3
|
+
addedIn: string;
|
|
4
|
+
cliFlag: "experimental-sequence-activity-limit";
|
|
5
|
+
id: "experimental-sequence-activity-limit";
|
|
6
|
+
description: () => import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
ssrName: null;
|
|
8
|
+
docLink: string;
|
|
9
|
+
type: number;
|
|
10
|
+
getValue: ({ commandLine }: {
|
|
11
|
+
commandLine: Record<string, unknown>;
|
|
12
|
+
}) => {
|
|
13
|
+
value: number;
|
|
14
|
+
source: string;
|
|
15
|
+
};
|
|
16
|
+
setConfig(value: number): void;
|
|
17
|
+
getConfigValue: () => number | null;
|
|
18
|
+
reset: () => void;
|
|
19
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.experimentalSequenceActivityLimitOption = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const no_react_1 = require("remotion/no-react");
|
|
6
|
+
let configuredLimit = null;
|
|
7
|
+
const cliFlag = 'experimental-sequence-activity-limit';
|
|
8
|
+
exports.experimentalSequenceActivityLimitOption = {
|
|
9
|
+
name: 'Experimental hidden Sequence Activity limit',
|
|
10
|
+
addedIn: '4.0.535',
|
|
11
|
+
cliFlag,
|
|
12
|
+
id: cliFlag,
|
|
13
|
+
description: () => (jsx_runtime_1.jsx(jsx_runtime_1.Fragment, { children: "Limit nearby hidden scenes discovered by experimental Sequence Activity in the Remotion Studio, including nested sequences and their ancestors. Visible, premounted, and postmounted scenes always render. Defaults to 20. Must be a safe integer of at least -1. Set to 0 to disable hidden discovery, or -1 for unlimited discovery without admission delays. Only applies when experimental Sequence Activity is enabled. Does not affect rendering." })),
|
|
14
|
+
ssrName: null,
|
|
15
|
+
docLink: 'https://www.remotion.dev/docs/options/experimental-sequence-activity-limit',
|
|
16
|
+
type: 0,
|
|
17
|
+
getValue: ({ commandLine }) => {
|
|
18
|
+
if (commandLine[cliFlag] !== undefined) {
|
|
19
|
+
const value = commandLine[cliFlag];
|
|
20
|
+
if (!Number.isSafeInteger(value) || value < -1) {
|
|
21
|
+
throw new Error('Sequence Activity limit must be a safe integer of at least -1.');
|
|
22
|
+
}
|
|
23
|
+
return { value: value, source: 'cli' };
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
value: configuredLimit !== null && configuredLimit !== void 0 ? configuredLimit : no_react_1.NoReactInternals.DEFAULT_SEQUENCE_ACTIVITY_LIMIT,
|
|
27
|
+
source: 'config',
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
setConfig(value) {
|
|
31
|
+
if (!Number.isSafeInteger(value) || value < -1) {
|
|
32
|
+
throw new Error('Sequence Activity limit must be a safe integer of at least -1.');
|
|
33
|
+
}
|
|
34
|
+
configuredLimit = value;
|
|
35
|
+
},
|
|
36
|
+
getConfigValue: () => configuredLimit,
|
|
37
|
+
reset: () => {
|
|
38
|
+
configuredLimit = null;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const experimentalSequenceActivityOption: {
|
|
2
|
+
name: string;
|
|
3
|
+
addedIn: string;
|
|
4
|
+
cliFlag: "experimental-sequence-activity";
|
|
5
|
+
description: () => import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
ssrName: null;
|
|
7
|
+
docLink: string;
|
|
8
|
+
type: boolean;
|
|
9
|
+
getValue: ({ commandLine }: {
|
|
10
|
+
commandLine: Record<string, unknown>;
|
|
11
|
+
}) => {
|
|
12
|
+
value: boolean;
|
|
13
|
+
source: string;
|
|
14
|
+
};
|
|
15
|
+
setConfig(value: boolean): void;
|
|
16
|
+
getConfigValue: () => boolean | null;
|
|
17
|
+
reset: () => void;
|
|
18
|
+
id: "experimental-sequence-activity";
|
|
19
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.experimentalSequenceActivityOption = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
let experimentalSequenceActivityEnabled = false;
|
|
6
|
+
let configuredExperimentalSequenceActivityEnabled = null;
|
|
7
|
+
const cliFlag = 'experimental-sequence-activity';
|
|
8
|
+
exports.experimentalSequenceActivityOption = {
|
|
9
|
+
name: 'Enable experimental Sequence Activity',
|
|
10
|
+
addedIn: '4.0.535',
|
|
11
|
+
cliFlag,
|
|
12
|
+
description: () => (jsx_runtime_1.jsx(jsx_runtime_1.Fragment, { children: "Discover layers in nearby hidden scenes in the Remotion Studio before they become visible. This experimental feature may use more CPU. Disabled by default. Does not affect rendering." })),
|
|
13
|
+
ssrName: null,
|
|
14
|
+
docLink: 'https://www.remotion.dev/docs/options/experimental-sequence-activity',
|
|
15
|
+
type: false,
|
|
16
|
+
getValue: ({ commandLine }) => {
|
|
17
|
+
if (commandLine[cliFlag] !== undefined && commandLine[cliFlag] !== null) {
|
|
18
|
+
return {
|
|
19
|
+
value: commandLine[cliFlag],
|
|
20
|
+
source: 'cli',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
value: experimentalSequenceActivityEnabled,
|
|
25
|
+
source: 'config',
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
setConfig(value) {
|
|
29
|
+
experimentalSequenceActivityEnabled = value;
|
|
30
|
+
configuredExperimentalSequenceActivityEnabled = value;
|
|
31
|
+
},
|
|
32
|
+
getConfigValue: () => configuredExperimentalSequenceActivityEnabled,
|
|
33
|
+
reset: () => {
|
|
34
|
+
experimentalSequenceActivityEnabled = false;
|
|
35
|
+
configuredExperimentalSequenceActivityEnabled = null;
|
|
36
|
+
},
|
|
37
|
+
id: cliFlag,
|
|
38
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const experimentalTracksOption: {
|
|
2
|
+
name: string;
|
|
3
|
+
addedIn: string;
|
|
4
|
+
cliFlag: "experimental-tracks";
|
|
5
|
+
description: () => import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
ssrName: null;
|
|
7
|
+
docLink: string;
|
|
8
|
+
type: boolean;
|
|
9
|
+
getValue: ({ commandLine }: {
|
|
10
|
+
commandLine: Record<string, unknown>;
|
|
11
|
+
}) => {
|
|
12
|
+
value: boolean;
|
|
13
|
+
source: string;
|
|
14
|
+
};
|
|
15
|
+
setConfig(value: boolean): void;
|
|
16
|
+
getConfigValue: () => boolean | null;
|
|
17
|
+
reset: () => void;
|
|
18
|
+
id: "experimental-tracks";
|
|
19
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.experimentalTracksOption = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
let experimentalTracksEnabled = false;
|
|
6
|
+
let configuredExperimentalTracksEnabled = null;
|
|
7
|
+
const cliFlag = 'experimental-tracks';
|
|
8
|
+
exports.experimentalTracksOption = {
|
|
9
|
+
name: 'Enable experimental timeline tracks',
|
|
10
|
+
addedIn: '4.0.534',
|
|
11
|
+
cliFlag,
|
|
12
|
+
description: () => (jsx_runtime_1.jsxs(jsx_runtime_1.Fragment, { children: ["Enable experimental timeline tracks in the Remotion Studio. Group clips from ",
|
|
13
|
+
jsx_runtime_1.jsx("code", { children: '<Track>' }),
|
|
14
|
+
", ",
|
|
15
|
+
jsx_runtime_1.jsx("code", { children: '<Series>' }),
|
|
16
|
+
" and", ' ', jsx_runtime_1.jsx("code", { children: '<TransitionSeries>' }),
|
|
17
|
+
" on shared rows, with overlays on additional rows. Disabled by default."] })),
|
|
18
|
+
ssrName: null,
|
|
19
|
+
docLink: 'https://www.remotion.dev/docs/options/experimental-tracks',
|
|
20
|
+
type: false,
|
|
21
|
+
getValue: ({ commandLine }) => {
|
|
22
|
+
if (commandLine[cliFlag] !== undefined && commandLine[cliFlag] !== null) {
|
|
23
|
+
return {
|
|
24
|
+
value: commandLine[cliFlag],
|
|
25
|
+
source: 'cli',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
value: experimentalTracksEnabled,
|
|
30
|
+
source: 'config',
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
setConfig(value) {
|
|
34
|
+
experimentalTracksEnabled = value;
|
|
35
|
+
configuredExperimentalTracksEnabled = value;
|
|
36
|
+
},
|
|
37
|
+
getConfigValue: () => configuredExperimentalTracksEnabled,
|
|
38
|
+
reset: () => {
|
|
39
|
+
experimentalTracksEnabled = false;
|
|
40
|
+
configuredExperimentalTracksEnabled = null;
|
|
41
|
+
},
|
|
42
|
+
id: cliFlag,
|
|
43
|
+
};
|
|
@@ -8,7 +8,7 @@ exports.isProductionOption = {
|
|
|
8
8
|
name: 'Is Production',
|
|
9
9
|
addedIn: '4.0.409',
|
|
10
10
|
cliFlag,
|
|
11
|
-
description: () => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["Pass ", (0, jsx_runtime_1.jsx)("code", { children: "false" }), "
|
|
11
|
+
description: () => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["Pass ", (0, jsx_runtime_1.jsx)("code", { children: "false" }), " for a development render so it is not counted as a billable render on remotion.pro. Defaults to ", (0, jsx_runtime_1.jsx)("code", { children: "true" }), ". Only has an effect when a license key is provided."] })),
|
|
12
12
|
ssrName: 'isProduction',
|
|
13
13
|
docLink: 'https://www.remotion.dev/docs/options/is-production',
|
|
14
14
|
getValue: ({ commandLine }) => {
|
|
@@ -8,7 +8,7 @@ exports.licenseKeyOption = {
|
|
|
8
8
|
name: 'License key',
|
|
9
9
|
addedIn: '4.0.409',
|
|
10
10
|
cliFlag,
|
|
11
|
-
description: () => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["License key for sending a usage event using", ' ', (0, jsx_runtime_1.jsx)("code", { children: "@remotion/licensing" }), "."] })),
|
|
11
|
+
description: () => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["License key for sending a usage event using", ' ', (0, jsx_runtime_1.jsx)("code", { children: "@remotion/licensing" }), ". For ", (0, jsx_runtime_1.jsx)("code", { children: "npx remotion render" }), " and", ' ', (0, jsx_runtime_1.jsx)("code", { children: "npx remotion still" }), ", pass a private API key starting with", ' ', (0, jsx_runtime_1.jsx)("code", { children: "rm_sec_" }), " from the License keys page on", ' ', (0, jsx_runtime_1.jsx)("a", { href: "https://www.remotion.pro/dashboard", children: "remotion.pro" }), "."] })),
|
|
12
12
|
ssrName: 'licenseKey',
|
|
13
13
|
docLink: 'https://www.remotion.dev/docs/options/license-key',
|
|
14
14
|
type: null,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const hasEnoughMemoryForParallelEncoding: ({ freeMemory, estimatedUsage, }: {
|
|
2
|
+
freeMemory: number;
|
|
3
|
+
estimatedUsage: number;
|
|
4
|
+
}) => boolean;
|
|
5
|
+
export declare const shouldUseParallelEncoding: ({ width, height, logLevel, }: {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
logLevel: "error" | "info" | "trace" | "verbose" | "warn";
|
|
9
|
+
}) => {
|
|
10
|
+
hasEnoughMemory: boolean;
|
|
11
|
+
freeMemory: number;
|
|
12
|
+
estimatedUsage: number;
|
|
13
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.shouldUseParallelEncoding = exports.hasEnoughMemoryForParallelEncoding = void 0;
|
|
4
|
+
const get_available_memory_1 = require("./memory/get-available-memory");
|
|
5
|
+
const MINIMUM_MEMORY_LEFT = 2000000000;
|
|
6
|
+
const hasEnoughMemoryForParallelEncoding = ({ freeMemory, estimatedUsage, }) => {
|
|
7
|
+
return freeMemory - estimatedUsage > MINIMUM_MEMORY_LEFT;
|
|
8
|
+
};
|
|
9
|
+
exports.hasEnoughMemoryForParallelEncoding = hasEnoughMemoryForParallelEncoding;
|
|
10
|
+
const estimateMemoryUsageForPrestitcher = ({ width, height, }) => {
|
|
11
|
+
// Empirically we detected that per 1 million pixels, FFMPEG uses around 1GB of memory, relatively independent of
|
|
12
|
+
// the duration of the video.
|
|
13
|
+
const memoryUsageFor4K = 1000000000;
|
|
14
|
+
const memoryUsageOfPixel = memoryUsageFor4K / 1000000;
|
|
15
|
+
return memoryUsageOfPixel * width * height;
|
|
16
|
+
};
|
|
17
|
+
const shouldUseParallelEncoding = ({ width, height, logLevel, }) => {
|
|
18
|
+
const freeMemory = (0, get_available_memory_1.getAvailableMemory)(logLevel);
|
|
19
|
+
const estimatedUsage = estimateMemoryUsageForPrestitcher({
|
|
20
|
+
height,
|
|
21
|
+
width,
|
|
22
|
+
});
|
|
23
|
+
const hasEnoughMemory = (0, exports.hasEnoughMemoryForParallelEncoding)({
|
|
24
|
+
estimatedUsage,
|
|
25
|
+
freeMemory,
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
hasEnoughMemory,
|
|
29
|
+
freeMemory,
|
|
30
|
+
estimatedUsage,
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
exports.shouldUseParallelEncoding = shouldUseParallelEncoding;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderPartitions = void 0;
|
|
4
|
+
const renderPartitions = ({ frames, concurrency, }) => {
|
|
5
|
+
const partitions = [];
|
|
6
|
+
let start = 0;
|
|
7
|
+
for (let i = 0; i < concurrency; i++) {
|
|
8
|
+
const end = start + Math.ceil((frames.length - start) / (concurrency - i));
|
|
9
|
+
partitions.push(frames.slice(start, end));
|
|
10
|
+
start = end;
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
partitions,
|
|
14
|
+
getNextFrame: (pageIndex) => {
|
|
15
|
+
if (partitions[pageIndex].length === 0) {
|
|
16
|
+
const partitionLengths = partitions.map((p) => p.length);
|
|
17
|
+
if (partitionLengths.every((p) => p === 0)) {
|
|
18
|
+
throw new Error('No more frames to render');
|
|
19
|
+
}
|
|
20
|
+
let longestRemainingPartitionIndex = -1;
|
|
21
|
+
for (let i = 0; i < partitions.length; i++) {
|
|
22
|
+
if (longestRemainingPartitionIndex === -1) {
|
|
23
|
+
longestRemainingPartitionIndex = i;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (partitions[i].length >
|
|
27
|
+
partitions[longestRemainingPartitionIndex].length) {
|
|
28
|
+
longestRemainingPartitionIndex = i;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (longestRemainingPartitionIndex === -1) {
|
|
32
|
+
throw new Error('No more frames to render');
|
|
33
|
+
}
|
|
34
|
+
// Round up - let's say only one frame is left to render, we should take this one
|
|
35
|
+
const slicePoint = Math.ceil(partitions[longestRemainingPartitionIndex].length / 2) -
|
|
36
|
+
// but don't forget it's zero indexed :)
|
|
37
|
+
1;
|
|
38
|
+
partitions[pageIndex] =
|
|
39
|
+
partitions[longestRemainingPartitionIndex].slice(slicePoint);
|
|
40
|
+
partitions[longestRemainingPartitionIndex] = partitions[longestRemainingPartitionIndex].slice(0, slicePoint);
|
|
41
|
+
}
|
|
42
|
+
const value = partitions[pageIndex].shift();
|
|
43
|
+
if (value === undefined) {
|
|
44
|
+
throw new Error('No more frames to render');
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
exports.renderPartitions = renderPartitions;
|
package/dist/serve-static.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.serveStatic = void 0;
|
|
|
7
7
|
const node_http_1 = __importDefault(require("node:http"));
|
|
8
8
|
const get_port_1 = require("./get-port");
|
|
9
9
|
const offthread_video_server_1 = require("./offthread-video-server");
|
|
10
|
+
const port_config_1 = require("./port-config");
|
|
10
11
|
const serve_handler_1 = require("./serve-handler");
|
|
11
12
|
const serveStatic = async (path, options) => {
|
|
12
13
|
const { listener: offthreadRequest, close: closeCompositor, compositor, } = (0, offthread_video_server_1.startOffthreadVideoServer)({
|
|
@@ -55,6 +56,8 @@ const serveStatic = async (path, options) => {
|
|
|
55
56
|
const maxTries = 10;
|
|
56
57
|
// Rendering only needs local access, unlike the Studio server.
|
|
57
58
|
const host = '127.0.0.1';
|
|
59
|
+
// URLs use localhost, which can resolve to another supported address family.
|
|
60
|
+
const { hostsToTry } = (0, port_config_1.getPortConfig)(options.forceIPv4);
|
|
58
61
|
for (let i = 0; i < maxTries; i++) {
|
|
59
62
|
let unlock = () => { };
|
|
60
63
|
try {
|
|
@@ -64,7 +67,7 @@ const serveStatic = async (path, options) => {
|
|
|
64
67
|
desiredPort: (_a = options === null || options === void 0 ? void 0 : options.port) !== null && _a !== void 0 ? _a : undefined,
|
|
65
68
|
from: 3000,
|
|
66
69
|
to: 3100,
|
|
67
|
-
hostsToTry
|
|
70
|
+
hostsToTry,
|
|
68
71
|
})
|
|
69
72
|
.then(({ port, unlockPort }) => {
|
|
70
73
|
unlock = unlockPort;
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"url": "https://github.com/remotion-dev/remotion/tree/main/packages/renderer"
|
|
4
4
|
},
|
|
5
5
|
"name": "@remotion/renderer",
|
|
6
|
-
"version": "4.0.
|
|
6
|
+
"version": "4.0.539",
|
|
7
7
|
"description": "Render Remotion videos using Node.js or Bun",
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"types": "dist/index.d.ts",
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"execa": "5.1.1",
|
|
25
|
-
"remotion": "4.0.
|
|
26
|
-
"@remotion/streaming": "4.0.
|
|
25
|
+
"remotion": "4.0.539",
|
|
26
|
+
"@remotion/streaming": "4.0.539",
|
|
27
27
|
"source-map": "0.8.0",
|
|
28
28
|
"ws": "8.21.0",
|
|
29
|
-
"@remotion/licensing": "4.0.
|
|
29
|
+
"@remotion/licensing": "4.0.539"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"react": ">=16.8.0",
|
|
@@ -40,21 +40,21 @@
|
|
|
40
40
|
"react-dom": "19.2.3",
|
|
41
41
|
"typescript": "7.1.0-beta",
|
|
42
42
|
"@types/ws": "8.5.10",
|
|
43
|
-
"@remotion/example-videos": "4.0.
|
|
44
|
-
"@remotion/eslint-config-internal": "4.0.
|
|
43
|
+
"@remotion/example-videos": "4.0.539",
|
|
44
|
+
"@remotion/eslint-config-internal": "4.0.539",
|
|
45
45
|
"eslint": "9.19.0",
|
|
46
46
|
"@types/node": "20.12.14",
|
|
47
47
|
"@types/pngjs": "6.0.5",
|
|
48
48
|
"pngjs": "7.0.0"
|
|
49
49
|
},
|
|
50
50
|
"optionalDependencies": {
|
|
51
|
-
"@remotion/compositor-darwin-arm64": "4.0.
|
|
52
|
-
"@remotion/compositor-darwin-x64": "4.0.
|
|
53
|
-
"@remotion/compositor-linux-arm64-gnu": "4.0.
|
|
54
|
-
"@remotion/compositor-linux-arm64-musl": "4.0.
|
|
55
|
-
"@remotion/compositor-linux-x64-gnu": "4.0.
|
|
56
|
-
"@remotion/compositor-linux-x64-musl": "4.0.
|
|
57
|
-
"@remotion/compositor-win32-x64-msvc": "4.0.
|
|
51
|
+
"@remotion/compositor-darwin-arm64": "4.0.539",
|
|
52
|
+
"@remotion/compositor-darwin-x64": "4.0.539",
|
|
53
|
+
"@remotion/compositor-linux-arm64-gnu": "4.0.539",
|
|
54
|
+
"@remotion/compositor-linux-arm64-musl": "4.0.539",
|
|
55
|
+
"@remotion/compositor-linux-x64-gnu": "4.0.539",
|
|
56
|
+
"@remotion/compositor-linux-x64-musl": "4.0.539",
|
|
57
|
+
"@remotion/compositor-win32-x64-msvc": "4.0.539"
|
|
58
58
|
},
|
|
59
59
|
"keywords": [
|
|
60
60
|
"remotion",
|