@simon_he/vue-tui 1.1.6 → 1.1.7
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 +1 -0
- package/dist/TAgentTerminalGraphic-B8-YZfuP.js +1198 -0
- package/dist/{TSelect-CMBOOFwK.js → TSelect-CCsjjCOr.js} +41 -12
- package/dist/{TTree-kk8ntrpg.js → TTree-3W5A2_EH.js} +4 -3
- package/dist/TVideo-D9AOiyq2.js +948 -0
- package/dist/agent.cjs +4926 -4364
- package/dist/agent.js +11 -9
- package/dist/{block-source-DByORvKG.js → block-source-BhvdZomx.js} +3 -3
- package/dist/cli.cjs +8 -8
- package/dist/cli.js +15 -5
- package/dist/core.js +2 -2
- package/dist/experimental/browser.cjs +3705 -0
- package/dist/experimental/browser.d.cts +3 -0
- package/dist/experimental/browser.d.ts +3 -0
- package/dist/experimental/browser.js +413 -0
- package/dist/experimental.cjs +1 -1
- package/dist/experimental.js +9 -947
- package/dist/{linkify-CL9wwnZN.js → frame-mailbox-DaTrfyaF.js} +2 -297
- package/dist/index.cjs +25 -8
- package/dist/index.js +6 -5
- package/dist/linkify-CklfA1vp.js +298 -0
- package/dist/markdown.cjs +4907 -4362
- package/dist/markdown.js +4 -4
- package/dist/{mermaid-CWy4wtCL.js → mermaid-D0ov4_A1.js} +3 -3
- package/dist/mermaid.cjs +1 -1
- package/dist/mermaid.js +4 -4
- package/dist/{path-suggest-Dndu4dTi.js → path-suggest-fPzRsgPv.js} +1 -1
- package/dist/renderer/terminal-graphics.d.ts +1 -1
- package/dist/{terminal-graphics-D6KErRBY.js → terminal-graphics-DpSL29OQ.js} +1 -1
- package/dist/{tlog-theme-DIrlCw6m.js → tlog-theme-BnILsD3n.js} +10 -1203
- package/dist/vue/browser/types.d.ts +52 -0
- package/dist/vue/components/TBrowser.d.ts +231 -0
- package/dist/vue.cjs +25 -8
- package/dist/vue.js +8 -7
- package/package.json +16 -2
- /package/dist/{TMermaidText-wLnMMvVS.js → TMermaidText-C1akgJPU.js} +0 -0
- /package/dist/{math-image-BZhmfUeu.js → math-image-CcUpBnW0.js} +0 -0
- /package/dist/{path-mnSgEpCm.js → path-BI9W15Zt.js} +0 -0
- /package/dist/{wheel-scroll-DASkEZr-.js → wheel-scroll-CQ22Hcyj.js} +0 -0
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
import { TerminalGraphicsActivityKey, intersectRect, textCellWidth$1 as textCellWidth, translateRect, useLayout, useTerminal, useVisibility } from "./rect-JzWMJxxc.js";
|
|
2
|
+
import { TText, TView, createFrameMailbox } from "./frame-mailbox-DaTrfyaF.js";
|
|
3
|
+
import { createTerminalGraphicPngSequence, getTerminalGraphicsOutput, getTerminalGraphicsOutputVersion, subscribeTerminalGraphicsOutput } from "./terminal-graphics-DpSL29OQ.js";
|
|
4
|
+
import { TAgentTerminalGraphic } from "./TAgentTerminalGraphic-B8-YZfuP.js";
|
|
5
|
+
import { computed, defineComponent, getCurrentInstance, h, inject, markRaw, onBeforeUnmount, shallowRef, watch } from "vue";
|
|
6
|
+
|
|
7
|
+
//#region src/vue/components/TVideo.ts
|
|
8
|
+
const DEFAULT_MAX_FPS = 12;
|
|
9
|
+
const DEFAULT_MAX_PIXEL_WIDTH = 640;
|
|
10
|
+
const DEFAULT_MAX_PIXEL_HEIGHT = 360;
|
|
11
|
+
const MAX_PIXEL_DIMENSION = 4096;
|
|
12
|
+
const MAX_FRAME_BYTES = 8 * 1024 * 1024;
|
|
13
|
+
const MAX_GRAY8_FPS = 10;
|
|
14
|
+
const MAX_GRAY8_WIDTH = 512;
|
|
15
|
+
const MAX_GRAY8_HEIGHT = 256;
|
|
16
|
+
const ASCII_RAMP = " .:-=+*#%@";
|
|
17
|
+
const CONTROL_REFRESH_MS = 250;
|
|
18
|
+
const PLAYBACK_RATES = [
|
|
19
|
+
1,
|
|
20
|
+
2,
|
|
21
|
+
3
|
|
22
|
+
];
|
|
23
|
+
function positiveInt(value, fallback, max) {
|
|
24
|
+
const parsed = Math.floor(Number(value));
|
|
25
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return Math.min(max, fallback);
|
|
26
|
+
return Math.min(max, parsed);
|
|
27
|
+
}
|
|
28
|
+
function positiveNumber(value, fallback, max) {
|
|
29
|
+
const parsed = Number(value);
|
|
30
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
31
|
+
return Math.min(max, parsed);
|
|
32
|
+
}
|
|
33
|
+
function playbackRate(value) {
|
|
34
|
+
return value === 2 || value === 3 ? value : 1;
|
|
35
|
+
}
|
|
36
|
+
function optionalDuration(value) {
|
|
37
|
+
const parsed = Number(value);
|
|
38
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
39
|
+
}
|
|
40
|
+
function formatPlaybackTime(valueMs, hourDigits = 0) {
|
|
41
|
+
if (valueMs == null) return "--:--";
|
|
42
|
+
const seconds = Math.max(0, Math.floor(valueMs / 1e3));
|
|
43
|
+
const hours = Math.floor(seconds / 3600);
|
|
44
|
+
const minutes = Math.floor(seconds % 3600 / 60);
|
|
45
|
+
const remainder = seconds % 60;
|
|
46
|
+
if (hours > 0 || hourDigits > 0) return `${String(hours).padStart(Math.max(1, hourDigits), "0")}:${String(minutes).padStart(2, "0")}:${String(remainder).padStart(2, "0")}`;
|
|
47
|
+
return `${String(minutes).padStart(2, "0")}:${String(remainder).padStart(2, "0")}`;
|
|
48
|
+
}
|
|
49
|
+
function readUint32(bytes, offset) {
|
|
50
|
+
return bytes[offset] * 16777216 + (bytes[offset + 1] << 16) + (bytes[offset + 2] << 8) + bytes[offset + 3] >>> 0;
|
|
51
|
+
}
|
|
52
|
+
function normalizeFrame(frame) {
|
|
53
|
+
if (frame.format === "gray8") {
|
|
54
|
+
const pixelWidth$1 = Math.floor(Number(frame.pixelWidth));
|
|
55
|
+
const pixelHeight$1 = Math.floor(Number(frame.pixelHeight));
|
|
56
|
+
const pixels = frame.pixels;
|
|
57
|
+
if (!Number.isFinite(pixelWidth$1) || !Number.isFinite(pixelHeight$1) || pixelWidth$1 <= 0 || pixelHeight$1 <= 0 || pixelWidth$1 > MAX_GRAY8_WIDTH || pixelHeight$1 > MAX_GRAY8_HEIGHT || !(pixels instanceof Uint8Array) || pixels.length !== pixelWidth$1 * pixelHeight$1 || pixels.length > MAX_FRAME_BYTES) throw new Error("TVideo gray8 frame must match its bounded pixel dimensions");
|
|
58
|
+
return {
|
|
59
|
+
format: "gray8",
|
|
60
|
+
pixels,
|
|
61
|
+
timestampMs: Math.max(0, Number(frame.timestampMs) || 0),
|
|
62
|
+
pixelWidth: pixelWidth$1,
|
|
63
|
+
pixelHeight: pixelHeight$1,
|
|
64
|
+
fingerprint: frame.fingerprint,
|
|
65
|
+
durationMs: optionalDuration(frame.durationMs)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const png = frame.png;
|
|
69
|
+
if (!(png instanceof Uint8Array) || png.length < 24 || png.length > MAX_FRAME_BYTES) throw new Error("TVideo frame must contain a bounded PNG Uint8Array");
|
|
70
|
+
if (png[0] !== 137 || png[1] !== 80 || png[2] !== 78 || png[3] !== 71 || png[4] !== 13 || png[5] !== 10 || png[6] !== 26 || png[7] !== 10) throw new Error("TVideo frame source emitted invalid PNG data");
|
|
71
|
+
const pixelWidth = readUint32(png, 16);
|
|
72
|
+
const pixelHeight = readUint32(png, 20);
|
|
73
|
+
if (pixelWidth <= 0 || pixelHeight <= 0 || pixelWidth > MAX_PIXEL_DIMENSION || pixelHeight > MAX_PIXEL_DIMENSION) throw new Error(`TVideo frame has an unsupported size: ${pixelWidth}x${pixelHeight}`);
|
|
74
|
+
return {
|
|
75
|
+
format: "png",
|
|
76
|
+
png,
|
|
77
|
+
timestampMs: Math.max(0, Number(frame.timestampMs) || 0),
|
|
78
|
+
pixelWidth,
|
|
79
|
+
pixelHeight,
|
|
80
|
+
fingerprint: frame.fingerprint,
|
|
81
|
+
durationMs: optionalDuration(frame.durationMs)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function frameBytes(frame) {
|
|
85
|
+
return frame.format === "gray8" ? frame.pixels : frame.png;
|
|
86
|
+
}
|
|
87
|
+
function gray8ToAscii(frame, cellWidth) {
|
|
88
|
+
const lines = [];
|
|
89
|
+
const width = Math.max(1, Math.floor(cellWidth));
|
|
90
|
+
for (let y = 0; y < frame.pixelHeight; y++) {
|
|
91
|
+
let line = "";
|
|
92
|
+
const rowStart = y * frame.pixelWidth;
|
|
93
|
+
for (let x = 0; x < frame.pixelWidth; x++) {
|
|
94
|
+
const value = frame.pixels[rowStart + x];
|
|
95
|
+
const glyph = ASCII_RAMP[Math.min(ASCII_RAMP.length - 1, value * ASCII_RAMP.length >> 8)];
|
|
96
|
+
line += `${glyph}${glyph}`;
|
|
97
|
+
}
|
|
98
|
+
lines.push(line.slice(0, width));
|
|
99
|
+
}
|
|
100
|
+
return lines.join("\n");
|
|
101
|
+
}
|
|
102
|
+
function sameBytes(left, right) {
|
|
103
|
+
if (left.length !== right.length) return false;
|
|
104
|
+
for (let index = 0; index < left.length; index++) if (left[index] !== right[index]) return false;
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
function bytesToBase64(bytes) {
|
|
108
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
109
|
+
const chunks = [];
|
|
110
|
+
let chunk = "";
|
|
111
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
112
|
+
const a = bytes[index];
|
|
113
|
+
const hasB = index + 1 < bytes.length;
|
|
114
|
+
const hasC = index + 2 < bytes.length;
|
|
115
|
+
const b = hasB ? bytes[index + 1] : 0;
|
|
116
|
+
const c = hasC ? bytes[index + 2] : 0;
|
|
117
|
+
chunk += alphabet[a >> 2] + alphabet[(a & 3) << 4 | b >> 4] + (hasB ? alphabet[(b & 15) << 2 | c >> 6] : "=") + (hasC ? alphabet[c & 63] : "=");
|
|
118
|
+
if (chunk.length >= 16384) {
|
|
119
|
+
chunks.push(chunk);
|
|
120
|
+
chunk = "";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (chunk) chunks.push(chunk);
|
|
124
|
+
return chunks.join("");
|
|
125
|
+
}
|
|
126
|
+
function createFrameRenderer(frame, cellWidth) {
|
|
127
|
+
if (frame.format === "gray8") {
|
|
128
|
+
const text = gray8ToAscii(frame, cellWidth);
|
|
129
|
+
return () => ({
|
|
130
|
+
type: "text",
|
|
131
|
+
text
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
let base64;
|
|
135
|
+
return (_content, context) => {
|
|
136
|
+
if (!context.visible || !context.rawVisible) return null;
|
|
137
|
+
base64 ?? (base64 = bytesToBase64(frame.png));
|
|
138
|
+
const protocol = context.protocol;
|
|
139
|
+
if (protocol !== "kitty" && protocol !== "iterm2") return null;
|
|
140
|
+
return createTerminalGraphicPngSequence({
|
|
141
|
+
protocol,
|
|
142
|
+
base64,
|
|
143
|
+
imageId: context.imageId,
|
|
144
|
+
placementId: context.placementId,
|
|
145
|
+
cols: context.width,
|
|
146
|
+
rows: context.height,
|
|
147
|
+
sourceWidth: frame.pixelWidth,
|
|
148
|
+
sourceHeight: frame.pixelHeight,
|
|
149
|
+
placementColumns: Math.max(1, Math.floor(context.viewport.rect.w)),
|
|
150
|
+
placementRows: Math.max(1, Math.floor(context.viewport.rect.h)),
|
|
151
|
+
rect: context.viewport.rect,
|
|
152
|
+
fullRect: context.viewport.fullRect,
|
|
153
|
+
freeImageData: true
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function errorMessage(error) {
|
|
158
|
+
return error instanceof Error ? error.message : String(error);
|
|
159
|
+
}
|
|
160
|
+
const tVideoProps = {
|
|
161
|
+
x: {
|
|
162
|
+
type: Number,
|
|
163
|
+
required: true
|
|
164
|
+
},
|
|
165
|
+
y: {
|
|
166
|
+
type: Number,
|
|
167
|
+
required: true
|
|
168
|
+
},
|
|
169
|
+
w: {
|
|
170
|
+
type: Number,
|
|
171
|
+
required: true
|
|
172
|
+
},
|
|
173
|
+
h: {
|
|
174
|
+
type: Number,
|
|
175
|
+
required: true
|
|
176
|
+
},
|
|
177
|
+
zIndex: {
|
|
178
|
+
type: Number,
|
|
179
|
+
default: 0
|
|
180
|
+
},
|
|
181
|
+
src: {
|
|
182
|
+
type: String,
|
|
183
|
+
required: true
|
|
184
|
+
},
|
|
185
|
+
frameSource: {
|
|
186
|
+
type: Function,
|
|
187
|
+
required: true
|
|
188
|
+
},
|
|
189
|
+
paused: {
|
|
190
|
+
type: Boolean,
|
|
191
|
+
default: void 0
|
|
192
|
+
},
|
|
193
|
+
playbackRate: {
|
|
194
|
+
type: Number,
|
|
195
|
+
default: void 0
|
|
196
|
+
},
|
|
197
|
+
controls: {
|
|
198
|
+
type: Boolean,
|
|
199
|
+
default: false
|
|
200
|
+
},
|
|
201
|
+
controlsLayout: {
|
|
202
|
+
type: String,
|
|
203
|
+
default: "compact"
|
|
204
|
+
},
|
|
205
|
+
durationMs: {
|
|
206
|
+
type: Number,
|
|
207
|
+
default: void 0
|
|
208
|
+
},
|
|
209
|
+
loop: {
|
|
210
|
+
type: Boolean,
|
|
211
|
+
default: false
|
|
212
|
+
},
|
|
213
|
+
maxFps: {
|
|
214
|
+
type: Number,
|
|
215
|
+
default: DEFAULT_MAX_FPS
|
|
216
|
+
},
|
|
217
|
+
pixelWidth: {
|
|
218
|
+
type: Number,
|
|
219
|
+
default: void 0
|
|
220
|
+
},
|
|
221
|
+
pixelHeight: {
|
|
222
|
+
type: Number,
|
|
223
|
+
default: void 0
|
|
224
|
+
},
|
|
225
|
+
fallback: {
|
|
226
|
+
type: String,
|
|
227
|
+
default: "[video]"
|
|
228
|
+
},
|
|
229
|
+
style: {
|
|
230
|
+
type: Object,
|
|
231
|
+
default: void 0
|
|
232
|
+
},
|
|
233
|
+
clear: {
|
|
234
|
+
type: Boolean,
|
|
235
|
+
default: true
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
const TVideo = defineComponent({
|
|
239
|
+
name: "TVideo",
|
|
240
|
+
props: tVideoProps,
|
|
241
|
+
emits: {
|
|
242
|
+
frame: (_event) => true,
|
|
243
|
+
ended: () => true,
|
|
244
|
+
error: (_error) => true,
|
|
245
|
+
seek: (_event) => true,
|
|
246
|
+
"update:paused": (_paused) => true,
|
|
247
|
+
"update:playbackRate": (_rate) => true
|
|
248
|
+
},
|
|
249
|
+
setup(props, { emit }) {
|
|
250
|
+
const instance = getCurrentInstance();
|
|
251
|
+
const { terminal, scheduler } = useTerminal();
|
|
252
|
+
const layout = useLayout();
|
|
253
|
+
const { visible, rootProps } = useVisibility({ provide: true });
|
|
254
|
+
const graphicsActivity = inject(TerminalGraphicsActivityKey, null);
|
|
255
|
+
const frameRenderer = shallowRef();
|
|
256
|
+
const playbackError = shallowRef("");
|
|
257
|
+
const internalPaused = shallowRef(props.paused ?? false);
|
|
258
|
+
const internalPlaybackRate = shallowRef(playbackRate(props.playbackRate));
|
|
259
|
+
const discoveredDurationMs = shallowRef();
|
|
260
|
+
const displayedTimestampMs = shallowRef(0);
|
|
261
|
+
const seekPreviewMs = shallowRef();
|
|
262
|
+
const seeking = shallowRef(false);
|
|
263
|
+
const terminalSizeVersion = shallowRef(0);
|
|
264
|
+
const graphicsOutputVersion = shallowRef(getTerminalGraphicsOutputVersion(terminal));
|
|
265
|
+
const placementKey = `TVideo:${instance?.uid ?? "unknown"}`;
|
|
266
|
+
const mailboxId = `${placementKey}:frame`;
|
|
267
|
+
let alive = true;
|
|
268
|
+
let generation = 0;
|
|
269
|
+
let activeAbort = null;
|
|
270
|
+
let lastSourceBytes = null;
|
|
271
|
+
let lastQueuedBytes = null;
|
|
272
|
+
let lastQueuedFormat = null;
|
|
273
|
+
let lastQueuedFingerprint;
|
|
274
|
+
let lastTimestampMs = 0;
|
|
275
|
+
let droppedFrames = 0;
|
|
276
|
+
let pendingEndedGeneration = null;
|
|
277
|
+
let suppressNextVideoClick = false;
|
|
278
|
+
let controlPointerCapture = null;
|
|
279
|
+
const unsubscribeTerminalResize = terminal.on("resize", () => {
|
|
280
|
+
terminalSizeVersion.value++;
|
|
281
|
+
});
|
|
282
|
+
const unsubscribeGraphicsOutput = subscribeTerminalGraphicsOutput(terminal, () => {
|
|
283
|
+
graphicsOutputVersion.value = getTerminalGraphicsOutputVersion(terminal);
|
|
284
|
+
});
|
|
285
|
+
const showControls = computed(() => props.controls && Math.floor(props.w) >= 14 && Math.floor(props.h) >= 2);
|
|
286
|
+
const resolvedControlsLayout = computed(() => props.controlsLayout === "cinema" && Math.floor(props.h) >= 3 ? "cinema" : "compact");
|
|
287
|
+
const controlRows = computed(() => showControls.value ? resolvedControlsLayout.value === "cinema" ? 2 : 1 : 0);
|
|
288
|
+
const videoHeight = computed(() => Math.max(1, Math.floor(props.h) - controlRows.value));
|
|
289
|
+
const resolvedMaxFps = computed(() => positiveNumber(props.maxFps, DEFAULT_MAX_FPS, 60));
|
|
290
|
+
const resolvedPixelSize = computed(() => {
|
|
291
|
+
const desiredWidth = Math.max(2, Math.floor(props.w) * 8);
|
|
292
|
+
const desiredHeight = Math.max(2, videoHeight.value * 16);
|
|
293
|
+
if (props.pixelWidth == null && props.pixelHeight == null) {
|
|
294
|
+
const scale = Math.min(1, DEFAULT_MAX_PIXEL_WIDTH / desiredWidth, DEFAULT_MAX_PIXEL_HEIGHT / desiredHeight);
|
|
295
|
+
return {
|
|
296
|
+
width: Math.max(2, Math.floor(desiredWidth * scale)),
|
|
297
|
+
height: Math.max(2, Math.floor(desiredHeight * scale))
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (props.pixelWidth != null && props.pixelHeight == null) {
|
|
301
|
+
const width = positiveInt(props.pixelWidth, desiredWidth, MAX_PIXEL_DIMENSION);
|
|
302
|
+
const inferredHeight = width * desiredHeight / desiredWidth;
|
|
303
|
+
const scale = Math.min(1, MAX_PIXEL_DIMENSION / inferredHeight);
|
|
304
|
+
return {
|
|
305
|
+
width: Math.max(2, Math.floor(width * scale)),
|
|
306
|
+
height: Math.max(2, Math.floor(inferredHeight * scale))
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (props.pixelWidth == null && props.pixelHeight != null) {
|
|
310
|
+
const height = positiveInt(props.pixelHeight, desiredHeight, MAX_PIXEL_DIMENSION);
|
|
311
|
+
const inferredWidth = height * desiredWidth / desiredHeight;
|
|
312
|
+
const scale = Math.min(1, MAX_PIXEL_DIMENSION / inferredWidth);
|
|
313
|
+
return {
|
|
314
|
+
width: Math.max(2, Math.floor(inferredWidth * scale)),
|
|
315
|
+
height: Math.max(2, Math.floor(height * scale))
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
width: positiveInt(props.pixelWidth, desiredWidth, MAX_PIXEL_DIMENSION),
|
|
320
|
+
height: positiveInt(props.pixelHeight, desiredHeight, MAX_PIXEL_DIMENSION)
|
|
321
|
+
};
|
|
322
|
+
});
|
|
323
|
+
const resolvedPixelWidth = computed(() => resolvedPixelSize.value.width);
|
|
324
|
+
const resolvedPixelHeight = computed(() => resolvedPixelSize.value.height);
|
|
325
|
+
const paintable = computed(() => {
|
|
326
|
+
terminalSizeVersion.value;
|
|
327
|
+
if (!visible.value) return false;
|
|
328
|
+
const size = terminal.size();
|
|
329
|
+
const terminalRect = {
|
|
330
|
+
x: 0,
|
|
331
|
+
y: 0,
|
|
332
|
+
w: Math.max(0, Math.floor(size.cols)),
|
|
333
|
+
h: Math.max(0, Math.floor(size.rows))
|
|
334
|
+
};
|
|
335
|
+
const clip = layout.clipRect ? intersectRect(layout.clipRect, terminalRect) : terminalRect;
|
|
336
|
+
if (!clip) return false;
|
|
337
|
+
const rect = translateRect({
|
|
338
|
+
x: props.x,
|
|
339
|
+
y: props.y,
|
|
340
|
+
w: props.w,
|
|
341
|
+
h: videoHeight.value
|
|
342
|
+
}, layout.originX, layout.originY);
|
|
343
|
+
const visibleRect = intersectRect(rect, clip);
|
|
344
|
+
return Boolean(visibleRect && visibleRect.w > 0 && visibleRect.h > 0);
|
|
345
|
+
});
|
|
346
|
+
const graphicsProtocol = computed(() => {
|
|
347
|
+
graphicsOutputVersion.value;
|
|
348
|
+
return getTerminalGraphicsOutput(terminal)?.capabilities.preferredProtocol ?? null;
|
|
349
|
+
});
|
|
350
|
+
const preferredFormat = computed(() => graphicsProtocol.value === "kitty" || graphicsProtocol.value === "iterm2" ? "png" : "gray8");
|
|
351
|
+
const resolvedDecodeFps = computed(() => preferredFormat.value === "gray8" ? Math.min(MAX_GRAY8_FPS, resolvedMaxFps.value) : resolvedMaxFps.value);
|
|
352
|
+
const resolvedDecodeWidth = computed(() => preferredFormat.value === "gray8" ? positiveInt(Math.ceil(props.w / 2), 1, MAX_GRAY8_WIDTH) : resolvedPixelWidth.value);
|
|
353
|
+
const resolvedDecodeHeight = computed(() => preferredFormat.value === "gray8" ? positiveInt(videoHeight.value, 1, MAX_GRAY8_HEIGHT) : resolvedPixelHeight.value);
|
|
354
|
+
const resolvedDurationMs = computed(() => optionalDuration(props.durationMs) ?? discoveredDurationMs.value);
|
|
355
|
+
const effectivePaused = computed(() => props.paused ?? internalPaused.value);
|
|
356
|
+
const effectivePlaybackRate = computed(() => props.playbackRate == null ? internalPlaybackRate.value : playbackRate(props.playbackRate));
|
|
357
|
+
const controlTimestampMs = computed(() => seekPreviewMs.value ?? displayedTimestampMs.value);
|
|
358
|
+
const timeHourDigits = computed(() => {
|
|
359
|
+
const duration = resolvedDurationMs.value;
|
|
360
|
+
if (!duration || duration < 36e5) return 0;
|
|
361
|
+
return String(Math.floor(duration / 36e5)).length;
|
|
362
|
+
});
|
|
363
|
+
const currentTimeText = computed(() => formatPlaybackTime(controlTimestampMs.value, timeHourDigits.value));
|
|
364
|
+
const durationTimeText = computed(() => formatPlaybackTime(resolvedDurationMs.value, timeHourDigits.value));
|
|
365
|
+
const controlLayout = computed(() => {
|
|
366
|
+
const width = Math.max(1, Math.floor(props.w));
|
|
367
|
+
const progressY = videoHeight.value;
|
|
368
|
+
const current = currentTimeText.value;
|
|
369
|
+
const duration = durationTimeText.value;
|
|
370
|
+
const timeWidth = Math.max(textCellWidth(current), textCellWidth(duration));
|
|
371
|
+
const ratesWidth = 8;
|
|
372
|
+
if (resolvedControlsLayout.value === "cinema") {
|
|
373
|
+
const ratesX$1 = Math.max(3, width - ratesWidth);
|
|
374
|
+
const timeText = `${current} / ${duration}`;
|
|
375
|
+
return {
|
|
376
|
+
width,
|
|
377
|
+
kind: "cinema",
|
|
378
|
+
progressX: 0,
|
|
379
|
+
progressY,
|
|
380
|
+
progressWidth: width,
|
|
381
|
+
actionY: progressY + 1,
|
|
382
|
+
ratesX: ratesX$1,
|
|
383
|
+
showAllRates: true,
|
|
384
|
+
showTimes: 3 + textCellWidth(timeText) < ratesX$1,
|
|
385
|
+
current,
|
|
386
|
+
duration,
|
|
387
|
+
timeText,
|
|
388
|
+
timeWidth,
|
|
389
|
+
currentX: 3,
|
|
390
|
+
totalX: 0
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
const ratesX = width - ratesWidth;
|
|
394
|
+
const progressX = timeWidth + 4;
|
|
395
|
+
const totalX = ratesX - timeWidth - 1;
|
|
396
|
+
const progressWidth = totalX - progressX - 1;
|
|
397
|
+
if (progressWidth >= 4) return {
|
|
398
|
+
width,
|
|
399
|
+
kind: "compact",
|
|
400
|
+
progressX,
|
|
401
|
+
progressY,
|
|
402
|
+
progressWidth,
|
|
403
|
+
actionY: progressY,
|
|
404
|
+
ratesX,
|
|
405
|
+
showAllRates: true,
|
|
406
|
+
showTimes: true,
|
|
407
|
+
current,
|
|
408
|
+
duration,
|
|
409
|
+
timeText: "",
|
|
410
|
+
timeWidth,
|
|
411
|
+
currentX: 3,
|
|
412
|
+
totalX
|
|
413
|
+
};
|
|
414
|
+
const compactRatesX = Math.max(4, width - 2);
|
|
415
|
+
return {
|
|
416
|
+
width,
|
|
417
|
+
kind: "compact",
|
|
418
|
+
progressX: 3,
|
|
419
|
+
progressY,
|
|
420
|
+
progressWidth: Math.max(1, compactRatesX - 4),
|
|
421
|
+
actionY: progressY,
|
|
422
|
+
ratesX: compactRatesX,
|
|
423
|
+
showAllRates: false,
|
|
424
|
+
showTimes: false,
|
|
425
|
+
current,
|
|
426
|
+
duration,
|
|
427
|
+
timeText: "",
|
|
428
|
+
timeWidth,
|
|
429
|
+
currentX: 0,
|
|
430
|
+
totalX: 0
|
|
431
|
+
};
|
|
432
|
+
});
|
|
433
|
+
const rateStyles = computed(() => PLAYBACK_RATES.map((rate) => rate === effectivePlaybackRate.value ? {
|
|
434
|
+
...props.style,
|
|
435
|
+
inverse: true
|
|
436
|
+
} : props.style));
|
|
437
|
+
const remainingProgressStyle = computed(() => ({
|
|
438
|
+
...props.style,
|
|
439
|
+
dim: true
|
|
440
|
+
}));
|
|
441
|
+
const progressThumbStyle = computed(() => ({
|
|
442
|
+
...props.style,
|
|
443
|
+
inverse: true
|
|
444
|
+
}));
|
|
445
|
+
const progressSegments = computed(() => {
|
|
446
|
+
const { progressWidth } = controlLayout.value;
|
|
447
|
+
const duration = resolvedDurationMs.value;
|
|
448
|
+
if (!duration) return {
|
|
449
|
+
playedWidth: 0,
|
|
450
|
+
thumbWidth: 0,
|
|
451
|
+
remainingWidth: progressWidth
|
|
452
|
+
};
|
|
453
|
+
const ratio = Math.max(0, Math.min(1, controlTimestampMs.value / duration));
|
|
454
|
+
const marker = Math.round(ratio * Math.max(0, progressWidth - 1));
|
|
455
|
+
return {
|
|
456
|
+
playedWidth: marker,
|
|
457
|
+
thumbWidth: 1,
|
|
458
|
+
remainingWidth: progressWidth - marker - 1
|
|
459
|
+
};
|
|
460
|
+
});
|
|
461
|
+
const controlFill = computed(() => " ".repeat(controlLayout.value.width));
|
|
462
|
+
const progressGlyphs = computed(() => ({
|
|
463
|
+
played: "━".repeat(progressSegments.value.playedWidth),
|
|
464
|
+
remaining: "─".repeat(progressSegments.value.remainingWidth)
|
|
465
|
+
}));
|
|
466
|
+
const playbackEnabled = computed(() => alive && !effectivePaused.value && !graphicsActivity?.scrolling.value && props.src.trim().length > 0 && paintable.value);
|
|
467
|
+
const shouldPlay = computed(() => playbackEnabled.value && !seeking.value);
|
|
468
|
+
const visibleFallback = computed(() => playbackError.value ? `${props.fallback}: ${playbackError.value}` : props.fallback);
|
|
469
|
+
function timelineTimestamp(timestampMs) {
|
|
470
|
+
const duration = resolvedDurationMs.value;
|
|
471
|
+
if (!duration) return Math.max(0, timestampMs);
|
|
472
|
+
if (props.loop) return Math.max(0, timestampMs) % duration;
|
|
473
|
+
return Math.max(0, Math.min(duration, timestampMs));
|
|
474
|
+
}
|
|
475
|
+
function seekTimestamp(timestampMs) {
|
|
476
|
+
const duration = resolvedDurationMs.value;
|
|
477
|
+
if (!duration) return Math.max(0, timestampMs);
|
|
478
|
+
const end = props.loop ? Math.max(0, duration - 1) : duration;
|
|
479
|
+
return Math.max(0, Math.min(end, timestampMs));
|
|
480
|
+
}
|
|
481
|
+
function updateTimestamp(timestampMs, force = false) {
|
|
482
|
+
const next = timelineTimestamp(timestampMs);
|
|
483
|
+
lastTimestampMs = next;
|
|
484
|
+
if (!showControls.value) return;
|
|
485
|
+
const threshold = CONTROL_REFRESH_MS * effectivePlaybackRate.value;
|
|
486
|
+
if (force || next < displayedTimestampMs.value || Math.abs(next - displayedTimestampMs.value) >= threshold) displayedTimestampMs.value = next;
|
|
487
|
+
}
|
|
488
|
+
function commitFrame(frame, dropped) {
|
|
489
|
+
if (!alive) return;
|
|
490
|
+
droppedFrames += dropped;
|
|
491
|
+
frameRenderer.value = markRaw(createFrameRenderer(frame, props.w));
|
|
492
|
+
updateTimestamp(frame.timestampMs);
|
|
493
|
+
const durationMs = resolvedDurationMs.value;
|
|
494
|
+
emit("frame", {
|
|
495
|
+
timestampMs: frame.timestampMs,
|
|
496
|
+
pixelWidth: frame.pixelWidth,
|
|
497
|
+
pixelHeight: frame.pixelHeight,
|
|
498
|
+
droppedFrames,
|
|
499
|
+
...durationMs ? { durationMs } : {},
|
|
500
|
+
playbackRate: effectivePlaybackRate.value
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
const mailbox = createFrameMailbox({
|
|
504
|
+
scheduler,
|
|
505
|
+
id: mailboxId,
|
|
506
|
+
reason: "stream",
|
|
507
|
+
priority: "low",
|
|
508
|
+
apply(frame, _context, meta) {
|
|
509
|
+
commitFrame(frame, meta.dropped);
|
|
510
|
+
if (pendingEndedGeneration === generation) {
|
|
511
|
+
pendingEndedGeneration = null;
|
|
512
|
+
emit("ended");
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
function stopPlayback() {
|
|
517
|
+
generation++;
|
|
518
|
+
pendingEndedGeneration = null;
|
|
519
|
+
const discardedPendingFrame = mailbox.hasPending();
|
|
520
|
+
mailbox.cancel();
|
|
521
|
+
if (discardedPendingFrame) {
|
|
522
|
+
lastQueuedBytes = null;
|
|
523
|
+
lastQueuedFormat = null;
|
|
524
|
+
lastQueuedFingerprint = void 0;
|
|
525
|
+
}
|
|
526
|
+
activeAbort?.abort();
|
|
527
|
+
activeAbort = null;
|
|
528
|
+
}
|
|
529
|
+
function resetQueuedFrameIdentity() {
|
|
530
|
+
lastSourceBytes = null;
|
|
531
|
+
lastQueuedBytes = null;
|
|
532
|
+
lastQueuedFormat = null;
|
|
533
|
+
lastQueuedFingerprint = void 0;
|
|
534
|
+
}
|
|
535
|
+
function setPaused(paused) {
|
|
536
|
+
if (effectivePaused.value === paused) return;
|
|
537
|
+
if (props.paused == null) internalPaused.value = paused;
|
|
538
|
+
emit("update:paused", paused);
|
|
539
|
+
}
|
|
540
|
+
function setPlaybackRate(rate) {
|
|
541
|
+
const next = playbackRate(rate);
|
|
542
|
+
if (effectivePlaybackRate.value === next) return;
|
|
543
|
+
if (props.playbackRate == null) internalPlaybackRate.value = next;
|
|
544
|
+
emit("update:playbackRate", next);
|
|
545
|
+
}
|
|
546
|
+
function applySeek(timestampMs, restart) {
|
|
547
|
+
const parsed = Number(timestampMs);
|
|
548
|
+
if (!Number.isFinite(parsed)) return;
|
|
549
|
+
stopPlayback();
|
|
550
|
+
resetQueuedFrameIdentity();
|
|
551
|
+
const next = seekTimestamp(parsed);
|
|
552
|
+
updateTimestamp(next, true);
|
|
553
|
+
seekPreviewMs.value = void 0;
|
|
554
|
+
const durationMs = resolvedDurationMs.value;
|
|
555
|
+
emit("seek", {
|
|
556
|
+
timestampMs: next,
|
|
557
|
+
...durationMs ? { durationMs } : {}
|
|
558
|
+
});
|
|
559
|
+
if (restart) startPlayback(effectivePaused.value);
|
|
560
|
+
}
|
|
561
|
+
function controlCell(event) {
|
|
562
|
+
return {
|
|
563
|
+
x: Math.floor(event.cellX - layout.originX - props.x),
|
|
564
|
+
y: Math.floor(event.cellY - layout.originY - props.y)
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function previewSeekAt(cellX) {
|
|
568
|
+
const duration = resolvedDurationMs.value;
|
|
569
|
+
if (!duration) return;
|
|
570
|
+
const { progressX, progressWidth } = controlLayout.value;
|
|
571
|
+
const offset = Math.max(0, Math.min(progressWidth - 1, cellX - progressX));
|
|
572
|
+
seekPreviewMs.value = progressWidth <= 1 ? 0 : offset / Math.max(1, progressWidth - 1) * duration;
|
|
573
|
+
}
|
|
574
|
+
function captureControlPointer(event) {
|
|
575
|
+
const native = event.nativeEvent;
|
|
576
|
+
const pointerId = native?.pointerId;
|
|
577
|
+
const target = native?.target;
|
|
578
|
+
if (pointerId == null || !target?.setPointerCapture) return;
|
|
579
|
+
target.setPointerCapture(pointerId);
|
|
580
|
+
controlPointerCapture = {
|
|
581
|
+
pointerId,
|
|
582
|
+
target
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function releaseControlPointer() {
|
|
586
|
+
if (!controlPointerCapture) return;
|
|
587
|
+
controlPointerCapture.target.releasePointerCapture?.(controlPointerCapture.pointerId);
|
|
588
|
+
controlPointerCapture = null;
|
|
589
|
+
}
|
|
590
|
+
function rateAt(cellX) {
|
|
591
|
+
const { ratesX, showAllRates } = controlLayout.value;
|
|
592
|
+
const offset = cellX - ratesX;
|
|
593
|
+
if (!showAllRates) {
|
|
594
|
+
if (offset < 0 || offset >= 2) return void 0;
|
|
595
|
+
const index = PLAYBACK_RATES.indexOf(effectivePlaybackRate.value);
|
|
596
|
+
return PLAYBACK_RATES[(index + 1) % PLAYBACK_RATES.length];
|
|
597
|
+
}
|
|
598
|
+
if (offset < 0 || offset % 3 >= 2) return void 0;
|
|
599
|
+
return PLAYBACK_RATES[Math.floor(offset / 3)];
|
|
600
|
+
}
|
|
601
|
+
function onInteractivePointerDown(event) {
|
|
602
|
+
if (event.button != null && event.button !== 0) return;
|
|
603
|
+
const { x: cellX, y: cellY } = controlCell(event);
|
|
604
|
+
if (cellY < videoHeight.value) {
|
|
605
|
+
suppressNextVideoClick = false;
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (!showControls.value) return;
|
|
609
|
+
const { actionY, progressY, progressX, progressWidth } = controlLayout.value;
|
|
610
|
+
const rate = rateAt(cellX);
|
|
611
|
+
if (cellY === actionY && cellX <= 1) {
|
|
612
|
+
event.preventDefault();
|
|
613
|
+
event.stopPropagation();
|
|
614
|
+
suppressNextVideoClick = true;
|
|
615
|
+
setPaused(!effectivePaused.value);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (cellY === actionY && rate) {
|
|
619
|
+
event.preventDefault();
|
|
620
|
+
event.stopPropagation();
|
|
621
|
+
suppressNextVideoClick = true;
|
|
622
|
+
setPlaybackRate(rate);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (cellY !== progressY || !resolvedDurationMs.value || cellX < progressX || cellX >= progressX + progressWidth) return;
|
|
626
|
+
event.preventDefault();
|
|
627
|
+
event.stopPropagation();
|
|
628
|
+
suppressNextVideoClick = true;
|
|
629
|
+
seeking.value = true;
|
|
630
|
+
stopPlayback();
|
|
631
|
+
previewSeekAt(cellX);
|
|
632
|
+
captureControlPointer(event);
|
|
633
|
+
}
|
|
634
|
+
function onInteractivePointerMove(event) {
|
|
635
|
+
if (!seeking.value) return;
|
|
636
|
+
if (event.buttons === 0) {
|
|
637
|
+
onInteractivePointerUp(event);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
event.preventDefault();
|
|
641
|
+
event.stopPropagation();
|
|
642
|
+
previewSeekAt(controlCell(event).x);
|
|
643
|
+
}
|
|
644
|
+
function onInteractivePointerUp(event) {
|
|
645
|
+
if (!seeking.value) return;
|
|
646
|
+
event.preventDefault();
|
|
647
|
+
event.stopPropagation();
|
|
648
|
+
previewSeekAt(controlCell(event).x);
|
|
649
|
+
const timestampMs = seekPreviewMs.value ?? lastTimestampMs;
|
|
650
|
+
applySeek(timestampMs, false);
|
|
651
|
+
seeking.value = false;
|
|
652
|
+
releaseControlPointer();
|
|
653
|
+
startPlayback(effectivePaused.value);
|
|
654
|
+
}
|
|
655
|
+
function onVideoClick(event) {
|
|
656
|
+
if (event.button != null && event.button !== 0) return;
|
|
657
|
+
if (suppressNextVideoClick) {
|
|
658
|
+
suppressNextVideoClick = false;
|
|
659
|
+
event.preventDefault();
|
|
660
|
+
event.stopPropagation();
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (controlCell(event).y >= videoHeight.value) return;
|
|
664
|
+
event.preventDefault();
|
|
665
|
+
event.stopPropagation();
|
|
666
|
+
setPaused(!effectivePaused.value);
|
|
667
|
+
}
|
|
668
|
+
function finishControlSeek() {
|
|
669
|
+
if (!seeking.value) return;
|
|
670
|
+
const timestampMs = seekPreviewMs.value ?? lastTimestampMs;
|
|
671
|
+
applySeek(timestampMs, false);
|
|
672
|
+
seeking.value = false;
|
|
673
|
+
releaseControlPointer();
|
|
674
|
+
startPlayback(effectivePaused.value);
|
|
675
|
+
}
|
|
676
|
+
function onControlKeydown(event) {
|
|
677
|
+
if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
|
|
678
|
+
event.preventDefault();
|
|
679
|
+
event.stopPropagation();
|
|
680
|
+
setPaused(!effectivePaused.value);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
|
684
|
+
event.preventDefault();
|
|
685
|
+
event.stopPropagation();
|
|
686
|
+
const index = PLAYBACK_RATES.indexOf(effectivePlaybackRate.value);
|
|
687
|
+
const offset = event.key === "ArrowUp" ? 1 : -1;
|
|
688
|
+
setPlaybackRate(PLAYBACK_RATES[Math.max(0, Math.min(2, index + offset))]);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
if (!event.repeat && (event.key === "Home" || event.key === "End") && resolvedDurationMs.value) {
|
|
692
|
+
event.preventDefault();
|
|
693
|
+
event.stopPropagation();
|
|
694
|
+
applySeek(event.key === "Home" ? 0 : resolvedDurationMs.value, true);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if (event.key === "1" || event.key === "2" || event.key === "3") {
|
|
698
|
+
event.preventDefault();
|
|
699
|
+
event.stopPropagation();
|
|
700
|
+
setPlaybackRate(Number(event.key));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function queueFrame(frame, expectedFormat) {
|
|
704
|
+
let normalized = normalizeFrame(frame);
|
|
705
|
+
if (normalized.format !== expectedFormat) throw new Error(`TVideo frame source did not provide requested ${expectedFormat} frames`);
|
|
706
|
+
if (normalized.durationMs != null && normalized.durationMs !== discoveredDurationMs.value) discoveredDurationMs.value = normalized.durationMs;
|
|
707
|
+
normalized = {
|
|
708
|
+
...normalized,
|
|
709
|
+
timestampMs: timelineTimestamp(normalized.timestampMs)
|
|
710
|
+
};
|
|
711
|
+
const sourceBytes = frameBytes(normalized);
|
|
712
|
+
const reusesSourceBuffer = sourceBytes === lastSourceBytes;
|
|
713
|
+
lastSourceBytes = sourceBytes;
|
|
714
|
+
const stableFrame = reusesSourceBuffer ? normalized.format === "gray8" ? {
|
|
715
|
+
...normalized,
|
|
716
|
+
pixels: new Uint8Array(normalized.pixels)
|
|
717
|
+
} : {
|
|
718
|
+
...normalized,
|
|
719
|
+
png: new Uint8Array(normalized.png)
|
|
720
|
+
} : normalized;
|
|
721
|
+
const bytes = frameBytes(stableFrame);
|
|
722
|
+
const sameFingerprint = normalized.format === lastQueuedFormat && normalized.fingerprint != null && lastQueuedFingerprint != null && normalized.fingerprint === lastQueuedFingerprint;
|
|
723
|
+
const canCompareBytes = normalized.fingerprint == null && lastQueuedFingerprint == null && !reusesSourceBuffer;
|
|
724
|
+
if (sameFingerprint || canCompareBytes && normalized.format === lastQueuedFormat && lastQueuedBytes && sameBytes(lastQueuedBytes, sourceBytes)) {
|
|
725
|
+
lastQueuedBytes = bytes;
|
|
726
|
+
lastQueuedFormat = normalized.format;
|
|
727
|
+
lastQueuedFingerprint = normalized.fingerprint;
|
|
728
|
+
updateTimestamp(normalized.timestampMs);
|
|
729
|
+
if (mailbox.hasPending()) mailbox.replacePending(stableFrame);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
lastQueuedBytes = bytes;
|
|
733
|
+
lastQueuedFormat = normalized.format;
|
|
734
|
+
lastQueuedFingerprint = normalized.fingerprint;
|
|
735
|
+
if (!mailbox.queue(stableFrame)) {
|
|
736
|
+
commitFrame(stableFrame, 0);
|
|
737
|
+
scheduler.invalidate({
|
|
738
|
+
priority: "low",
|
|
739
|
+
reason: "stream"
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function startPlayback(oneFrame = false) {
|
|
744
|
+
stopPlayback();
|
|
745
|
+
if (!shouldPlay.value && !(oneFrame && alive && props.src.trim().length > 0 && paintable.value && !graphicsActivity?.scrolling.value)) return;
|
|
746
|
+
const currentGeneration = generation;
|
|
747
|
+
const abort = new AbortController();
|
|
748
|
+
activeAbort = abort;
|
|
749
|
+
playbackError.value = "";
|
|
750
|
+
const source = props.frameSource;
|
|
751
|
+
const format = preferredFormat.value;
|
|
752
|
+
const context = {
|
|
753
|
+
src: props.src,
|
|
754
|
+
signal: abort.signal,
|
|
755
|
+
maxFps: resolvedDecodeFps.value,
|
|
756
|
+
pixelWidth: resolvedDecodeWidth.value,
|
|
757
|
+
pixelHeight: resolvedDecodeHeight.value,
|
|
758
|
+
startAtMs: lastTimestampMs,
|
|
759
|
+
preferredFormat: format,
|
|
760
|
+
playbackRate: effectivePlaybackRate.value,
|
|
761
|
+
loop: props.loop
|
|
762
|
+
};
|
|
763
|
+
(async () => {
|
|
764
|
+
try {
|
|
765
|
+
const frames = await source(context);
|
|
766
|
+
for await (const frame of frames) {
|
|
767
|
+
if (!alive || abort.signal.aborted || currentGeneration !== generation) return;
|
|
768
|
+
queueFrame(frame, format);
|
|
769
|
+
if (oneFrame) return;
|
|
770
|
+
}
|
|
771
|
+
if (!alive || abort.signal.aborted || currentGeneration !== generation) return;
|
|
772
|
+
if (mailbox.hasPending()) pendingEndedGeneration = currentGeneration;
|
|
773
|
+
else emit("ended");
|
|
774
|
+
} catch (error) {
|
|
775
|
+
if (!alive || abort.signal.aborted || currentGeneration !== generation) return;
|
|
776
|
+
playbackError.value = errorMessage(error);
|
|
777
|
+
emit("error", error);
|
|
778
|
+
scheduler.invalidate({
|
|
779
|
+
priority: "low",
|
|
780
|
+
reason: "data"
|
|
781
|
+
});
|
|
782
|
+
} finally {
|
|
783
|
+
if (activeAbort === abort) activeAbort = null;
|
|
784
|
+
}
|
|
785
|
+
})();
|
|
786
|
+
}
|
|
787
|
+
watch(() => props.paused, (paused) => {
|
|
788
|
+
if (paused != null) internalPaused.value = paused;
|
|
789
|
+
});
|
|
790
|
+
watch(() => props.playbackRate, (rate) => {
|
|
791
|
+
if (rate != null) internalPlaybackRate.value = playbackRate(rate);
|
|
792
|
+
});
|
|
793
|
+
watch([
|
|
794
|
+
() => props.src,
|
|
795
|
+
() => props.frameSource,
|
|
796
|
+
playbackEnabled,
|
|
797
|
+
resolvedDecodeFps,
|
|
798
|
+
resolvedDecodeWidth,
|
|
799
|
+
resolvedDecodeHeight,
|
|
800
|
+
preferredFormat,
|
|
801
|
+
effectivePlaybackRate,
|
|
802
|
+
() => props.loop
|
|
803
|
+
], (next, previous) => {
|
|
804
|
+
const sourceChanged = !previous || next[0] !== previous[0] || next[1] !== previous[1];
|
|
805
|
+
const formatChanged = !previous || next[6] !== previous[6];
|
|
806
|
+
if (sourceChanged || formatChanged) {
|
|
807
|
+
resetQueuedFrameIdentity();
|
|
808
|
+
if (sourceChanged) {
|
|
809
|
+
lastTimestampMs = 0;
|
|
810
|
+
displayedTimestampMs.value = 0;
|
|
811
|
+
discoveredDurationMs.value = void 0;
|
|
812
|
+
seekPreviewMs.value = void 0;
|
|
813
|
+
seeking.value = false;
|
|
814
|
+
suppressNextVideoClick = false;
|
|
815
|
+
releaseControlPointer();
|
|
816
|
+
droppedFrames = 0;
|
|
817
|
+
}
|
|
818
|
+
playbackError.value = "";
|
|
819
|
+
frameRenderer.value = void 0;
|
|
820
|
+
}
|
|
821
|
+
startPlayback();
|
|
822
|
+
}, {
|
|
823
|
+
immediate: true,
|
|
824
|
+
flush: "post"
|
|
825
|
+
});
|
|
826
|
+
onBeforeUnmount(() => {
|
|
827
|
+
alive = false;
|
|
828
|
+
stopPlayback();
|
|
829
|
+
releaseControlPointer();
|
|
830
|
+
mailbox.dispose();
|
|
831
|
+
unsubscribeTerminalResize();
|
|
832
|
+
unsubscribeGraphicsOutput();
|
|
833
|
+
});
|
|
834
|
+
return () => {
|
|
835
|
+
const children = [h(TAgentTerminalGraphic, {
|
|
836
|
+
x: props.x,
|
|
837
|
+
y: props.y,
|
|
838
|
+
w: props.w,
|
|
839
|
+
h: videoHeight.value,
|
|
840
|
+
zIndex: props.zIndex,
|
|
841
|
+
content: props.src.trim() ? props.src : "video",
|
|
842
|
+
fallback: visibleFallback.value,
|
|
843
|
+
style: props.style,
|
|
844
|
+
clear: props.clear,
|
|
845
|
+
final: false,
|
|
846
|
+
streaming: true,
|
|
847
|
+
renderer: frameRenderer.value,
|
|
848
|
+
placementKey,
|
|
849
|
+
preserveRawWhileRendering: true,
|
|
850
|
+
ignoreSamePlaneRawCoverage: true,
|
|
851
|
+
suspended: !paintable.value
|
|
852
|
+
})];
|
|
853
|
+
if (props.controls) children.push(h(TView, {
|
|
854
|
+
x: props.x,
|
|
855
|
+
y: props.y,
|
|
856
|
+
w: Math.max(0, Math.floor(props.w)),
|
|
857
|
+
h: Math.max(0, Math.floor(props.h)),
|
|
858
|
+
zIndex: props.zIndex,
|
|
859
|
+
focusable: true,
|
|
860
|
+
selectable: false,
|
|
861
|
+
onClick: onVideoClick,
|
|
862
|
+
onPointerdown: onInteractivePointerDown,
|
|
863
|
+
onPointermove: onInteractivePointerMove,
|
|
864
|
+
onPointerup: onInteractivePointerUp,
|
|
865
|
+
onKeydown: onControlKeydown,
|
|
866
|
+
onBlur: finishControlSeek
|
|
867
|
+
}, () => {
|
|
868
|
+
if (!showControls.value) return [];
|
|
869
|
+
const layout$1 = controlLayout.value;
|
|
870
|
+
const segments = progressSegments.value;
|
|
871
|
+
const controls = Array.from({ length: controlRows.value }, (_, index) => h(TText, {
|
|
872
|
+
x: 0,
|
|
873
|
+
y: videoHeight.value + index,
|
|
874
|
+
w: layout$1.width,
|
|
875
|
+
value: controlFill.value,
|
|
876
|
+
style: props.style
|
|
877
|
+
}));
|
|
878
|
+
controls.push(h(TText, {
|
|
879
|
+
x: 0,
|
|
880
|
+
y: layout$1.actionY,
|
|
881
|
+
w: 2,
|
|
882
|
+
value: effectivePaused.value ? "> " : "||",
|
|
883
|
+
style: props.style
|
|
884
|
+
}));
|
|
885
|
+
if (layout$1.showTimes) if (layout$1.kind === "cinema") controls.push(h(TText, {
|
|
886
|
+
x: layout$1.currentX,
|
|
887
|
+
y: layout$1.actionY,
|
|
888
|
+
w: textCellWidth(layout$1.timeText),
|
|
889
|
+
value: layout$1.timeText,
|
|
890
|
+
style: props.style
|
|
891
|
+
}));
|
|
892
|
+
else controls.push(h(TText, {
|
|
893
|
+
x: layout$1.currentX,
|
|
894
|
+
y: layout$1.actionY,
|
|
895
|
+
w: layout$1.timeWidth,
|
|
896
|
+
value: layout$1.current,
|
|
897
|
+
style: props.style
|
|
898
|
+
}), h(TText, {
|
|
899
|
+
x: layout$1.totalX,
|
|
900
|
+
y: layout$1.actionY,
|
|
901
|
+
w: layout$1.timeWidth,
|
|
902
|
+
value: layout$1.duration,
|
|
903
|
+
style: props.style
|
|
904
|
+
}));
|
|
905
|
+
if (segments.playedWidth > 0) controls.push(h(TText, {
|
|
906
|
+
x: layout$1.progressX,
|
|
907
|
+
y: layout$1.progressY,
|
|
908
|
+
w: segments.playedWidth,
|
|
909
|
+
value: progressGlyphs.value.played,
|
|
910
|
+
style: props.style
|
|
911
|
+
}));
|
|
912
|
+
if (segments.thumbWidth > 0) controls.push(h(TText, {
|
|
913
|
+
x: layout$1.progressX + segments.playedWidth,
|
|
914
|
+
y: layout$1.progressY,
|
|
915
|
+
w: 1,
|
|
916
|
+
value: "╋",
|
|
917
|
+
style: progressThumbStyle.value
|
|
918
|
+
}));
|
|
919
|
+
if (segments.remainingWidth > 0) controls.push(h(TText, {
|
|
920
|
+
x: layout$1.progressX + segments.playedWidth + segments.thumbWidth,
|
|
921
|
+
y: layout$1.progressY,
|
|
922
|
+
w: segments.remainingWidth,
|
|
923
|
+
value: progressGlyphs.value.remaining,
|
|
924
|
+
style: remainingProgressStyle.value
|
|
925
|
+
}));
|
|
926
|
+
if (layout$1.showAllRates) controls.push(...PLAYBACK_RATES.map((rate, index) => h(TText, {
|
|
927
|
+
x: layout$1.ratesX + index * 3,
|
|
928
|
+
y: layout$1.actionY,
|
|
929
|
+
w: 2,
|
|
930
|
+
value: `${rate}x`,
|
|
931
|
+
style: rateStyles.value[index]
|
|
932
|
+
})));
|
|
933
|
+
else controls.push(h(TText, {
|
|
934
|
+
x: layout$1.ratesX,
|
|
935
|
+
y: layout$1.actionY,
|
|
936
|
+
w: 2,
|
|
937
|
+
value: `${effectivePlaybackRate.value}x`,
|
|
938
|
+
style: rateStyles.value[PLAYBACK_RATES.indexOf(effectivePlaybackRate.value)]
|
|
939
|
+
}));
|
|
940
|
+
return controls;
|
|
941
|
+
}));
|
|
942
|
+
return h("span", rootProps, children);
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
//#endregion
|
|
948
|
+
export { TVideo };
|