pa_encoder 0.2.8 → 0.3.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 +14 -7
- package/dist/browser/index.js +2 -2
- package/dist/index.js +219 -69
- package/dist/ui/encoder.html +38 -1
- package/dist/ui/encoder.js +121 -11
- package/dist/ui/preview.js +159 -18
- package/dist/worker.js +11 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ async function startLiveCapture({
|
|
|
5
5
|
fps = 30,
|
|
6
6
|
concurrency = 2,
|
|
7
7
|
maxQueue = 8,
|
|
8
|
+
maxPendingCaptures = null,
|
|
8
9
|
policy = "drop",
|
|
9
10
|
// "drop" | "block"
|
|
10
11
|
onProgress,
|
|
@@ -22,6 +23,9 @@ async function startLiveCapture({
|
|
|
22
23
|
throw new TypeError("concurrency must be >= 1");
|
|
23
24
|
if (!Number.isInteger(maxQueue) || maxQueue < 0)
|
|
24
25
|
throw new TypeError("maxQueue must be >= 0");
|
|
26
|
+
if (maxPendingCaptures != null && (!Number.isInteger(maxPendingCaptures) || maxPendingCaptures < 1)) {
|
|
27
|
+
throw new TypeError("maxPendingCaptures must be >= 1");
|
|
28
|
+
}
|
|
25
29
|
if (policy !== "drop" && policy !== "block")
|
|
26
30
|
throw new TypeError('policy must be "drop" or "block"');
|
|
27
31
|
const worker = new Worker(new URL("./worker.js", import.meta.url), {
|
|
@@ -32,22 +36,31 @@ async function startLiveCapture({
|
|
|
32
36
|
var _a, _b;
|
|
33
37
|
if (((_a = ev.data) == null ? void 0 : _a.type) === "ready") {
|
|
34
38
|
worker.removeEventListener("message", onMsg);
|
|
39
|
+
worker.removeEventListener("error", onErr);
|
|
35
40
|
resolve();
|
|
36
41
|
} else if (((_b = ev.data) == null ? void 0 : _b.type) === "error") {
|
|
37
42
|
worker.removeEventListener("message", onMsg);
|
|
43
|
+
worker.removeEventListener("error", onErr);
|
|
38
44
|
reject(new Error(ev.data.message || "Worker error"));
|
|
39
45
|
}
|
|
40
46
|
};
|
|
47
|
+
const onErr = (ev) => {
|
|
48
|
+
worker.removeEventListener("message", onMsg);
|
|
49
|
+
worker.removeEventListener("error", onErr);
|
|
50
|
+
reject(ev.error || new Error("Worker failed to initialize"));
|
|
51
|
+
};
|
|
41
52
|
worker.addEventListener("message", onMsg);
|
|
42
|
-
worker.addEventListener("error",
|
|
53
|
+
worker.addEventListener("error", onErr);
|
|
43
54
|
});
|
|
44
55
|
const stats = {
|
|
45
56
|
captured: 0,
|
|
46
57
|
encoded: 0,
|
|
47
58
|
written: 0,
|
|
48
59
|
dropped: 0,
|
|
60
|
+
failed: 0,
|
|
49
61
|
queueMax: 0,
|
|
50
62
|
inFlightMax: 0,
|
|
63
|
+
pendingCaptureMax: 0,
|
|
51
64
|
lastBitmapMs: 0,
|
|
52
65
|
bitmapMsAvg: 0,
|
|
53
66
|
lastEncodeMs: 0,
|
|
@@ -55,65 +68,171 @@ async function startLiveCapture({
|
|
|
55
68
|
lastWriteMs: 0,
|
|
56
69
|
writeMsAvg: 0
|
|
57
70
|
};
|
|
71
|
+
const maxCaptureTasks = maxPendingCaptures == null ? Math.max(1, Math.min(4, concurrency)) : maxPendingCaptures;
|
|
72
|
+
const frameIntervalMs = 1e3 / fps;
|
|
73
|
+
const maxCatchUpFrames = 4;
|
|
74
|
+
const maxAccumMs = frameIntervalMs * maxCatchUpFrames;
|
|
58
75
|
let stopped = false;
|
|
59
76
|
let rafId = null;
|
|
77
|
+
let fatalError = null;
|
|
60
78
|
let inFlight = 0;
|
|
79
|
+
let pendingCaptures = 0;
|
|
80
|
+
let accMs = 0;
|
|
81
|
+
let lastTickMs = null;
|
|
82
|
+
let seq = 0;
|
|
61
83
|
const queue = [];
|
|
62
84
|
const waiters = [];
|
|
63
85
|
const ack = /* @__PURE__ */ new Map();
|
|
86
|
+
const captureTasks = /* @__PURE__ */ new Set();
|
|
87
|
+
let doneSettled = false;
|
|
88
|
+
let resolveDone = null;
|
|
89
|
+
const done = new Promise((resolve) => {
|
|
90
|
+
resolveDone = resolve;
|
|
91
|
+
});
|
|
92
|
+
function isAbortError(err) {
|
|
93
|
+
return (err == null ? void 0 : err.name) === "AbortError" || (err == null ? void 0 : err.message) === "stopped" || (err == null ? void 0 : err.message) === "Aborted";
|
|
94
|
+
}
|
|
95
|
+
function emitProgress() {
|
|
96
|
+
onProgress == null ? void 0 : onProgress({ ...stats });
|
|
97
|
+
}
|
|
64
98
|
function throwIfAborted() {
|
|
99
|
+
if (fatalError) throw fatalError;
|
|
65
100
|
if (signal == null ? void 0 : signal.aborted) {
|
|
66
101
|
throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
67
102
|
}
|
|
68
103
|
}
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
104
|
+
function wakeAllWaiters() {
|
|
105
|
+
while (waiters.length) {
|
|
106
|
+
const w = waiters.shift();
|
|
107
|
+
if (w) w();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function releaseBitmap(bitmap) {
|
|
111
|
+
if (typeof (bitmap == null ? void 0 : bitmap.close) === "function") {
|
|
112
|
+
try {
|
|
113
|
+
bitmap.close();
|
|
114
|
+
} catch {
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function settleDone(value) {
|
|
119
|
+
if (doneSettled) return;
|
|
120
|
+
doneSettled = true;
|
|
121
|
+
resolveDone == null ? void 0 : resolveDone(value);
|
|
122
|
+
}
|
|
123
|
+
function dispatchFrame(item) {
|
|
124
|
+
inFlight++;
|
|
125
|
+
stats.inFlightMax = Math.max(stats.inFlightMax, inFlight);
|
|
126
|
+
const { seq: seq2, bitmap, w, h } = item;
|
|
127
|
+
const p = new Promise((resolve, reject) => ack.set(seq2, { resolve, reject }));
|
|
128
|
+
worker.postMessage(
|
|
129
|
+
{ type: "encode", frameIndex: seq2, bitmap, width: w, height: h },
|
|
130
|
+
[bitmap]
|
|
131
|
+
);
|
|
132
|
+
p.catch(() => {
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function dispatchIfPossible() {
|
|
136
|
+
while (!stopped && inFlight < concurrency && queue.length > 0) {
|
|
137
|
+
const item = queue.shift();
|
|
138
|
+
dispatchFrame(item);
|
|
139
|
+
wakeAllWaiters();
|
|
140
|
+
}
|
|
72
141
|
}
|
|
73
|
-
async function
|
|
142
|
+
async function pushCapturedFrame(item) {
|
|
143
|
+
if (stopped) {
|
|
144
|
+
releaseBitmap(item.bitmap);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
if (inFlight < concurrency) {
|
|
148
|
+
dispatchFrame(item);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
74
151
|
if (policy === "drop") {
|
|
75
152
|
if (queue.length >= maxQueue) {
|
|
76
153
|
stats.dropped++;
|
|
77
|
-
|
|
78
|
-
onProgress == null ? void 0 : onProgress({ ...stats });
|
|
154
|
+
releaseBitmap(item.bitmap);
|
|
79
155
|
return false;
|
|
80
156
|
}
|
|
81
157
|
queue.push(item);
|
|
82
158
|
stats.queueMax = Math.max(stats.queueMax, queue.length);
|
|
83
159
|
return true;
|
|
84
160
|
}
|
|
85
|
-
|
|
161
|
+
if (maxQueue === 0) {
|
|
162
|
+
while (!stopped && inFlight >= concurrency) {
|
|
163
|
+
await new Promise((r) => waiters.push(r));
|
|
164
|
+
throwIfAborted();
|
|
165
|
+
}
|
|
166
|
+
if (stopped) {
|
|
167
|
+
releaseBitmap(item.bitmap);
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
dispatchFrame(item);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
while (!stopped && queue.length >= maxQueue) {
|
|
86
174
|
await new Promise((r) => waiters.push(r));
|
|
87
175
|
throwIfAborted();
|
|
88
|
-
|
|
176
|
+
}
|
|
177
|
+
if (stopped) {
|
|
178
|
+
releaseBitmap(item.bitmap);
|
|
179
|
+
return false;
|
|
89
180
|
}
|
|
90
181
|
queue.push(item);
|
|
91
182
|
stats.queueMax = Math.max(stats.queueMax, queue.length);
|
|
92
183
|
return true;
|
|
93
184
|
}
|
|
94
|
-
function
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
inFlight
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const p = new Promise(
|
|
102
|
-
(resolve, reject) => ack.set(seq2, { resolve, reject })
|
|
103
|
-
);
|
|
104
|
-
worker.postMessage(
|
|
105
|
-
{ type: "encode", frameIndex: seq2, bitmap, width: w, height: h },
|
|
106
|
-
[bitmap]
|
|
107
|
-
);
|
|
108
|
-
p.catch(() => {
|
|
109
|
-
});
|
|
185
|
+
function canScheduleCapture() {
|
|
186
|
+
if (stopped) return false;
|
|
187
|
+
if (pendingCaptures >= maxCaptureTasks) return false;
|
|
188
|
+
if (policy === "drop") {
|
|
189
|
+
const buffered = queue.length + inFlight + pendingCaptures;
|
|
190
|
+
const capacity = maxQueue + concurrency;
|
|
191
|
+
return buffered < capacity;
|
|
110
192
|
}
|
|
193
|
+
if (maxQueue === 0) return inFlight < concurrency;
|
|
194
|
+
return queue.length < maxQueue || inFlight < concurrency;
|
|
195
|
+
}
|
|
196
|
+
function scheduleCaptureFrame() {
|
|
197
|
+
pendingCaptures++;
|
|
198
|
+
stats.pendingCaptureMax = Math.max(stats.pendingCaptureMax, pendingCaptures);
|
|
199
|
+
let task = null;
|
|
200
|
+
task = (async () => {
|
|
201
|
+
throwIfAborted();
|
|
202
|
+
const w = canvas.width;
|
|
203
|
+
const h = canvas.height;
|
|
204
|
+
const b0 = performance.now();
|
|
205
|
+
const bitmap = await createImageBitmap(canvas);
|
|
206
|
+
const b1 = performance.now();
|
|
207
|
+
const bitmapMs = b1 - b0;
|
|
208
|
+
stats.lastBitmapMs = bitmapMs;
|
|
209
|
+
stats.bitmapMsAvg = stats.bitmapMsAvg ? stats.bitmapMsAvg * 0.9 + bitmapMs * 0.1 : bitmapMs;
|
|
210
|
+
stats.captured++;
|
|
211
|
+
await pushCapturedFrame({
|
|
212
|
+
seq: seq++,
|
|
213
|
+
bitmap,
|
|
214
|
+
w,
|
|
215
|
+
h
|
|
216
|
+
});
|
|
217
|
+
})().catch((err) => {
|
|
218
|
+
if (!stopped && !isAbortError(err)) {
|
|
219
|
+
stats.failed++;
|
|
220
|
+
console.error("capture failed:", err);
|
|
221
|
+
}
|
|
222
|
+
}).finally(() => {
|
|
223
|
+
pendingCaptures--;
|
|
224
|
+
captureTasks.delete(task);
|
|
225
|
+
dispatchIfPossible();
|
|
226
|
+
wakeAllWaiters();
|
|
227
|
+
emitProgress();
|
|
228
|
+
});
|
|
229
|
+
captureTasks.add(task);
|
|
111
230
|
}
|
|
112
231
|
worker.addEventListener("message", async (ev) => {
|
|
113
232
|
const msg = ev.data;
|
|
114
233
|
if (!msg || typeof msg !== "object") return;
|
|
115
234
|
if (msg.type === "frame") {
|
|
116
|
-
const { frameIndex:
|
|
235
|
+
const { frameIndex: frameSeq, blob, encodeMs } = msg;
|
|
117
236
|
stats.encoded++;
|
|
118
237
|
if (typeof encodeMs === "number") {
|
|
119
238
|
stats.lastEncodeMs = encodeMs;
|
|
@@ -121,31 +240,45 @@ async function startLiveCapture({
|
|
|
121
240
|
}
|
|
122
241
|
try {
|
|
123
242
|
const w0 = performance.now();
|
|
124
|
-
await exporter.write(
|
|
243
|
+
await exporter.write(frameSeq, blob);
|
|
125
244
|
const w1 = performance.now();
|
|
126
245
|
stats.written++;
|
|
127
246
|
const writeMs = w1 - w0;
|
|
128
247
|
stats.lastWriteMs = writeMs;
|
|
129
248
|
stats.writeMsAvg = stats.writeMsAvg ? stats.writeMsAvg * 0.9 + writeMs * 0.1 : writeMs;
|
|
130
|
-
const entry = ack.get(
|
|
249
|
+
const entry = ack.get(frameSeq);
|
|
131
250
|
if (entry) entry.resolve();
|
|
132
251
|
} catch (err) {
|
|
133
|
-
|
|
252
|
+
stats.failed++;
|
|
253
|
+
const entry = ack.get(frameSeq);
|
|
134
254
|
if (entry) entry.reject(err);
|
|
135
255
|
} finally {
|
|
136
|
-
ack.delete(
|
|
137
|
-
inFlight
|
|
256
|
+
ack.delete(frameSeq);
|
|
257
|
+
inFlight = Math.max(0, inFlight - 1);
|
|
138
258
|
dispatchIfPossible();
|
|
139
|
-
|
|
259
|
+
wakeAllWaiters();
|
|
260
|
+
emitProgress();
|
|
140
261
|
}
|
|
141
|
-
|
|
142
|
-
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (msg.type === "error") {
|
|
265
|
+
const frameSeq = Number(msg.frameIndex);
|
|
266
|
+
const err = new Error(msg.message || "Worker error");
|
|
267
|
+
stats.failed++;
|
|
268
|
+
if (Number.isInteger(frameSeq)) {
|
|
269
|
+
const entry = ack.get(frameSeq);
|
|
270
|
+
if (entry) entry.reject(err);
|
|
271
|
+
ack.delete(frameSeq);
|
|
272
|
+
inFlight = Math.max(0, inFlight - 1);
|
|
273
|
+
dispatchIfPossible();
|
|
274
|
+
wakeAllWaiters();
|
|
275
|
+
} else {
|
|
276
|
+
fatalError = err;
|
|
277
|
+
}
|
|
278
|
+
console.error("Worker error:", err.message);
|
|
279
|
+
emitProgress();
|
|
143
280
|
}
|
|
144
281
|
});
|
|
145
|
-
const frameIntervalMs = 1e3 / fps;
|
|
146
|
-
let accMs = 0;
|
|
147
|
-
let lastTickMs = null;
|
|
148
|
-
let seq = 0;
|
|
149
282
|
function tick(ts) {
|
|
150
283
|
if (stopped) return;
|
|
151
284
|
try {
|
|
@@ -159,47 +292,64 @@ async function startLiveCapture({
|
|
|
159
292
|
const dt = ts - lastTickMs;
|
|
160
293
|
lastTickMs = ts;
|
|
161
294
|
accMs += dt;
|
|
162
|
-
|
|
163
|
-
accMs
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
295
|
+
if (accMs > maxAccumMs) {
|
|
296
|
+
const dropFrames = Math.floor((accMs - maxAccumMs) / frameIntervalMs);
|
|
297
|
+
if (dropFrames > 0) {
|
|
298
|
+
stats.dropped += dropFrames;
|
|
299
|
+
accMs -= dropFrames * frameIntervalMs;
|
|
300
|
+
}
|
|
301
|
+
if (accMs > maxAccumMs) accMs = maxAccumMs;
|
|
302
|
+
}
|
|
303
|
+
let loopGuard = 0;
|
|
304
|
+
while (accMs >= frameIntervalMs && loopGuard < maxCatchUpFrames) {
|
|
305
|
+
if (!canScheduleCapture()) {
|
|
306
|
+
if (policy === "drop") {
|
|
307
|
+
stats.dropped++;
|
|
308
|
+
accMs -= frameIntervalMs;
|
|
309
|
+
loopGuard++;
|
|
310
|
+
continue;
|
|
173
311
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
h: canvas.height
|
|
180
|
-
});
|
|
181
|
-
if (ok) dispatchIfPossible();
|
|
182
|
-
onProgress == null ? void 0 : onProgress({ ...stats });
|
|
183
|
-
}).catch((e) => console.error("createImageBitmap failed:", e));
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
accMs -= frameIntervalMs;
|
|
315
|
+
scheduleCaptureFrame();
|
|
316
|
+
loopGuard++;
|
|
184
317
|
}
|
|
318
|
+
emitProgress();
|
|
185
319
|
rafId = requestAnimationFrame(tick);
|
|
186
320
|
}
|
|
187
321
|
rafId = requestAnimationFrame(tick);
|
|
188
322
|
async function stop() {
|
|
189
|
-
if (stopped) return;
|
|
323
|
+
if (stopped) return done;
|
|
190
324
|
stopped = true;
|
|
191
325
|
if (rafId != null) cancelAnimationFrame(rafId);
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
326
|
+
let stopError = null;
|
|
327
|
+
try {
|
|
328
|
+
wakeAllWaiters();
|
|
329
|
+
const tasks = Array.from(captureTasks);
|
|
330
|
+
if (tasks.length > 0) {
|
|
331
|
+
await Promise.allSettled(tasks);
|
|
332
|
+
}
|
|
333
|
+
for (const item of queue.splice(0, queue.length)) {
|
|
334
|
+
releaseBitmap(item.bitmap);
|
|
335
|
+
}
|
|
336
|
+
while (inFlight > 0 || ack.size > 0) {
|
|
337
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
338
|
+
}
|
|
339
|
+
await exporter.finalize();
|
|
340
|
+
} catch (err) {
|
|
341
|
+
stopError = err;
|
|
342
|
+
throw err;
|
|
343
|
+
} finally {
|
|
344
|
+
try {
|
|
345
|
+
worker.terminate();
|
|
346
|
+
} catch {
|
|
347
|
+
}
|
|
348
|
+
settleDone({ error: stopError ?? fatalError ?? null });
|
|
198
349
|
}
|
|
199
|
-
|
|
200
|
-
worker.terminate();
|
|
350
|
+
return done;
|
|
201
351
|
}
|
|
202
|
-
return { stop, stats };
|
|
352
|
+
return { stop, stats, done };
|
|
203
353
|
}
|
|
204
354
|
|
|
205
355
|
// src/virtual_time.js
|
package/dist/ui/encoder.html
CHANGED
|
@@ -140,6 +140,20 @@
|
|
|
140
140
|
word-break: break-word;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
.stats {
|
|
144
|
+
margin-top: 12px;
|
|
145
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
146
|
+
border-radius: 12px;
|
|
147
|
+
padding: 10px;
|
|
148
|
+
background: rgba(0, 0, 0, 0.22);
|
|
149
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
|
150
|
+
"Liberation Mono", "Courier New", monospace;
|
|
151
|
+
font-size: 12.5px;
|
|
152
|
+
line-height: 1.4;
|
|
153
|
+
white-space: pre-wrap;
|
|
154
|
+
word-break: break-word;
|
|
155
|
+
}
|
|
156
|
+
|
|
143
157
|
/* Inputs */
|
|
144
158
|
button,
|
|
145
159
|
input,
|
|
@@ -418,7 +432,26 @@
|
|
|
418
432
|
<label>MaxQueue</label>
|
|
419
433
|
<input id="maxQueue" type="number" min="0" step="1" value="8" />
|
|
420
434
|
<label>Duration</label>
|
|
421
|
-
<input id="duration" type="number" min="
|
|
435
|
+
<input id="duration" type="number" min="0" step="1" value="10" />
|
|
436
|
+
</div>
|
|
437
|
+
|
|
438
|
+
<div class="row2">
|
|
439
|
+
<label>PendingCap</label>
|
|
440
|
+
<input
|
|
441
|
+
id="maxPendingCaptures"
|
|
442
|
+
type="number"
|
|
443
|
+
min="1"
|
|
444
|
+
step="1"
|
|
445
|
+
value="2"
|
|
446
|
+
/>
|
|
447
|
+
<label>Stats(ms)</label>
|
|
448
|
+
<input
|
|
449
|
+
id="statsIntervalMs"
|
|
450
|
+
type="number"
|
|
451
|
+
min="50"
|
|
452
|
+
step="10"
|
|
453
|
+
value="180"
|
|
454
|
+
/>
|
|
422
455
|
</div>
|
|
423
456
|
</details>
|
|
424
457
|
|
|
@@ -442,6 +475,8 @@
|
|
|
442
475
|
<button id="btnStop" class="btnDanger" disabled>Stop</button>
|
|
443
476
|
</div>
|
|
444
477
|
|
|
478
|
+
<div id="stats" class="stats">status: idle</div>
|
|
479
|
+
|
|
445
480
|
<div class="hint">
|
|
446
481
|
Hotkeys (only when not recording):
|
|
447
482
|
<br />
|
|
@@ -455,6 +490,8 @@
|
|
|
455
490
|
<br />
|
|
456
491
|
- Space: start
|
|
457
492
|
<br />
|
|
493
|
+
Duration=0 means manual stop.
|
|
494
|
+
<br />
|
|
458
495
|
During recording, keyboard is not captured by the UI. Use Focus
|
|
459
496
|
Sketch if needed, then click the canvas to ensure focus.
|
|
460
497
|
</div>
|
package/dist/ui/encoder.js
CHANGED
|
@@ -14,6 +14,8 @@ const policyEl = $("policy");
|
|
|
14
14
|
const concurrencyEl = $("concurrency");
|
|
15
15
|
const maxQueueEl = $("maxQueue");
|
|
16
16
|
const durationEl = $("duration");
|
|
17
|
+
const maxPendingCapturesEl = $("maxPendingCaptures");
|
|
18
|
+
const statsIntervalMsEl = $("statsIntervalMs");
|
|
17
19
|
|
|
18
20
|
const framesEl = $("frames");
|
|
19
21
|
const warmupEl = $("warmup");
|
|
@@ -33,6 +35,7 @@ const btnPassthrough = $("btnPassthrough");
|
|
|
33
35
|
const btnFocus = $("btnFocus");
|
|
34
36
|
|
|
35
37
|
const logEl = $("log");
|
|
38
|
+
const statsEl = $("stats");
|
|
36
39
|
const preview = $("preview");
|
|
37
40
|
|
|
38
41
|
let running = false;
|
|
@@ -42,11 +45,37 @@ const docks = ["dock-br", "dock-bl", "dock-tr", "dock-tl"];
|
|
|
42
45
|
let dockIndex = 0;
|
|
43
46
|
|
|
44
47
|
let passthrough = false;
|
|
48
|
+
let lastStatusLog = "";
|
|
49
|
+
|
|
50
|
+
const LOG_MAX_LINES = 220;
|
|
51
|
+
const logLines = [];
|
|
52
|
+
const runtimeState = {
|
|
53
|
+
status: "idle",
|
|
54
|
+
message: "",
|
|
55
|
+
progressDone: 0,
|
|
56
|
+
progressTotal: 0,
|
|
57
|
+
stats: null,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function toPositiveInt(v, fallback) {
|
|
61
|
+
const n = Number(v);
|
|
62
|
+
if (!Number.isInteger(n) || n <= 0) return fallback;
|
|
63
|
+
return n;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function toNonNegativeNumber(v, fallback) {
|
|
67
|
+
const n = Number(v);
|
|
68
|
+
if (!Number.isFinite(n) || n < 0) return fallback;
|
|
69
|
+
return n;
|
|
70
|
+
}
|
|
45
71
|
|
|
46
72
|
function appendLog(line) {
|
|
47
|
-
const s = String(line);
|
|
48
|
-
|
|
49
|
-
|
|
73
|
+
const s = String(line ?? "");
|
|
74
|
+
logLines.push(s);
|
|
75
|
+
if (logLines.length > LOG_MAX_LINES) {
|
|
76
|
+
logLines.splice(0, logLines.length - LOG_MAX_LINES);
|
|
77
|
+
}
|
|
78
|
+
logEl.textContent = logLines.join("\n");
|
|
50
79
|
logEl.scrollTop = logEl.scrollHeight;
|
|
51
80
|
}
|
|
52
81
|
|
|
@@ -65,6 +94,52 @@ function setRunning(v) {
|
|
|
65
94
|
}
|
|
66
95
|
}
|
|
67
96
|
|
|
97
|
+
function nfmt(v, d = 2) {
|
|
98
|
+
if (!Number.isFinite(v)) return "-";
|
|
99
|
+
return Number(v).toFixed(d);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderStats() {
|
|
103
|
+
const s = runtimeState.stats || null;
|
|
104
|
+
const lines = [];
|
|
105
|
+
|
|
106
|
+
lines.push(
|
|
107
|
+
`status: ${runtimeState.status}${
|
|
108
|
+
runtimeState.message ? ` (${runtimeState.message})` : ""
|
|
109
|
+
}`
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (runtimeState.progressTotal > 0) {
|
|
113
|
+
lines.push(`progress: ${runtimeState.progressDone}/${runtimeState.progressTotal}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!s) {
|
|
117
|
+
lines.push("captured: - encoded: - written: -");
|
|
118
|
+
lines.push("dropped: - failed: -");
|
|
119
|
+
lines.push("bitmap ms: -/- encode ms: -/- write ms: -/-");
|
|
120
|
+
lines.push("queueMax: - inFlightMax: - pendingMax: -");
|
|
121
|
+
} else {
|
|
122
|
+
lines.push(
|
|
123
|
+
`captured: ${s.captured ?? 0} encoded: ${s.encoded ?? 0} written: ${
|
|
124
|
+
s.written ?? 0
|
|
125
|
+
}`
|
|
126
|
+
);
|
|
127
|
+
lines.push(`dropped: ${s.dropped ?? 0} failed: ${s.failed ?? 0}`);
|
|
128
|
+
lines.push(
|
|
129
|
+
`bitmap ms: ${nfmt(s.lastBitmapMs)}/${nfmt(s.bitmapMsAvg)} ` +
|
|
130
|
+
`encode ms: ${nfmt(s.lastEncodeMs)}/${nfmt(s.encodeMsAvg)} ` +
|
|
131
|
+
`write ms: ${nfmt(s.lastWriteMs)}/${nfmt(s.writeMsAvg)}`
|
|
132
|
+
);
|
|
133
|
+
lines.push(
|
|
134
|
+
`queueMax: ${s.queueMax ?? 0} inFlightMax: ${
|
|
135
|
+
s.inFlightMax ?? 0
|
|
136
|
+
} pendingMax: ${s.pendingCaptureMax ?? 0}`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
statsEl.textContent = lines.join("\n");
|
|
141
|
+
}
|
|
142
|
+
|
|
68
143
|
function buildPreviewUrl(entry) {
|
|
69
144
|
const e = entry || "/src/main.js";
|
|
70
145
|
return `/__pa_encoder__/ui/preview.html?entry=${encodeURIComponent(e)}`;
|
|
@@ -161,9 +236,13 @@ function syncModeUI() {
|
|
|
161
236
|
}
|
|
162
237
|
|
|
163
238
|
function currentPayload() {
|
|
164
|
-
const fps = Number(fpsEl.value || "60");
|
|
239
|
+
const fps = Math.max(1, Math.floor(Number(fpsEl.value || "60")));
|
|
165
240
|
const kind = modeEl.value === "frame" ? "frame" : "live";
|
|
166
241
|
const canvasSelector = selectedCanvasSelector();
|
|
242
|
+
const statsIntervalMs = Math.max(
|
|
243
|
+
50,
|
|
244
|
+
toPositiveInt(statsIntervalMsEl.value, 180)
|
|
245
|
+
);
|
|
167
246
|
|
|
168
247
|
if (kind === "frame") {
|
|
169
248
|
const frames = Math.max(1, Math.floor(Number(framesEl.value || "300")));
|
|
@@ -174,11 +253,17 @@ function currentPayload() {
|
|
|
174
253
|
frames,
|
|
175
254
|
warmup,
|
|
176
255
|
canvasSelector,
|
|
256
|
+
statsIntervalMs,
|
|
177
257
|
...exporterPayload(),
|
|
178
258
|
};
|
|
179
259
|
}
|
|
180
260
|
|
|
181
|
-
const duration =
|
|
261
|
+
const duration = toNonNegativeNumber(durationEl.value, 10);
|
|
262
|
+
const maxPendingCaptures = Math.max(
|
|
263
|
+
1,
|
|
264
|
+
toPositiveInt(maxPendingCapturesEl.value, 2)
|
|
265
|
+
);
|
|
266
|
+
|
|
182
267
|
return {
|
|
183
268
|
kind: "live",
|
|
184
269
|
fps,
|
|
@@ -187,6 +272,8 @@ function currentPayload() {
|
|
|
187
272
|
policy: policyEl.value,
|
|
188
273
|
concurrency: Math.max(1, Math.floor(Number(concurrencyEl.value || "2"))),
|
|
189
274
|
maxQueue: Math.max(0, Math.floor(Number(maxQueueEl.value || "8"))),
|
|
275
|
+
maxPendingCaptures,
|
|
276
|
+
statsIntervalMs,
|
|
190
277
|
...exporterPayload(),
|
|
191
278
|
};
|
|
192
279
|
}
|
|
@@ -201,6 +288,12 @@ function start() {
|
|
|
201
288
|
appendLog(
|
|
202
289
|
`start: ${payload.kind} fps=${payload.fps} canvas=${payload.canvasSelector}`
|
|
203
290
|
);
|
|
291
|
+
runtimeState.status = "running";
|
|
292
|
+
runtimeState.message = "starting";
|
|
293
|
+
runtimeState.progressDone = 0;
|
|
294
|
+
runtimeState.progressTotal = 0;
|
|
295
|
+
runtimeState.stats = null;
|
|
296
|
+
renderStats();
|
|
204
297
|
|
|
205
298
|
// start recording -> prefer passthrough + focus canvas
|
|
206
299
|
setPassthrough(true, { silent: true });
|
|
@@ -219,7 +312,12 @@ function start() {
|
|
|
219
312
|
}
|
|
220
313
|
|
|
221
314
|
clearTimeout(stopTimer);
|
|
222
|
-
|
|
315
|
+
if (payload.duration > 0) {
|
|
316
|
+
stopTimer = setTimeout(() => stop(), payload.duration * 1000);
|
|
317
|
+
} else {
|
|
318
|
+
stopTimer = null;
|
|
319
|
+
appendLog("live duration=0 (manual stop)");
|
|
320
|
+
}
|
|
223
321
|
}
|
|
224
322
|
|
|
225
323
|
function stop() {
|
|
@@ -317,7 +415,14 @@ window.addEventListener("message", (ev) => {
|
|
|
317
415
|
if (msg.type === "pa_status") {
|
|
318
416
|
const status = msg.status || "";
|
|
319
417
|
const message = msg.message || "";
|
|
320
|
-
|
|
418
|
+
const line = message ? `${status}: ${message}` : String(status);
|
|
419
|
+
if (line !== lastStatusLog) {
|
|
420
|
+
appendLog(line);
|
|
421
|
+
lastStatusLog = line;
|
|
422
|
+
}
|
|
423
|
+
runtimeState.status = status || "idle";
|
|
424
|
+
runtimeState.message = message || "";
|
|
425
|
+
renderStats();
|
|
321
426
|
|
|
322
427
|
if (status === "idle") {
|
|
323
428
|
setRunning(false);
|
|
@@ -332,21 +437,25 @@ window.addEventListener("message", (ev) => {
|
|
|
332
437
|
}
|
|
333
438
|
|
|
334
439
|
if (msg.type === "pa_progress") {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
440
|
+
runtimeState.progressDone = Number(msg.done ?? 0);
|
|
441
|
+
runtimeState.progressTotal = Number(msg.total ?? 0);
|
|
442
|
+
renderStats();
|
|
338
443
|
return;
|
|
339
444
|
}
|
|
340
445
|
|
|
341
446
|
if (msg.type === "pa_stats") {
|
|
342
447
|
const safe = { ...msg };
|
|
343
448
|
delete safe.type;
|
|
344
|
-
|
|
449
|
+
runtimeState.stats = safe;
|
|
450
|
+
renderStats();
|
|
345
451
|
return;
|
|
346
452
|
}
|
|
347
453
|
|
|
348
454
|
if (msg.type === "pa_error") {
|
|
349
455
|
appendLog(`ERROR: ${msg.message || "unknown error"}`);
|
|
456
|
+
runtimeState.status = "error";
|
|
457
|
+
runtimeState.message = msg.message || "unknown error";
|
|
458
|
+
renderStats();
|
|
350
459
|
setRunning(false);
|
|
351
460
|
}
|
|
352
461
|
});
|
|
@@ -361,6 +470,7 @@ window.addEventListener("message", (ev) => {
|
|
|
361
470
|
|
|
362
471
|
setRunning(false);
|
|
363
472
|
syncModeUI();
|
|
473
|
+
renderStats();
|
|
364
474
|
|
|
365
475
|
fillCanvasDropdown([]);
|
|
366
476
|
appendLog("loading preview...");
|