@simon_he/vue-tui 1.0.2 → 1.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/README.md +84 -26
- package/dist/TMermaidText-frVr8Ysb.js +885 -0
- package/dist/{TSelect-DR0FomSu.js → TSelect-tJ87rKPM.js} +59 -242
- package/dist/{TTree-kzhlB5P0.js → TTree-CGdE-CYu.js} +138 -102
- package/dist/agent/mermaid.cjs +2 -0
- package/dist/agent/mermaid.d.cts +2 -0
- package/dist/agent/mermaid.d.ts +2 -0
- package/dist/agent/mermaid.js +1 -0
- package/dist/agent.cjs +31188 -24907
- package/dist/agent.d.cts +11 -1
- package/dist/agent.d.ts +11 -1
- package/dist/agent.js +516 -11
- package/dist/{block-source-CKUOnkgT.js → block-source-CrlmaBM5.js} +1039 -66
- package/dist/cli/input.d.ts +8 -0
- package/dist/cli/stdin-buffer.d.ts +1 -1
- package/dist/cli/video/ffmpeg-frame-source.d.ts +10 -0
- package/dist/cli/video/png-frame-parser.d.ts +4 -0
- package/dist/cli/video/raw-frame-parser.d.ts +1 -0
- package/dist/cli/video/yt-dlp-frame-source.d.ts +11 -0
- package/dist/cli.cjs +5146 -6767
- package/dist/cli.d.cts +5 -1
- package/dist/cli.d.ts +5 -1
- package/dist/cli.js +2869 -154
- package/dist/core/buffer/eaw-ranges-unicode-17.d.ts +14 -0
- package/dist/core/perf/instrumentation-noop.d.ts +48 -0
- package/dist/core/perf/instrumentation.d.ts +136 -0
- package/dist/core/terminal/create-terminal.d.ts +1 -0
- package/dist/core.cjs +843 -1469
- package/dist/core.js +4 -5
- package/dist/create-terminal-app.d.ts +24 -1
- package/dist/debug-logger-CmsGI4of.js +74 -0
- package/dist/{dom-renderer-BniJSQyX.js → dom-renderer-DXx8fWzD.js} +2 -3
- package/dist/experimental/3d/bun/contributor-avatar-atlas.d.ts +12 -0
- package/dist/experimental/3d/bun/png.d.ts +1 -0
- package/dist/experimental/3d/bun/terminal-badge-scene.d.ts +11 -0
- package/dist/experimental/3d/bun/terminal-badge.wgsl.d.ts +1 -0
- package/dist/experimental/3d/bun/webgpu-renderer.d.ts +26 -0
- package/dist/experimental/3d/bun.cjs +7271 -0
- package/dist/experimental/3d/bun.d.cts +4 -0
- package/dist/experimental/3d/bun.d.ts +4 -0
- package/dist/experimental/3d/bun.js +7390 -0
- package/dist/experimental/video/node.cjs +497 -0
- package/dist/experimental/video/node.d.cts +4 -0
- package/dist/experimental/video/node.d.ts +4 -0
- package/dist/experimental/video/node.js +610 -0
- package/dist/experimental.cjs +6876 -6192
- package/dist/experimental.d.cts +7 -0
- package/dist/experimental.d.ts +7 -0
- package/dist/experimental.js +3005 -9
- package/dist/frame-perf-CHpWhbC4.js +111 -0
- package/dist/{create-terminal-DBFNYr8f.js → hyperlink-_1FAj7Wq.js} +440 -13
- package/dist/index.cjs +5017 -10339
- package/dist/index.js +14 -13
- package/dist/{linkify-D_gzn9Xq.js → linkify-D68T7rdW.js} +193 -3
- package/dist/markdown.cjs +21295 -12691
- package/dist/markdown.d.cts +1 -1
- package/dist/markdown.d.ts +1 -1
- package/dist/markdown.js +211 -51
- package/dist/mermaid-BNhM1jAZ.js +123 -0
- package/dist/mermaid.cjs +1707 -0
- package/dist/mermaid.d.cts +3 -0
- package/dist/mermaid.d.ts +3 -0
- package/dist/mermaid.js +6 -0
- package/dist/observability/frame-perf-store.d.ts +2 -1
- package/dist/observability/frame-perf.d.ts +33 -0
- package/dist/observability.cjs +106 -107
- package/dist/observability.d.cts +2 -2
- package/dist/observability.d.ts +2 -2
- package/dist/observability.js +3 -3
- package/dist/{wheel-scroll-vBRaQ7p-.js → rect-DUpeDfBg.js} +253 -117
- package/dist/renderer/cli/stdout-metrics.d.ts +10 -0
- package/dist/renderer/cli/stdout-renderer.d.ts +3 -0
- package/dist/renderer/terminal-graphic-render-queue.d.ts +50 -0
- package/dist/renderer/terminal-graphics-trace.d.ts +53 -0
- package/dist/renderer/terminal-graphics.d.ts +147 -0
- package/dist/renderer-dom.cjs +364 -903
- package/dist/renderer-dom.js +4 -5
- package/dist/runtime/index.d.ts +2 -0
- package/dist/{runtime-rfcZCyJq.js → runtime-Byf8XXOT.js} +28 -15
- package/dist/runtime.cjs +427 -889
- package/dist/runtime.js +2 -2
- package/dist/terminal-graphics-Bbe83wSr.js +597 -0
- package/dist/{terminal-selection-DBinO6p5.js → terminal-selection-4W1SJj4c.js} +4 -1
- package/dist/{tlog-theme-CvLFNMwk.js → tlog-theme-Bul2Na3u.js} +1237 -12
- package/dist/{trace-CjzYeIxN.js → trace-Bh8WnoMo.js} +5 -0
- package/dist/vue/agent/create-png-terminal-graphic-renderer.d.ts +19 -0
- package/dist/vue/components/T3DViewport.d.ts +473 -0
- package/dist/vue/components/TAgentTerminalGraphic.d.ts +494 -0
- package/dist/vue/components/TCharts.d.ts +1121 -0
- package/dist/vue/components/TFeedback.d.ts +1 -1
- package/dist/vue/components/TFlex.d.ts +389 -0
- package/dist/vue/components/TForm.d.ts +4 -4
- package/dist/vue/components/TLogSearchBar.d.ts +3 -3
- package/dist/vue/components/TLogSearchPager.d.ts +2 -2
- package/dist/vue/components/TMarkdownText.d.ts +162 -4
- package/dist/vue/components/TMermaidText.d.ts +900 -0
- package/dist/vue/components/TVideo.d.ts +291 -0
- package/dist/vue/components/TVirtualList.d.ts +9 -0
- package/dist/vue/components/TVirtualMarkdown.d.ts +170 -9
- package/dist/vue/components/TVirtualRows.d.ts +9 -0
- package/dist/vue/components/TerminalProvider.d.ts +2 -0
- package/dist/vue/context.d.ts +24 -0
- package/dist/vue/index.d.ts +5 -0
- package/dist/vue/layout/flex.d.ts +67 -0
- package/dist/vue/markdown/ast.d.ts +5 -2
- package/dist/vue/markdown/document.d.ts +5 -0
- package/dist/vue/markdown/image-actions.d.ts +14 -0
- package/dist/vue/markdown/image.d.ts +11 -0
- package/dist/vue/markdown/link-actions.d.ts +14 -0
- package/dist/vue/markdown/math-actions.d.ts +14 -0
- package/dist/vue/markdown/math.d.ts +6 -0
- package/dist/vue/markdown/render.d.ts +28 -0
- package/dist/vue/markdown/types.d.ts +78 -0
- package/dist/vue/mermaid/beautiful-mermaid.d.ts +1436 -0
- package/dist/vue/render/render-manager.d.ts +7 -0
- package/dist/vue/video/types.d.ts +39 -0
- package/dist/vue.cjs +7207 -12437
- package/dist/vue.js +704 -38
- package/dist/wheel-scroll-DASkEZr-.js +106 -0
- package/dist/{hyperlink-Bn02UXJb.js → width-GqllnV8C.js} +166 -111
- package/package.json +119 -14
- package/dist/frame-perf-C6Xfg85O.js +0 -24
- package/dist/sgr-BmzxEjAO.js +0 -304
- /package/dist/{ansi-palette-CNXqFciC.js → ansi-palette-CH6jc2Z7.js} +0 -0
- /package/dist/{path-suggest-BNqcuE3-.js → path-suggest-Dndu4dTi.js} +0 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
//#region src/cli/video/png-frame-parser.ts
|
|
2
|
+
const PNG_SIGNATURE = new Uint8Array([
|
|
3
|
+
137,
|
|
4
|
+
80,
|
|
5
|
+
78,
|
|
6
|
+
71,
|
|
7
|
+
13,
|
|
8
|
+
10,
|
|
9
|
+
26,
|
|
10
|
+
10
|
|
11
|
+
]);
|
|
12
|
+
function readUint32(bytes, offset) {
|
|
13
|
+
return bytes[offset] * 16777216 + (bytes[offset + 1] << 16) + (bytes[offset + 2] << 8) + bytes[offset + 3] >>> 0;
|
|
14
|
+
}
|
|
15
|
+
function hasPngSignature(bytes, offset) {
|
|
16
|
+
for (let index = 0; index < PNG_SIGNATURE.length; index++) if (bytes[offset + index] !== PNG_SIGNATURE[index]) return false;
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
function isChunkType(bytes, offset, type) {
|
|
20
|
+
return bytes[offset] === type.charCodeAt(0) && bytes[offset + 1] === type.charCodeAt(1) && bytes[offset + 2] === type.charCodeAt(2) && bytes[offset + 3] === type.charCodeAt(3);
|
|
21
|
+
}
|
|
22
|
+
function validatePngHeader(bytes, offset, maxDimension) {
|
|
23
|
+
if (readUint32(bytes, offset + 8) !== 13 || !isChunkType(bytes, offset + 12, "IHDR")) throw new Error("FFmpeg emitted a PNG without a valid IHDR chunk");
|
|
24
|
+
const width = readUint32(bytes, offset + 16);
|
|
25
|
+
const height = readUint32(bytes, offset + 20);
|
|
26
|
+
if (width <= 0 || height <= 0 || width > maxDimension || height > maxDimension) throw new Error(`FFmpeg emitted an unsupported PNG size: ${width}x${height}`);
|
|
27
|
+
}
|
|
28
|
+
/** @internal */
|
|
29
|
+
async function* parsePngFrames(chunks, options) {
|
|
30
|
+
let pending = new Uint8Array(Math.min(64 * 1024, options.maxFrameBytes));
|
|
31
|
+
let length = 0;
|
|
32
|
+
let frameStart = 0;
|
|
33
|
+
let cursor = 0;
|
|
34
|
+
let headerValidated = false;
|
|
35
|
+
for await (const chunk of chunks) {
|
|
36
|
+
if (frameStart > 0) {
|
|
37
|
+
pending.copyWithin(0, frameStart, length);
|
|
38
|
+
length -= frameStart;
|
|
39
|
+
cursor -= frameStart;
|
|
40
|
+
frameStart = 0;
|
|
41
|
+
}
|
|
42
|
+
const required = length + chunk.length;
|
|
43
|
+
if (required > pending.length) {
|
|
44
|
+
let capacity = Math.max(1, pending.length);
|
|
45
|
+
while (capacity < required && capacity < options.maxFrameBytes) capacity = Math.min(options.maxFrameBytes, capacity * 2);
|
|
46
|
+
const grown = new Uint8Array(Math.max(required, capacity));
|
|
47
|
+
grown.set(pending.subarray(0, length));
|
|
48
|
+
pending = grown;
|
|
49
|
+
}
|
|
50
|
+
pending.set(chunk, length);
|
|
51
|
+
length = required;
|
|
52
|
+
while (length - frameStart >= PNG_SIGNATURE.length) {
|
|
53
|
+
if (cursor === frameStart) {
|
|
54
|
+
if (!hasPngSignature(pending, frameStart)) throw new Error("FFmpeg emitted invalid PNG data");
|
|
55
|
+
cursor = frameStart + PNG_SIGNATURE.length;
|
|
56
|
+
}
|
|
57
|
+
if (!headerValidated) {
|
|
58
|
+
if (length - frameStart < 33) break;
|
|
59
|
+
validatePngHeader(pending, frameStart, options.maxDimension);
|
|
60
|
+
headerValidated = true;
|
|
61
|
+
}
|
|
62
|
+
if (length - cursor < 12) break;
|
|
63
|
+
const chunkBytes = readUint32(pending, cursor);
|
|
64
|
+
const chunkEnd = cursor + 12 + chunkBytes;
|
|
65
|
+
if (chunkEnd - frameStart > options.maxFrameBytes) throw new Error(`FFmpeg PNG frame exceeded ${options.maxFrameBytes} bytes`);
|
|
66
|
+
if (chunkEnd > length) break;
|
|
67
|
+
if (isChunkType(pending, cursor + 4, "IEND")) {
|
|
68
|
+
if (chunkBytes !== 0) throw new Error("FFmpeg emitted an invalid PNG IEND chunk");
|
|
69
|
+
yield new Uint8Array(pending.subarray(frameStart, chunkEnd));
|
|
70
|
+
frameStart = chunkEnd;
|
|
71
|
+
cursor = frameStart;
|
|
72
|
+
headerValidated = false;
|
|
73
|
+
if (frameStart === length) {
|
|
74
|
+
length = 0;
|
|
75
|
+
frameStart = 0;
|
|
76
|
+
cursor = 0;
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
cursor = chunkEnd;
|
|
81
|
+
}
|
|
82
|
+
if (length - frameStart > options.maxFrameBytes) throw new Error(`FFmpeg PNG frame exceeded ${options.maxFrameBytes} bytes`);
|
|
83
|
+
}
|
|
84
|
+
if (length !== frameStart) throw new Error("FFmpeg PNG stream ended with a truncated frame");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/cli/video/raw-frame-parser.ts
|
|
89
|
+
/** @internal */
|
|
90
|
+
async function* parseRawFrames(chunks, frameBytes) {
|
|
91
|
+
if (!Number.isSafeInteger(frameBytes) || frameBytes <= 0) throw new Error("FFmpeg raw video frame size must be a positive integer");
|
|
92
|
+
let frame = new Uint8Array(frameBytes);
|
|
93
|
+
let frameOffset = 0;
|
|
94
|
+
for await (const chunk of chunks) {
|
|
95
|
+
let chunkOffset = 0;
|
|
96
|
+
while (chunkOffset < chunk.length) {
|
|
97
|
+
const copyBytes = Math.min(frameBytes - frameOffset, chunk.length - chunkOffset);
|
|
98
|
+
frame.set(chunk.subarray(chunkOffset, chunkOffset + copyBytes), frameOffset);
|
|
99
|
+
frameOffset += copyBytes;
|
|
100
|
+
chunkOffset += copyBytes;
|
|
101
|
+
if (frameOffset === frameBytes) {
|
|
102
|
+
yield frame;
|
|
103
|
+
frame = new Uint8Array(frameBytes);
|
|
104
|
+
frameOffset = 0;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (frameOffset !== 0) throw new Error("FFmpeg raw video stream ended with a truncated frame");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/cli/video/ffmpeg-frame-source.ts
|
|
113
|
+
const DEFAULT_MAX_FRAME_BYTES = 8 * 1024 * 1024;
|
|
114
|
+
const MAX_PNG_DIMENSION = 4096;
|
|
115
|
+
const MAX_STDERR_CHARS$1 = 16384;
|
|
116
|
+
const KILL_TIMEOUT_MS$1 = 1e3;
|
|
117
|
+
const MIN_FPS = .001;
|
|
118
|
+
function positiveInt$1(value, fallback, max) {
|
|
119
|
+
const parsed = Math.floor(Number(value));
|
|
120
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
121
|
+
return Math.min(max, parsed);
|
|
122
|
+
}
|
|
123
|
+
function positiveNumber(value, fallback, max) {
|
|
124
|
+
const parsed = Number(value);
|
|
125
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
126
|
+
return Math.max(MIN_FPS, Math.min(max, parsed));
|
|
127
|
+
}
|
|
128
|
+
function ffmpegNumber(value) {
|
|
129
|
+
return Number(value.toFixed(3)).toString();
|
|
130
|
+
}
|
|
131
|
+
function ffmpegHttpHeaders(headers) {
|
|
132
|
+
if (!headers) return "";
|
|
133
|
+
const lines = [];
|
|
134
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
135
|
+
const hasControlCharacter = [...value].some((character) => {
|
|
136
|
+
const code = character.charCodeAt(0);
|
|
137
|
+
return code <= 31 || code === 127;
|
|
138
|
+
});
|
|
139
|
+
if (!/^[!#$%&'*+.^_`|~\w-]+$/u.test(name) || hasControlCharacter) throw new Error("Invalid FFmpeg HTTP header");
|
|
140
|
+
lines.push(`${name}: ${value}`);
|
|
141
|
+
}
|
|
142
|
+
return lines.length ? `${lines.join("\r\n")}\r\n` : "";
|
|
143
|
+
}
|
|
144
|
+
function classifyInput(src, live) {
|
|
145
|
+
var _exec;
|
|
146
|
+
if (!src.trim()) throw new Error("TVideo src must not be empty");
|
|
147
|
+
if (/^[a-zA-Z]:[\\/]/u.test(src)) return {
|
|
148
|
+
protocolWhitelist: "file",
|
|
149
|
+
readAtNativeRate: !live
|
|
150
|
+
};
|
|
151
|
+
const scheme = (_exec = /^([a-zA-Z][a-zA-Z\d+.-]*):/u.exec(src)) === null || _exec === void 0 || (_exec = _exec[1]) === null || _exec === void 0 ? void 0 : _exec.toLowerCase();
|
|
152
|
+
if (!scheme || scheme === "file") return {
|
|
153
|
+
protocolWhitelist: "file",
|
|
154
|
+
readAtNativeRate: !live
|
|
155
|
+
};
|
|
156
|
+
if (scheme === "http" || scheme === "https") return {
|
|
157
|
+
protocolWhitelist: "http,https,httpproxy,tcp,tls",
|
|
158
|
+
readAtNativeRate: !live
|
|
159
|
+
};
|
|
160
|
+
throw new Error(`Unsupported TVideo source protocol: ${scheme}`);
|
|
161
|
+
}
|
|
162
|
+
function pngDimensions(png) {
|
|
163
|
+
return {
|
|
164
|
+
width: png[16] * 16777216 + (png[17] << 16) + (png[18] << 8) + png[19] >>> 0,
|
|
165
|
+
height: png[20] * 16777216 + (png[21] << 16) + (png[22] << 8) + png[23] >>> 0
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function sanitizeProcessError$1(value) {
|
|
169
|
+
let out = "";
|
|
170
|
+
for (const character of value) {
|
|
171
|
+
const code = character.charCodeAt(0);
|
|
172
|
+
if (code <= 8 || code === 11 || code === 12 || code >= 14 && code <= 31 || code >= 127 && code <= 159) continue;
|
|
173
|
+
out += character;
|
|
174
|
+
}
|
|
175
|
+
return out.trim();
|
|
176
|
+
}
|
|
177
|
+
function abortError$1() {
|
|
178
|
+
const error = new Error("FFmpeg video decoding aborted");
|
|
179
|
+
error.name = "AbortError";
|
|
180
|
+
return error;
|
|
181
|
+
}
|
|
182
|
+
/** @internal */
|
|
183
|
+
function buildFfmpegVideoArgs(context, options = {}) {
|
|
184
|
+
const live = options.live === true;
|
|
185
|
+
const realtime = options.realtime !== false;
|
|
186
|
+
const input = classifyInput(context.src, live);
|
|
187
|
+
const fps = positiveNumber(context.maxFps, 12, 60);
|
|
188
|
+
const playbackRate = positiveNumber(context.playbackRate, 1, 3);
|
|
189
|
+
const width = positiveInt$1(context.pixelWidth, 320, MAX_PNG_DIMENSION);
|
|
190
|
+
const height = positiveInt$1(context.pixelHeight, 180, MAX_PNG_DIMENSION);
|
|
191
|
+
const preferredFormat = context.preferredFormat ?? "png";
|
|
192
|
+
const startAtMs = Math.max(0, Number(context.startAtMs) || 0);
|
|
193
|
+
const httpHeaders = input.protocolWhitelist === "file" ? "" : ffmpegHttpHeaders(options.httpHeaders);
|
|
194
|
+
const filters = [
|
|
195
|
+
playbackRate === 1 ? "setpts=PTS-STARTPTS" : `setpts=(PTS-STARTPTS)/${ffmpegNumber(playbackRate)}`,
|
|
196
|
+
`fps=${ffmpegNumber(fps)}`,
|
|
197
|
+
...realtime ? ["realtime"] : [],
|
|
198
|
+
`scale=${width}:${height}:force_original_aspect_ratio=decrease:flags=fast_bilinear`,
|
|
199
|
+
`pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2`
|
|
200
|
+
];
|
|
201
|
+
return [
|
|
202
|
+
"-nostdin",
|
|
203
|
+
"-hide_banner",
|
|
204
|
+
"-loglevel",
|
|
205
|
+
"error",
|
|
206
|
+
...context.loop || options.loop ? ["-stream_loop", "-1"] : [],
|
|
207
|
+
...startAtMs > 0 && !live ? ["-ss", ffmpegNumber(startAtMs / 1e3)] : [],
|
|
208
|
+
...realtime && input.readAtNativeRate ? ["-readrate", ffmpegNumber(playbackRate)] : [],
|
|
209
|
+
"-protocol_whitelist",
|
|
210
|
+
input.protocolWhitelist,
|
|
211
|
+
...httpHeaders ? ["-headers", httpHeaders] : [],
|
|
212
|
+
"-i",
|
|
213
|
+
context.src,
|
|
214
|
+
"-map",
|
|
215
|
+
"0:V:0",
|
|
216
|
+
"-an",
|
|
217
|
+
"-sn",
|
|
218
|
+
"-dn",
|
|
219
|
+
"-vf",
|
|
220
|
+
filters.join(","),
|
|
221
|
+
"-fps_mode",
|
|
222
|
+
"passthrough",
|
|
223
|
+
"-threads",
|
|
224
|
+
"1",
|
|
225
|
+
...preferredFormat === "gray8" ? [
|
|
226
|
+
"-f",
|
|
227
|
+
"rawvideo",
|
|
228
|
+
"-c:v",
|
|
229
|
+
"rawvideo",
|
|
230
|
+
"-pix_fmt",
|
|
231
|
+
"gray"
|
|
232
|
+
] : [
|
|
233
|
+
"-f",
|
|
234
|
+
"image2pipe",
|
|
235
|
+
"-c:v",
|
|
236
|
+
"png",
|
|
237
|
+
"-pix_fmt",
|
|
238
|
+
"rgb24",
|
|
239
|
+
"-compression_level",
|
|
240
|
+
"1"
|
|
241
|
+
],
|
|
242
|
+
"pipe:1"
|
|
243
|
+
];
|
|
244
|
+
}
|
|
245
|
+
function createFfmpegVideoFrameSource(options = {}) {
|
|
246
|
+
return async function* ffmpegVideoFrames(context) {
|
|
247
|
+
var _options$ffmpegPath;
|
|
248
|
+
if (context.signal.aborted) throw abortError$1();
|
|
249
|
+
const fps = positiveNumber(context.maxFps, 12, 60);
|
|
250
|
+
const playbackRate = positiveNumber(context.playbackRate, 1, 3);
|
|
251
|
+
const startAtMs = Math.max(0, Number(context.startAtMs) || 0);
|
|
252
|
+
const width = positiveInt$1(context.pixelWidth, 320, MAX_PNG_DIMENSION);
|
|
253
|
+
const height = positiveInt$1(context.pixelHeight, 180, MAX_PNG_DIMENSION);
|
|
254
|
+
const preferredFormat = context.preferredFormat ?? "png";
|
|
255
|
+
const maxFrameBytes = positiveInt$1(options.maxFrameBytes, DEFAULT_MAX_FRAME_BYTES, DEFAULT_MAX_FRAME_BYTES);
|
|
256
|
+
const rawFrameBytes = width * height;
|
|
257
|
+
if (preferredFormat === "gray8" && rawFrameBytes > maxFrameBytes) throw new Error(`FFmpeg raw video frame exceeded ${maxFrameBytes} bytes`);
|
|
258
|
+
const { spawn } = await import("node:child_process");
|
|
259
|
+
if (context.signal.aborted) throw abortError$1();
|
|
260
|
+
const executable = ((_options$ffmpegPath = options.ffmpegPath) === null || _options$ffmpegPath === void 0 ? void 0 : _options$ffmpegPath.trim()) || "ffmpeg";
|
|
261
|
+
const args = buildFfmpegVideoArgs(context, options);
|
|
262
|
+
const child = spawn(executable, args, {
|
|
263
|
+
shell: false,
|
|
264
|
+
windowsHide: true,
|
|
265
|
+
stdio: [
|
|
266
|
+
"ignore",
|
|
267
|
+
"pipe",
|
|
268
|
+
"pipe"
|
|
269
|
+
]
|
|
270
|
+
});
|
|
271
|
+
const stdout = child.stdout;
|
|
272
|
+
const stderr = child.stderr;
|
|
273
|
+
const decoder = new TextDecoder();
|
|
274
|
+
let stderrTail = "";
|
|
275
|
+
let spawnError;
|
|
276
|
+
let closed = false;
|
|
277
|
+
let aborted = false;
|
|
278
|
+
let killTimer = null;
|
|
279
|
+
stderr.on("data", (chunk) => {
|
|
280
|
+
stderrTail = `${stderrTail}${decoder.decode(chunk, { stream: true })}`.slice(-MAX_STDERR_CHARS$1);
|
|
281
|
+
});
|
|
282
|
+
const closePromise = new Promise((resolve) => {
|
|
283
|
+
child.once("error", (error) => {
|
|
284
|
+
spawnError = error;
|
|
285
|
+
});
|
|
286
|
+
child.once("close", (code, signal) => {
|
|
287
|
+
closed = true;
|
|
288
|
+
if (killTimer != null) clearTimeout(killTimer);
|
|
289
|
+
killTimer = null;
|
|
290
|
+
stderrTail = `${stderrTail}${decoder.decode()}`.slice(-MAX_STDERR_CHARS$1);
|
|
291
|
+
resolve({
|
|
292
|
+
code,
|
|
293
|
+
signal
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
const terminate = () => {
|
|
298
|
+
var _killTimer$unref;
|
|
299
|
+
if (closed) return;
|
|
300
|
+
child.kill("SIGTERM");
|
|
301
|
+
if (killTimer != null) return;
|
|
302
|
+
killTimer = setTimeout(() => {
|
|
303
|
+
if (!closed) child.kill("SIGKILL");
|
|
304
|
+
}, KILL_TIMEOUT_MS$1);
|
|
305
|
+
(_killTimer$unref = killTimer.unref) === null || _killTimer$unref === void 0 || _killTimer$unref.call(killTimer);
|
|
306
|
+
};
|
|
307
|
+
const onAbort = () => {
|
|
308
|
+
aborted = true;
|
|
309
|
+
terminate();
|
|
310
|
+
};
|
|
311
|
+
context.signal.addEventListener("abort", onAbort, { once: true });
|
|
312
|
+
if (context.signal.aborted) onAbort();
|
|
313
|
+
let frameIndex = 0;
|
|
314
|
+
try {
|
|
315
|
+
if (preferredFormat === "gray8") for await (const pixels of parseRawFrames(stdout, rawFrameBytes)) {
|
|
316
|
+
if (aborted || context.signal.aborted) throw abortError$1();
|
|
317
|
+
const timestampMs = startAtMs + frameIndex * 1e3 * playbackRate / fps;
|
|
318
|
+
frameIndex++;
|
|
319
|
+
yield {
|
|
320
|
+
format: "gray8",
|
|
321
|
+
pixels,
|
|
322
|
+
timestampMs,
|
|
323
|
+
pixelWidth: width,
|
|
324
|
+
pixelHeight: height
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
else for await (const png of parsePngFrames(stdout, {
|
|
328
|
+
maxFrameBytes,
|
|
329
|
+
maxDimension: MAX_PNG_DIMENSION
|
|
330
|
+
})) {
|
|
331
|
+
if (aborted || context.signal.aborted) throw abortError$1();
|
|
332
|
+
const timestampMs = startAtMs + frameIndex * 1e3 * playbackRate / fps;
|
|
333
|
+
frameIndex++;
|
|
334
|
+
const dimensions = pngDimensions(png);
|
|
335
|
+
yield {
|
|
336
|
+
png,
|
|
337
|
+
timestampMs,
|
|
338
|
+
pixelWidth: dimensions.width,
|
|
339
|
+
pixelHeight: dimensions.height
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
const exit = await closePromise;
|
|
343
|
+
if (aborted || context.signal.aborted) throw abortError$1();
|
|
344
|
+
if (spawnError) throw spawnError;
|
|
345
|
+
if (exit.code !== 0) {
|
|
346
|
+
const details = sanitizeProcessError$1(stderrTail);
|
|
347
|
+
throw new Error(details ? `FFmpeg video decoding failed (${exit.code ?? exit.signal}): ${details}` : `FFmpeg video decoding failed (${exit.code ?? exit.signal})`);
|
|
348
|
+
}
|
|
349
|
+
} catch (error) {
|
|
350
|
+
if (!closed) terminate();
|
|
351
|
+
await closePromise;
|
|
352
|
+
if (aborted || context.signal.aborted) throw abortError$1();
|
|
353
|
+
if (spawnError) throw spawnError;
|
|
354
|
+
throw error;
|
|
355
|
+
} finally {
|
|
356
|
+
context.signal.removeEventListener("abort", onAbort);
|
|
357
|
+
if (!closed) terminate();
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region src/cli/video/yt-dlp-frame-source.ts
|
|
364
|
+
const DEFAULT_MAX_SOURCE_HEIGHT = 720;
|
|
365
|
+
const MAX_SOURCE_HEIGHT = 4320;
|
|
366
|
+
const MAX_METADATA_BYTES = 128 * 1024;
|
|
367
|
+
const MAX_STDERR_CHARS = 16384;
|
|
368
|
+
const KILL_TIMEOUT_MS = 1e3;
|
|
369
|
+
const RESOLVED_INPUT_TTL_MS = 5 * 60 * 1e3;
|
|
370
|
+
const METADATA_TEMPLATE = "%(.{url,http_headers,protocol,width,height,fps,vcodec,duration})j";
|
|
371
|
+
const COMMON_SOURCE_HEIGHTS = [
|
|
372
|
+
144,
|
|
373
|
+
240,
|
|
374
|
+
360,
|
|
375
|
+
480,
|
|
376
|
+
720,
|
|
377
|
+
1080,
|
|
378
|
+
1440,
|
|
379
|
+
2160,
|
|
380
|
+
4320
|
|
381
|
+
];
|
|
382
|
+
function positiveInt(value, fallback, max) {
|
|
383
|
+
const parsed = Math.floor(Number(value));
|
|
384
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
385
|
+
return Math.min(max, parsed);
|
|
386
|
+
}
|
|
387
|
+
function resolveSourceHeight(options, context) {
|
|
388
|
+
const maxSourceHeight = positiveInt(options.maxSourceHeight, DEFAULT_MAX_SOURCE_HEIGHT, MAX_SOURCE_HEIGHT);
|
|
389
|
+
if (!context) return maxSourceHeight;
|
|
390
|
+
const minimum = context.preferredFormat === "gray8" ? 144 : 360;
|
|
391
|
+
const requested = Math.max(minimum, positiveInt(context.pixelHeight, minimum, MAX_SOURCE_HEIGHT));
|
|
392
|
+
const rounded = COMMON_SOURCE_HEIGHTS.find((height) => height >= requested) ?? MAX_SOURCE_HEIGHT;
|
|
393
|
+
return Math.min(maxSourceHeight, rounded);
|
|
394
|
+
}
|
|
395
|
+
function resolveSourceFps(context) {
|
|
396
|
+
const requested = Number(context === null || context === void 0 ? void 0 : context.maxFps);
|
|
397
|
+
return Math.max(30, Number.isFinite(requested) && requested > 0 ? Math.ceil(requested) : 30);
|
|
398
|
+
}
|
|
399
|
+
function abortError() {
|
|
400
|
+
const error = new Error("yt-dlp video URL resolution aborted");
|
|
401
|
+
error.name = "AbortError";
|
|
402
|
+
return error;
|
|
403
|
+
}
|
|
404
|
+
function sanitizeProcessError(value) {
|
|
405
|
+
let out = "";
|
|
406
|
+
for (const character of value) {
|
|
407
|
+
const code = character.charCodeAt(0);
|
|
408
|
+
if (code <= 8 || code === 11 || code === 12 || code >= 14 && code <= 31 || code >= 127 && code <= 159) continue;
|
|
409
|
+
out += character;
|
|
410
|
+
}
|
|
411
|
+
return out.trim();
|
|
412
|
+
}
|
|
413
|
+
function assertHttpUrl(value, label) {
|
|
414
|
+
let url;
|
|
415
|
+
try {
|
|
416
|
+
url = new URL(value);
|
|
417
|
+
} catch {
|
|
418
|
+
throw new Error(`${label} must be an HTTP(S) URL`);
|
|
419
|
+
}
|
|
420
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`${label} must be an HTTP(S) URL`);
|
|
421
|
+
return url.href;
|
|
422
|
+
}
|
|
423
|
+
/** @internal */
|
|
424
|
+
function buildYtDlpVideoArgs(src, options = {}, context) {
|
|
425
|
+
var _options$format;
|
|
426
|
+
const source = assertHttpUrl(src, "yt-dlp source");
|
|
427
|
+
const sourceHeight = resolveSourceHeight(options, context);
|
|
428
|
+
const sourceFps = resolveSourceFps(context);
|
|
429
|
+
const format = ((_options$format = options.format) === null || _options$format === void 0 ? void 0 : _options$format.trim()) || [
|
|
430
|
+
`bestvideo[height<=${sourceHeight}][fps<=${sourceFps}][protocol^=http][vcodec^=avc1]`,
|
|
431
|
+
`bestvideo[height<=${sourceHeight}][fps<=${sourceFps}][protocol^=http]`,
|
|
432
|
+
`bestvideo[height<=${sourceHeight}][protocol^=http][vcodec^=avc1]`,
|
|
433
|
+
`bestvideo[height<=${sourceHeight}][protocol^=http]`,
|
|
434
|
+
`best[height<=${sourceHeight}][fps<=${sourceFps}][protocol^=http]`,
|
|
435
|
+
`best[height<=${sourceHeight}][protocol^=http]`
|
|
436
|
+
].join("/");
|
|
437
|
+
return [
|
|
438
|
+
"--no-config",
|
|
439
|
+
"--no-playlist",
|
|
440
|
+
"--no-warnings",
|
|
441
|
+
"--simulate",
|
|
442
|
+
"--print",
|
|
443
|
+
METADATA_TEMPLATE,
|
|
444
|
+
"--format",
|
|
445
|
+
format,
|
|
446
|
+
"--",
|
|
447
|
+
source
|
|
448
|
+
];
|
|
449
|
+
}
|
|
450
|
+
async function resolveYtDlpVideoInput(src, signal, options, context) {
|
|
451
|
+
var _options$ytDlpPath;
|
|
452
|
+
if (signal.aborted) throw abortError();
|
|
453
|
+
const { spawn } = await import("node:child_process");
|
|
454
|
+
if (signal.aborted) throw abortError();
|
|
455
|
+
const executable = ((_options$ytDlpPath = options.ytDlpPath) === null || _options$ytDlpPath === void 0 ? void 0 : _options$ytDlpPath.trim()) || "yt-dlp";
|
|
456
|
+
const child = spawn(executable, buildYtDlpVideoArgs(src, options, context), {
|
|
457
|
+
shell: false,
|
|
458
|
+
windowsHide: true,
|
|
459
|
+
stdio: [
|
|
460
|
+
"ignore",
|
|
461
|
+
"pipe",
|
|
462
|
+
"pipe"
|
|
463
|
+
]
|
|
464
|
+
});
|
|
465
|
+
const decoder = new TextDecoder();
|
|
466
|
+
const stderrDecoder = new TextDecoder();
|
|
467
|
+
let stdout = "";
|
|
468
|
+
let stdoutBytes = 0;
|
|
469
|
+
let stderrTail = "";
|
|
470
|
+
let spawnError;
|
|
471
|
+
let metadataTooLarge = false;
|
|
472
|
+
let closed = false;
|
|
473
|
+
let aborted = false;
|
|
474
|
+
let killTimer = null;
|
|
475
|
+
const terminate = () => {
|
|
476
|
+
var _killTimer$unref;
|
|
477
|
+
if (closed) return;
|
|
478
|
+
child.kill("SIGTERM");
|
|
479
|
+
if (killTimer != null) return;
|
|
480
|
+
killTimer = setTimeout(() => {
|
|
481
|
+
if (!closed) child.kill("SIGKILL");
|
|
482
|
+
}, KILL_TIMEOUT_MS);
|
|
483
|
+
(_killTimer$unref = killTimer.unref) === null || _killTimer$unref === void 0 || _killTimer$unref.call(killTimer);
|
|
484
|
+
};
|
|
485
|
+
child.stdout.on("data", (chunk) => {
|
|
486
|
+
stdoutBytes += chunk.byteLength;
|
|
487
|
+
if (stdoutBytes > MAX_METADATA_BYTES) {
|
|
488
|
+
metadataTooLarge = true;
|
|
489
|
+
terminate();
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
stdout += decoder.decode(chunk, { stream: true });
|
|
493
|
+
});
|
|
494
|
+
child.stderr.on("data", (chunk) => {
|
|
495
|
+
stderrTail = `${stderrTail}${stderrDecoder.decode(chunk, { stream: true })}`.slice(-MAX_STDERR_CHARS);
|
|
496
|
+
});
|
|
497
|
+
const closePromise = new Promise((resolve) => {
|
|
498
|
+
child.once("error", (error) => {
|
|
499
|
+
spawnError = error;
|
|
500
|
+
});
|
|
501
|
+
child.once("close", (code, closeSignal) => {
|
|
502
|
+
closed = true;
|
|
503
|
+
if (killTimer != null) clearTimeout(killTimer);
|
|
504
|
+
killTimer = null;
|
|
505
|
+
stdout += decoder.decode();
|
|
506
|
+
stderrTail = `${stderrTail}${stderrDecoder.decode()}`.slice(-MAX_STDERR_CHARS);
|
|
507
|
+
resolve({
|
|
508
|
+
code,
|
|
509
|
+
signal: closeSignal
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
const onAbort = () => {
|
|
514
|
+
aborted = true;
|
|
515
|
+
terminate();
|
|
516
|
+
};
|
|
517
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
518
|
+
if (signal.aborted) onAbort();
|
|
519
|
+
try {
|
|
520
|
+
const exit = await closePromise;
|
|
521
|
+
if (aborted || signal.aborted) throw abortError();
|
|
522
|
+
if (metadataTooLarge) throw new Error("yt-dlp video metadata exceeded 131072 bytes");
|
|
523
|
+
if (spawnError) {
|
|
524
|
+
const details = spawnError instanceof Error ? spawnError.message : String(spawnError);
|
|
525
|
+
throw new Error(`yt-dlp video URL resolution failed: ${details}`);
|
|
526
|
+
}
|
|
527
|
+
if (exit.code !== 0) {
|
|
528
|
+
const details = sanitizeProcessError(stderrTail);
|
|
529
|
+
throw new Error(details ? `yt-dlp video URL resolution failed (${exit.code ?? exit.signal}): ${details}` : `yt-dlp video URL resolution failed (${exit.code ?? exit.signal})`);
|
|
530
|
+
}
|
|
531
|
+
const lines = stdout.trim().split(/\r?\n/u).filter(Boolean);
|
|
532
|
+
if (lines.length !== 1) throw new Error("yt-dlp returned invalid video metadata");
|
|
533
|
+
let metadata;
|
|
534
|
+
try {
|
|
535
|
+
metadata = JSON.parse(lines[0]);
|
|
536
|
+
} catch {
|
|
537
|
+
throw new Error("yt-dlp returned invalid video metadata");
|
|
538
|
+
}
|
|
539
|
+
if (!metadata || typeof metadata !== "object") throw new Error("yt-dlp returned invalid video metadata");
|
|
540
|
+
const raw = metadata;
|
|
541
|
+
if (typeof raw.url !== "string") throw new Error("yt-dlp returned invalid video metadata");
|
|
542
|
+
const url = assertHttpUrl(raw.url, "yt-dlp resolved video URL");
|
|
543
|
+
const httpHeaders = {};
|
|
544
|
+
if (raw.http_headers != null) {
|
|
545
|
+
if (typeof raw.http_headers !== "object" || Array.isArray(raw.http_headers)) throw new Error("yt-dlp returned invalid video metadata");
|
|
546
|
+
for (const [name, value] of Object.entries(raw.http_headers)) {
|
|
547
|
+
if (typeof value !== "string") throw new Error("yt-dlp returned invalid video metadata");
|
|
548
|
+
httpHeaders[name] = value;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
let durationMs;
|
|
552
|
+
if (raw.duration != null) {
|
|
553
|
+
if (typeof raw.duration !== "number" || !Number.isFinite(raw.duration) || raw.duration < 0) throw new Error("yt-dlp returned invalid video metadata");
|
|
554
|
+
durationMs = Math.round(raw.duration * 1e3);
|
|
555
|
+
}
|
|
556
|
+
return {
|
|
557
|
+
url,
|
|
558
|
+
httpHeaders,
|
|
559
|
+
durationMs
|
|
560
|
+
};
|
|
561
|
+
} finally {
|
|
562
|
+
signal.removeEventListener("abort", onAbort);
|
|
563
|
+
if (!closed) terminate();
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function createYtDlpVideoFrameSource(options = {}) {
|
|
567
|
+
const resolvedInputs = new Map();
|
|
568
|
+
return async function* ytDlpVideoFrames(context) {
|
|
569
|
+
if (context.signal.aborted) throw abortError();
|
|
570
|
+
const cacheKey = JSON.stringify(buildYtDlpVideoArgs(context.src, options, context));
|
|
571
|
+
const cached = resolvedInputs.get(cacheKey);
|
|
572
|
+
let input;
|
|
573
|
+
if (cached && cached.expiresAtMs > Date.now()) input = cached.input;
|
|
574
|
+
else {
|
|
575
|
+
if (cached) resolvedInputs.delete(cacheKey);
|
|
576
|
+
input = await resolveYtDlpVideoInput(context.src, context.signal, options, context);
|
|
577
|
+
resolvedInputs.set(cacheKey, {
|
|
578
|
+
input,
|
|
579
|
+
expiresAtMs: Date.now() + RESOLVED_INPUT_TTL_MS
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
const ffmpegSource = createFfmpegVideoFrameSource({
|
|
583
|
+
ffmpegPath: options.ffmpegPath,
|
|
584
|
+
live: options.live,
|
|
585
|
+
realtime: options.realtime,
|
|
586
|
+
maxFrameBytes: options.maxFrameBytes,
|
|
587
|
+
httpHeaders: input.httpHeaders
|
|
588
|
+
});
|
|
589
|
+
const frames = await ffmpegSource({
|
|
590
|
+
...context,
|
|
591
|
+
src: input.url
|
|
592
|
+
});
|
|
593
|
+
try {
|
|
594
|
+
if (input.durationMs == null) {
|
|
595
|
+
yield* frames;
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
for await (const frame of frames) yield {
|
|
599
|
+
...frame,
|
|
600
|
+
durationMs: input.durationMs
|
|
601
|
+
};
|
|
602
|
+
} catch (error) {
|
|
603
|
+
if (!(error instanceof Error && error.name === "AbortError")) resolvedInputs.delete(cacheKey);
|
|
604
|
+
throw error;
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
//#endregion
|
|
610
|
+
export { createFfmpegVideoFrameSource, createYtDlpVideoFrameSource };
|