bytecut 0.0.0-stage → 0.1.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.
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/core/video/codecs.ts
4
+ import { open } from "fs/promises";
5
+ function* boxes(buf, start, end) {
6
+ let at = start;
7
+ while (at + 8 <= end) {
8
+ let size = buf.readUInt32BE(at);
9
+ const type = buf.toString("latin1", at + 4, at + 8);
10
+ let header = 8;
11
+ if (size === 1) {
12
+ if (at + 16 > end) return;
13
+ size = Number(buf.readBigUInt64BE(at + 8));
14
+ header = 16;
15
+ } else if (size === 0) {
16
+ size = end - at;
17
+ }
18
+ if (size < header || at + size > end) return;
19
+ yield { type, start: at + header, end: at + size };
20
+ at += size;
21
+ }
22
+ }
23
+ function find(buf, parent, type) {
24
+ for (const box of boxes(buf, parent.start, parent.end)) if (box.type === type) return box;
25
+ return void 0;
26
+ }
27
+ var hex2 = (n) => n.toString(16).padStart(2, "0");
28
+ function avcCodecString(avcC) {
29
+ if (avcC.length < 4) return null;
30
+ return `avc1.${hex2(avcC[1])}${hex2(avcC[2])}${hex2(avcC[3])}`;
31
+ }
32
+ function av1CodecString(av1C) {
33
+ if (av1C.length < 4 || (av1C[0] & 128) === 0) return null;
34
+ const profile = av1C[1] >> 5;
35
+ const level = av1C[1] & 31;
36
+ const tier = av1C[2] >> 7 & 1;
37
+ const highBitdepth = av1C[2] >> 6 & 1;
38
+ const twelveBit = av1C[2] >> 5 & 1;
39
+ const depth = highBitdepth ? twelveBit ? 12 : 10 : 8;
40
+ return `av01.${profile}.${String(level).padStart(2, "0")}${tier ? "H" : "M"}.${String(depth).padStart(2, "0")}`;
41
+ }
42
+ async function readVideoCodecString(path) {
43
+ const file = await open(path, "r");
44
+ try {
45
+ const { size } = await file.stat();
46
+ const header = Buffer.alloc(16);
47
+ let offset = 0;
48
+ for (let guard = 0; guard < 64 && offset + 8 <= size; guard++) {
49
+ const { bytesRead } = await file.read(header, 0, 16, offset);
50
+ if (bytesRead < 8) return null;
51
+ let boxSize = header.readUInt32BE(0);
52
+ const type = header.toString("latin1", 4, 8);
53
+ let headerSize = 8;
54
+ if (boxSize === 1) {
55
+ if (bytesRead < 16) return null;
56
+ boxSize = Number(header.readBigUInt64BE(8));
57
+ headerSize = 16;
58
+ } else if (boxSize === 0) {
59
+ boxSize = size - offset;
60
+ }
61
+ if (boxSize < headerSize) return null;
62
+ if (type === "moov") {
63
+ const payload = Buffer.alloc(boxSize - headerSize);
64
+ await file.read(payload, 0, payload.length, offset + headerSize);
65
+ return codecFromMoov(payload);
66
+ }
67
+ offset += boxSize;
68
+ }
69
+ return null;
70
+ } finally {
71
+ await file.close();
72
+ }
73
+ }
74
+ function codecFromMoov(moov) {
75
+ const root = { start: 0, end: moov.length };
76
+ for (const trak of boxes(moov, root.start, root.end)) {
77
+ if (trak.type !== "trak") continue;
78
+ const mdia = find(moov, trak, "mdia");
79
+ if (!mdia) continue;
80
+ const hdlr = find(moov, mdia, "hdlr");
81
+ if (!hdlr || moov.toString("latin1", hdlr.start + 8, hdlr.start + 12) !== "vide") continue;
82
+ const minf = find(moov, mdia, "minf");
83
+ const stbl = minf && find(moov, minf, "stbl");
84
+ const stsd = stbl && find(moov, stbl, "stsd");
85
+ if (!stsd) continue;
86
+ for (const entry of boxes(moov, stsd.start + 8, stsd.end)) {
87
+ const childStart = entry.start + 78;
88
+ if (entry.type === "avc1" || entry.type === "avc3") {
89
+ const avcC = find(moov, { start: childStart, end: entry.end }, "avcC");
90
+ if (avcC) return avcCodecString(moov.subarray(avcC.start, avcC.end));
91
+ }
92
+ if (entry.type === "av01") {
93
+ const av1C = find(moov, { start: childStart, end: entry.end }, "av1C");
94
+ if (av1C) return av1CodecString(moov.subarray(av1C.start, av1C.end));
95
+ }
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+ function fallbackAv1CodecString(width, height) {
101
+ const pixels = width * height;
102
+ const level = pixels <= 147456 ? 0 : pixels <= 278784 ? 1 : pixels <= 665856 ? 4 : pixels <= 1065024 ? 5 : pixels <= 2359296 ? 8 : pixels <= 8912896 ? 12 : 16;
103
+ return `av01.0.${String(level).padStart(2, "0")}M.08`;
104
+ }
105
+
106
+ export {
107
+ readVideoCodecString,
108
+ fallbackAv1CodecString
109
+ };
@@ -0,0 +1,490 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ BytecutError,
4
+ detectProject,
5
+ hint,
6
+ matchesGlob
7
+ } from "./chunk-FY53B4NB.js";
8
+
9
+ // src/core/presets/photo.ts
10
+ var OTHER_ENCODERS = {
11
+ // PNG has no quality; `effort` is libpng's/oxipng-style effort (1–10).
12
+ png: { quality: 100, effort: 7 }
13
+ };
14
+ var PHOTO_PRESET = {
15
+ name: "photo",
16
+ widths: [],
17
+ maxEdge: 1920,
18
+ formats: ["jpg"],
19
+ encoders: {
20
+ ...OTHER_ENCODERS,
21
+ jpg: { quality: 80, effort: 0 },
22
+ avif: { quality: 60, effort: 4 },
23
+ webp: { quality: 78, effort: 4 }
24
+ }
25
+ };
26
+
27
+ // src/core/presets/hero.ts
28
+ var HERO_PRESET = {
29
+ name: "hero",
30
+ widths: [],
31
+ maxEdge: 1920,
32
+ formats: ["jpg"],
33
+ encoders: {
34
+ ...OTHER_ENCODERS,
35
+ jpg: { quality: 85, effort: 0 },
36
+ avif: { quality: 65, effort: 4 },
37
+ webp: { quality: 82, effort: 4 }
38
+ },
39
+ priority: true
40
+ };
41
+
42
+ // src/core/presets/icon.ts
43
+ var ICON_PRESET = {
44
+ name: "icon",
45
+ widths: [64, 128, 256, 512],
46
+ formats: ["png"],
47
+ encoders: {
48
+ ...OTHER_ENCODERS,
49
+ jpg: { quality: 90, effort: 0 },
50
+ avif: { quality: 80, effort: 4 },
51
+ webp: { quality: 90, effort: 4 }
52
+ },
53
+ lossless: true
54
+ };
55
+
56
+ // src/core/presets/index.ts
57
+ var IMAGE_PRESETS = {
58
+ photo: PHOTO_PRESET,
59
+ hero: HERO_PRESET,
60
+ icon: ICON_PRESET
61
+ };
62
+ function getImagePreset(name, catalog) {
63
+ const presets = catalog?.image ?? IMAGE_PRESETS;
64
+ const preset = presets[name];
65
+ if (!preset) {
66
+ throw new BytecutError(
67
+ "BAD_ARGUMENTS",
68
+ `Unknown preset "${name}". Image presets: ${Object.keys(presets).join(", ")}.`
69
+ );
70
+ }
71
+ return preset;
72
+ }
73
+
74
+ // src/core/presets/video.ts
75
+ var POSTER = { quality: 55, effort: 4, at: 0.1 };
76
+ var BACKGROUND_VIDEO_PRESET = {
77
+ name: "background-video",
78
+ codecs: ["av1", "h264"],
79
+ audio: "strip",
80
+ audioBitrate: "96k",
81
+ maxWidth: 1920,
82
+ maxFps: 30,
83
+ crf: { av1: 38, h264: 28 },
84
+ speed: { av1: "6", h264: "slow" },
85
+ poster: POSTER
86
+ };
87
+ var CONTENT_VIDEO_PRESET = {
88
+ name: "content-video",
89
+ codecs: ["av1", "h264"],
90
+ audio: "keep",
91
+ audioBitrate: "128k",
92
+ maxWidth: null,
93
+ maxFps: null,
94
+ crf: { av1: 32, h264: 23 },
95
+ speed: { av1: "6", h264: "slow" },
96
+ poster: POSTER
97
+ };
98
+ var GIF_TO_VIDEO_PRESET = {
99
+ name: "gif-to-video",
100
+ codecs: ["av1", "h264"],
101
+ audio: "strip",
102
+ audioBitrate: "96k",
103
+ maxWidth: 1920,
104
+ maxFps: null,
105
+ crf: { av1: 36, h264: 26 },
106
+ speed: { av1: "6", h264: "slow" },
107
+ poster: POSTER
108
+ };
109
+ var VIDEO_PRESETS = {
110
+ "background-video": BACKGROUND_VIDEO_PRESET,
111
+ "content-video": CONTENT_VIDEO_PRESET,
112
+ "gif-to-video": GIF_TO_VIDEO_PRESET
113
+ };
114
+ function getVideoPreset(name, catalog) {
115
+ return (catalog?.video ?? VIDEO_PRESETS)[name];
116
+ }
117
+
118
+ // src/core/presets/catalog.ts
119
+ var BUILTIN_CATALOG = {
120
+ image: IMAGE_PRESETS,
121
+ video: VIDEO_PRESETS,
122
+ root: Object.fromEntries(
123
+ [...Object.keys(IMAGE_PRESETS), ...Object.keys(VIDEO_PRESETS)].map((n) => [n, n])
124
+ ),
125
+ origin: {}
126
+ };
127
+ function rootPreset(catalog, name) {
128
+ return catalog?.root[name] ?? name;
129
+ }
130
+ var BUILTIN_POLICY = {
131
+ catalog: BUILTIN_CATALOG,
132
+ assetFor: () => void 0
133
+ };
134
+
135
+ // src/core/config/presets.ts
136
+ var IMAGE_TARGETS = {
137
+ low: { jpg: 65, avif: 45, webp: 65 },
138
+ balanced: { jpg: 80, avif: 60, webp: 78 },
139
+ high: { jpg: 90, avif: 75, webp: 88 }
140
+ };
141
+ var VIDEO_TARGET_CRF_OFFSET = { low: 4, balanced: 0, high: -4 };
142
+ var IMAGE_ONLY = [
143
+ "widths",
144
+ "maxEdge",
145
+ "formats",
146
+ "quality",
147
+ "effort",
148
+ "lossless",
149
+ "priority",
150
+ "floor",
151
+ "maxSize"
152
+ ];
153
+ var VIDEO_ONLY = ["audio", "maxWidth", "maxFps", "codecs", "crf"];
154
+ var METRICS = ["ssimulacra2", "vmaf"];
155
+ function fail(message) {
156
+ throw new BytecutError("CONFIG_INVALID", `presets: ${message}`);
157
+ }
158
+ function applyImage(name, base, def) {
159
+ for (const key of VIDEO_ONLY) {
160
+ if (def[key] !== void 0) {
161
+ fail(`"${name}" extends an image preset, so \`${key}\` (a video setting) does not apply.`);
162
+ }
163
+ }
164
+ if (def.vmaf !== void 0) {
165
+ fail(`"${name}" extends an image preset, so \`vmaf\` (a video metric) does not apply.`);
166
+ }
167
+ const next = {
168
+ ...base,
169
+ name,
170
+ widths: [...base.widths],
171
+ formats: [...base.formats],
172
+ encoders: {
173
+ jpg: { ...base.encoders.jpg },
174
+ png: { ...base.encoders.png },
175
+ avif: { ...base.encoders.avif },
176
+ webp: { ...base.encoders.webp }
177
+ }
178
+ };
179
+ if (def.target === "lossless") next.lossless = true;
180
+ else if (def.target) {
181
+ next.encoders.jpg.quality = IMAGE_TARGETS[def.target].jpg;
182
+ next.encoders.avif.quality = IMAGE_TARGETS[def.target].avif;
183
+ next.encoders.webp.quality = IMAGE_TARGETS[def.target].webp;
184
+ }
185
+ if (def.widths) {
186
+ next.widths = [...new Set(def.widths)].sort((a, b) => a - b);
187
+ delete next.maxEdge;
188
+ }
189
+ if (def.maxEdge !== void 0) {
190
+ next.maxEdge = def.maxEdge;
191
+ next.widths = [];
192
+ }
193
+ if (def.formats) next.formats = [...new Set(def.formats)];
194
+ if (def.quality?.jpg !== void 0) next.encoders.jpg.quality = def.quality.jpg;
195
+ if (def.quality?.avif !== void 0) next.encoders.avif.quality = def.quality.avif;
196
+ if (def.quality?.webp !== void 0) next.encoders.webp.quality = def.quality.webp;
197
+ if (def.effort?.avif !== void 0) next.encoders.avif.effort = def.effort.avif;
198
+ if (def.effort?.webp !== void 0) next.encoders.webp.effort = def.effort.webp;
199
+ if (def.lossless !== void 0) next.lossless = def.lossless;
200
+ if (def.priority !== void 0) next.priority = def.priority;
201
+ if (def.ssimulacra2 !== void 0) {
202
+ next.search = {
203
+ metric: "ssimulacra2",
204
+ target: def.ssimulacra2,
205
+ ...def.floor !== void 0 ? { floor: def.floor } : {},
206
+ ...def.maxSize !== void 0 ? { maxBytes: def.maxSize } : {}
207
+ };
208
+ }
209
+ return next;
210
+ }
211
+ function applyVideo(name, base, def) {
212
+ for (const key of IMAGE_ONLY) {
213
+ if (def[key] !== void 0) {
214
+ fail(`"${name}" extends a video preset, so \`${key}\` (an image setting) does not apply.`);
215
+ }
216
+ }
217
+ if (def.target === "lossless") {
218
+ fail(`"${name}": \`target: "lossless"\` is not available for video; use "high".`);
219
+ }
220
+ const next = {
221
+ ...base,
222
+ name,
223
+ codecs: [...base.codecs],
224
+ crf: { ...base.crf },
225
+ speed: { ...base.speed },
226
+ poster: { ...base.poster }
227
+ };
228
+ if (def.target) {
229
+ const offset = VIDEO_TARGET_CRF_OFFSET[def.target];
230
+ next.crf = { av1: base.crf.av1 + offset, h264: base.crf.h264 + offset };
231
+ }
232
+ if (def.crf?.av1 !== void 0) next.crf.av1 = def.crf.av1;
233
+ if (def.crf?.h264 !== void 0) next.crf.h264 = def.crf.h264;
234
+ if (def.codecs) next.codecs = [...new Set(def.codecs)];
235
+ if (def.audio) next.audio = def.audio;
236
+ if (def.maxWidth !== void 0) next.maxWidth = def.maxWidth;
237
+ if (def.maxFps !== void 0) next.maxFps = def.maxFps;
238
+ if (def.ssimulacra2 !== void 0)
239
+ next.search = { metric: "ssimulacra2", target: def.ssimulacra2 };
240
+ if (def.vmaf !== void 0) next.search = { metric: "vmaf", target: def.vmaf };
241
+ return next;
242
+ }
243
+ var CHANGE_KEYS = [...IMAGE_ONLY, ...VIDEO_ONLY, ...METRICS, "target"];
244
+ function buildCatalog(defs) {
245
+ const image = { ...BUILTIN_CATALOG.image };
246
+ const video = { ...BUILTIN_CATALOG.video };
247
+ const root = { ...BUILTIN_CATALOG.root };
248
+ const origin = {};
249
+ if (!defs) return { image, video, root, origin };
250
+ const builtinNames = new Set(Object.keys(BUILTIN_CATALOG.root));
251
+ const done = /* @__PURE__ */ new Set();
252
+ function resolve(name, stack) {
253
+ if (done.has(name)) return;
254
+ const def = defs[name];
255
+ if (!def) return;
256
+ if (stack.includes(name)) fail(`extends cycle: ${[...stack, name].join(" \u2192 ")}.`);
257
+ const parent = def.extends ?? (builtinNames.has(name) ? name : void 0);
258
+ if (parent === void 0) {
259
+ fail(
260
+ `"${name}" must say what it extends, e.g. { "extends": "photo" }. Built-ins: ${[...builtinNames].join(", ")}.`
261
+ );
262
+ }
263
+ if (parent === "svg") fail(`"${name}" extends "svg", which has no output yet.`);
264
+ if (parent === name) {
265
+ if (!builtinNames.has(name)) fail(`"${name}" extends itself.`);
266
+ } else if (defs[parent]) {
267
+ resolve(parent, [...stack, name]);
268
+ } else if (!builtinNames.has(parent)) {
269
+ const known = [...builtinNames, ...Object.keys(defs)];
270
+ fail(`"${name}" extends unknown preset "${parent}"${hint(parent, known)}.`);
271
+ }
272
+ const baseName = parent === name ? name : parent;
273
+ const chain = [];
274
+ if (parent === name) chain.push(name);
275
+ else chain.push(...origin[parent]?.chain ?? [parent], name);
276
+ if (image[baseName]) {
277
+ image[name] = applyImage(name, image[baseName], def);
278
+ } else if (video[baseName]) {
279
+ video[name] = applyVideo(name, video[baseName], def);
280
+ } else {
281
+ fail(`"${name}" extends "${baseName}", which is not an image or video preset.`);
282
+ }
283
+ root[name] = parent === name ? name : root[parent] ?? parent;
284
+ origin[name] = {
285
+ chain,
286
+ overrides: CHANGE_KEYS.filter((key) => def[key] !== void 0)
287
+ };
288
+ done.add(name);
289
+ }
290
+ for (const name of Object.keys(defs)) resolve(name, []);
291
+ return { image, video, root, origin };
292
+ }
293
+
294
+ // src/core/config/resolve.ts
295
+ function resolveConfigData(config, project, loaded) {
296
+ const name = loaded?.name ?? "config";
297
+ const catalog = buildCatalog(config.presets);
298
+ const assets = Object.entries(config.assets ?? {}).map(([pattern, preset]) => ({
299
+ pattern,
300
+ preset
301
+ }));
302
+ for (const { pattern, preset } of assets) {
303
+ if (!catalog.image[preset] && !catalog.video[preset]) {
304
+ const known = [...Object.keys(catalog.image), ...Object.keys(catalog.video)];
305
+ throw new BytecutError(
306
+ "CONFIG_INVALID",
307
+ `${name}: assets["${pattern}"]: unknown preset "${preset}"${hint(preset, known)}. Known presets: ${known.join(", ")}.`
308
+ );
309
+ }
310
+ }
311
+ const rules = {};
312
+ for (const [id, setting] of Object.entries(config.audit?.rules ?? {})) {
313
+ rules[id] = setting;
314
+ }
315
+ const budgets = {};
316
+ if (config.budgets?.maxFileSize !== void 0) budgets.maxFileBytes = config.budgets.maxFileSize;
317
+ if (config.budgets?.maxTotalSize !== void 0)
318
+ budgets.maxTotalBytes = config.budgets.maxTotalSize;
319
+ return {
320
+ loaded,
321
+ project: {
322
+ ...project,
323
+ ...config.include ? { include: config.include } : {},
324
+ ...config.outDir ? { outDir: config.outDir } : {},
325
+ ...config.publicPath ? { publicPath: config.publicPath } : {}
326
+ },
327
+ provenance: {
328
+ framework: config.framework ? "config" : "detected",
329
+ include: config.include ? "config" : "detected",
330
+ outDir: config.outDir ? "config" : "detected",
331
+ publicPath: config.publicPath ? "config" : "detected"
332
+ },
333
+ exclude: config.exclude ?? [],
334
+ catalog,
335
+ policy: {
336
+ catalog,
337
+ assetFor: (source) => assets.find((rule) => matchesGlob(rule.pattern, source))
338
+ },
339
+ assets,
340
+ rules,
341
+ auditIgnore: config.audit?.ignore ?? [],
342
+ budgets,
343
+ placeholder: config.manifest?.placeholder,
344
+ concurrency: config.concurrency,
345
+ ffmpegPath: config.ffmpegPath,
346
+ ffprobePath: config.ffprobePath,
347
+ ssimulacra2Path: config.ssimulacra2Path
348
+ };
349
+ }
350
+ async function resolveConfig(projectRoot, loaded) {
351
+ const framework = loaded?.config.framework;
352
+ const project = await detectProject(projectRoot, { framework });
353
+ return resolveConfigData(loaded?.config ?? {}, project, loaded);
354
+ }
355
+
356
+ // src/core/ffmpeg/resolve.ts
357
+ import { delimiter, dirname, join } from "path";
358
+ var ENV_VAR = {
359
+ ffmpeg: "BYTECUT_FFMPEG",
360
+ ffprobe: "BYTECUT_FFPROBE",
361
+ ssimulacra2: "BYTECUT_SSIMULACRA2"
362
+ };
363
+ function executableNames(name, platform, env) {
364
+ if (platform !== "win32") return [name];
365
+ const exts = (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean);
366
+ return [name, ...exts.map((ext) => name + ext.toLowerCase())];
367
+ }
368
+ function resolveBinary(name, options, deps) {
369
+ const explicit = [
370
+ ["flag", options.flag],
371
+ ["env", options.env[ENV_VAR[name]]],
372
+ ["config", options.config]
373
+ ];
374
+ for (const [source, value] of explicit) {
375
+ if (value === void 0 || value === "") continue;
376
+ return deps.isFile(value) ? { found: true, path: value, source } : { found: false, source, attempted: value };
377
+ }
378
+ const names = executableNames(name, options.platform, options.env);
379
+ if (name === "ffprobe" && options.ffmpegDir) {
380
+ for (const candidate of names) {
381
+ const path = join(options.ffmpegDir, candidate);
382
+ if (deps.isFile(path)) return { found: true, path, source: "ffmpeg-dir" };
383
+ }
384
+ }
385
+ const dirs = (options.env.PATH ?? "").split(delimiter).filter(Boolean);
386
+ for (const dir of dirs) {
387
+ for (const candidate of names) {
388
+ const path = join(dir, candidate);
389
+ if (deps.isFile(path)) return { found: true, path, source: "path" };
390
+ }
391
+ }
392
+ return { found: false, source: null };
393
+ }
394
+ function ffmpegDirOf(resolved) {
395
+ return resolved.found ? dirname(resolved.path) : void 0;
396
+ }
397
+
398
+ // src/core/ffmpeg/capabilities.ts
399
+ function parseVersion(output) {
400
+ const match = /^\w+ version (\S+)/m.exec(output);
401
+ return match?.[1] ?? null;
402
+ }
403
+ function parseListing(output, kind) {
404
+ const entry = kind === "encoders" ? /^\s*[VAS][A-Z.]{5}\s+(\S+)/ : /^\s*[T.][S.][C.]\s+(\S+)/;
405
+ const names = /* @__PURE__ */ new Set();
406
+ for (const line of output.split(/\r?\n/)) {
407
+ const name = entry.exec(line)?.[1];
408
+ if (name && name !== "=") names.add(name);
409
+ }
410
+ return names;
411
+ }
412
+ function toCapabilities(encoders, filters) {
413
+ return {
414
+ libsvtav1: encoders.has("libsvtav1"),
415
+ libaomAv1: encoders.has("libaom-av1"),
416
+ libx264: encoders.has("libx264"),
417
+ libvpxVp9: encoders.has("libvpx-vp9"),
418
+ libvmaf: filters.has("libvmaf")
419
+ };
420
+ }
421
+ function pickAv1Encoder(caps) {
422
+ if (caps.libsvtav1) return "libsvtav1";
423
+ if (caps.libaomAv1) return "libaom-av1";
424
+ return null;
425
+ }
426
+ async function probeFfmpeg(path, run, cache = /* @__PURE__ */ new Map()) {
427
+ const { stdout } = await run(path, ["-hide_banner", "-version"]);
428
+ const version = parseVersion(stdout) ?? "unknown";
429
+ const key = `${path}\0${version}`;
430
+ const cached = cache.get(key);
431
+ if (cached) return cached;
432
+ const [encoders, filters] = await Promise.all([
433
+ run(path, ["-hide_banner", "-encoders"]),
434
+ run(path, ["-hide_banner", "-filters"])
435
+ ]);
436
+ const info = {
437
+ path,
438
+ version,
439
+ capabilities: toCapabilities(
440
+ parseListing(encoders.stdout, "encoders"),
441
+ parseListing(filters.stdout, "filters")
442
+ )
443
+ };
444
+ cache.set(key, info);
445
+ return info;
446
+ }
447
+ async function probeFfprobeVersion(path, run) {
448
+ const { stdout } = await run(path, ["-hide_banner", "-version"]);
449
+ return parseVersion(stdout) ?? "unknown";
450
+ }
451
+
452
+ // src/core/ffmpeg/install-hint.ts
453
+ function installHint(platform, osReleaseId) {
454
+ switch (platform) {
455
+ case "darwin":
456
+ return "brew install ffmpeg";
457
+ case "win32":
458
+ return "winget install ffmpeg";
459
+ default:
460
+ switch (osReleaseId) {
461
+ case "fedora":
462
+ case "rhel":
463
+ case "centos":
464
+ return "sudo dnf install ffmpeg";
465
+ case "arch":
466
+ case "manjaro":
467
+ return "sudo pacman -S ffmpeg";
468
+ case "alpine":
469
+ return "sudo apk add ffmpeg";
470
+ default:
471
+ return "sudo apt install ffmpeg";
472
+ }
473
+ }
474
+ }
475
+
476
+ export {
477
+ IMAGE_PRESETS,
478
+ getImagePreset,
479
+ getVideoPreset,
480
+ rootPreset,
481
+ BUILTIN_POLICY,
482
+ resolveBinary,
483
+ ffmpegDirOf,
484
+ pickAv1Encoder,
485
+ probeFfmpeg,
486
+ probeFfprobeVersion,
487
+ installHint,
488
+ resolveConfigData,
489
+ resolveConfig
490
+ };
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ escapeGlob
4
+ } from "./chunk-FY53B4NB.js";
5
+
6
+ // src/core/config/choices.ts
7
+ import { stringify as stringifyToml } from "smol-toml";
8
+ function isEmpty(choices) {
9
+ return choices.outDir === void 0 && choices.publicPath === void 0 && choices.placeholder === void 0 && Object.keys(choices.presets ?? {}).length === 0;
10
+ }
11
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
12
+ function applyChoices(existing, choices) {
13
+ const next = isObject(existing) ? { ...existing } : {};
14
+ if (choices.outDir !== void 0) next.outDir = choices.outDir;
15
+ if (choices.publicPath !== void 0) next.publicPath = choices.publicPath;
16
+ if (choices.placeholder !== void 0) {
17
+ next.manifest = {
18
+ ...isObject(next.manifest) ? next.manifest : {},
19
+ placeholder: choices.placeholder
20
+ };
21
+ }
22
+ const presets = choices.presets ?? {};
23
+ if (Object.keys(presets).length > 0) {
24
+ const mine = Object.fromEntries(
25
+ Object.entries(presets).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([source, preset]) => [escapeGlob(source), preset])
26
+ );
27
+ const others = Object.entries(isObject(next.assets) ? next.assets : {}).filter(
28
+ ([pattern]) => !(pattern in mine)
29
+ );
30
+ next.assets = { ...mine, ...Object.fromEntries(others) };
31
+ }
32
+ return next;
33
+ }
34
+ function canRewrite(format) {
35
+ return format === "json";
36
+ }
37
+ function renderConfig(format, data) {
38
+ if (format === "json") return `${JSON.stringify(data, null, 2)}
39
+ `;
40
+ if (format === "toml") return `${stringifyToml(data)}
41
+ `;
42
+ return `export default ${JSON.stringify(data, null, 2)};
43
+ `;
44
+ }
45
+ function renderChoicesSnippet(format, choices) {
46
+ const fragment = applyChoices({}, choices);
47
+ return renderConfig(
48
+ format === "toml" ? "toml" : format === "json" ? "json" : "ts",
49
+ fragment
50
+ ).trimEnd();
51
+ }
52
+
53
+ export {
54
+ isEmpty,
55
+ applyChoices,
56
+ canRewrite,
57
+ renderConfig,
58
+ renderChoicesSnippet
59
+ };