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.
- package/LICENSE +21 -0
- package/README.md +140 -2
- package/dist/audit-PEKVNXKA.js +97 -0
- package/dist/check-BYNAWCDE.js +348 -0
- package/dist/chunk-3DBJKHOD.js +109 -0
- package/dist/chunk-3PF6BBFK.js +490 -0
- package/dist/chunk-4FIB6VNC.js +59 -0
- package/dist/chunk-4PWCQ4NF.js +620 -0
- package/dist/chunk-7DHE25UE.js +319 -0
- package/dist/chunk-AYYKUSRP.js +57 -0
- package/dist/chunk-CSFBJ3UH.js +259 -0
- package/dist/chunk-DJ45NQZD.js +29 -0
- package/dist/chunk-FY53B4NB.js +1439 -0
- package/dist/chunk-IZ4JEL3Q.js +212 -0
- package/dist/chunk-KN5QOB7R.js +186 -0
- package/dist/chunk-LTWUPQDT.js +26 -0
- package/dist/chunk-MV6J66AP.js +21 -0
- package/dist/chunk-P5PFI3AO.js +2170 -0
- package/dist/chunk-PQRBLHCC.js +188 -0
- package/dist/chunk-QOG2F4SF.js +536 -0
- package/dist/chunk-S3FQYUTI.js +59 -0
- package/dist/chunk-TCQFQ354.js +282 -0
- package/dist/chunk-WZ6MMRNB.js +238 -0
- package/dist/chunk-XUBQWBWH.js +382 -0
- package/dist/cli.js +138 -0
- package/dist/compare-MPPGTUC5.js +84 -0
- package/dist/doctor-PWCUEBD5.js +77 -0
- package/dist/explain-TS6XF5TX.js +267 -0
- package/dist/init-CWDWVJ55.js +92 -0
- package/dist/optimize-47YALM3D.js +216 -0
- package/dist/snippet-IIPVF6PS.js +114 -0
- package/dist/tui-CDF23I73.js +1691 -0
- package/package.json +46 -3
|
@@ -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
|
+
};
|