aivee 0.1.0-alpha.0 → 0.1.0-dev.59872e1
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/CHANGELOG.md +34 -0
- package/LICENSE +21 -21
- package/README.md +120 -118
- package/docs/architecture.md +401 -401
- package/docs/aspect-ratio-presets.md +72 -72
- package/docs/releasing.md +19 -19
- package/docs/usage.md +1193 -1193
- package/package.json +52 -52
- package/requirements-otio.txt +1 -1
- package/src/capabilities.mjs +307 -307
- package/src/cli.mjs +646 -646
- package/src/doctor.mjs +133 -133
- package/src/edits/analysis-to-edl.mjs +43 -43
- package/src/edits/detect-silence.mjs +51 -51
- package/src/edits/find-mistake-candidates.mjs +42 -42
- package/src/edits/plan-shot-placements.mjs +105 -105
- package/src/edits/plan-silence-cuts.mjs +49 -49
- package/src/edits/retime-transcript.mjs +105 -105
- package/src/files.mjs +80 -80
- package/src/index.mjs +44 -44
- package/src/interchange/otio.mjs +204 -204
- package/src/interchange/otio_bridge.py +521 -521
- package/src/media/analysis-provider.mjs +75 -75
- package/src/media/compatibility.mjs +45 -45
- package/src/media/ffmpeg-capabilities.mjs +129 -129
- package/src/media/frame-rate.mjs +58 -58
- package/src/media/frame-time.mjs +22 -22
- package/src/media/preview-assets.mjs +350 -350
- package/src/media/probe.mjs +149 -149
- package/src/media/render-cuts.mjs +90 -90
- package/src/media/scene-detection.mjs +107 -107
- package/src/media/scene-review.mjs +205 -205
- package/src/project/assets.mjs +1194 -1194
- package/src/project/create.mjs +100 -100
- package/src/project/presets.mjs +44 -44
- package/src/render/checkpoints.mjs +192 -192
- package/src/render/formats.mjs +163 -163
- package/src/render/hyperframes.mjs +590 -590
- package/src/render/process.mjs +111 -111
- package/src/render/progress.mjs +46 -46
- package/src/render/timeline-control.mjs +53 -53
- package/src/timeline/assemble.mjs +119 -119
- package/src/timeline/audio-effects.mjs +75 -75
- package/src/timeline/audio-mastering.mjs +236 -236
- package/src/timeline/audio-mix.mjs +105 -105
- package/src/timeline/audio-track-effects.mjs +139 -139
- package/src/timeline/audio-volume.mjs +63 -63
- package/src/timeline/background.mjs +8 -8
- package/src/timeline/blend-mode.mjs +34 -34
- package/src/timeline/caption-layouts.mjs +62 -61
- package/src/timeline/captions.mjs +80 -80
- package/src/timeline/edit.mjs +965 -965
- package/src/timeline/fingerprint.mjs +6 -6
- package/src/timeline/from-edl.mjs +31 -31
- package/src/timeline/history-store.mjs +281 -281
- package/src/timeline/history.mjs +69 -69
- package/src/timeline/inspect.mjs +117 -117
- package/src/timeline/keyframes.mjs +158 -158
- package/src/timeline/layers.mjs +18 -18
- package/src/timeline/mask.mjs +53 -53
- package/src/timeline/project.mjs +52 -52
- package/src/timeline/render.mjs +759 -759
- package/src/timeline/sidechain-render.mjs +209 -209
- package/src/timeline/source-fit.mjs +15 -15
- package/src/timeline/speech-bubble.mjs +26 -26
- package/src/timeline/speed-ramp.mjs +188 -188
- package/src/timeline/track-state.mjs +22 -22
- package/src/timeline/transition.mjs +34 -34
- package/src/timeline/validate.mjs +322 -322
- package/src/transcript/corrections.mjs +21 -21
- package/src/transcript/normalize.mjs +17 -17
- package/src/transcript/provider.mjs +42 -42
- package/src/transcript/subtitles.mjs +57 -57
- package/src/transcript/validate.mjs +39 -39
package/src/render/formats.mjs
CHANGED
|
@@ -1,163 +1,163 @@
|
|
|
1
|
-
const EXTENSIONS = new Map([
|
|
2
|
-
['.mp4', 'mp4'],
|
|
3
|
-
['.webm', 'webm'],
|
|
4
|
-
['.mov', 'mov'],
|
|
5
|
-
['.gif', 'gif'],
|
|
6
|
-
]);
|
|
7
|
-
|
|
8
|
-
export function resolveRenderFormat(outputPath, requestedFormat) {
|
|
9
|
-
if (requestedFormat === 'png-sequence') return 'png-sequence';
|
|
10
|
-
const extension = outputPath ? outputPath.slice(outputPath.lastIndexOf('.')).toLowerCase() : undefined;
|
|
11
|
-
const inferred = EXTENSIONS.get(extension);
|
|
12
|
-
const format = requestedFormat ?? inferred ?? 'mp4';
|
|
13
|
-
if (!['mp4', 'webm', 'mov', 'gif', 'png-sequence'].includes(format)) {
|
|
14
|
-
throw new TypeError('Format must be mp4, webm, mov, gif, or png-sequence.');
|
|
15
|
-
}
|
|
16
|
-
if (requestedFormat && inferred && requestedFormat !== inferred) {
|
|
17
|
-
throw new TypeError(`Output extension ${extension} does not match requested ${requestedFormat} format.`);
|
|
18
|
-
}
|
|
19
|
-
if (outputPath && extension && !inferred) throw new TypeError(`Unsupported render output extension: ${extension}`);
|
|
20
|
-
const resolvedExtension = `.${format}`;
|
|
21
|
-
if (outputPath && format !== 'png-sequence' && extension !== resolvedExtension) {
|
|
22
|
-
throw new TypeError(`Render output for ${format} must use the ${resolvedExtension} extension.`);
|
|
23
|
-
}
|
|
24
|
-
return format;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function resolveEncoderArgs(format, options = {}) {
|
|
28
|
-
const encoder = resolveVideoEncoder(format, options);
|
|
29
|
-
if (encoder.codec) return [];
|
|
30
|
-
if (encoder.crf !== undefined) return ['--crf', String(encoder.crf)];
|
|
31
|
-
if (encoder.videoBitrate !== undefined) return ['--video-bitrate', encoder.videoBitrate];
|
|
32
|
-
return [];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function resolveColorProfile(profile, format) {
|
|
36
|
-
if (profile === undefined) return undefined;
|
|
37
|
-
if (profile === 'hdr10' && !['mp4', 'mov'].includes(format)) {
|
|
38
|
-
throw new TypeError(`The hdr10 color profile requires MP4 or MOV output, not ${format}.`);
|
|
39
|
-
}
|
|
40
|
-
if (!['mp4', 'mov', 'webm'].includes(format)) {
|
|
41
|
-
throw new TypeError(`Color profiles are supported for MP4, MOV, and WebM output, not ${format}.`);
|
|
42
|
-
}
|
|
43
|
-
const profiles = {
|
|
44
|
-
'rec709': {
|
|
45
|
-
id: 'rec709',
|
|
46
|
-
filter: 'colorspace=iall=bt709:space=bt709:primaries=bt709:trc=bt709:range=tv',
|
|
47
|
-
colorSpace: 'bt709',
|
|
48
|
-
colorTransfer: 'bt709',
|
|
49
|
-
colorPrimaries: 'bt709',
|
|
50
|
-
colorRange: 'tv',
|
|
51
|
-
requiredFilter: 'colorspace',
|
|
52
|
-
},
|
|
53
|
-
'rec2020-sdr': {
|
|
54
|
-
id: 'rec2020-sdr',
|
|
55
|
-
filter: 'colorspace=iall=bt709:space=bt2020nc:primaries=bt2020:trc=bt709:range=tv',
|
|
56
|
-
colorSpace: 'bt2020nc',
|
|
57
|
-
colorTransfer: 'bt709',
|
|
58
|
-
colorPrimaries: 'bt2020',
|
|
59
|
-
colorRange: 'tv',
|
|
60
|
-
requiredFilter: 'colorspace',
|
|
61
|
-
},
|
|
62
|
-
hdr10: {
|
|
63
|
-
id: 'hdr10',
|
|
64
|
-
filter: 'zscale=pin=bt709:tin=bt709:min=bt709:rin=tv:p=bt2020:t=smpte2084:m=bt2020nc:r=tv:npl=203,format=yuv420p10le',
|
|
65
|
-
colorSpace: 'bt2020nc',
|
|
66
|
-
colorTransfer: 'smpte2084',
|
|
67
|
-
colorPrimaries: 'bt2020',
|
|
68
|
-
colorRange: 'tv',
|
|
69
|
-
pixelFormat: 'yuv420p10le',
|
|
70
|
-
requiredFilter: 'zscale',
|
|
71
|
-
masteringDisplay: 'G(8500,39850)B(6550,2300)R(35400,14600)WP(15635,16450)L(10000000,1)',
|
|
72
|
-
contentLightLevel: '203,203',
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
const resolved = profiles[profile];
|
|
76
|
-
if (!resolved) throw new TypeError(`Unsupported colorProfile ${String(profile)}; choose rec709, rec2020-sdr, or hdr10.`);
|
|
77
|
-
return resolved;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function resolveProfileEncoder(format, encoder, colorProfile) {
|
|
81
|
-
if (colorProfile?.id === 'hdr10') {
|
|
82
|
-
if (encoder.codec && encoder.codecId !== 'h265') {
|
|
83
|
-
throw new TypeError('The hdr10 color profile requires the h265 codec.');
|
|
84
|
-
}
|
|
85
|
-
return encoder.codec ? encoder : { ...encoder, codecId: 'h265', codec: 'libx265' };
|
|
86
|
-
}
|
|
87
|
-
if (encoder.codec) return encoder;
|
|
88
|
-
const codecId = format === 'webm' ? 'vp9' : 'h264';
|
|
89
|
-
const codec = format === 'webm' ? 'libvpx-vp9' : 'libx264';
|
|
90
|
-
return { ...encoder, codecId, codec };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function resolveVideoEncoder(format, options = {}) {
|
|
94
|
-
if (!options || typeof options !== 'object' || Array.isArray(options)) throw new TypeError('Render options must be an object.');
|
|
95
|
-
for (const field of Object.keys(options)) if (!['codec', 'crf', 'videoBitrate'].includes(field)) throw new TypeError(`Unsupported encoder option: ${field}.`);
|
|
96
|
-
if (options.crf !== undefined && options.videoBitrate !== undefined) throw new TypeError('Choose either crf or videoBitrate, not both.');
|
|
97
|
-
|
|
98
|
-
const codecs = format === 'webm'
|
|
99
|
-
? { vp9: 'libvpx-vp9' }
|
|
100
|
-
: format === 'mov'
|
|
101
|
-
? { h264: 'libx264', h265: 'libx265', prores: 'prores_ks' }
|
|
102
|
-
: format === 'mp4'
|
|
103
|
-
? { h264: 'libx264', h265: 'libx265' }
|
|
104
|
-
: {};
|
|
105
|
-
if (options.codec !== undefined && (typeof options.codec !== 'string' || codecs[options.codec] === undefined)) {
|
|
106
|
-
throw new TypeError(`Render codec is unsupported for ${format}; choose one of: ${Object.keys(codecs).join(', ') || 'none'}.`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (options.crf !== undefined) {
|
|
110
|
-
if (!['mp4', 'mov', 'webm'].includes(format)) throw new TypeError(`CRF control is not supported for ${format} output.`);
|
|
111
|
-
const maximum = options.codec === 'vp9' || (format === 'webm' && options.codec === undefined) ? 63 : 51;
|
|
112
|
-
if (options.codec === 'prores'
|
|
113
|
-
|| !Number.isSafeInteger(options.crf) || options.crf < 0 || options.crf > maximum) {
|
|
114
|
-
throw new TypeError(`Render crf must be an integer from 0 to ${maximum} for ${options.codec ?? format}.`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
if (options.videoBitrate !== undefined && (!['mp4', 'mov', 'webm'].includes(format)
|
|
118
|
-
|| options.codec === 'prores'
|
|
119
|
-
|| typeof options.videoBitrate !== 'string'
|
|
120
|
-
|| !/^(?:[1-9]\d{0,5})(?:\.\d{1,2})?[KMG]?$/.test(options.videoBitrate))) {
|
|
121
|
-
throw new TypeError('Render videoBitrate must be a positive bitrate such as 800K, 5M, or 1.5M on a bitrate-based encoder.');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
...(options.codec ? { codec: codecs[options.codec], codecId: options.codec } : {}),
|
|
126
|
-
...(options.crf !== undefined ? { crf: options.crf } : {}),
|
|
127
|
-
...(options.videoBitrate !== undefined ? { videoBitrate: options.videoBitrate } : {}),
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function resolveRenderControlArgs(options = {}) {
|
|
132
|
-
if (!options || typeof options !== 'object' || Array.isArray(options)) throw new TypeError('Render options must be an object.');
|
|
133
|
-
for (const field of Object.keys(options)) {
|
|
134
|
-
if (!['format', 'encoder', 'colorProfile', 'workers', 'lowMemoryMode', 'timeoutSeconds', 'resumeCheckpoint', 'checkpointMaxBytes', 'checkpointMaxAgeHours'].includes(field)) throw new TypeError(`Unsupported render option: ${field}.`);
|
|
135
|
-
}
|
|
136
|
-
if (options.workers !== undefined
|
|
137
|
-
&& options.workers !== 'auto'
|
|
138
|
-
&& (!Number.isSafeInteger(options.workers) || options.workers < 1 || options.workers > 64)) {
|
|
139
|
-
throw new TypeError('Render workers must be auto or an integer from 1 to 64.');
|
|
140
|
-
}
|
|
141
|
-
if (options.lowMemoryMode !== undefined && typeof options.lowMemoryMode !== 'boolean') {
|
|
142
|
-
throw new TypeError('Render lowMemoryMode must be a boolean.');
|
|
143
|
-
}
|
|
144
|
-
if (options.timeoutSeconds !== undefined
|
|
145
|
-
&& (!Number.isSafeInteger(options.timeoutSeconds) || options.timeoutSeconds < 1 || options.timeoutSeconds > 86_400)) {
|
|
146
|
-
throw new TypeError('Render timeoutSeconds must be an integer from 1 to 86400.');
|
|
147
|
-
}
|
|
148
|
-
if (options.resumeCheckpoint !== undefined && typeof options.resumeCheckpoint !== 'boolean') {
|
|
149
|
-
throw new TypeError('Render resumeCheckpoint must be a boolean.');
|
|
150
|
-
}
|
|
151
|
-
if (options.checkpointMaxBytes !== undefined
|
|
152
|
-
&& (!Number.isSafeInteger(options.checkpointMaxBytes) || options.checkpointMaxBytes < 1024 * 1024 || options.checkpointMaxBytes > 100 * 1024 ** 3)) {
|
|
153
|
-
throw new TypeError('checkpointMaxBytes must be an integer from 1048576 to 107374182400.');
|
|
154
|
-
}
|
|
155
|
-
if (options.checkpointMaxAgeHours !== undefined
|
|
156
|
-
&& (!Number.isSafeInteger(options.checkpointMaxAgeHours) || options.checkpointMaxAgeHours < 1 || options.checkpointMaxAgeHours > 720)) {
|
|
157
|
-
throw new TypeError('checkpointMaxAgeHours must be an integer from 1 to 720.');
|
|
158
|
-
}
|
|
159
|
-
return [
|
|
160
|
-
...(options.workers !== undefined ? ['--workers', String(options.workers)] : []),
|
|
161
|
-
...(options.lowMemoryMode === true ? ['--low-memory-mode'] : options.lowMemoryMode === false ? ['--no-low-memory-mode'] : []),
|
|
162
|
-
];
|
|
163
|
-
}
|
|
1
|
+
const EXTENSIONS = new Map([
|
|
2
|
+
['.mp4', 'mp4'],
|
|
3
|
+
['.webm', 'webm'],
|
|
4
|
+
['.mov', 'mov'],
|
|
5
|
+
['.gif', 'gif'],
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
export function resolveRenderFormat(outputPath, requestedFormat) {
|
|
9
|
+
if (requestedFormat === 'png-sequence') return 'png-sequence';
|
|
10
|
+
const extension = outputPath ? outputPath.slice(outputPath.lastIndexOf('.')).toLowerCase() : undefined;
|
|
11
|
+
const inferred = EXTENSIONS.get(extension);
|
|
12
|
+
const format = requestedFormat ?? inferred ?? 'mp4';
|
|
13
|
+
if (!['mp4', 'webm', 'mov', 'gif', 'png-sequence'].includes(format)) {
|
|
14
|
+
throw new TypeError('Format must be mp4, webm, mov, gif, or png-sequence.');
|
|
15
|
+
}
|
|
16
|
+
if (requestedFormat && inferred && requestedFormat !== inferred) {
|
|
17
|
+
throw new TypeError(`Output extension ${extension} does not match requested ${requestedFormat} format.`);
|
|
18
|
+
}
|
|
19
|
+
if (outputPath && extension && !inferred) throw new TypeError(`Unsupported render output extension: ${extension}`);
|
|
20
|
+
const resolvedExtension = `.${format}`;
|
|
21
|
+
if (outputPath && format !== 'png-sequence' && extension !== resolvedExtension) {
|
|
22
|
+
throw new TypeError(`Render output for ${format} must use the ${resolvedExtension} extension.`);
|
|
23
|
+
}
|
|
24
|
+
return format;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveEncoderArgs(format, options = {}) {
|
|
28
|
+
const encoder = resolveVideoEncoder(format, options);
|
|
29
|
+
if (encoder.codec) return [];
|
|
30
|
+
if (encoder.crf !== undefined) return ['--crf', String(encoder.crf)];
|
|
31
|
+
if (encoder.videoBitrate !== undefined) return ['--video-bitrate', encoder.videoBitrate];
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveColorProfile(profile, format) {
|
|
36
|
+
if (profile === undefined) return undefined;
|
|
37
|
+
if (profile === 'hdr10' && !['mp4', 'mov'].includes(format)) {
|
|
38
|
+
throw new TypeError(`The hdr10 color profile requires MP4 or MOV output, not ${format}.`);
|
|
39
|
+
}
|
|
40
|
+
if (!['mp4', 'mov', 'webm'].includes(format)) {
|
|
41
|
+
throw new TypeError(`Color profiles are supported for MP4, MOV, and WebM output, not ${format}.`);
|
|
42
|
+
}
|
|
43
|
+
const profiles = {
|
|
44
|
+
'rec709': {
|
|
45
|
+
id: 'rec709',
|
|
46
|
+
filter: 'colorspace=iall=bt709:space=bt709:primaries=bt709:trc=bt709:range=tv',
|
|
47
|
+
colorSpace: 'bt709',
|
|
48
|
+
colorTransfer: 'bt709',
|
|
49
|
+
colorPrimaries: 'bt709',
|
|
50
|
+
colorRange: 'tv',
|
|
51
|
+
requiredFilter: 'colorspace',
|
|
52
|
+
},
|
|
53
|
+
'rec2020-sdr': {
|
|
54
|
+
id: 'rec2020-sdr',
|
|
55
|
+
filter: 'colorspace=iall=bt709:space=bt2020nc:primaries=bt2020:trc=bt709:range=tv',
|
|
56
|
+
colorSpace: 'bt2020nc',
|
|
57
|
+
colorTransfer: 'bt709',
|
|
58
|
+
colorPrimaries: 'bt2020',
|
|
59
|
+
colorRange: 'tv',
|
|
60
|
+
requiredFilter: 'colorspace',
|
|
61
|
+
},
|
|
62
|
+
hdr10: {
|
|
63
|
+
id: 'hdr10',
|
|
64
|
+
filter: 'zscale=pin=bt709:tin=bt709:min=bt709:rin=tv:p=bt2020:t=smpte2084:m=bt2020nc:r=tv:npl=203,format=yuv420p10le',
|
|
65
|
+
colorSpace: 'bt2020nc',
|
|
66
|
+
colorTransfer: 'smpte2084',
|
|
67
|
+
colorPrimaries: 'bt2020',
|
|
68
|
+
colorRange: 'tv',
|
|
69
|
+
pixelFormat: 'yuv420p10le',
|
|
70
|
+
requiredFilter: 'zscale',
|
|
71
|
+
masteringDisplay: 'G(8500,39850)B(6550,2300)R(35400,14600)WP(15635,16450)L(10000000,1)',
|
|
72
|
+
contentLightLevel: '203,203',
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
const resolved = profiles[profile];
|
|
76
|
+
if (!resolved) throw new TypeError(`Unsupported colorProfile ${String(profile)}; choose rec709, rec2020-sdr, or hdr10.`);
|
|
77
|
+
return resolved;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function resolveProfileEncoder(format, encoder, colorProfile) {
|
|
81
|
+
if (colorProfile?.id === 'hdr10') {
|
|
82
|
+
if (encoder.codec && encoder.codecId !== 'h265') {
|
|
83
|
+
throw new TypeError('The hdr10 color profile requires the h265 codec.');
|
|
84
|
+
}
|
|
85
|
+
return encoder.codec ? encoder : { ...encoder, codecId: 'h265', codec: 'libx265' };
|
|
86
|
+
}
|
|
87
|
+
if (encoder.codec) return encoder;
|
|
88
|
+
const codecId = format === 'webm' ? 'vp9' : 'h264';
|
|
89
|
+
const codec = format === 'webm' ? 'libvpx-vp9' : 'libx264';
|
|
90
|
+
return { ...encoder, codecId, codec };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function resolveVideoEncoder(format, options = {}) {
|
|
94
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) throw new TypeError('Render options must be an object.');
|
|
95
|
+
for (const field of Object.keys(options)) if (!['codec', 'crf', 'videoBitrate'].includes(field)) throw new TypeError(`Unsupported encoder option: ${field}.`);
|
|
96
|
+
if (options.crf !== undefined && options.videoBitrate !== undefined) throw new TypeError('Choose either crf or videoBitrate, not both.');
|
|
97
|
+
|
|
98
|
+
const codecs = format === 'webm'
|
|
99
|
+
? { vp9: 'libvpx-vp9' }
|
|
100
|
+
: format === 'mov'
|
|
101
|
+
? { h264: 'libx264', h265: 'libx265', prores: 'prores_ks' }
|
|
102
|
+
: format === 'mp4'
|
|
103
|
+
? { h264: 'libx264', h265: 'libx265' }
|
|
104
|
+
: {};
|
|
105
|
+
if (options.codec !== undefined && (typeof options.codec !== 'string' || codecs[options.codec] === undefined)) {
|
|
106
|
+
throw new TypeError(`Render codec is unsupported for ${format}; choose one of: ${Object.keys(codecs).join(', ') || 'none'}.`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (options.crf !== undefined) {
|
|
110
|
+
if (!['mp4', 'mov', 'webm'].includes(format)) throw new TypeError(`CRF control is not supported for ${format} output.`);
|
|
111
|
+
const maximum = options.codec === 'vp9' || (format === 'webm' && options.codec === undefined) ? 63 : 51;
|
|
112
|
+
if (options.codec === 'prores'
|
|
113
|
+
|| !Number.isSafeInteger(options.crf) || options.crf < 0 || options.crf > maximum) {
|
|
114
|
+
throw new TypeError(`Render crf must be an integer from 0 to ${maximum} for ${options.codec ?? format}.`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (options.videoBitrate !== undefined && (!['mp4', 'mov', 'webm'].includes(format)
|
|
118
|
+
|| options.codec === 'prores'
|
|
119
|
+
|| typeof options.videoBitrate !== 'string'
|
|
120
|
+
|| !/^(?:[1-9]\d{0,5})(?:\.\d{1,2})?[KMG]?$/.test(options.videoBitrate))) {
|
|
121
|
+
throw new TypeError('Render videoBitrate must be a positive bitrate such as 800K, 5M, or 1.5M on a bitrate-based encoder.');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
...(options.codec ? { codec: codecs[options.codec], codecId: options.codec } : {}),
|
|
126
|
+
...(options.crf !== undefined ? { crf: options.crf } : {}),
|
|
127
|
+
...(options.videoBitrate !== undefined ? { videoBitrate: options.videoBitrate } : {}),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function resolveRenderControlArgs(options = {}) {
|
|
132
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) throw new TypeError('Render options must be an object.');
|
|
133
|
+
for (const field of Object.keys(options)) {
|
|
134
|
+
if (!['format', 'encoder', 'colorProfile', 'workers', 'lowMemoryMode', 'timeoutSeconds', 'resumeCheckpoint', 'checkpointMaxBytes', 'checkpointMaxAgeHours'].includes(field)) throw new TypeError(`Unsupported render option: ${field}.`);
|
|
135
|
+
}
|
|
136
|
+
if (options.workers !== undefined
|
|
137
|
+
&& options.workers !== 'auto'
|
|
138
|
+
&& (!Number.isSafeInteger(options.workers) || options.workers < 1 || options.workers > 64)) {
|
|
139
|
+
throw new TypeError('Render workers must be auto or an integer from 1 to 64.');
|
|
140
|
+
}
|
|
141
|
+
if (options.lowMemoryMode !== undefined && typeof options.lowMemoryMode !== 'boolean') {
|
|
142
|
+
throw new TypeError('Render lowMemoryMode must be a boolean.');
|
|
143
|
+
}
|
|
144
|
+
if (options.timeoutSeconds !== undefined
|
|
145
|
+
&& (!Number.isSafeInteger(options.timeoutSeconds) || options.timeoutSeconds < 1 || options.timeoutSeconds > 86_400)) {
|
|
146
|
+
throw new TypeError('Render timeoutSeconds must be an integer from 1 to 86400.');
|
|
147
|
+
}
|
|
148
|
+
if (options.resumeCheckpoint !== undefined && typeof options.resumeCheckpoint !== 'boolean') {
|
|
149
|
+
throw new TypeError('Render resumeCheckpoint must be a boolean.');
|
|
150
|
+
}
|
|
151
|
+
if (options.checkpointMaxBytes !== undefined
|
|
152
|
+
&& (!Number.isSafeInteger(options.checkpointMaxBytes) || options.checkpointMaxBytes < 1024 * 1024 || options.checkpointMaxBytes > 100 * 1024 ** 3)) {
|
|
153
|
+
throw new TypeError('checkpointMaxBytes must be an integer from 1048576 to 107374182400.');
|
|
154
|
+
}
|
|
155
|
+
if (options.checkpointMaxAgeHours !== undefined
|
|
156
|
+
&& (!Number.isSafeInteger(options.checkpointMaxAgeHours) || options.checkpointMaxAgeHours < 1 || options.checkpointMaxAgeHours > 720)) {
|
|
157
|
+
throw new TypeError('checkpointMaxAgeHours must be an integer from 1 to 720.');
|
|
158
|
+
}
|
|
159
|
+
return [
|
|
160
|
+
...(options.workers !== undefined ? ['--workers', String(options.workers)] : []),
|
|
161
|
+
...(options.lowMemoryMode === true ? ['--low-memory-mode'] : options.lowMemoryMode === false ? ['--no-low-memory-mode'] : []),
|
|
162
|
+
];
|
|
163
|
+
}
|