@camera.ui/browser 0.0.203 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +62 -20
- package/dist/index.js +2507 -647
- package/package.json +16 -16
package/dist/index.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { Logger } from "@camera.ui/logger";
|
|
2
|
-
import { computed, effectScope, inject, markRaw, onBeforeUnmount,
|
|
2
|
+
import { computed, effectScope, inject, markRaw, onBeforeUnmount, readonly, ref, shallowReactive, shallowRef, toValue, watch } from "vue";
|
|
3
3
|
import { tryOnScopeDispose, useTimeoutFn, whenever } from "@vueuse/core";
|
|
4
4
|
import { SensorType } from "@camera.ui/sdk";
|
|
5
|
+
import { isNoRespondersError } from "@camera.ui/rpc";
|
|
6
|
+
import { classifyClose } from "@camera.ui/transport";
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod, mod.exports), cb = null), mod.exports);
|
|
9
|
+
//#endregion
|
|
5
10
|
//#region src/utils/createDebouncedCache.ts
|
|
6
11
|
function createDebouncedCache(options = {}) {
|
|
7
12
|
const { releaseDelay = 1e3, onRelease } = options;
|
|
@@ -38,11 +43,12 @@ function createDebouncedCache(options = {}) {
|
|
|
38
43
|
});
|
|
39
44
|
return value;
|
|
40
45
|
}
|
|
41
|
-
function release(key) {
|
|
46
|
+
function release(key, value) {
|
|
42
47
|
const cached = cache.get(key);
|
|
43
|
-
if (!cached) return;
|
|
48
|
+
if (!cached || cached.refCount <= 0) return;
|
|
49
|
+
if (value !== void 0 && cached.value !== value) return;
|
|
44
50
|
cached.refCount--;
|
|
45
|
-
if (cached.refCount
|
|
51
|
+
if (cached.refCount === 0) {
|
|
46
52
|
const timer = setTimeout(() => {
|
|
47
53
|
const stillCached = cache.get(key);
|
|
48
54
|
if (stillCached && stillCached.refCount <= 0) doRelease(key);
|
|
@@ -83,6 +89,34 @@ function createDebouncedCache(options = {}) {
|
|
|
83
89
|
};
|
|
84
90
|
}
|
|
85
91
|
//#endregion
|
|
92
|
+
//#region src/utils/createRetryTimer.ts
|
|
93
|
+
var RETRY_MIN_MS = 1e3;
|
|
94
|
+
var RETRY_MAX_MS$1 = 3e4;
|
|
95
|
+
function createRetryTimer() {
|
|
96
|
+
let timer;
|
|
97
|
+
let delay = RETRY_MIN_MS;
|
|
98
|
+
function schedule(fn) {
|
|
99
|
+
if (timer) return;
|
|
100
|
+
timer = setTimeout(() => {
|
|
101
|
+
timer = void 0;
|
|
102
|
+
fn();
|
|
103
|
+
}, delay);
|
|
104
|
+
delay = Math.min(delay * 2, RETRY_MAX_MS$1);
|
|
105
|
+
}
|
|
106
|
+
function cancel() {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
timer = void 0;
|
|
109
|
+
}
|
|
110
|
+
function reset() {
|
|
111
|
+
delay = RETRY_MIN_MS;
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
schedule,
|
|
115
|
+
cancel,
|
|
116
|
+
reset
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
86
120
|
//#region src/composables/useCameraUi.ts
|
|
87
121
|
function useCameraUi() {
|
|
88
122
|
const context = inject(CAMERA_UI_INJECTION_KEY);
|
|
@@ -91,6 +125,9 @@ function useCameraUi() {
|
|
|
91
125
|
}
|
|
92
126
|
//#endregion
|
|
93
127
|
//#region ../../externals/camera.ui/server/src/utils/camera.ts
|
|
128
|
+
var import___vite_browser_external = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
129
|
+
module.exports = {};
|
|
130
|
+
})))();
|
|
94
131
|
function normalizeCameraName(name) {
|
|
95
132
|
return name.replace(/ /g, "_").toLowerCase();
|
|
96
133
|
}
|
|
@@ -136,6 +173,7 @@ var NamespaceManager = class {
|
|
|
136
173
|
static cameraNamespaces(cameraId) {
|
|
137
174
|
return {
|
|
138
175
|
cameraSubject: `camera.${cameraId}.subscriber`,
|
|
176
|
+
cameraStateSubject: `camera.${cameraId}.state`,
|
|
139
177
|
cameraControllerRpc: `camera.${cameraId}.controller.rpc`
|
|
140
178
|
};
|
|
141
179
|
}
|
|
@@ -165,7 +203,10 @@ var NamespaceManager = class {
|
|
|
165
203
|
return { sensorWriteSubject: `camera.${cameraId}.sensors.writes` };
|
|
166
204
|
}
|
|
167
205
|
static sensorEventNamespaces(sensorId) {
|
|
168
|
-
return {
|
|
206
|
+
return {
|
|
207
|
+
sensorSubject: `sensor.${sensorId}.subject`,
|
|
208
|
+
sensorStateSubject: `sensor.${sensorId}.state`
|
|
209
|
+
};
|
|
169
210
|
}
|
|
170
211
|
static sensorProviderNamespaces(pluginId, sensorId) {
|
|
171
212
|
return { sensorRpc: `plugin.${pluginId}.sensor.${sensorId}.rpc` };
|
|
@@ -191,46 +232,81 @@ var NamespaceManager = class {
|
|
|
191
232
|
static workerAgentRpc(agentId) {
|
|
192
233
|
return `worker.${agentId}.rpc`;
|
|
193
234
|
}
|
|
194
|
-
static workerManagerRpc() {
|
|
235
|
+
static workerManagerRpc(agentId) {
|
|
236
|
+
return `workers.${agentId}.manager.rpc`;
|
|
237
|
+
}
|
|
238
|
+
static sharedWorkerManagerRpc() {
|
|
195
239
|
return "workers.manager.rpc";
|
|
196
240
|
}
|
|
197
241
|
static workerSync(agentId) {
|
|
198
242
|
return `worker.${agentId}.sync`;
|
|
199
243
|
}
|
|
200
|
-
static
|
|
244
|
+
static workerLinkSubject(agentId) {
|
|
245
|
+
return `worker.${agentId}.link`;
|
|
246
|
+
}
|
|
247
|
+
static workerDisconnect(agentId) {
|
|
248
|
+
return `workers.${agentId}.disconnect`;
|
|
249
|
+
}
|
|
250
|
+
static sharedWorkerDisconnect() {
|
|
201
251
|
return "workers.disconnect";
|
|
202
252
|
}
|
|
203
|
-
static workerLogs() {
|
|
253
|
+
static workerLogs(agentId) {
|
|
254
|
+
return `workers.${agentId}.logs`;
|
|
255
|
+
}
|
|
256
|
+
static sharedWorkerLogs() {
|
|
204
257
|
return "workers.logs";
|
|
205
258
|
}
|
|
259
|
+
static workerConnId(agentId, secret) {
|
|
260
|
+
return (0, import___vite_browser_external.createHmac)("sha256", secret).update(agentId).digest("hex").slice(0, 32);
|
|
261
|
+
}
|
|
206
262
|
};
|
|
207
263
|
//#endregion
|
|
208
264
|
//#region src/composables/useRpc.ts
|
|
209
265
|
var DEFAULT_MAX_RETRIES = 3;
|
|
210
266
|
var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
|
|
267
|
+
var LINK_SETTLE_MS = 3e4;
|
|
268
|
+
var LOCAL_NOT_CONNECTED = /* @__PURE__ */ new Set(["Not connected", "rpc: not connected"]);
|
|
269
|
+
var linkRestoredAt = /* @__PURE__ */ new WeakMap();
|
|
270
|
+
function markLinkRestored(rpcRef) {
|
|
271
|
+
linkRestoredAt.set(rpcRef, Date.now());
|
|
272
|
+
}
|
|
273
|
+
function isLinkSettling(rpcRef) {
|
|
274
|
+
return Date.now() - (linkRestoredAt.get(rpcRef) ?? -Infinity) < LINK_SETTLE_MS;
|
|
275
|
+
}
|
|
276
|
+
function isLocalNoResponders(err) {
|
|
277
|
+
return err instanceof Error && err.code === void 0 && err.name === "NoResponders";
|
|
278
|
+
}
|
|
211
279
|
async function rpcCall(rpcRef, fn, options = {}) {
|
|
212
280
|
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
213
281
|
const awaitConnect = options.awaitConnect ?? true;
|
|
214
282
|
const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
215
283
|
const retryDelayMs = options.retryDelayMs ?? defaultRetryDelayMs;
|
|
216
|
-
const shouldRetry = options.shouldRetry
|
|
284
|
+
const shouldRetry = options.shouldRetry;
|
|
285
|
+
const retryOnTimeout = options.retryOnTimeout ?? false;
|
|
217
286
|
const signal = options.signal;
|
|
287
|
+
let linkChanges = 0;
|
|
288
|
+
const stopLinkWatch = watchDetached(rpcRef, () => linkChanges++, { flush: "sync" });
|
|
218
289
|
let lastError;
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (!
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
290
|
+
try {
|
|
291
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
292
|
+
if (signal?.aborted) throw makeAbortError();
|
|
293
|
+
let rpc = rpcRef.value;
|
|
294
|
+
if (!rpc) {
|
|
295
|
+
if (!awaitConnect) throw new Error("rpc: not connected");
|
|
296
|
+
rpc = await waitForRpc(rpcRef, connectTimeoutMs, signal);
|
|
297
|
+
}
|
|
298
|
+
const changesAtStart = linkChanges;
|
|
299
|
+
try {
|
|
300
|
+
return await fn(rpc);
|
|
301
|
+
} catch (err) {
|
|
302
|
+
lastError = err;
|
|
303
|
+
if (attempt >= maxRetries) throw err;
|
|
304
|
+
if (!(shouldRetry ? shouldRetry(err, attempt) : isLinkFailure(err, rpc, rpcRef, linkChanges !== changesAtStart, retryOnTimeout))) throw err;
|
|
305
|
+
await sleep(retryDelayMs(attempt), signal);
|
|
306
|
+
}
|
|
233
307
|
}
|
|
308
|
+
} finally {
|
|
309
|
+
stopLinkWatch();
|
|
234
310
|
}
|
|
235
311
|
throw lastError ?? /* @__PURE__ */ new Error("rpc: max retries exceeded");
|
|
236
312
|
}
|
|
@@ -288,11 +364,13 @@ function useRpcSubscription(subscribeFn, options = {}) {
|
|
|
288
364
|
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
289
365
|
const retryDelayMs = options.retryDelayMs ?? defaultRetryDelayMs;
|
|
290
366
|
let unsubscribe;
|
|
367
|
+
let boundClient;
|
|
291
368
|
let pending = false;
|
|
292
369
|
let disposed = false;
|
|
293
370
|
async function bind(rpc) {
|
|
294
371
|
if (pending || disposed) return;
|
|
295
372
|
pending = true;
|
|
373
|
+
boundClient = rpc;
|
|
296
374
|
try {
|
|
297
375
|
if (unsubscribe) {
|
|
298
376
|
try {
|
|
@@ -301,9 +379,14 @@ function useRpcSubscription(subscribeFn, options = {}) {
|
|
|
301
379
|
unsubscribe = void 0;
|
|
302
380
|
}
|
|
303
381
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
304
|
-
if (disposed) return;
|
|
382
|
+
if (disposed || boundClient !== rpc) return;
|
|
305
383
|
try {
|
|
306
|
-
|
|
384
|
+
const off = await subscribeFn(rpc);
|
|
385
|
+
if (disposed || boundClient !== rpc) {
|
|
386
|
+
off();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
unsubscribe = off;
|
|
307
390
|
isActive.value = true;
|
|
308
391
|
return;
|
|
309
392
|
} catch (err) {
|
|
@@ -312,16 +395,17 @@ function useRpcSubscription(subscribeFn, options = {}) {
|
|
|
312
395
|
options.onError?.(err);
|
|
313
396
|
return;
|
|
314
397
|
}
|
|
315
|
-
await sleep
|
|
398
|
+
await sleep(retryDelayMs(attempt));
|
|
316
399
|
}
|
|
317
400
|
}
|
|
318
401
|
} finally {
|
|
319
402
|
pending = false;
|
|
320
403
|
const current = ctx.rpc.value;
|
|
321
|
-
if (!disposed && current && current !==
|
|
404
|
+
if (!disposed && ctx.isConnected.value && current && current !== boundClient) bind(current);
|
|
322
405
|
}
|
|
323
406
|
}
|
|
324
407
|
function unbind() {
|
|
408
|
+
boundClient = void 0;
|
|
325
409
|
if (unsubscribe) {
|
|
326
410
|
try {
|
|
327
411
|
unsubscribe();
|
|
@@ -335,9 +419,9 @@ function useRpcSubscription(subscribeFn, options = {}) {
|
|
|
335
419
|
if (rpc) await bind(rpc);
|
|
336
420
|
}
|
|
337
421
|
if (ctx.rpc.value) bind(ctx.rpc.value);
|
|
338
|
-
const stopWatch = watch(ctx.rpc, (rpc,
|
|
339
|
-
if (
|
|
340
|
-
else if (
|
|
422
|
+
const stopWatch = watch([ctx.rpc, ctx.isConnected], ([rpc, connected]) => {
|
|
423
|
+
if (!connected) unbind();
|
|
424
|
+
else if (rpc && (rpc !== boundClient || !isActive.value)) bind(rpc);
|
|
341
425
|
});
|
|
342
426
|
tryOnScopeDispose(() => {
|
|
343
427
|
disposed = true;
|
|
@@ -352,18 +436,19 @@ function useRpcSubscription(subscribeFn, options = {}) {
|
|
|
352
436
|
function defaultRetryDelayMs(attempt) {
|
|
353
437
|
return Math.min(100 * Math.pow(2, attempt), 1e3);
|
|
354
438
|
}
|
|
355
|
-
function
|
|
439
|
+
function isLinkFailure(err, rpc, rpcRef, linkChanged, retryOnTimeout) {
|
|
440
|
+
if (rpcRef.value !== rpc) return true;
|
|
441
|
+
if (rpc.isConnected === false && rpc.isClosed === false) return true;
|
|
356
442
|
if (!(err instanceof Error)) return false;
|
|
357
443
|
const code = err.code;
|
|
358
|
-
if (code ===
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
return false;
|
|
444
|
+
if (code === void 0 && LOCAL_NOT_CONNECTED.has(err.message)) return true;
|
|
445
|
+
if (isLocalNoResponders(err) && isLinkSettling(rpcRef)) return true;
|
|
446
|
+
return code === "TIMEOUT" && (retryOnTimeout || linkChanged);
|
|
447
|
+
}
|
|
448
|
+
function watchDetached(source, cb, options) {
|
|
449
|
+
const scope = effectScope(true);
|
|
450
|
+
scope.run(() => watch(source, cb, options));
|
|
451
|
+
return () => scope.stop();
|
|
367
452
|
}
|
|
368
453
|
async function waitForRpc(rpcRef, timeoutMs, signal) {
|
|
369
454
|
if (rpcRef.value) return rpcRef.value;
|
|
@@ -379,7 +464,7 @@ async function waitForRpc(rpcRef, timeoutMs, signal) {
|
|
|
379
464
|
reject(makeAbortError());
|
|
380
465
|
};
|
|
381
466
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
382
|
-
const stopWatch =
|
|
467
|
+
const stopWatch = watchDetached(rpcRef, (next) => {
|
|
383
468
|
if (next) {
|
|
384
469
|
clearTimeout(timer);
|
|
385
470
|
stopWatch();
|
|
@@ -389,7 +474,7 @@ async function waitForRpc(rpcRef, timeoutMs, signal) {
|
|
|
389
474
|
});
|
|
390
475
|
});
|
|
391
476
|
}
|
|
392
|
-
async function sleep
|
|
477
|
+
async function sleep(ms, signal) {
|
|
393
478
|
return new Promise((resolve, reject) => {
|
|
394
479
|
if (signal?.aborted) {
|
|
395
480
|
reject(makeAbortError());
|
|
@@ -413,10 +498,69 @@ function makeAbortError() {
|
|
|
413
498
|
return err;
|
|
414
499
|
}
|
|
415
500
|
//#endregion
|
|
501
|
+
//#region src/composables/bulkLane.ts
|
|
502
|
+
var PROBE_TIMEOUT_MS = 2e3;
|
|
503
|
+
var RECHECK_AFTER_MS = 6e4;
|
|
504
|
+
var deadMarks = /* @__PURE__ */ new WeakMap();
|
|
505
|
+
var runningProbes = /* @__PURE__ */ new WeakMap();
|
|
506
|
+
function liveBulkClient(lane) {
|
|
507
|
+
const client = lane?.value;
|
|
508
|
+
if (!lane || !client) return void 0;
|
|
509
|
+
const mark = deadMarks.get(lane);
|
|
510
|
+
if (!mark || mark.client !== client) return client;
|
|
511
|
+
if (performance.now() - mark.since >= RECHECK_AFTER_MS && !runningProbes.has(client)) recheck(lane, mark);
|
|
512
|
+
}
|
|
513
|
+
function markBulkDead(lane, client) {
|
|
514
|
+
if (lane.value !== client || deadMarks.has(lane)) return;
|
|
515
|
+
const stop = watchDetached(lane, () => clearMark(lane), { flush: "sync" });
|
|
516
|
+
deadMarks.set(lane, {
|
|
517
|
+
client,
|
|
518
|
+
since: performance.now(),
|
|
519
|
+
stop
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
function noticeBulkTimeout(lane, client) {
|
|
523
|
+
if (lane.value !== client || deadMarks.has(lane)) return;
|
|
524
|
+
probeBulkClient(client).then((alive) => {
|
|
525
|
+
if (!alive) markBulkDead(lane, client);
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
function probeBulkClient(client) {
|
|
529
|
+
let alive = runningProbes.get(client);
|
|
530
|
+
if (!alive) {
|
|
531
|
+
alive = answersPing(client);
|
|
532
|
+
runningProbes.set(client, alive);
|
|
533
|
+
alive.then(() => runningProbes.delete(client));
|
|
534
|
+
}
|
|
535
|
+
return alive;
|
|
536
|
+
}
|
|
537
|
+
async function answersPing(client) {
|
|
538
|
+
try {
|
|
539
|
+
await client.flush(PROBE_TIMEOUT_MS);
|
|
540
|
+
return true;
|
|
541
|
+
} catch {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function recheck(lane, mark) {
|
|
546
|
+
probeBulkClient(mark.client).then((alive) => {
|
|
547
|
+
if (deadMarks.get(lane) !== mark) return;
|
|
548
|
+
if (alive) clearMark(lane);
|
|
549
|
+
else mark.since = performance.now();
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
function clearMark(lane) {
|
|
553
|
+
deadMarks.get(lane)?.stop();
|
|
554
|
+
deadMarks.delete(lane);
|
|
555
|
+
}
|
|
556
|
+
//#endregion
|
|
416
557
|
//#region src/composables/useSnapshot.ts
|
|
417
558
|
var REVOKE_DELAY_MS = 5e3;
|
|
559
|
+
var SERVER_CLOCK_STEP_MS = 6e4;
|
|
418
560
|
var snapshotCache = /* @__PURE__ */ new Map();
|
|
419
561
|
var timestampCache = /* @__PURE__ */ new Map();
|
|
562
|
+
var takenAtCache = /* @__PURE__ */ new Map();
|
|
563
|
+
var stampCache = /* @__PURE__ */ new Map();
|
|
420
564
|
var urlCache = /* @__PURE__ */ new Map();
|
|
421
565
|
var subscribers = /* @__PURE__ */ new Map();
|
|
422
566
|
var pendingLoads$2 = /* @__PURE__ */ new Map();
|
|
@@ -432,16 +576,16 @@ function fetchSnapshotShared(deviceManager, id, forceFresh = false) {
|
|
|
432
576
|
if (!device) return void 0;
|
|
433
577
|
return await device.fetchSnapshot(void 0, forceFresh ? true : void 0) ?? void 0;
|
|
434
578
|
} finally {
|
|
435
|
-
if (device) releaseCameraDevice(
|
|
579
|
+
if (device) releaseCameraDevice(device);
|
|
436
580
|
}
|
|
437
581
|
})().finally(() => pendingLoads$2.delete(id));
|
|
438
582
|
pendingLoads$2.set(id, promise);
|
|
439
583
|
return promise;
|
|
440
584
|
}
|
|
441
|
-
var
|
|
585
|
+
var hookedContexts = /* @__PURE__ */ new WeakSet();
|
|
442
586
|
function hookReconnectRefresh(cameraUi, deviceManager) {
|
|
443
|
-
if (
|
|
444
|
-
|
|
587
|
+
if (hookedContexts.has(cameraUi)) return;
|
|
588
|
+
hookedContexts.add(cameraUi);
|
|
445
589
|
cameraUi.on("reconnected", () => {
|
|
446
590
|
setTimeout(() => {
|
|
447
591
|
let delay = 0;
|
|
@@ -460,7 +604,19 @@ function hookReconnectRefresh(cameraUi, deviceManager) {
|
|
|
460
604
|
function deferRevoke(url) {
|
|
461
605
|
setTimeout(() => URL.revokeObjectURL(url), REVOKE_DELAY_MS);
|
|
462
606
|
}
|
|
463
|
-
function
|
|
607
|
+
function isOlderThanHeld(cameraId, takenAt, stamp) {
|
|
608
|
+
const held = stampCache.get(cameraId);
|
|
609
|
+
if (stamp && held) {
|
|
610
|
+
const behind = held.fetchedAt - stamp.fetchedAt;
|
|
611
|
+
return behind > 0 && behind <= SERVER_CLOCK_STEP_MS;
|
|
612
|
+
}
|
|
613
|
+
return takenAt !== void 0 && takenAt < (takenAtCache.get(cameraId) ?? -Infinity);
|
|
614
|
+
}
|
|
615
|
+
function setSnapshot(cameraId, data, fetchedAt, takenAt, stamp) {
|
|
616
|
+
if (isOlderThanHeld(cameraId, takenAt, stamp)) return;
|
|
617
|
+
takenAtCache.set(cameraId, takenAt ?? Date.now());
|
|
618
|
+
if (stamp) stampCache.set(cameraId, stamp);
|
|
619
|
+
else stampCache.delete(cameraId);
|
|
464
620
|
if (snapshotCache.get(cameraId) !== data) {
|
|
465
621
|
const oldUrl = urlCache.get(cameraId);
|
|
466
622
|
if (oldUrl) {
|
|
@@ -475,6 +631,10 @@ function setSnapshot(cameraId, data, fetchedAt) {
|
|
|
475
631
|
function getSnapshot(cameraId) {
|
|
476
632
|
return snapshotCache.get(cameraId);
|
|
477
633
|
}
|
|
634
|
+
function getSnapshotStamp(cameraId, sourceId) {
|
|
635
|
+
const stamp = stampCache.get(cameraId);
|
|
636
|
+
return stamp?.sourceId === sourceId ? stamp.fetchedAt : void 0;
|
|
637
|
+
}
|
|
478
638
|
function subscribeSnapshot(cameraId, cb) {
|
|
479
639
|
if (!subscribers.has(cameraId)) subscribers.set(cameraId, /* @__PURE__ */ new Set());
|
|
480
640
|
subscribers.get(cameraId).add(cb);
|
|
@@ -497,6 +657,8 @@ function clearSnapshotCache() {
|
|
|
497
657
|
urlCache.clear();
|
|
498
658
|
snapshotCache.clear();
|
|
499
659
|
timestampCache.clear();
|
|
660
|
+
takenAtCache.clear();
|
|
661
|
+
stampCache.clear();
|
|
500
662
|
}
|
|
501
663
|
function useSnapshot(cameraIdOrName) {
|
|
502
664
|
const cameraUi = useCameraUi();
|
|
@@ -529,27 +691,52 @@ function useSnapshot(cameraIdOrName) {
|
|
|
529
691
|
}
|
|
530
692
|
let unsubscribe;
|
|
531
693
|
let heldDeviceId;
|
|
694
|
+
let heldDevice;
|
|
532
695
|
let pendingHoldId;
|
|
696
|
+
const holdRetry = createRetryTimer();
|
|
697
|
+
let disposed = false;
|
|
698
|
+
function currentId() {
|
|
699
|
+
const idOrName = toValue(cameraIdOrName);
|
|
700
|
+
return typeof idOrName === "string" ? idOrName : idOrName._id;
|
|
701
|
+
}
|
|
533
702
|
async function holdDevice(id) {
|
|
534
703
|
if (heldDeviceId === id || pendingHoldId === id) return;
|
|
535
704
|
pendingHoldId = id;
|
|
536
705
|
try {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
if (
|
|
540
|
-
releaseCameraDevice(
|
|
706
|
+
const device = await acquireCameraDevice(deviceManager, id);
|
|
707
|
+
if (!device) return;
|
|
708
|
+
if (disposed || currentId() !== id || heldDeviceId === id) {
|
|
709
|
+
releaseCameraDevice(device);
|
|
541
710
|
return;
|
|
542
711
|
}
|
|
543
712
|
releaseHeldDevice();
|
|
544
713
|
heldDeviceId = id;
|
|
545
|
-
|
|
714
|
+
heldDevice = device;
|
|
715
|
+
cancelHoldRetry();
|
|
716
|
+
} catch {
|
|
717
|
+
if (currentId() === id) scheduleHoldRetry(id);
|
|
718
|
+
} finally {
|
|
546
719
|
pendingHoldId = void 0;
|
|
547
720
|
}
|
|
548
721
|
}
|
|
722
|
+
function scheduleHoldRetry(id) {
|
|
723
|
+
if (disposed) return;
|
|
724
|
+
holdRetry.schedule(() => {
|
|
725
|
+
if (!isConnected.value || currentId() !== id || isCameraDisabled(toValue(cameraIdOrName))) return;
|
|
726
|
+
holdDevice(id).then(() => {
|
|
727
|
+
if (heldDeviceId === id) return loadSnapshot(id);
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
function cancelHoldRetry() {
|
|
732
|
+
holdRetry.cancel();
|
|
733
|
+
holdRetry.reset();
|
|
734
|
+
}
|
|
549
735
|
function releaseHeldDevice() {
|
|
550
736
|
if (heldDeviceId) {
|
|
551
|
-
releaseCameraDevice(
|
|
737
|
+
if (heldDevice) releaseCameraDevice(heldDevice);
|
|
552
738
|
heldDeviceId = void 0;
|
|
739
|
+
heldDevice = void 0;
|
|
553
740
|
}
|
|
554
741
|
}
|
|
555
742
|
function showCached(id) {
|
|
@@ -565,7 +752,7 @@ function useSnapshot(cameraIdOrName) {
|
|
|
565
752
|
_isLoading.value = true;
|
|
566
753
|
try {
|
|
567
754
|
const result = await fetchSnapshotShared(deviceManager, id);
|
|
568
|
-
if (result) snapshot.value = result;
|
|
755
|
+
if (result) snapshot.value = snapshotCache.get(id) ?? result;
|
|
569
756
|
} catch {} finally {
|
|
570
757
|
_isLoading.value = false;
|
|
571
758
|
initialLoadDone.value = true;
|
|
@@ -582,7 +769,7 @@ function useSnapshot(cameraIdOrName) {
|
|
|
582
769
|
_isLoading.value = true;
|
|
583
770
|
try {
|
|
584
771
|
const result = await fetchSnapshotShared(deviceManager, id, true);
|
|
585
|
-
if (result) snapshot.value = result;
|
|
772
|
+
if (result) snapshot.value = snapshotCache.get(id) ?? result;
|
|
586
773
|
} catch {} finally {
|
|
587
774
|
_isLoading.value = false;
|
|
588
775
|
initialLoadDone.value = true;
|
|
@@ -593,10 +780,12 @@ function useSnapshot(cameraIdOrName) {
|
|
|
593
780
|
() => toValue(cameraIdOrName),
|
|
594
781
|
() => isCameraDisabled(toValue(cameraIdOrName))
|
|
595
782
|
], async ([connected, cameraIdName, disabled]) => {
|
|
783
|
+
cancelHoldRetry();
|
|
596
784
|
unsubscribe?.();
|
|
597
785
|
unsubscribe = void 0;
|
|
598
786
|
const id = typeof cameraIdName === "string" ? cameraIdName : cameraIdName._id;
|
|
599
787
|
if (connected && id && !disabled) {
|
|
788
|
+
if (typeof cameraIdName === "object") rememberCameraName(cameraIdName.name, id);
|
|
600
789
|
unsubscribe = subscribe(id);
|
|
601
790
|
showCached(id);
|
|
602
791
|
await holdDevice(id);
|
|
@@ -611,6 +800,8 @@ function useSnapshot(cameraIdOrName) {
|
|
|
611
800
|
}
|
|
612
801
|
}, { immediate: true });
|
|
613
802
|
tryOnScopeDispose(() => {
|
|
803
|
+
disposed = true;
|
|
804
|
+
cancelHoldRetry();
|
|
614
805
|
unsubscribe?.();
|
|
615
806
|
releaseHeldDevice();
|
|
616
807
|
});
|
|
@@ -624,13 +815,25 @@ function useSnapshot(cameraIdOrName) {
|
|
|
624
815
|
}
|
|
625
816
|
//#endregion
|
|
626
817
|
//#region src/composables/useCamera.ts
|
|
818
|
+
var NOT_MODIFIED = "not-modified";
|
|
819
|
+
var snapshotWithMetaSupport = /* @__PURE__ */ new WeakMap();
|
|
820
|
+
var sensorEventFanouts = /* @__PURE__ */ new WeakMap();
|
|
627
821
|
async function createReactiveCameraDevice(rpcOrContext, initialCamera) {
|
|
628
822
|
const cameraNamespaces = NamespaceManager.cameraNamespaces(initialCamera._id);
|
|
629
|
-
const sensorNamespaces = NamespaceManager.sensorRegistryNamespaces();
|
|
630
823
|
const ctx = "rpc" in rpcOrContext && "value" in rpcOrContext.rpc ? rpcOrContext : void 0;
|
|
631
824
|
const rpcRef = ctx ? ctx.rpc : shallowRef(rpcOrContext);
|
|
825
|
+
const bulkRpc = ctx?.bulkRpc;
|
|
632
826
|
let closeSubscription;
|
|
633
827
|
let closeSensorSubscription;
|
|
828
|
+
let closed = false;
|
|
829
|
+
let reconnecting;
|
|
830
|
+
let reconnectAgain = false;
|
|
831
|
+
const reconnectRetry = createRetryTimer();
|
|
832
|
+
let statesSeq = 0;
|
|
833
|
+
let appliedStatesSeq = 0;
|
|
834
|
+
let statesRun;
|
|
835
|
+
let statesNext;
|
|
836
|
+
let sensorRefreshQueued = false;
|
|
634
837
|
const camera = shallowRef(initialCamera);
|
|
635
838
|
const connected = ref(false);
|
|
636
839
|
const frameWorkerConnected = ref(false);
|
|
@@ -674,38 +877,130 @@ async function createReactiveCameraDevice(rpcOrContext, initialCamera) {
|
|
|
674
877
|
const hasPtz = computed(() => existingSensorTypes.value.has(SensorType.PTZ));
|
|
675
878
|
async function fetchSnapshot(sourceId, forceNew) {
|
|
676
879
|
snapshotLoading.value = true;
|
|
880
|
+
const requestedAt = Date.now();
|
|
677
881
|
try {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
if (result && result.byteLength > 0) {
|
|
688
|
-
setSnapshot(initialCamera._id, result, forceNew ? Date.now() : void 0);
|
|
689
|
-
return result;
|
|
690
|
-
}
|
|
882
|
+
const result = await requestSnapshot(sourceId, forceNew);
|
|
883
|
+
if (result === NOT_MODIFIED) return getSnapshot(initialCamera._id);
|
|
884
|
+
if (result && result.data.byteLength > 0) {
|
|
885
|
+
const stamp = result.stamp === void 0 ? void 0 : {
|
|
886
|
+
sourceId,
|
|
887
|
+
fetchedAt: result.stamp
|
|
888
|
+
};
|
|
889
|
+
setSnapshot(initialCamera._id, result.data, result.fetchedAt, result.fetchedAt ?? requestedAt, stamp);
|
|
890
|
+
return result.data;
|
|
691
891
|
}
|
|
892
|
+
return;
|
|
692
893
|
} finally {
|
|
693
894
|
snapshotLoading.value = false;
|
|
694
895
|
}
|
|
695
896
|
}
|
|
897
|
+
async function requestSnapshot(sourceId, forceNew) {
|
|
898
|
+
let lastClient;
|
|
899
|
+
let bulkDead = false;
|
|
900
|
+
try {
|
|
901
|
+
return await rpcCall(rpcRef, async (client) => {
|
|
902
|
+
lastClient = snapshotClient(client, bulkDead);
|
|
903
|
+
try {
|
|
904
|
+
return await snapshotFrom(lastClient, sourceId, forceNew);
|
|
905
|
+
} catch (err) {
|
|
906
|
+
if (!bulkRpc || lastClient === client || err.code !== "TIMEOUT" || await probeBulkClient(lastClient)) throw err;
|
|
907
|
+
bulkDead = true;
|
|
908
|
+
markBulkDead(bulkRpc, lastClient);
|
|
909
|
+
lastClient = client;
|
|
910
|
+
return snapshotFrom(client, sourceId, forceNew);
|
|
911
|
+
}
|
|
912
|
+
}, { retryOnTimeout: !forceNew });
|
|
913
|
+
} catch (err) {
|
|
914
|
+
if (err.code !== "TIMEOUT" || !lastClient || snapshotWithMetaSupport.has(lastClient)) throw err;
|
|
915
|
+
return rpcCall(rpcRef, (client) => legacySnapshotFrom(snapshotClient(client, bulkDead), sourceId, forceNew));
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
function snapshotClient(client, mainOnly = false) {
|
|
919
|
+
return mainOnly ? client : liveBulkClient(bulkRpc) ?? client;
|
|
920
|
+
}
|
|
921
|
+
async function snapshotFrom(client, sourceId, forceNew) {
|
|
922
|
+
const supported = snapshotWithMetaSupport.get(client);
|
|
923
|
+
if (supported === false) return legacySnapshotFrom(client, sourceId, forceNew);
|
|
924
|
+
try {
|
|
925
|
+
const controller = client.createProxy(cameraNamespaces.cameraControllerRpc);
|
|
926
|
+
const known = forceNew ? void 0 : getSnapshotStamp(initialCamera._id, sourceId);
|
|
927
|
+
const result = known === void 0 ? await controller.snapshotWithMeta(sourceId, forceNew) : await controller.snapshotWithMeta(sourceId, forceNew, known);
|
|
928
|
+
snapshotWithMetaSupport.set(client, true);
|
|
929
|
+
if (result?.notModified) return NOT_MODIFIED;
|
|
930
|
+
return result?.data ? {
|
|
931
|
+
data: result.data,
|
|
932
|
+
fetchedAt: Date.now() - (result.ageMs ?? 0),
|
|
933
|
+
stamp: result.fetchedAt
|
|
934
|
+
} : void 0;
|
|
935
|
+
} catch (err) {
|
|
936
|
+
if (err.code === "METHOD_NOT_FOUND") {
|
|
937
|
+
snapshotWithMetaSupport.set(client, false);
|
|
938
|
+
return legacySnapshotFrom(client, sourceId, forceNew);
|
|
939
|
+
}
|
|
940
|
+
if (supported || !isLocalNoResponders(err)) throw err;
|
|
941
|
+
}
|
|
942
|
+
const result = await legacySnapshotFrom(client, sourceId, forceNew);
|
|
943
|
+
if (!isLinkSettling(rpcRef)) snapshotWithMetaSupport.set(client, false);
|
|
944
|
+
return result;
|
|
945
|
+
}
|
|
946
|
+
async function legacySnapshotFrom(client, sourceId, forceNew) {
|
|
947
|
+
const data = await client.createProxy(cameraNamespaces.cameraControllerRpc).snapshot(sourceId, forceNew);
|
|
948
|
+
return data ? {
|
|
949
|
+
data,
|
|
950
|
+
fetchedAt: forceNew ? Date.now() : void 0
|
|
951
|
+
} : void 0;
|
|
952
|
+
}
|
|
696
953
|
async function probeStream(sourceId, probeConfig, refresh = false) {
|
|
697
|
-
return rpcCall(rpcRef, (client) => client.createProxy(cameraNamespaces.cameraControllerRpc).probeStream(sourceId, probeConfig, refresh));
|
|
954
|
+
return rpcCall(rpcRef, (client) => client.createProxy(cameraNamespaces.cameraControllerRpc).probeStream(sourceId, probeConfig, refresh), { retryOnTimeout: !refresh });
|
|
698
955
|
}
|
|
699
956
|
async function streamUrl(sourceId) {
|
|
700
957
|
return rpcCall(rpcRef, (client) => client.createProxy(cameraNamespaces.cameraControllerRpc).streamUrl(sourceId));
|
|
701
958
|
}
|
|
702
|
-
|
|
959
|
+
function refreshStates() {
|
|
960
|
+
statesNext = void 0;
|
|
961
|
+
return startStatesRun();
|
|
962
|
+
}
|
|
963
|
+
function refreshStatesCoalesced() {
|
|
964
|
+
if (statesNext) return statesNext;
|
|
965
|
+
if (!statesRun) return startStatesRun();
|
|
966
|
+
const queuedAt = statesSeq;
|
|
967
|
+
statesNext = statesRun.catch(() => {}).then(() => {
|
|
968
|
+
if (statesSeq !== queuedAt) return statesRun;
|
|
969
|
+
statesNext = void 0;
|
|
970
|
+
return startStatesRun();
|
|
971
|
+
});
|
|
972
|
+
return statesNext;
|
|
973
|
+
}
|
|
974
|
+
function startStatesRun() {
|
|
975
|
+
const run = fetchStates(++statesSeq).finally(() => {
|
|
976
|
+
if (statesRun === run) statesRun = void 0;
|
|
977
|
+
});
|
|
978
|
+
statesRun = run;
|
|
979
|
+
return run;
|
|
980
|
+
}
|
|
981
|
+
async function fetchStates(seq) {
|
|
703
982
|
const response = await rpcCall(rpcRef, (client) => client.createProxy(cameraNamespaces.cameraControllerRpc).refreshStates());
|
|
983
|
+
if (seq < appliedStatesSeq) return;
|
|
984
|
+
appliedStatesSeq = seq;
|
|
704
985
|
camera.value = response.camera;
|
|
705
986
|
connected.value = response.cameraState;
|
|
706
987
|
frameWorkerConnected.value = response.frameWorkerState;
|
|
707
988
|
sensorStates.value = response.sensorStates;
|
|
708
989
|
}
|
|
990
|
+
function refreshForSensors() {
|
|
991
|
+
if (sensorRefreshQueued || closed) return;
|
|
992
|
+
sensorRefreshQueued = true;
|
|
993
|
+
queueMicrotask(() => {
|
|
994
|
+
sensorRefreshQueued = false;
|
|
995
|
+
if (!closed) refreshStatesCoalesced().catch(() => {});
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
function dropSensorState(sensorId) {
|
|
999
|
+
if (sensorStates.value[sensorId] === void 0) return;
|
|
1000
|
+
const newStates = { ...sensorStates.value };
|
|
1001
|
+
delete newStates[sensorId];
|
|
1002
|
+
sensorStates.value = newStates;
|
|
1003
|
+
}
|
|
709
1004
|
async function handleCameraEvent(event) {
|
|
710
1005
|
switch (event.type) {
|
|
711
1006
|
case "removed":
|
|
@@ -720,7 +1015,14 @@ async function createReactiveCameraDevice(rpcOrContext, initialCamera) {
|
|
|
720
1015
|
case "frameWorkerState":
|
|
721
1016
|
frameWorkerConnected.value = event.data;
|
|
722
1017
|
break;
|
|
723
|
-
case "snapshot:updated":
|
|
1018
|
+
case "snapshot:updated": {
|
|
1019
|
+
const { sourceId, snapshot: data, fetchedAt } = event.data;
|
|
1020
|
+
setSnapshot(initialCamera._id, data, fetchedAt, void 0, sourceId && fetchedAt !== void 0 ? {
|
|
1021
|
+
sourceId,
|
|
1022
|
+
fetchedAt
|
|
1023
|
+
} : void 0);
|
|
1024
|
+
break;
|
|
1025
|
+
}
|
|
724
1026
|
}
|
|
725
1027
|
}
|
|
726
1028
|
function handleSensorEvent(event) {
|
|
@@ -732,38 +1034,72 @@ async function createReactiveCameraDevice(rpcOrContext, initialCamera) {
|
|
|
732
1034
|
...sensorStates.value,
|
|
733
1035
|
[addedEvent.sensor.id]: addedEvent.state
|
|
734
1036
|
};
|
|
735
|
-
} else if (event.type === "sensor:deleted")
|
|
736
|
-
|
|
737
|
-
const newStates = { ...sensorStates.value };
|
|
738
|
-
delete newStates[deletedEvent.sensorId];
|
|
739
|
-
sensorStates.value = newStates;
|
|
740
|
-
} else if (event.type === "sensor:connected:changed") {
|
|
1037
|
+
} else if (event.type === "sensor:deleted") dropSensorState(event.data.sensorId);
|
|
1038
|
+
else if (event.type === "sensor:connected:changed") {
|
|
741
1039
|
const connectedEvent = event.data;
|
|
742
|
-
if (connectedEvent.connected)
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
sensorStates.value = newStates;
|
|
747
|
-
}
|
|
1040
|
+
if (connectedEvent.connected) {
|
|
1041
|
+
if (connectedEvent.assignedCameraIds && !connectedEvent.assignedCameraIds.includes(initialCamera._id)) return;
|
|
1042
|
+
refreshForSensors();
|
|
1043
|
+
} else dropSensorState(connectedEvent.sensorId);
|
|
748
1044
|
} else if (event.type === "sensor:assignment:changed") {
|
|
749
1045
|
const assignmentEvent = event.data;
|
|
750
1046
|
if (assignmentEvent.cameraId !== initialCamera._id) return;
|
|
751
|
-
if (assignmentEvent.assigned)
|
|
752
|
-
else
|
|
753
|
-
const newStates = { ...sensorStates.value };
|
|
754
|
-
delete newStates[assignmentEvent.sensorId];
|
|
755
|
-
sensorStates.value = newStates;
|
|
756
|
-
}
|
|
1047
|
+
if (assignmentEvent.assigned) refreshForSensors();
|
|
1048
|
+
else dropSensorState(assignmentEvent.sensorId);
|
|
757
1049
|
}
|
|
758
1050
|
}
|
|
759
1051
|
async function init() {
|
|
760
1052
|
closeSubscription?.();
|
|
1053
|
+
closeSubscription = void 0;
|
|
761
1054
|
closeSensorSubscription?.();
|
|
762
|
-
|
|
763
|
-
|
|
1055
|
+
closeSensorSubscription = void 0;
|
|
1056
|
+
const cameraSubscription = await rpcCall(rpcRef, (client) => client.subscribe(cameraNamespaces.cameraSubject, handleCameraEvent));
|
|
1057
|
+
if (closed) {
|
|
1058
|
+
cameraSubscription();
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
closeSubscription = cameraSubscription;
|
|
1062
|
+
const sensorSubscription = await rpcCall(rpcRef, (client) => joinSensorEvents(client, handleSensorEvent));
|
|
1063
|
+
if (closed) {
|
|
1064
|
+
sensorSubscription();
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
closeSensorSubscription = sensorSubscription;
|
|
764
1068
|
await refreshStates();
|
|
765
1069
|
}
|
|
1070
|
+
function reconnect() {
|
|
1071
|
+
if (closed) return Promise.resolve();
|
|
1072
|
+
reconnectRetry.cancel();
|
|
1073
|
+
if (reconnecting) {
|
|
1074
|
+
reconnectAgain = true;
|
|
1075
|
+
return reconnecting;
|
|
1076
|
+
}
|
|
1077
|
+
reconnecting = runReconnect();
|
|
1078
|
+
return reconnecting;
|
|
1079
|
+
}
|
|
1080
|
+
async function runReconnect() {
|
|
1081
|
+
try {
|
|
1082
|
+
do {
|
|
1083
|
+
reconnectAgain = false;
|
|
1084
|
+
await init();
|
|
1085
|
+
} while (reconnectAgain && !closed);
|
|
1086
|
+
reconnectRetry.reset();
|
|
1087
|
+
} catch (err) {
|
|
1088
|
+
scheduleReconnect();
|
|
1089
|
+
throw err;
|
|
1090
|
+
} finally {
|
|
1091
|
+
reconnecting = void 0;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
function scheduleReconnect() {
|
|
1095
|
+
if (closed) return;
|
|
1096
|
+
reconnectRetry.schedule(() => {
|
|
1097
|
+
reconnect().catch(() => {});
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
766
1100
|
async function close() {
|
|
1101
|
+
closed = true;
|
|
1102
|
+
reconnectRetry.cancel();
|
|
767
1103
|
closeSubscription?.();
|
|
768
1104
|
closeSubscription = void 0;
|
|
769
1105
|
closeSensorSubscription?.();
|
|
@@ -782,7 +1118,12 @@ async function createReactiveCameraDevice(rpcOrContext, initialCamera) {
|
|
|
782
1118
|
if (!id) return Promise.resolve(void 0);
|
|
783
1119
|
return probeStream(id, probeConfig, refresh);
|
|
784
1120
|
}
|
|
785
|
-
|
|
1121
|
+
try {
|
|
1122
|
+
await init();
|
|
1123
|
+
} catch (err) {
|
|
1124
|
+
await close();
|
|
1125
|
+
throw err;
|
|
1126
|
+
}
|
|
786
1127
|
return {
|
|
787
1128
|
id: initialCamera._id,
|
|
788
1129
|
name,
|
|
@@ -815,10 +1156,41 @@ async function createReactiveCameraDevice(rpcOrContext, initialCamera) {
|
|
|
815
1156
|
probeStream: probeStreamFn,
|
|
816
1157
|
streamUrl,
|
|
817
1158
|
refreshStates,
|
|
818
|
-
reconnect
|
|
1159
|
+
reconnect,
|
|
819
1160
|
close
|
|
820
1161
|
};
|
|
821
1162
|
}
|
|
1163
|
+
async function joinSensorEvents(client, handler) {
|
|
1164
|
+
let fanout = sensorEventFanouts.get(client);
|
|
1165
|
+
if (!fanout) {
|
|
1166
|
+
const handlers = /* @__PURE__ */ new Set();
|
|
1167
|
+
const subject = NamespaceManager.sensorRegistryNamespaces().sensorsSubject;
|
|
1168
|
+
fanout = {
|
|
1169
|
+
handlers,
|
|
1170
|
+
subscribed: client.subscribe(subject, (event) => {
|
|
1171
|
+
for (const each of handlers) try {
|
|
1172
|
+
each(event);
|
|
1173
|
+
} catch {}
|
|
1174
|
+
})
|
|
1175
|
+
};
|
|
1176
|
+
sensorEventFanouts.set(client, fanout);
|
|
1177
|
+
}
|
|
1178
|
+
const joined = fanout;
|
|
1179
|
+
joined.handlers.add(handler);
|
|
1180
|
+
try {
|
|
1181
|
+
await joined.subscribed;
|
|
1182
|
+
} catch (err) {
|
|
1183
|
+
joined.handlers.delete(handler);
|
|
1184
|
+
if (sensorEventFanouts.get(client) === joined) sensorEventFanouts.delete(client);
|
|
1185
|
+
throw err;
|
|
1186
|
+
}
|
|
1187
|
+
return () => leaveSensorEvents(client, joined, handler);
|
|
1188
|
+
}
|
|
1189
|
+
function leaveSensorEvents(client, fanout, handler) {
|
|
1190
|
+
if (!fanout.handlers.delete(handler) || fanout.handlers.size > 0) return;
|
|
1191
|
+
if (sensorEventFanouts.get(client) === fanout) sensorEventFanouts.delete(client);
|
|
1192
|
+
fanout.subscribed.then((off) => off()).catch(() => {});
|
|
1193
|
+
}
|
|
822
1194
|
//#endregion
|
|
823
1195
|
//#region src/composables/useDeviceManager.ts
|
|
824
1196
|
function useDeviceManager() {
|
|
@@ -837,36 +1209,74 @@ var cameraCache = createDebouncedCache({
|
|
|
837
1209
|
onRelease: (_key, device) => device.close()
|
|
838
1210
|
});
|
|
839
1211
|
var pendingLoads$1 = /* @__PURE__ */ new Map();
|
|
1212
|
+
var knownNames = /* @__PURE__ */ new Map();
|
|
840
1213
|
function clearCameraCache() {
|
|
841
1214
|
cameraCache.clear();
|
|
842
1215
|
pendingLoads$1.clear();
|
|
1216
|
+
knownNames.clear();
|
|
843
1217
|
}
|
|
844
|
-
|
|
845
|
-
if (
|
|
1218
|
+
function rememberCameraName(name, id) {
|
|
1219
|
+
if (name && id && name !== id) knownNames.set(name, id);
|
|
1220
|
+
}
|
|
1221
|
+
function cachedIdFor(key) {
|
|
1222
|
+
if (cameraCache.has(key)) return key;
|
|
1223
|
+
let found;
|
|
1224
|
+
cameraCache.forEachValue((device, id) => {
|
|
1225
|
+
if (found === void 0 && device.name.value === key) found = id;
|
|
1226
|
+
});
|
|
1227
|
+
return found;
|
|
1228
|
+
}
|
|
1229
|
+
function pendingLoadFor(key) {
|
|
1230
|
+
const own = pendingLoads$1.get(key);
|
|
1231
|
+
if (own) return own;
|
|
1232
|
+
for (const [name, id] of knownNames) {
|
|
1233
|
+
const other = id === key ? pendingLoads$1.get(name) : name === key ? pendingLoads$1.get(id) : void 0;
|
|
1234
|
+
if (other) return other;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
function acquireCached(key) {
|
|
1238
|
+
const id = cachedIdFor(key);
|
|
1239
|
+
if (id === void 0) return void 0;
|
|
1240
|
+
return cameraCache.acquire(id, () => {
|
|
846
1241
|
throw new Error("Should not create - already cached");
|
|
847
1242
|
});
|
|
848
|
-
|
|
1243
|
+
}
|
|
1244
|
+
async function acquireCameraDevice(deviceManager, key) {
|
|
1245
|
+
const cached = acquireCached(key);
|
|
1246
|
+
if (cached) return cached;
|
|
1247
|
+
const pending = pendingLoadFor(key);
|
|
849
1248
|
if (pending) {
|
|
850
1249
|
const device = await pending;
|
|
851
|
-
|
|
1250
|
+
const joined = acquireCached(key);
|
|
1251
|
+
if (joined || !device) return joined;
|
|
852
1252
|
}
|
|
853
|
-
const loadPromise = deviceManager.getCamera(
|
|
854
|
-
pendingLoads$1.set(
|
|
1253
|
+
const loadPromise = deviceManager.getCamera(key);
|
|
1254
|
+
pendingLoads$1.set(key, loadPromise);
|
|
855
1255
|
try {
|
|
856
1256
|
const device = await loadPromise;
|
|
857
|
-
return device ?
|
|
1257
|
+
return device ? cacheLoadedDevice(device) : void 0;
|
|
858
1258
|
} finally {
|
|
859
|
-
pendingLoads$1.delete(
|
|
1259
|
+
if (pendingLoads$1.get(key) === loadPromise) pendingLoads$1.delete(key);
|
|
860
1260
|
}
|
|
861
1261
|
}
|
|
862
|
-
function
|
|
863
|
-
cameraCache.
|
|
1262
|
+
function cacheLoadedDevice(device) {
|
|
1263
|
+
const held = cameraCache.acquire(device.id, () => device);
|
|
1264
|
+
if (held !== device) device.close();
|
|
1265
|
+
return held;
|
|
1266
|
+
}
|
|
1267
|
+
function releaseCameraDevice(device) {
|
|
1268
|
+
cameraCache.release(device.id, device);
|
|
864
1269
|
}
|
|
865
1270
|
function reconnectAllCameraDevices() {
|
|
866
1271
|
cameraCache.forEachValue((device) => {
|
|
867
1272
|
device.reconnect().catch(() => {});
|
|
868
1273
|
});
|
|
869
1274
|
}
|
|
1275
|
+
function refreshAllCameraDevices() {
|
|
1276
|
+
cameraCache.forEachValue((device) => {
|
|
1277
|
+
device.refreshStates().catch(() => device.reconnect().catch(() => {}));
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
870
1280
|
function useCameraById(cameraIdOrName) {
|
|
871
1281
|
const { isConnected } = useCameraUi();
|
|
872
1282
|
const deviceManager = useDeviceManager();
|
|
@@ -874,89 +1284,90 @@ function useCameraById(cameraIdOrName) {
|
|
|
874
1284
|
const _isLoading = ref(false);
|
|
875
1285
|
const initialLoadDone = ref(false);
|
|
876
1286
|
const error = ref();
|
|
877
|
-
let
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
1287
|
+
let boundKey;
|
|
1288
|
+
const loadRetry = createRetryTimer();
|
|
1289
|
+
let disposed = false;
|
|
1290
|
+
function bindCamera(key, device) {
|
|
1291
|
+
boundKey = key;
|
|
1292
|
+
camera.value = device;
|
|
1293
|
+
error.value = void 0;
|
|
1294
|
+
cancelRetry();
|
|
1295
|
+
}
|
|
1296
|
+
function unbindCamera() {
|
|
1297
|
+
if (camera.value) releaseCameraDevice(camera.value);
|
|
1298
|
+
camera.value = void 0;
|
|
1299
|
+
boundKey = void 0;
|
|
1300
|
+
}
|
|
1301
|
+
function failLoad(key, err) {
|
|
1302
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
1303
|
+
scheduleRetry(key);
|
|
1304
|
+
}
|
|
1305
|
+
function scheduleRetry(key) {
|
|
1306
|
+
if (disposed) return;
|
|
1307
|
+
loadRetry.schedule(() => {
|
|
1308
|
+
if (isConnected.value && toValue(cameraIdOrName) === key && boundKey !== key) loadCamera(key, true).catch(() => {});
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
function cancelRetry() {
|
|
1312
|
+
loadRetry.cancel();
|
|
1313
|
+
loadRetry.reset();
|
|
1314
|
+
}
|
|
1315
|
+
async function loadCamera(key, retry = false) {
|
|
1316
|
+
if (disposed || !isConnected.value || !key) return;
|
|
1317
|
+
if (boundKey !== void 0 && boundKey !== key) {
|
|
1318
|
+
if (camera.value && (camera.value.id === key || camera.value.name.value === key)) {
|
|
1319
|
+
boundKey = key;
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
unbindCamera();
|
|
884
1323
|
}
|
|
885
|
-
if (
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
currentCameraId = id;
|
|
890
|
-
camera.value = cached;
|
|
1324
|
+
if (camera.value) return;
|
|
1325
|
+
const cached = acquireCached(key);
|
|
1326
|
+
if (cached) {
|
|
1327
|
+
bindCamera(key, cached);
|
|
891
1328
|
initialLoadDone.value = true;
|
|
892
1329
|
return;
|
|
893
1330
|
}
|
|
894
|
-
|
|
895
|
-
if (pending) {
|
|
1331
|
+
if (!retry) {
|
|
896
1332
|
_isLoading.value = true;
|
|
897
|
-
|
|
898
|
-
const device = await pending;
|
|
899
|
-
if (device && cameraCache.has(id) && toValue(cameraIdOrName) === id) {
|
|
900
|
-
const cached = cameraCache.acquire(id, () => device);
|
|
901
|
-
currentCameraId = id;
|
|
902
|
-
camera.value = cached;
|
|
903
|
-
}
|
|
904
|
-
} catch (err) {
|
|
905
|
-
error.value = err instanceof Error ? err : new Error(String(err));
|
|
906
|
-
} finally {
|
|
907
|
-
_isLoading.value = false;
|
|
908
|
-
initialLoadDone.value = true;
|
|
909
|
-
}
|
|
910
|
-
return;
|
|
1333
|
+
error.value = void 0;
|
|
911
1334
|
}
|
|
912
|
-
_isLoading.value = true;
|
|
913
|
-
error.value = void 0;
|
|
914
|
-
const loadPromise = deviceManager.getCamera(id);
|
|
915
|
-
pendingLoads$1.set(id, loadPromise);
|
|
916
1335
|
try {
|
|
917
|
-
const device = await
|
|
918
|
-
if (device)
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
return;
|
|
923
|
-
}
|
|
924
|
-
currentCameraId = id;
|
|
925
|
-
camera.value = device;
|
|
1336
|
+
const device = await acquireCameraDevice(deviceManager, key);
|
|
1337
|
+
if (!device) return;
|
|
1338
|
+
if (disposed || toValue(cameraIdOrName) !== key || camera.value) {
|
|
1339
|
+
releaseCameraDevice(device);
|
|
1340
|
+
return;
|
|
926
1341
|
}
|
|
1342
|
+
bindCamera(key, device);
|
|
927
1343
|
} catch (err) {
|
|
928
|
-
|
|
1344
|
+
failLoad(key, err);
|
|
929
1345
|
} finally {
|
|
930
|
-
pendingLoads$1.delete(id);
|
|
931
1346
|
_isLoading.value = false;
|
|
932
1347
|
initialLoadDone.value = true;
|
|
933
1348
|
}
|
|
934
1349
|
}
|
|
935
1350
|
async function refresh() {
|
|
936
|
-
const
|
|
937
|
-
if (!
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1351
|
+
const key = toValue(cameraIdOrName);
|
|
1352
|
+
if (!key) return;
|
|
1353
|
+
const device = camera.value;
|
|
1354
|
+
if (device) {
|
|
1355
|
+
if (cameraCache.get(device.id) === device && cameraCache.getRefCount(device.id) <= 1) cameraCache.forceRelease(device.id);
|
|
1356
|
+
else releaseCameraDevice(device);
|
|
941
1357
|
camera.value = void 0;
|
|
942
|
-
|
|
1358
|
+
boundKey = void 0;
|
|
943
1359
|
}
|
|
944
|
-
await loadCamera(
|
|
1360
|
+
await loadCamera(key);
|
|
945
1361
|
}
|
|
946
|
-
watch([isConnected, () => toValue(cameraIdOrName)], async ([connected,
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
camera.value = void 0;
|
|
951
|
-
currentCameraId = void 0;
|
|
952
|
-
}
|
|
1362
|
+
watch([isConnected, () => toValue(cameraIdOrName)], async ([connected, key]) => {
|
|
1363
|
+
cancelRetry();
|
|
1364
|
+
if (connected && key) await loadCamera(key);
|
|
1365
|
+
else if (!connected && boundKey !== void 0) unbindCamera();
|
|
953
1366
|
}, { immediate: true });
|
|
954
1367
|
tryOnScopeDispose(() => {
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
currentCameraId = void 0;
|
|
959
|
-
}
|
|
1368
|
+
disposed = true;
|
|
1369
|
+
cancelRetry();
|
|
1370
|
+
unbindCamera();
|
|
960
1371
|
});
|
|
961
1372
|
return {
|
|
962
1373
|
camera,
|
|
@@ -966,12 +1377,163 @@ function useCameraById(cameraIdOrName) {
|
|
|
966
1377
|
};
|
|
967
1378
|
}
|
|
968
1379
|
//#endregion
|
|
1380
|
+
//#region src/composables/resolvePluginId.ts
|
|
1381
|
+
var pluginIdCache = /* @__PURE__ */ new Map();
|
|
1382
|
+
var pluginNames = /* @__PURE__ */ new Map();
|
|
1383
|
+
var pendingResolves = /* @__PURE__ */ new Map();
|
|
1384
|
+
function clearPluginIdCache() {
|
|
1385
|
+
pluginIdCache.clear();
|
|
1386
|
+
pluginNames.clear();
|
|
1387
|
+
}
|
|
1388
|
+
function pluginNameOf(pluginId) {
|
|
1389
|
+
return pluginNames.get(pluginId);
|
|
1390
|
+
}
|
|
1391
|
+
function forgetPluginId(pluginName) {
|
|
1392
|
+
pluginIdCache.delete(pluginName);
|
|
1393
|
+
}
|
|
1394
|
+
async function resolvePluginId(rpcRef, pluginName) {
|
|
1395
|
+
const cached = pluginIdCache.get(pluginName);
|
|
1396
|
+
if (cached) return cached;
|
|
1397
|
+
const pending = pendingResolves.get(pluginName);
|
|
1398
|
+
if (pending) return pending;
|
|
1399
|
+
const promise = (async () => {
|
|
1400
|
+
const namespaces = NamespaceManager.coreManagerNamespaces();
|
|
1401
|
+
const pluginInfo = await rpcCall(rpcRef, (rpc) => rpc.createProxy(namespaces.coreManagerRpc).getPlugin(pluginName));
|
|
1402
|
+
if (pluginInfo) {
|
|
1403
|
+
pluginIdCache.set(pluginName, pluginInfo.id);
|
|
1404
|
+
pluginNames.set(pluginInfo.id, pluginName);
|
|
1405
|
+
return pluginInfo.id;
|
|
1406
|
+
}
|
|
1407
|
+
})().finally(() => pendingResolves.delete(pluginName));
|
|
1408
|
+
pendingResolves.set(pluginName, promise);
|
|
1409
|
+
return promise;
|
|
1410
|
+
}
|
|
1411
|
+
//#endregion
|
|
1412
|
+
//#region src/composables/useOAuth.ts
|
|
1413
|
+
var POLL_ACTIVE_MS = 1500;
|
|
1414
|
+
var POLL_IDLE_MS = 3e4;
|
|
1415
|
+
var oauthStates = /* @__PURE__ */ new Map();
|
|
1416
|
+
function clearOAuthCache() {
|
|
1417
|
+
for (const entry of oauthStates.values()) entry.stop();
|
|
1418
|
+
oauthStates.clear();
|
|
1419
|
+
}
|
|
1420
|
+
function createOAuthState(pluginName) {
|
|
1421
|
+
const ctx = useCameraUi();
|
|
1422
|
+
const state = ref({ status: "disconnected" });
|
|
1423
|
+
const metadata = ref(null);
|
|
1424
|
+
let timer;
|
|
1425
|
+
let timerMs = 0;
|
|
1426
|
+
let users = 0;
|
|
1427
|
+
let polling = false;
|
|
1428
|
+
let seq = 0;
|
|
1429
|
+
let appliedSeq = 0;
|
|
1430
|
+
function proxyFor(rpc, id) {
|
|
1431
|
+
return rpc.createProxy(NamespaceManager.pluginNamespaces(id).pluginChildRpc);
|
|
1432
|
+
}
|
|
1433
|
+
const call = (fn) => rpcCall(ctx.rpc, async (rpc) => {
|
|
1434
|
+
const id = await resolvePluginId(ctx.rpc, pluginName);
|
|
1435
|
+
if (!id) throw new Error(`Plugin "${pluginName}" not found`);
|
|
1436
|
+
try {
|
|
1437
|
+
return await fn(proxyFor(rpc, id));
|
|
1438
|
+
} catch (err) {
|
|
1439
|
+
if (!isNoRespondersError(err)) throw err;
|
|
1440
|
+
forgetPluginId(pluginName);
|
|
1441
|
+
const freshId = await resolvePluginId(ctx.rpc, pluginName);
|
|
1442
|
+
if (!freshId || freshId === id) throw err;
|
|
1443
|
+
return fn(proxyFor(rpc, freshId));
|
|
1444
|
+
}
|
|
1445
|
+
});
|
|
1446
|
+
function apply(next, at = ++seq) {
|
|
1447
|
+
if (at < appliedSeq) return;
|
|
1448
|
+
appliedSeq = at;
|
|
1449
|
+
state.value = next;
|
|
1450
|
+
schedule();
|
|
1451
|
+
}
|
|
1452
|
+
async function refresh() {
|
|
1453
|
+
const at = ++seq;
|
|
1454
|
+
apply(await call((proxy) => proxy.getOAuthState()), at);
|
|
1455
|
+
if (!metadata.value) metadata.value = await call((proxy) => proxy.getOAuthMetadata());
|
|
1456
|
+
}
|
|
1457
|
+
function poll() {
|
|
1458
|
+
if (polling) return;
|
|
1459
|
+
polling = true;
|
|
1460
|
+
refresh().catch(() => void 0).finally(() => {
|
|
1461
|
+
polling = false;
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
function schedule() {
|
|
1465
|
+
const { status } = state.value;
|
|
1466
|
+
const ms = users === 0 ? 0 : status === "awaiting_user" || status === "polling" ? POLL_ACTIVE_MS : POLL_IDLE_MS;
|
|
1467
|
+
if (ms === timerMs) return;
|
|
1468
|
+
clearInterval(timer);
|
|
1469
|
+
timer = ms ? setInterval(poll, ms) : void 0;
|
|
1470
|
+
timerMs = ms;
|
|
1471
|
+
}
|
|
1472
|
+
function retain() {
|
|
1473
|
+
if (users++ > 0) return;
|
|
1474
|
+
schedule();
|
|
1475
|
+
poll();
|
|
1476
|
+
}
|
|
1477
|
+
function release() {
|
|
1478
|
+
if (users === 0) return;
|
|
1479
|
+
users--;
|
|
1480
|
+
schedule();
|
|
1481
|
+
}
|
|
1482
|
+
function stop() {
|
|
1483
|
+
users = 0;
|
|
1484
|
+
schedule();
|
|
1485
|
+
}
|
|
1486
|
+
return {
|
|
1487
|
+
state,
|
|
1488
|
+
metadata,
|
|
1489
|
+
call,
|
|
1490
|
+
refresh,
|
|
1491
|
+
apply,
|
|
1492
|
+
retain,
|
|
1493
|
+
release,
|
|
1494
|
+
stop
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
function useOAuth(pluginName) {
|
|
1498
|
+
if (!oauthStates.has(pluginName)) oauthStates.set(pluginName, createOAuthState(pluginName));
|
|
1499
|
+
const entry = oauthStates.get(pluginName);
|
|
1500
|
+
const { state, metadata, call, refresh, apply } = entry;
|
|
1501
|
+
entry.retain();
|
|
1502
|
+
tryOnScopeDispose(entry.release);
|
|
1503
|
+
async function startDeviceFlow(scope) {
|
|
1504
|
+
apply(await call((proxy) => proxy.startDeviceFlow(scope)));
|
|
1505
|
+
}
|
|
1506
|
+
async function startAuthCodeFlow(scope) {
|
|
1507
|
+
apply(await call((proxy) => proxy.startAuthCodeFlow(scope)));
|
|
1508
|
+
}
|
|
1509
|
+
async function cancel() {
|
|
1510
|
+
if (state.value.status === "awaiting_user" || state.value.status === "polling") await call((proxy) => proxy.cancelDeviceFlow?.() ?? proxy.cancelAuthCodeFlow?.() ?? Promise.resolve());
|
|
1511
|
+
await refresh();
|
|
1512
|
+
}
|
|
1513
|
+
async function disconnect() {
|
|
1514
|
+
await call((proxy) => proxy.disconnect());
|
|
1515
|
+
await refresh();
|
|
1516
|
+
}
|
|
1517
|
+
return {
|
|
1518
|
+
state,
|
|
1519
|
+
metadata,
|
|
1520
|
+
startDeviceFlow,
|
|
1521
|
+
startAuthCodeFlow,
|
|
1522
|
+
cancel,
|
|
1523
|
+
disconnect,
|
|
1524
|
+
refresh
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
//#endregion
|
|
969
1528
|
//#region src/composables/usePlugin.ts
|
|
970
1529
|
var RETRY_BASE_MS = 2e3;
|
|
971
1530
|
var RETRY_MAX_MS = 3e4;
|
|
972
1531
|
var pluginCache = createDebouncedCache({ releaseDelay: 1e3 });
|
|
973
1532
|
var pendingLoads = /* @__PURE__ */ new Map();
|
|
1533
|
+
var pendingChecks = /* @__PURE__ */ new Map();
|
|
974
1534
|
var instances = /* @__PURE__ */ new Set();
|
|
1535
|
+
var statusListeners = /* @__PURE__ */ new Set();
|
|
1536
|
+
var pluginRunning = /* @__PURE__ */ new Map();
|
|
975
1537
|
var reloaders = /* @__PURE__ */ new Map();
|
|
976
1538
|
var retryTimers = /* @__PURE__ */ new Map();
|
|
977
1539
|
var retryAttempts = /* @__PURE__ */ new Map();
|
|
@@ -980,6 +1542,8 @@ var statusUnsubscribe;
|
|
|
980
1542
|
function clearPluginCache() {
|
|
981
1543
|
pluginCache.clear();
|
|
982
1544
|
pendingLoads.clear();
|
|
1545
|
+
pendingChecks.clear();
|
|
1546
|
+
forgetPluginStatus();
|
|
983
1547
|
for (const reset of instances) try {
|
|
984
1548
|
reset();
|
|
985
1549
|
} catch {}
|
|
@@ -1003,12 +1567,72 @@ function scheduleRetry(name) {
|
|
|
1003
1567
|
}, Math.min(RETRY_BASE_MS * 2 ** attempt, RETRY_MAX_MS));
|
|
1004
1568
|
retryTimers.set(name, timer);
|
|
1005
1569
|
}
|
|
1006
|
-
function
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1570
|
+
function onPluginStatus(listener) {
|
|
1571
|
+
statusListeners.add(listener);
|
|
1572
|
+
return () => {
|
|
1573
|
+
statusListeners.delete(listener);
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
function isPluginStopped(pluginName) {
|
|
1577
|
+
return pluginRunning.get(pluginName) === false;
|
|
1578
|
+
}
|
|
1579
|
+
function forgetPluginStatus() {
|
|
1580
|
+
pluginRunning.clear();
|
|
1581
|
+
}
|
|
1582
|
+
function applyStatus(pluginName, running, stale = !running) {
|
|
1583
|
+
pluginRunning.set(pluginName, running);
|
|
1584
|
+
if (stale) {
|
|
1585
|
+
pluginCache.forceRelease(pluginName);
|
|
1586
|
+
forgetPluginId(pluginName);
|
|
1587
|
+
}
|
|
1010
1588
|
stopRetry(pluginName);
|
|
1011
1589
|
notifyReloaders(pluginName);
|
|
1590
|
+
for (const listener of [...statusListeners]) try {
|
|
1591
|
+
listener(pluginName, running);
|
|
1592
|
+
} catch {}
|
|
1593
|
+
}
|
|
1594
|
+
function handleStatusMessage(message) {
|
|
1595
|
+
if (message?.type !== "pluginStatusChanged") return;
|
|
1596
|
+
applyStatus(message.data.pluginName, message.data.running);
|
|
1597
|
+
}
|
|
1598
|
+
function checkPlugin(name, rpc) {
|
|
1599
|
+
const cached = pluginCache.get(name);
|
|
1600
|
+
if (!cached || pendingChecks.has(name)) return;
|
|
1601
|
+
const check = rpcCall(rpc, (client) => client.createProxy(NamespaceManager.coreManagerNamespaces().coreManagerRpc).getPlugin(name)).then((info) => {
|
|
1602
|
+
const pluginInfo = info;
|
|
1603
|
+
if (pluginCache.get(name) !== cached) return;
|
|
1604
|
+
const running = !!pluginInfo && pluginInfo.running !== false;
|
|
1605
|
+
const moved = running && NamespaceManager.pluginNamespaces(pluginInfo.id).pluginChildRpc !== cached.namespace;
|
|
1606
|
+
if (running && !moved) return;
|
|
1607
|
+
applyStatus(name, running, true);
|
|
1608
|
+
}).catch(() => {}).finally(() => {
|
|
1609
|
+
if (pendingChecks.get(name) === check) pendingChecks.delete(name);
|
|
1610
|
+
});
|
|
1611
|
+
pendingChecks.set(name, check);
|
|
1612
|
+
}
|
|
1613
|
+
function createLaneProxy(namespace, main, lane) {
|
|
1614
|
+
const proxies = /* @__PURE__ */ new WeakMap();
|
|
1615
|
+
return new Proxy(main, { get(_target, prop) {
|
|
1616
|
+
const client = liveBulkClient(lane);
|
|
1617
|
+
if (!client) return Reflect.get(main, prop);
|
|
1618
|
+
let proxy = proxies.get(client);
|
|
1619
|
+
if (!proxy) {
|
|
1620
|
+
proxy = client.createProxy(namespace);
|
|
1621
|
+
proxies.set(client, proxy);
|
|
1622
|
+
}
|
|
1623
|
+
const member = Reflect.get(proxy, prop);
|
|
1624
|
+
return typeof member === "function" ? watchTimeouts(member, lane, client) : member;
|
|
1625
|
+
} });
|
|
1626
|
+
}
|
|
1627
|
+
function watchTimeouts(member, lane, client) {
|
|
1628
|
+
return new Proxy(member, { apply(target, thisArg, args) {
|
|
1629
|
+
const result = Reflect.apply(target, thisArg, args);
|
|
1630
|
+
if (!(result instanceof Promise)) return result;
|
|
1631
|
+
return result.catch((err) => {
|
|
1632
|
+
if (err?.code === "TIMEOUT") noticeBulkTimeout(lane, client);
|
|
1633
|
+
throw err;
|
|
1634
|
+
});
|
|
1635
|
+
} });
|
|
1012
1636
|
}
|
|
1013
1637
|
async function ensureStatusSubscription(rpc) {
|
|
1014
1638
|
const client = rpc.value;
|
|
@@ -1022,14 +1646,17 @@ async function ensureStatusSubscription(rpc) {
|
|
|
1022
1646
|
statusClient = void 0;
|
|
1023
1647
|
}
|
|
1024
1648
|
}
|
|
1025
|
-
function usePlugin(pluginName) {
|
|
1026
|
-
const
|
|
1649
|
+
function usePlugin(pluginName, options = {}) {
|
|
1650
|
+
const ctx = useCameraUi();
|
|
1651
|
+
const { rpc, bulkRpc, isConnected } = ctx;
|
|
1652
|
+
const lane = options.lane === "bulk" ? bulkRpc : void 0;
|
|
1027
1653
|
const plugin = shallowRef();
|
|
1028
1654
|
const contract = ref();
|
|
1029
1655
|
const _isLoading = ref(false);
|
|
1030
1656
|
const initialLoadDone = ref(false);
|
|
1031
1657
|
const error = ref();
|
|
1032
1658
|
let currentPluginName;
|
|
1659
|
+
let heldPlugin;
|
|
1033
1660
|
let watchedName;
|
|
1034
1661
|
function acquirePlugin(name) {
|
|
1035
1662
|
if (pluginCache.has(name)) return pluginCache.acquire(name, () => {
|
|
@@ -1037,7 +1664,8 @@ function usePlugin(pluginName) {
|
|
|
1037
1664
|
});
|
|
1038
1665
|
}
|
|
1039
1666
|
function releasePlugin(name) {
|
|
1040
|
-
pluginCache.release(name);
|
|
1667
|
+
pluginCache.release(name, heldPlugin);
|
|
1668
|
+
heldPlugin = void 0;
|
|
1041
1669
|
}
|
|
1042
1670
|
function watchPlugin(name) {
|
|
1043
1671
|
if (watchedName === name) return;
|
|
@@ -1073,7 +1701,9 @@ function usePlugin(pluginName) {
|
|
|
1073
1701
|
}
|
|
1074
1702
|
function adopt(name, cached) {
|
|
1075
1703
|
currentPluginName = name;
|
|
1076
|
-
|
|
1704
|
+
heldPlugin = cached;
|
|
1705
|
+
if (lane) cached.bulkProxy ??= createLaneProxy(cached.namespace, cached.proxy, lane);
|
|
1706
|
+
plugin.value = lane ? cached.bulkProxy : cached.proxy;
|
|
1077
1707
|
contract.value = cached.contract;
|
|
1078
1708
|
error.value = void 0;
|
|
1079
1709
|
stopRetry(name);
|
|
@@ -1089,8 +1719,9 @@ function usePlugin(pluginName) {
|
|
|
1089
1719
|
}
|
|
1090
1720
|
watchPlugin(name);
|
|
1091
1721
|
ensureStatusSubscription(rpc);
|
|
1092
|
-
if (currentPluginName === name &&
|
|
1722
|
+
if (currentPluginName === name && pluginCache.get(name) !== heldPlugin) {
|
|
1093
1723
|
currentPluginName = void 0;
|
|
1724
|
+
heldPlugin = void 0;
|
|
1094
1725
|
plugin.value = void 0;
|
|
1095
1726
|
contract.value = void 0;
|
|
1096
1727
|
} else if (currentPluginName === name && plugin.value) return;
|
|
@@ -1128,22 +1759,25 @@ function usePlugin(pluginName) {
|
|
|
1128
1759
|
const pluginInfo = await client.createProxy(coreNamespaces.coreManagerRpc).getPlugin(name);
|
|
1129
1760
|
if (!pluginInfo) throw new Error(`Plugin "${name}" not found`);
|
|
1130
1761
|
if (pluginInfo.running === false) return;
|
|
1131
|
-
const
|
|
1762
|
+
const namespace = NamespaceManager.pluginNamespaces(pluginInfo.id).pluginChildRpc;
|
|
1132
1763
|
return {
|
|
1133
|
-
proxy: client.createProxy(
|
|
1134
|
-
contract: pluginInfo.contract
|
|
1764
|
+
proxy: client.createProxy(namespace),
|
|
1765
|
+
contract: pluginInfo.contract,
|
|
1766
|
+
namespace,
|
|
1767
|
+
client
|
|
1135
1768
|
};
|
|
1136
1769
|
});
|
|
1137
1770
|
pendingLoads.set(name, loadPromise);
|
|
1138
1771
|
try {
|
|
1139
1772
|
const result = await loadPromise;
|
|
1773
|
+
pluginRunning.set(name, !!result);
|
|
1140
1774
|
if (result) {
|
|
1141
|
-
pluginCache.acquire(name, () => result);
|
|
1775
|
+
const held = pluginCache.acquire(name, () => result);
|
|
1142
1776
|
if (toValue(pluginName) !== name) {
|
|
1143
|
-
pluginCache.release(name);
|
|
1777
|
+
pluginCache.release(name, held);
|
|
1144
1778
|
return;
|
|
1145
1779
|
}
|
|
1146
|
-
adopt(name,
|
|
1780
|
+
adopt(name, held);
|
|
1147
1781
|
} else if (toValue(pluginName) === name) markUnavailable(name);
|
|
1148
1782
|
} catch (err) {
|
|
1149
1783
|
if (!silent) error.value = err instanceof Error ? err : new Error(String(err));
|
|
@@ -1158,8 +1792,10 @@ function usePlugin(pluginName) {
|
|
|
1158
1792
|
const name = toValue(pluginName);
|
|
1159
1793
|
if (!name) return;
|
|
1160
1794
|
if (currentPluginName) {
|
|
1161
|
-
if (pluginCache.
|
|
1162
|
-
|
|
1795
|
+
if (pluginCache.get(currentPluginName) === heldPlugin && pluginCache.getRefCount(currentPluginName) <= 1) {
|
|
1796
|
+
pluginCache.forceRelease(currentPluginName);
|
|
1797
|
+
heldPlugin = void 0;
|
|
1798
|
+
} else releasePlugin(currentPluginName);
|
|
1163
1799
|
plugin.value = void 0;
|
|
1164
1800
|
contract.value = void 0;
|
|
1165
1801
|
currentPluginName = void 0;
|
|
@@ -1171,22 +1807,28 @@ function usePlugin(pluginName) {
|
|
|
1171
1807
|
plugin.value = void 0;
|
|
1172
1808
|
contract.value = void 0;
|
|
1173
1809
|
currentPluginName = void 0;
|
|
1810
|
+
heldPlugin = void 0;
|
|
1174
1811
|
};
|
|
1175
1812
|
instances.add(resetInstance);
|
|
1813
|
+
function onReconnected() {
|
|
1814
|
+
if (currentPluginName) checkPlugin(currentPluginName, rpc);
|
|
1815
|
+
}
|
|
1816
|
+
ctx.on("reconnected", onReconnected);
|
|
1176
1817
|
watch([
|
|
1177
1818
|
isConnected,
|
|
1178
1819
|
() => toValue(pluginName),
|
|
1179
1820
|
rpc
|
|
1180
|
-
], async ([connected, name, currentClient]
|
|
1181
|
-
const
|
|
1182
|
-
if (currentPluginName &&
|
|
1821
|
+
], async ([connected, name, currentClient]) => {
|
|
1822
|
+
const renamed = !!currentPluginName && name !== currentPluginName;
|
|
1823
|
+
if (currentPluginName && (renamed || currentClient && heldPlugin && heldPlugin.client !== currentClient)) {
|
|
1824
|
+
if (renamed) unwatchPlugin(currentPluginName);
|
|
1183
1825
|
releasePlugin(currentPluginName);
|
|
1184
1826
|
plugin.value = void 0;
|
|
1185
1827
|
contract.value = void 0;
|
|
1186
1828
|
currentPluginName = void 0;
|
|
1187
1829
|
}
|
|
1188
1830
|
if (connected && name && currentClient) await loadPlugin(name);
|
|
1189
|
-
else if (
|
|
1831
|
+
else if (!connected && currentPluginName) {
|
|
1190
1832
|
unwatchPlugin(currentPluginName);
|
|
1191
1833
|
releasePlugin(currentPluginName);
|
|
1192
1834
|
plugin.value = void 0;
|
|
@@ -1196,6 +1838,7 @@ function usePlugin(pluginName) {
|
|
|
1196
1838
|
}, { immediate: true });
|
|
1197
1839
|
tryOnScopeDispose(() => {
|
|
1198
1840
|
instances.delete(resetInstance);
|
|
1841
|
+
ctx.off("reconnected", onReconnected);
|
|
1199
1842
|
unwatchPlugin(toValue(pluginName));
|
|
1200
1843
|
if (currentPluginName) {
|
|
1201
1844
|
unwatchPlugin(currentPluginName);
|
|
@@ -1256,14 +1899,23 @@ var isReactivePowerSensor = createSensorTypeGuard(SensorType.Power);
|
|
|
1256
1899
|
var isReactiveIlluminanceInfo = createSensorTypeGuard(SensorType.Illuminance);
|
|
1257
1900
|
var isReactiveCarbonDioxideInfo = createSensorTypeGuard(SensorType.CarbonDioxide);
|
|
1258
1901
|
var isReactiveGarageControl = createSensorTypeGuard(SensorType.Garage);
|
|
1902
|
+
function sameList(a, b) {
|
|
1903
|
+
return a.length === b.length && a.every((value, i) => value === b[i]);
|
|
1904
|
+
}
|
|
1905
|
+
function sameIdentity(sensor, data) {
|
|
1906
|
+
return sensor.type === data.type && sensor.name === data.name && sensor.pluginId === data.pluginId && sensor.nativeId === data.nativeId && sensor.boundCameraId === data.boundCameraId;
|
|
1907
|
+
}
|
|
1259
1908
|
function createReactiveSensor(data, state, rpcRef) {
|
|
1260
1909
|
const displayName = ref(state.displayName ?? data.displayName ?? data.name);
|
|
1261
1910
|
const capabilities = ref(state.capabilities ?? data.capabilities ?? []);
|
|
1262
1911
|
const assignedCameraIds = ref([...data.assignedCameraIds]);
|
|
1263
1912
|
const exposed = ref(data.exposed);
|
|
1264
1913
|
const connected = ref(data.connected);
|
|
1265
|
-
const properties =
|
|
1914
|
+
const properties = shallowReactive({ ...state.properties });
|
|
1266
1915
|
const capabilityCallbacks = [];
|
|
1916
|
+
function notifyCapabilitiesChanged() {
|
|
1917
|
+
for (const callback of capabilityCallbacks) callback(capabilities.value);
|
|
1918
|
+
}
|
|
1267
1919
|
return {
|
|
1268
1920
|
id: data.id,
|
|
1269
1921
|
type: data.type,
|
|
@@ -1298,23 +1950,61 @@ function createReactiveSensor(data, state, rpcRef) {
|
|
|
1298
1950
|
if (index !== -1) capabilityCallbacks.splice(index, 1);
|
|
1299
1951
|
};
|
|
1300
1952
|
},
|
|
1301
|
-
_notifyCapabilitiesChanged
|
|
1302
|
-
|
|
1953
|
+
_notifyCapabilitiesChanged: notifyCapabilitiesChanged,
|
|
1954
|
+
_update(next, nextState) {
|
|
1955
|
+
displayName.value = nextState.displayName ?? next.displayName ?? next.name;
|
|
1956
|
+
if (!sameList(assignedCameraIds.value, next.assignedCameraIds)) assignedCameraIds.value = [...next.assignedCameraIds];
|
|
1957
|
+
exposed.value = next.exposed;
|
|
1958
|
+
connected.value = next.connected;
|
|
1959
|
+
const nextProperties = nextState.properties ?? {};
|
|
1960
|
+
for (const property of Object.keys(properties)) if (!(property in nextProperties)) delete properties[property];
|
|
1961
|
+
Object.assign(properties, nextProperties);
|
|
1962
|
+
const nextCapabilities = nextState.capabilities ?? next.capabilities ?? [];
|
|
1963
|
+
if (!sameList(capabilities.value, nextCapabilities)) {
|
|
1964
|
+
capabilities.value = nextCapabilities;
|
|
1965
|
+
notifyCapabilitiesChanged();
|
|
1966
|
+
}
|
|
1303
1967
|
}
|
|
1304
1968
|
};
|
|
1305
1969
|
}
|
|
1970
|
+
var LIVE_RELEASE_DELAY_MS = 1e3;
|
|
1971
|
+
var ALL_LIVE_SCOPE = "all";
|
|
1972
|
+
function cameraLiveScope(cameraId) {
|
|
1973
|
+
return `camera:${cameraId}`;
|
|
1974
|
+
}
|
|
1975
|
+
function sensorLiveScope(sensorId) {
|
|
1976
|
+
return `sensor:${sensorId}`;
|
|
1977
|
+
}
|
|
1306
1978
|
function createSensorManager(rpcOrContext) {
|
|
1979
|
+
const manager = createScopedSensorManager(rpcOrContext);
|
|
1980
|
+
manager.retainLive(ALL_LIVE_SCOPE);
|
|
1981
|
+
return manager;
|
|
1982
|
+
}
|
|
1983
|
+
function createScopedSensorManager(rpcOrContext) {
|
|
1307
1984
|
const { sensorsSubject, sensorsRpc } = NamespaceManager.sensorRegistryNamespaces();
|
|
1308
1985
|
const ctx = "rpc" in rpcOrContext && "value" in rpcOrContext.rpc ? rpcOrContext : void 0;
|
|
1309
1986
|
const rpcRef = ctx ? ctx.rpc : shallowRef(rpcOrContext);
|
|
1310
1987
|
const sensorMap = shallowRef(/* @__PURE__ */ new Map());
|
|
1311
1988
|
let globalUnsubscribe;
|
|
1312
1989
|
const sensorSubscriptions = /* @__PURE__ */ new Map();
|
|
1990
|
+
const pendingSubscriptions = /* @__PURE__ */ new Set();
|
|
1991
|
+
const refreshing = /* @__PURE__ */ new Map();
|
|
1992
|
+
const refreshRuns = /* @__PURE__ */ new Set();
|
|
1993
|
+
let refreshBatch;
|
|
1994
|
+
const refreshRetries = /* @__PURE__ */ new Set();
|
|
1995
|
+
const refreshRetry = createRetryTimer();
|
|
1996
|
+
const refreshOnConnect = /* @__PURE__ */ new Set();
|
|
1997
|
+
const liveHolds = /* @__PURE__ */ new Map();
|
|
1998
|
+
const liveReleaseTimers = /* @__PURE__ */ new Map();
|
|
1999
|
+
let subscriptionEpoch = 0;
|
|
1313
2000
|
const initialized = ref(false);
|
|
1314
2001
|
let initPromise;
|
|
2002
|
+
let generation = 0;
|
|
2003
|
+
const initRetry = createRetryTimer();
|
|
1315
2004
|
function handlePerSensorEvent(sensorId, message) {
|
|
1316
2005
|
const sensor = sensorMap.value.get(sensorId);
|
|
1317
2006
|
if (!sensor) return;
|
|
2007
|
+
refreshing.get(sensorId)?.add(message.type === "property:changed" ? message.data.property : message.type);
|
|
1318
2008
|
if (message.type === "property:changed") {
|
|
1319
2009
|
const event = message.data;
|
|
1320
2010
|
sensor.properties[event.property] = event.value;
|
|
@@ -1327,11 +2017,129 @@ function createSensorManager(rpcOrContext) {
|
|
|
1327
2017
|
sensor._notifyCapabilitiesChanged();
|
|
1328
2018
|
}
|
|
1329
2019
|
}
|
|
1330
|
-
|
|
1331
|
-
if (
|
|
2020
|
+
function isLive(sensor) {
|
|
2021
|
+
if (liveHolds.has(ALL_LIVE_SCOPE) || liveHolds.has(sensorLiveScope(sensor.id))) return true;
|
|
2022
|
+
const cameraIds = sensor.assignedCameraIds.value;
|
|
2023
|
+
return cameraIds.length === 0 || cameraIds.some((cameraId) => liveHolds.has(cameraLiveScope(cameraId)));
|
|
2024
|
+
}
|
|
2025
|
+
function goLive(sensor, staleValues) {
|
|
2026
|
+
if (sensorSubscriptions.has(sensor.id) || pendingSubscriptions.has(sensor.id)) return;
|
|
2027
|
+
if (staleValues) for (const property of Object.keys(sensor.properties)) delete sensor.properties[property];
|
|
2028
|
+
queueRefresh(sensor.id, subscribeSensorSubject(sensor.id).catch(() => void 0));
|
|
2029
|
+
}
|
|
2030
|
+
function queueRefresh(sensorId, subscribed) {
|
|
2031
|
+
const changed = /* @__PURE__ */ new Set();
|
|
2032
|
+
refreshing.set(sensorId, changed);
|
|
2033
|
+
if (!refreshBatch) {
|
|
2034
|
+
const batch = [];
|
|
2035
|
+
refreshBatch = batch;
|
|
2036
|
+
const run = Promise.resolve().then(() => {
|
|
2037
|
+
refreshBatch = void 0;
|
|
2038
|
+
return refreshStates(batch);
|
|
2039
|
+
}).catch(() => {}).finally(() => refreshRuns.delete(run));
|
|
2040
|
+
refreshRuns.add(run);
|
|
2041
|
+
}
|
|
2042
|
+
refreshBatch.push({
|
|
2043
|
+
sensorId,
|
|
2044
|
+
changed,
|
|
2045
|
+
subscribed
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
async function refreshStates(batch) {
|
|
2049
|
+
const subscriptions = await Promise.all(batch.map((refresh) => refresh.subscribed));
|
|
2050
|
+
const current = batch.flatMap((refresh, i) => {
|
|
2051
|
+
const unsubscribe = subscriptions[i];
|
|
2052
|
+
return unsubscribe && sensorSubscriptions.get(refresh.sensorId) === unsubscribe ? [{
|
|
2053
|
+
...refresh,
|
|
2054
|
+
unsubscribe
|
|
2055
|
+
}] : [];
|
|
2056
|
+
});
|
|
2057
|
+
let states;
|
|
2058
|
+
if (current.length > 0) try {
|
|
2059
|
+
states = await rpcCall(rpcRef, (c) => c.createProxy(sensorsRpc).getSensorStates());
|
|
2060
|
+
refreshRetry.reset();
|
|
2061
|
+
} catch {
|
|
2062
|
+
states = void 0;
|
|
2063
|
+
}
|
|
2064
|
+
for (const { sensorId, unsubscribe, changed } of current) applyRefreshedState(sensorId, unsubscribe, changed, states);
|
|
2065
|
+
for (const { sensorId, changed } of batch) if (refreshing.get(sensorId) === changed) refreshing.delete(sensorId);
|
|
2066
|
+
}
|
|
2067
|
+
function applyRefreshedState(sensorId, unsubscribe, changed, states) {
|
|
2068
|
+
const sensor = sensorMap.value.get(sensorId);
|
|
2069
|
+
if (!sensor || sensorSubscriptions.get(sensorId) !== unsubscribe) return;
|
|
2070
|
+
const state = states?.[sensorId];
|
|
2071
|
+
if (!state) {
|
|
2072
|
+
if (states && !sensor.connected.value) refreshOnConnect.add(sensorId);
|
|
2073
|
+
else retryRefresh(sensorId);
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
for (const [property, value] of Object.entries(state.properties ?? {})) if (!changed.has(property)) sensor.properties[property] = value;
|
|
2077
|
+
if (state.displayName !== void 0 && !changed.has("sensor:displayName:changed")) sensor.displayName.value = state.displayName;
|
|
2078
|
+
const current = sensor.capabilities.value;
|
|
2079
|
+
const capabilities = state.capabilities ?? current;
|
|
2080
|
+
if (!changed.has("sensor:capabilities:changed") && !sameList(capabilities, current)) {
|
|
2081
|
+
sensor.capabilities.value = capabilities;
|
|
2082
|
+
sensor._notifyCapabilitiesChanged();
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
function retryRefresh(sensorId) {
|
|
2086
|
+
refreshRetries.add(sensorId);
|
|
2087
|
+
refreshRetry.schedule(() => {
|
|
2088
|
+
const sensorIds = [...refreshRetries];
|
|
2089
|
+
refreshRetries.clear();
|
|
2090
|
+
for (const id of sensorIds) refreshLiveSensor(id);
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
function refreshLiveSensor(sensorId) {
|
|
2094
|
+
const unsubscribe = sensorSubscriptions.get(sensorId);
|
|
2095
|
+
if (unsubscribe) queueRefresh(sensorId, Promise.resolve(unsubscribe));
|
|
2096
|
+
}
|
|
2097
|
+
function whenRefreshed() {
|
|
2098
|
+
return Promise.all(refreshRuns).then(() => {});
|
|
2099
|
+
}
|
|
2100
|
+
async function subscribeSensorSubject(sensorId) {
|
|
2101
|
+
const epoch = subscriptionEpoch;
|
|
1332
2102
|
const namespace = NamespaceManager.sensorEventNamespaces(sensorId);
|
|
1333
|
-
|
|
2103
|
+
pendingSubscriptions.add(sensorId);
|
|
2104
|
+
let unsubscribe;
|
|
2105
|
+
try {
|
|
2106
|
+
unsubscribe = await rpcCall(rpcRef, (c) => c.subscribe(namespace.sensorSubject, (msg) => handlePerSensorEvent(sensorId, msg)));
|
|
2107
|
+
} finally {
|
|
2108
|
+
if (epoch === subscriptionEpoch) pendingSubscriptions.delete(sensorId);
|
|
2109
|
+
}
|
|
2110
|
+
const sensor = sensorMap.value.get(sensorId);
|
|
2111
|
+
if (epoch !== subscriptionEpoch || !sensor || !isLive(sensor)) {
|
|
2112
|
+
unsubscribe();
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
1334
2115
|
sensorSubscriptions.set(sensorId, unsubscribe);
|
|
2116
|
+
return unsubscribe;
|
|
2117
|
+
}
|
|
2118
|
+
function syncSensor(sensor, staleValues) {
|
|
2119
|
+
if (!initialized.value) return;
|
|
2120
|
+
if (isLive(sensor)) goLive(sensor, staleValues);
|
|
2121
|
+
else unsubscribeFromSensorEvents(sensor.id);
|
|
2122
|
+
}
|
|
2123
|
+
function syncLive() {
|
|
2124
|
+
for (const sensor of sensorMap.value.values()) syncSensor(sensor, true);
|
|
2125
|
+
}
|
|
2126
|
+
function retainLive(scope) {
|
|
2127
|
+
clearTimeout(liveReleaseTimers.get(scope));
|
|
2128
|
+
liveReleaseTimers.delete(scope);
|
|
2129
|
+
const held = liveHolds.has(scope);
|
|
2130
|
+
liveHolds.set(scope, (liveHolds.get(scope) ?? 0) + 1);
|
|
2131
|
+
if (!held) syncLive();
|
|
2132
|
+
}
|
|
2133
|
+
function releaseLive(scope) {
|
|
2134
|
+
const count = liveHolds.get(scope);
|
|
2135
|
+
if (!count) return;
|
|
2136
|
+
liveHolds.set(scope, count - 1);
|
|
2137
|
+
if (count > 1) return;
|
|
2138
|
+
liveReleaseTimers.set(scope, setTimeout(() => {
|
|
2139
|
+
liveReleaseTimers.delete(scope);
|
|
2140
|
+
liveHolds.delete(scope);
|
|
2141
|
+
syncLive();
|
|
2142
|
+
}, LIVE_RELEASE_DELAY_MS));
|
|
1335
2143
|
}
|
|
1336
2144
|
function unsubscribeFromSensorEvents(sensorId) {
|
|
1337
2145
|
const unsubscribe = sensorSubscriptions.get(sensorId);
|
|
@@ -1339,6 +2147,8 @@ function createSensorManager(rpcOrContext) {
|
|
|
1339
2147
|
unsubscribe();
|
|
1340
2148
|
sensorSubscriptions.delete(sensorId);
|
|
1341
2149
|
}
|
|
2150
|
+
refreshRetries.delete(sensorId);
|
|
2151
|
+
refreshOnConnect.delete(sensorId);
|
|
1342
2152
|
}
|
|
1343
2153
|
function handleGlobalSensorEvent(message) {
|
|
1344
2154
|
if (message.type === "sensor:added") {
|
|
@@ -1348,7 +2158,7 @@ function createSensorManager(rpcOrContext) {
|
|
|
1348
2158
|
const newMap = new Map(sensorMap.value);
|
|
1349
2159
|
newMap.set(event.sensor.id, reactiveSensor);
|
|
1350
2160
|
sensorMap.value = newMap;
|
|
1351
|
-
|
|
2161
|
+
syncSensor(reactiveSensor, false);
|
|
1352
2162
|
} else if (message.type === "sensor:deleted") {
|
|
1353
2163
|
const event = message.data;
|
|
1354
2164
|
unsubscribeFromSensorEvents(event.sensorId);
|
|
@@ -1358,7 +2168,9 @@ function createSensorManager(rpcOrContext) {
|
|
|
1358
2168
|
} else if (message.type === "sensor:connected:changed") {
|
|
1359
2169
|
const event = message.data;
|
|
1360
2170
|
const sensor = sensorMap.value.get(event.sensorId);
|
|
1361
|
-
if (sensor)
|
|
2171
|
+
if (!sensor) return;
|
|
2172
|
+
sensor.connected.value = event.connected;
|
|
2173
|
+
if (event.connected && refreshOnConnect.delete(sensor.id)) refreshLiveSensor(sensor.id);
|
|
1362
2174
|
} else if (message.type === "sensor:assignment:changed") {
|
|
1363
2175
|
const event = message.data;
|
|
1364
2176
|
const sensor = sensorMap.value.get(event.sensorId);
|
|
@@ -1367,45 +2179,86 @@ function createSensorManager(rpcOrContext) {
|
|
|
1367
2179
|
if (event.assigned) cameras.add(event.cameraId);
|
|
1368
2180
|
else cameras.delete(event.cameraId);
|
|
1369
2181
|
sensor.assignedCameraIds.value = [...cameras];
|
|
2182
|
+
syncSensor(sensor, true);
|
|
1370
2183
|
} else if (message.type === "sensor:exposed:changed") {
|
|
1371
2184
|
const event = message.data;
|
|
1372
2185
|
const sensor = sensorMap.value.get(event.sensorId);
|
|
1373
2186
|
if (sensor) sensor.exposed.value = event.exposed;
|
|
1374
2187
|
}
|
|
1375
2188
|
}
|
|
1376
|
-
|
|
2189
|
+
function dropSubscriptions() {
|
|
1377
2190
|
globalUnsubscribe?.();
|
|
1378
2191
|
globalUnsubscribe = void 0;
|
|
1379
2192
|
for (const unsubscribe of sensorSubscriptions.values()) unsubscribe();
|
|
1380
2193
|
sensorSubscriptions.clear();
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
2194
|
+
subscriptionEpoch++;
|
|
2195
|
+
pendingSubscriptions.clear();
|
|
2196
|
+
refreshing.clear();
|
|
2197
|
+
refreshRetries.clear();
|
|
2198
|
+
refreshOnConnect.clear();
|
|
2199
|
+
refreshRetry.cancel();
|
|
2200
|
+
refreshRetry.reset();
|
|
2201
|
+
}
|
|
2202
|
+
function assertCurrent(run) {
|
|
2203
|
+
if (run !== generation) throw new Error("sensor manager closed");
|
|
2204
|
+
}
|
|
2205
|
+
function scheduleRetry() {
|
|
2206
|
+
initRetry.schedule(() => {
|
|
2207
|
+
if (!initialized.value && !initPromise) launchInit().catch(() => {});
|
|
1390
2208
|
});
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
2209
|
+
}
|
|
2210
|
+
async function doInit(run) {
|
|
2211
|
+
assertCurrent(run);
|
|
2212
|
+
dropSubscriptions();
|
|
2213
|
+
const previous = sensorMap.value;
|
|
2214
|
+
let pendingEvents = [];
|
|
2215
|
+
try {
|
|
2216
|
+
globalUnsubscribe = await rpcCall(rpcRef, (c) => c.subscribe(sensorsSubject, (msg) => {
|
|
2217
|
+
if (pendingEvents) pendingEvents.push(msg);
|
|
2218
|
+
else handleGlobalSensorEvent(msg);
|
|
2219
|
+
}));
|
|
2220
|
+
assertCurrent(run);
|
|
2221
|
+
const [sensors, states] = await rpcCall(rpcRef, async (c) => {
|
|
2222
|
+
const proxy = c.createProxy(sensorsRpc);
|
|
2223
|
+
return Promise.all([proxy.getSensors(), proxy.getSensorStates()]);
|
|
2224
|
+
});
|
|
2225
|
+
assertCurrent(run);
|
|
2226
|
+
const newMap = /* @__PURE__ */ new Map();
|
|
2227
|
+
for (const sensor of sensors) {
|
|
2228
|
+
const state = states[sensor.id] ?? {
|
|
2229
|
+
properties: {},
|
|
2230
|
+
capabilities: [],
|
|
2231
|
+
type: sensor.type
|
|
2232
|
+
};
|
|
2233
|
+
const kept = previous.get(sensor.id);
|
|
2234
|
+
if (kept && sameIdentity(kept, sensor)) {
|
|
2235
|
+
kept._update(sensor, state);
|
|
2236
|
+
newMap.set(sensor.id, kept);
|
|
2237
|
+
} else newMap.set(sensor.id, createReactiveSensor(sensor, state, rpcRef));
|
|
2238
|
+
}
|
|
2239
|
+
sensorMap.value = newMap;
|
|
2240
|
+
for (const sensor of newMap.values()) {
|
|
2241
|
+
if (!isLive(sensor)) continue;
|
|
2242
|
+
await subscribeSensorSubject(sensor.id);
|
|
2243
|
+
assertCurrent(run);
|
|
2244
|
+
}
|
|
2245
|
+
} catch (err) {
|
|
2246
|
+
dropSubscriptions();
|
|
2247
|
+
if (run === generation) {
|
|
2248
|
+
sensorMap.value = previous;
|
|
2249
|
+
scheduleRetry();
|
|
2250
|
+
}
|
|
2251
|
+
throw err;
|
|
2252
|
+
}
|
|
1402
2253
|
const queued = pendingEvents;
|
|
1403
2254
|
pendingEvents = void 0;
|
|
1404
2255
|
for (const msg of queued) handleGlobalSensorEvent(msg);
|
|
2256
|
+
initRetry.reset();
|
|
1405
2257
|
initialized.value = true;
|
|
2258
|
+
syncLive();
|
|
1406
2259
|
}
|
|
1407
2260
|
function launchInit() {
|
|
1408
|
-
const p = doInit().finally(() => {
|
|
2261
|
+
const p = doInit(generation).finally(() => {
|
|
1409
2262
|
if (initPromise === p) initPromise = void 0;
|
|
1410
2263
|
});
|
|
1411
2264
|
initPromise = p;
|
|
@@ -1419,7 +2272,9 @@ function createSensorManager(rpcOrContext) {
|
|
|
1419
2272
|
async function reconnect() {
|
|
1420
2273
|
initialized.value = false;
|
|
1421
2274
|
if (initPromise) {
|
|
1422
|
-
const
|
|
2275
|
+
const current = initPromise;
|
|
2276
|
+
const run = generation;
|
|
2277
|
+
const p = current.catch(() => {}).then(() => doInit(run)).finally(() => {
|
|
1423
2278
|
if (initPromise === p) initPromise = void 0;
|
|
1424
2279
|
});
|
|
1425
2280
|
initPromise = p;
|
|
@@ -1428,11 +2283,10 @@ function createSensorManager(rpcOrContext) {
|
|
|
1428
2283
|
return launchInit();
|
|
1429
2284
|
}
|
|
1430
2285
|
function close() {
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
sensorMap.value.clear();
|
|
2286
|
+
generation++;
|
|
2287
|
+
initRetry.cancel();
|
|
2288
|
+
dropSubscriptions();
|
|
2289
|
+
sensorMap.value = /* @__PURE__ */ new Map();
|
|
1436
2290
|
initialized.value = false;
|
|
1437
2291
|
initPromise = void 0;
|
|
1438
2292
|
}
|
|
@@ -1454,7 +2308,10 @@ function createSensorManager(rpcOrContext) {
|
|
|
1454
2308
|
},
|
|
1455
2309
|
ensureInitialized,
|
|
1456
2310
|
reconnect,
|
|
1457
|
-
close
|
|
2311
|
+
close,
|
|
2312
|
+
retainLive,
|
|
2313
|
+
releaseLive,
|
|
2314
|
+
whenRefreshed
|
|
1458
2315
|
};
|
|
1459
2316
|
}
|
|
1460
2317
|
var sensorManagerCache = createDebouncedCache({
|
|
@@ -1466,72 +2323,112 @@ function clearSensorCache() {
|
|
|
1466
2323
|
}
|
|
1467
2324
|
function reconnectAllSensorManagers() {
|
|
1468
2325
|
sensorManagerCache.forEachValue((cached) => {
|
|
1469
|
-
cached.
|
|
2326
|
+
cached.manager.reconnect().catch(() => {});
|
|
1470
2327
|
});
|
|
1471
2328
|
}
|
|
1472
2329
|
var GLOBAL_SENSOR_MANAGER_KEY = "sensors";
|
|
1473
2330
|
function acquireSensorManager(rpcOrContext) {
|
|
1474
|
-
|
|
2331
|
+
const cached = acquireSharedManager(rpcOrContext);
|
|
2332
|
+
cached.manager.retainLive(ALL_LIVE_SCOPE);
|
|
2333
|
+
return cached;
|
|
1475
2334
|
}
|
|
1476
2335
|
function releaseSensorManager() {
|
|
1477
|
-
sensorManagerCache.
|
|
2336
|
+
sensorManagerCache.get(GLOBAL_SENSOR_MANAGER_KEY)?.manager.releaseLive(ALL_LIVE_SCOPE);
|
|
2337
|
+
releaseSharedManager();
|
|
2338
|
+
}
|
|
2339
|
+
function acquireSharedManager(rpcOrContext) {
|
|
2340
|
+
return sensorManagerCache.acquire(GLOBAL_SENSOR_MANAGER_KEY, () => ({ manager: createScopedSensorManager(rpcOrContext) }));
|
|
2341
|
+
}
|
|
2342
|
+
function releaseSharedManager(held) {
|
|
2343
|
+
const cached = sensorManagerCache.get(GLOBAL_SENSOR_MANAGER_KEY);
|
|
2344
|
+
if (!cached || held && cached.manager !== held) return;
|
|
2345
|
+
sensorManagerCache.release(GLOBAL_SENSOR_MANAGER_KEY, cached);
|
|
1478
2346
|
}
|
|
1479
2347
|
function createSensorComposableState() {
|
|
1480
2348
|
return {
|
|
1481
2349
|
acquired: false,
|
|
1482
|
-
cachedManager: void 0
|
|
2350
|
+
cachedManager: void 0,
|
|
2351
|
+
liveScope: void 0
|
|
1483
2352
|
};
|
|
1484
2353
|
}
|
|
1485
2354
|
function sensorsForCamera(manager, cameraId) {
|
|
1486
2355
|
return manager.sensors.value.filter((sensor) => sensor.assignedCameraIds.value.includes(cameraId));
|
|
1487
2356
|
}
|
|
1488
|
-
async function ensureSensorManager(state, cameraUi, isConnected, isLoading, error) {
|
|
1489
|
-
if (!
|
|
2357
|
+
async function ensureSensorManager(state, cameraUi, isConnected, isLoading, error, liveScope) {
|
|
2358
|
+
if (!isConnected) return void 0;
|
|
1490
2359
|
isLoading.value = true;
|
|
1491
2360
|
error.value = void 0;
|
|
1492
2361
|
try {
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
2362
|
+
if (!holdsCachedManager(state)) {
|
|
2363
|
+
state.cachedManager = acquireSharedManager(cameraUi).manager;
|
|
2364
|
+
state.acquired = true;
|
|
2365
|
+
state.liveScope = void 0;
|
|
2366
|
+
}
|
|
2367
|
+
holdLiveScope(state, liveScope);
|
|
2368
|
+
await state.cachedManager?.ensureInitialized();
|
|
2369
|
+
await state.cachedManager?.whenRefreshed();
|
|
1499
2370
|
} catch (err) {
|
|
1500
2371
|
error.value = err instanceof Error ? err : new Error(String(err));
|
|
1501
|
-
return;
|
|
1502
2372
|
} finally {
|
|
1503
2373
|
isLoading.value = false;
|
|
1504
2374
|
}
|
|
2375
|
+
return state.cachedManager;
|
|
2376
|
+
}
|
|
2377
|
+
function clearErrorOnRecovery(manager, error) {
|
|
2378
|
+
watch(() => manager.value?.isInitialized.value, (ready) => {
|
|
2379
|
+
if (ready) error.value = void 0;
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
function holdsCachedManager(state) {
|
|
2383
|
+
return state.acquired && sensorManagerCache.get(GLOBAL_SENSOR_MANAGER_KEY)?.manager === state.cachedManager;
|
|
2384
|
+
}
|
|
2385
|
+
function holdLiveScope(state, liveScope) {
|
|
2386
|
+
if (state.liveScope === liveScope) return;
|
|
2387
|
+
state.cachedManager?.retainLive(liveScope);
|
|
2388
|
+
if (state.liveScope) state.cachedManager?.releaseLive(state.liveScope);
|
|
2389
|
+
state.liveScope = liveScope;
|
|
1505
2390
|
}
|
|
1506
2391
|
function cleanupSensorComposable(state) {
|
|
1507
|
-
if (state
|
|
1508
|
-
|
|
1509
|
-
state.
|
|
1510
|
-
state.cachedManager = void 0;
|
|
2392
|
+
if (holdsCachedManager(state)) {
|
|
2393
|
+
if (state.liveScope) state.cachedManager?.releaseLive(state.liveScope);
|
|
2394
|
+
releaseSharedManager(state.cachedManager);
|
|
1511
2395
|
}
|
|
2396
|
+
state.acquired = false;
|
|
2397
|
+
state.cachedManager = void 0;
|
|
2398
|
+
state.liveScope = void 0;
|
|
1512
2399
|
}
|
|
1513
2400
|
function useSensorById(camera, sensorId) {
|
|
1514
2401
|
const cameraUi = useCameraUi();
|
|
1515
2402
|
const { isConnected } = cameraUi;
|
|
1516
2403
|
const sensor = shallowRef();
|
|
2404
|
+
const cachedManager = shallowRef();
|
|
1517
2405
|
const _isLoading = ref(false);
|
|
1518
2406
|
const initialLoadDone = ref(false);
|
|
1519
2407
|
const error = ref();
|
|
1520
2408
|
const state = createSensorComposableState();
|
|
2409
|
+
function resolveSensor() {
|
|
2410
|
+
const senId = toValue(sensorId);
|
|
2411
|
+
return senId ? cachedManager.value?.getSensor(senId) : void 0;
|
|
2412
|
+
}
|
|
1521
2413
|
watch([
|
|
1522
2414
|
isConnected,
|
|
1523
2415
|
() => extractCameraId(toValue(camera)),
|
|
1524
2416
|
() => toValue(sensorId)
|
|
1525
2417
|
], async ([connected, camId, senId]) => {
|
|
1526
2418
|
if (connected && camId && senId) {
|
|
1527
|
-
|
|
1528
|
-
sensor.value =
|
|
2419
|
+
cachedManager.value = await ensureSensorManager(state, cameraUi, connected, _isLoading, error, sensorLiveScope(senId));
|
|
2420
|
+
sensor.value = resolveSensor();
|
|
1529
2421
|
initialLoadDone.value = true;
|
|
1530
2422
|
} else {
|
|
1531
2423
|
cleanupSensorComposable(state);
|
|
2424
|
+
cachedManager.value = void 0;
|
|
1532
2425
|
sensor.value = void 0;
|
|
1533
2426
|
}
|
|
1534
2427
|
}, { immediate: true });
|
|
2428
|
+
watch(resolveSensor, (next) => {
|
|
2429
|
+
sensor.value = next;
|
|
2430
|
+
});
|
|
2431
|
+
clearErrorOnRecovery(cachedManager, error);
|
|
1535
2432
|
tryOnScopeDispose(() => {
|
|
1536
2433
|
cleanupSensorComposable(state);
|
|
1537
2434
|
sensor.value = void 0;
|
|
@@ -1546,24 +2443,35 @@ function useSensorByType(camera, sensorType) {
|
|
|
1546
2443
|
const cameraUi = useCameraUi();
|
|
1547
2444
|
const { isConnected } = cameraUi;
|
|
1548
2445
|
const sensor = shallowRef();
|
|
2446
|
+
const cachedManager = shallowRef();
|
|
1549
2447
|
const _isLoading = ref(false);
|
|
1550
2448
|
const initialLoadDone = ref(false);
|
|
1551
2449
|
const error = ref();
|
|
1552
2450
|
const state = createSensorComposableState();
|
|
2451
|
+
function resolveSensor() {
|
|
2452
|
+
const camId = extractCameraId(toValue(camera));
|
|
2453
|
+
const type = toValue(sensorType);
|
|
2454
|
+
return cachedManager.value && camId ? sensorsForCamera(cachedManager.value, camId).find((s) => s.type === type) : void 0;
|
|
2455
|
+
}
|
|
1553
2456
|
watch([
|
|
1554
2457
|
isConnected,
|
|
1555
2458
|
() => extractCameraId(toValue(camera)),
|
|
1556
2459
|
() => toValue(sensorType)
|
|
1557
|
-
], async ([connected, camId
|
|
2460
|
+
], async ([connected, camId]) => {
|
|
1558
2461
|
if (connected && camId) {
|
|
1559
|
-
|
|
1560
|
-
sensor.value =
|
|
2462
|
+
cachedManager.value = await ensureSensorManager(state, cameraUi, connected, _isLoading, error, cameraLiveScope(camId));
|
|
2463
|
+
sensor.value = resolveSensor();
|
|
1561
2464
|
initialLoadDone.value = true;
|
|
1562
2465
|
} else {
|
|
1563
2466
|
cleanupSensorComposable(state);
|
|
2467
|
+
cachedManager.value = void 0;
|
|
1564
2468
|
sensor.value = void 0;
|
|
1565
2469
|
}
|
|
1566
2470
|
}, { immediate: true });
|
|
2471
|
+
watch(resolveSensor, (next) => {
|
|
2472
|
+
sensor.value = next;
|
|
2473
|
+
});
|
|
2474
|
+
clearErrorOnRecovery(cachedManager, error);
|
|
1567
2475
|
tryOnScopeDispose(() => {
|
|
1568
2476
|
cleanupSensorComposable(state);
|
|
1569
2477
|
sensor.value = void 0;
|
|
@@ -1589,7 +2497,7 @@ function useSensorsByType(camera, sensorType) {
|
|
|
1589
2497
|
() => toValue(sensorType)
|
|
1590
2498
|
], async ([connected, camId, type]) => {
|
|
1591
2499
|
if (connected && camId) {
|
|
1592
|
-
const manager = await ensureSensorManager(state, cameraUi, connected, _isLoading, error);
|
|
2500
|
+
const manager = await ensureSensorManager(state, cameraUi, connected, _isLoading, error, cameraLiveScope(camId));
|
|
1593
2501
|
cachedManager.value = manager;
|
|
1594
2502
|
sensorsRef.value = manager ? sensorsForCamera(manager, camId).filter((s) => s.type === type) : [];
|
|
1595
2503
|
initialLoadDone.value = true;
|
|
@@ -1606,6 +2514,7 @@ function useSensorsByType(camera, sensorType) {
|
|
|
1606
2514
|
if (!camId) return [];
|
|
1607
2515
|
return sensorsForCamera(cachedManager.value, camId).filter((s) => s.type === type);
|
|
1608
2516
|
});
|
|
2517
|
+
clearErrorOnRecovery(cachedManager, error);
|
|
1609
2518
|
tryOnScopeDispose(() => {
|
|
1610
2519
|
cleanupSensorComposable(state);
|
|
1611
2520
|
sensorsRef.value = [];
|
|
@@ -1627,7 +2536,7 @@ function useSensors(camera) {
|
|
|
1627
2536
|
const state = createSensorComposableState();
|
|
1628
2537
|
watch([isConnected, () => extractCameraId(toValue(camera))], async ([connected, camId]) => {
|
|
1629
2538
|
if (connected && camId) {
|
|
1630
|
-
const manager = await ensureSensorManager(state, cameraUi, connected, _isLoading, error);
|
|
2539
|
+
const manager = await ensureSensorManager(state, cameraUi, connected, _isLoading, error, cameraLiveScope(camId));
|
|
1631
2540
|
cachedManager.value = manager;
|
|
1632
2541
|
sensorsRef.value = manager ? sensorsForCamera(manager, camId) : [];
|
|
1633
2542
|
initialLoadDone.value = true;
|
|
@@ -1643,6 +2552,7 @@ function useSensors(camera) {
|
|
|
1643
2552
|
if (!camId) return [];
|
|
1644
2553
|
return sensorsForCamera(cachedManager.value, camId);
|
|
1645
2554
|
});
|
|
2555
|
+
clearErrorOnRecovery(cachedManager, error);
|
|
1646
2556
|
tryOnScopeDispose(() => {
|
|
1647
2557
|
cleanupSensorComposable(state);
|
|
1648
2558
|
sensorsRef.value = [];
|
|
@@ -1663,7 +2573,7 @@ function useAllSensors() {
|
|
|
1663
2573
|
const state = createSensorComposableState();
|
|
1664
2574
|
watch([isConnected], async ([connected]) => {
|
|
1665
2575
|
if (connected) {
|
|
1666
|
-
cachedManager.value = await ensureSensorManager(state, cameraUi, connected, _isLoading, error);
|
|
2576
|
+
cachedManager.value = await ensureSensorManager(state, cameraUi, connected, _isLoading, error, ALL_LIVE_SCOPE);
|
|
1667
2577
|
initialLoadDone.value = true;
|
|
1668
2578
|
} else {
|
|
1669
2579
|
cleanupSensorComposable(state);
|
|
@@ -1671,6 +2581,7 @@ function useAllSensors() {
|
|
|
1671
2581
|
}
|
|
1672
2582
|
}, { immediate: true });
|
|
1673
2583
|
const sensors = computed(() => cachedManager.value?.sensors.value ?? []);
|
|
2584
|
+
clearErrorOnRecovery(cachedManager, error);
|
|
1674
2585
|
tryOnScopeDispose(() => {
|
|
1675
2586
|
cleanupSensorComposable(state);
|
|
1676
2587
|
});
|
|
@@ -1697,32 +2608,30 @@ function useTypedSensor(camera, sensorType, preferredPluginId) {
|
|
|
1697
2608
|
const initialLoadDone = ref(false);
|
|
1698
2609
|
const error = ref();
|
|
1699
2610
|
const state = createSensorComposableState();
|
|
2611
|
+
function resolveSensor() {
|
|
2612
|
+
const camId = extractCameraId(toValue(camera));
|
|
2613
|
+
if (!cachedManager.value || !camId) return void 0;
|
|
2614
|
+
return pickSensorOfType(sensorsForCamera(cachedManager.value, camId), sensorType, toValue(preferredPluginId));
|
|
2615
|
+
}
|
|
1700
2616
|
watch([
|
|
1701
2617
|
isConnected,
|
|
1702
2618
|
() => extractCameraId(toValue(camera)),
|
|
1703
2619
|
() => toValue(preferredPluginId)
|
|
1704
|
-
], async ([connected, camId
|
|
2620
|
+
], async ([connected, camId]) => {
|
|
1705
2621
|
if (connected && camId) {
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
sensor.value = manager ? pickSensorOfType(sensorsForCamera(manager, camId), sensorType, preferred) : void 0;
|
|
2622
|
+
cachedManager.value = await ensureSensorManager(state, cameraUi, connected, _isLoading, error, cameraLiveScope(camId));
|
|
2623
|
+
sensor.value = resolveSensor();
|
|
1709
2624
|
initialLoadDone.value = true;
|
|
1710
2625
|
} else {
|
|
1711
2626
|
cleanupSensorComposable(state);
|
|
2627
|
+
cachedManager.value = void 0;
|
|
1712
2628
|
sensor.value = void 0;
|
|
1713
2629
|
}
|
|
1714
2630
|
}, { immediate: true });
|
|
1715
|
-
watch(() => {
|
|
1716
|
-
|
|
1717
|
-
const camId = extractCameraId(toValue(camera));
|
|
1718
|
-
if (!camId) return void 0;
|
|
1719
|
-
return pickSensorOfType(sensorsForCamera(cachedManager.value, camId), sensorType, toValue(preferredPluginId))?.id;
|
|
1720
|
-
}, () => {
|
|
1721
|
-
if (cachedManager.value) {
|
|
1722
|
-
const camId = extractCameraId(toValue(camera));
|
|
1723
|
-
sensor.value = camId ? pickSensorOfType(sensorsForCamera(cachedManager.value, camId), sensorType, toValue(preferredPluginId)) : void 0;
|
|
1724
|
-
}
|
|
2631
|
+
watch(resolveSensor, (next) => {
|
|
2632
|
+
sensor.value = next;
|
|
1725
2633
|
});
|
|
2634
|
+
clearErrorOnRecovery(cachedManager, error);
|
|
1726
2635
|
tryOnScopeDispose(() => {
|
|
1727
2636
|
cleanupSensorComposable(state);
|
|
1728
2637
|
sensor.value = void 0;
|
|
@@ -1762,7 +2671,7 @@ function useClassifierSensors(camera) {
|
|
|
1762
2671
|
const state = createSensorComposableState();
|
|
1763
2672
|
watch([isConnected, () => extractCameraId(toValue(camera))], async ([connected, camId]) => {
|
|
1764
2673
|
if (connected && camId) {
|
|
1765
|
-
const manager = await ensureSensorManager(state, cameraUi, connected, _isLoading, error);
|
|
2674
|
+
const manager = await ensureSensorManager(state, cameraUi, connected, _isLoading, error, cameraLiveScope(camId));
|
|
1766
2675
|
cachedManager.value = manager;
|
|
1767
2676
|
sensorsRef.value = manager ? sensorsForCamera(manager, camId).filter((s) => s.type === SensorType.Classifier) : [];
|
|
1768
2677
|
initialLoadDone.value = true;
|
|
@@ -1778,6 +2687,7 @@ function useClassifierSensors(camera) {
|
|
|
1778
2687
|
if (!camId) return [];
|
|
1779
2688
|
return sensorsForCamera(cachedManager.value, camId).filter((s) => s.type === SensorType.Classifier);
|
|
1780
2689
|
});
|
|
2690
|
+
clearErrorOnRecovery(cachedManager, error);
|
|
1781
2691
|
tryOnScopeDispose(() => {
|
|
1782
2692
|
cleanupSensorComposable(state);
|
|
1783
2693
|
sensorsRef.value = [];
|
|
@@ -1789,29 +2699,6 @@ function useClassifierSensors(camera) {
|
|
|
1789
2699
|
};
|
|
1790
2700
|
}
|
|
1791
2701
|
//#endregion
|
|
1792
|
-
//#region src/composables/resolvePluginId.ts
|
|
1793
|
-
var pluginIdCache = /* @__PURE__ */ new Map();
|
|
1794
|
-
var pendingResolves = /* @__PURE__ */ new Map();
|
|
1795
|
-
function clearPluginIdCache() {
|
|
1796
|
-
pluginIdCache.clear();
|
|
1797
|
-
}
|
|
1798
|
-
async function resolvePluginId(rpcRef, pluginName) {
|
|
1799
|
-
const cached = pluginIdCache.get(pluginName);
|
|
1800
|
-
if (cached) return cached;
|
|
1801
|
-
const pending = pendingResolves.get(pluginName);
|
|
1802
|
-
if (pending) return pending;
|
|
1803
|
-
const promise = (async () => {
|
|
1804
|
-
const namespaces = NamespaceManager.coreManagerNamespaces();
|
|
1805
|
-
const pluginInfo = await rpcCall(rpcRef, (rpc) => rpc.createProxy(namespaces.coreManagerRpc).getPlugin(pluginName));
|
|
1806
|
-
if (pluginInfo) {
|
|
1807
|
-
pluginIdCache.set(pluginName, pluginInfo.id);
|
|
1808
|
-
return pluginInfo.id;
|
|
1809
|
-
}
|
|
1810
|
-
})().finally(() => pendingResolves.delete(pluginName));
|
|
1811
|
-
pendingResolves.set(pluginName, promise);
|
|
1812
|
-
return promise;
|
|
1813
|
-
}
|
|
1814
|
-
//#endregion
|
|
1815
2702
|
//#region src/composables/useStorage.ts
|
|
1816
2703
|
function createReactiveStorage(proxy, client) {
|
|
1817
2704
|
const config = shallowRef();
|
|
@@ -1902,8 +2789,8 @@ function acquireStorage(key, client, createProxy) {
|
|
|
1902
2789
|
if (cached && cached.client !== client) storageCache.forceRelease(key);
|
|
1903
2790
|
return storageCache.acquire(key, () => createReactiveStorage(createProxy(), client));
|
|
1904
2791
|
}
|
|
1905
|
-
function releaseStorage(key) {
|
|
1906
|
-
storageCache.release(key);
|
|
2792
|
+
function releaseStorage(key, storage) {
|
|
2793
|
+
storageCache.release(key, storage);
|
|
1907
2794
|
}
|
|
1908
2795
|
function clearStorageCache() {
|
|
1909
2796
|
storageCache.clear();
|
|
@@ -1912,12 +2799,22 @@ function clearStorageCache() {
|
|
|
1912
2799
|
function createStorageState() {
|
|
1913
2800
|
return {
|
|
1914
2801
|
currentStorageKey: void 0,
|
|
1915
|
-
cachedStorage: void 0
|
|
2802
|
+
cachedStorage: void 0,
|
|
2803
|
+
load: void 0,
|
|
2804
|
+
disposed: false
|
|
1916
2805
|
};
|
|
1917
2806
|
}
|
|
2807
|
+
function endLoad(state, config) {
|
|
2808
|
+
const load = state.load;
|
|
2809
|
+
if (!load) return;
|
|
2810
|
+
clearTimeout(load.timer);
|
|
2811
|
+
state.load = void 0;
|
|
2812
|
+
load.settle(config);
|
|
2813
|
+
}
|
|
1918
2814
|
function cleanupStorage(state, isConnected, config) {
|
|
2815
|
+
endLoad(state, void 0);
|
|
1919
2816
|
if (state.currentStorageKey) {
|
|
1920
|
-
releaseStorage(state.currentStorageKey);
|
|
2817
|
+
releaseStorage(state.currentStorageKey, state.cachedStorage);
|
|
1921
2818
|
state.currentStorageKey = void 0;
|
|
1922
2819
|
}
|
|
1923
2820
|
isConnected.value = false;
|
|
@@ -1927,7 +2824,7 @@ function cleanupStorage(state, isConnected, config) {
|
|
|
1927
2824
|
function bindStorage(state, storageKey, client, config, createProxy) {
|
|
1928
2825
|
if (state.currentStorageKey === storageKey && state.cachedStorage?.client === client) return;
|
|
1929
2826
|
if (state.currentStorageKey && state.currentStorageKey !== storageKey) {
|
|
1930
|
-
releaseStorage(state.currentStorageKey);
|
|
2827
|
+
releaseStorage(state.currentStorageKey, state.cachedStorage);
|
|
1931
2828
|
config.value = void 0;
|
|
1932
2829
|
}
|
|
1933
2830
|
state.currentStorageKey = storageKey;
|
|
@@ -1943,45 +2840,90 @@ var GET_CONFIG_RETRY_DELAYS = [
|
|
|
1943
2840
|
15e3,
|
|
1944
2841
|
15e3
|
|
1945
2842
|
];
|
|
1946
|
-
function
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
function
|
|
1950
|
-
|
|
1951
|
-
|
|
2843
|
+
function createStorageOperations(state, config, isLoading, error, rebind, status) {
|
|
2844
|
+
let visibleAttempts = 0;
|
|
2845
|
+
if (status) tryOnScopeDispose(onPluginStatus(onStatus));
|
|
2846
|
+
function onStatus(pluginName, running) {
|
|
2847
|
+
if (pluginName !== status?.pluginName()) return;
|
|
2848
|
+
if (!running) {
|
|
2849
|
+
if (state.load?.timer) endLoad(state, void 0);
|
|
2850
|
+
} else if (state.load || error.value) getConfig();
|
|
2851
|
+
}
|
|
1952
2852
|
function getConfig() {
|
|
1953
|
-
if (!
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
2853
|
+
if (!state.cachedStorage) return Promise.resolve(void 0);
|
|
2854
|
+
if (status) ensureStatusSubscription(status.rpc);
|
|
2855
|
+
if (state.load && state.load.key !== state.currentStorageKey) endLoad(state, void 0);
|
|
2856
|
+
const load = state.load ?? startLoad();
|
|
2857
|
+
load.retries = 0;
|
|
2858
|
+
load.rebound = false;
|
|
2859
|
+
if (load.storage !== state.cachedStorage) {
|
|
2860
|
+
clearTimeout(load.timer);
|
|
2861
|
+
load.timer = void 0;
|
|
2862
|
+
attempt(load, true);
|
|
2863
|
+
}
|
|
2864
|
+
return load.result;
|
|
2865
|
+
}
|
|
2866
|
+
function startLoad() {
|
|
2867
|
+
let settle;
|
|
2868
|
+
const result = new Promise((resolve) => settle = resolve);
|
|
2869
|
+
const load = {
|
|
2870
|
+
key: state.currentStorageKey,
|
|
2871
|
+
result,
|
|
2872
|
+
settle,
|
|
2873
|
+
storage: void 0,
|
|
2874
|
+
retries: 0,
|
|
2875
|
+
rebound: false,
|
|
2876
|
+
timer: void 0
|
|
2877
|
+
};
|
|
2878
|
+
state.load = load;
|
|
2879
|
+
return load;
|
|
1964
2880
|
}
|
|
1965
|
-
async function
|
|
2881
|
+
async function attempt(load, visible) {
|
|
1966
2882
|
const cached = state.cachedStorage;
|
|
1967
|
-
if (!cached)
|
|
1968
|
-
|
|
2883
|
+
if (!cached) {
|
|
2884
|
+
endLoad(state, void 0);
|
|
2885
|
+
return;
|
|
2886
|
+
}
|
|
2887
|
+
load.storage = cached;
|
|
2888
|
+
if (visible) {
|
|
2889
|
+
visibleAttempts++;
|
|
2890
|
+
isLoading.value = true;
|
|
2891
|
+
}
|
|
1969
2892
|
try {
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
if (
|
|
1975
|
-
|
|
2893
|
+
const result = await cached.getConfig();
|
|
2894
|
+
if (state.load !== load || state.cachedStorage !== cached) return;
|
|
2895
|
+
if (rebind && !load.rebound && isNoRespondersError(cached.error.value)) {
|
|
2896
|
+
load.rebound = true;
|
|
2897
|
+
if (await rebind()) {
|
|
2898
|
+
if (state.load !== load) return;
|
|
2899
|
+
load.key = state.currentStorageKey;
|
|
2900
|
+
await attempt(load, visible);
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2903
|
+
if (state.load !== load || state.cachedStorage !== cached) return;
|
|
1976
2904
|
}
|
|
1977
|
-
if (state.cachedStorage !== cached) return result;
|
|
1978
2905
|
config.value = cached.config.value;
|
|
1979
2906
|
error.value = cached.error.value;
|
|
1980
|
-
|
|
2907
|
+
if (cached.error.value) scheduleRetry(load);
|
|
2908
|
+
else endLoad(state, result);
|
|
1981
2909
|
} finally {
|
|
1982
|
-
|
|
2910
|
+
if (load.storage === cached) load.storage = void 0;
|
|
2911
|
+
if (visible && --visibleAttempts === 0) isLoading.value = false;
|
|
1983
2912
|
}
|
|
1984
2913
|
}
|
|
2914
|
+
function scheduleRetry(load) {
|
|
2915
|
+
const pluginName = status?.pluginName();
|
|
2916
|
+
const delay = pluginName && isPluginStopped(pluginName) ? void 0 : GET_CONFIG_RETRY_DELAYS[load.retries++];
|
|
2917
|
+
if (delay === void 0) {
|
|
2918
|
+
endLoad(state, void 0);
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
2921
|
+
load.timer = setTimeout(() => {
|
|
2922
|
+
load.timer = void 0;
|
|
2923
|
+
if (load.key === state.currentStorageKey) attempt(load, false);
|
|
2924
|
+
else endLoad(state, void 0);
|
|
2925
|
+
}, delay);
|
|
2926
|
+
}
|
|
1985
2927
|
async function setValue(key, value) {
|
|
1986
2928
|
const cached = state.cachedStorage;
|
|
1987
2929
|
if (!cached) throw new Error("Storage not connected");
|
|
@@ -2044,17 +2986,24 @@ function usePluginStorage(pluginName) {
|
|
|
2044
2986
|
const error = ref();
|
|
2045
2987
|
const isConnected = ref(false);
|
|
2046
2988
|
const state = createStorageState();
|
|
2047
|
-
const operations = createStorageOperations(state, config, _isLoading, error
|
|
2989
|
+
const operations = createStorageOperations(state, config, _isLoading, error, rebind, {
|
|
2990
|
+
rpc,
|
|
2991
|
+
pluginName: () => toValue(pluginName)
|
|
2992
|
+
});
|
|
2993
|
+
function bind(pluginId, client) {
|
|
2994
|
+
bindStorage(state, getPluginStorageKey(pluginId), client, config, () => {
|
|
2995
|
+
const namespaces = NamespaceManager.pluginNamespaces(pluginId);
|
|
2996
|
+
return client.createProxy(namespaces.pluginStorageRpc);
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2048
2999
|
async function connect(name) {
|
|
2049
3000
|
const client = rpc.value;
|
|
2050
3001
|
if (!client || !clientConnected.value) return false;
|
|
2051
3002
|
try {
|
|
2052
3003
|
const pluginId = await resolvePluginId(rpc, name);
|
|
3004
|
+
if (state.disposed) return false;
|
|
2053
3005
|
if (!pluginId) throw new Error(`Plugin "${name}" not found`);
|
|
2054
|
-
|
|
2055
|
-
const namespaces = NamespaceManager.pluginNamespaces(pluginId);
|
|
2056
|
-
return client.createProxy(namespaces.pluginStorageRpc);
|
|
2057
|
-
});
|
|
3006
|
+
bind(pluginId, client);
|
|
2058
3007
|
isConnected.value = true;
|
|
2059
3008
|
return true;
|
|
2060
3009
|
} catch (err) {
|
|
@@ -2063,6 +3012,17 @@ function usePluginStorage(pluginName) {
|
|
|
2063
3012
|
return false;
|
|
2064
3013
|
}
|
|
2065
3014
|
}
|
|
3015
|
+
async function rebind() {
|
|
3016
|
+
const name = toValue(pluginName);
|
|
3017
|
+
const storage = state.cachedStorage;
|
|
3018
|
+
if (!name || !storage) return false;
|
|
3019
|
+
forgetPluginId(name);
|
|
3020
|
+
const pluginId = await resolvePluginId(rpc, name).catch(() => void 0);
|
|
3021
|
+
if (!pluginId || state.disposed || state.cachedStorage !== storage) return false;
|
|
3022
|
+
const key = state.currentStorageKey;
|
|
3023
|
+
bind(pluginId, storage.client);
|
|
3024
|
+
return state.currentStorageKey !== key;
|
|
3025
|
+
}
|
|
2066
3026
|
async function getConfig() {
|
|
2067
3027
|
const name = toValue(pluginName);
|
|
2068
3028
|
if (!name) return void 0;
|
|
@@ -2080,6 +3040,7 @@ function usePluginStorage(pluginName) {
|
|
|
2080
3040
|
watch([clientConnected, () => toValue(pluginName)], sync, { immediate: true });
|
|
2081
3041
|
cameraUi.on("reconnected", sync);
|
|
2082
3042
|
tryOnScopeDispose(() => {
|
|
3043
|
+
state.disposed = true;
|
|
2083
3044
|
cameraUi.off("reconnected", sync);
|
|
2084
3045
|
cleanupStorage(state, isConnected, config);
|
|
2085
3046
|
});
|
|
@@ -2103,17 +3064,24 @@ function useCameraStorage(camera, pluginName) {
|
|
|
2103
3064
|
const error = ref();
|
|
2104
3065
|
const isConnected = ref(false);
|
|
2105
3066
|
const state = createStorageState();
|
|
2106
|
-
const operations = createStorageOperations(state, config, _isLoading, error
|
|
3067
|
+
const operations = createStorageOperations(state, config, _isLoading, error, rebind, {
|
|
3068
|
+
rpc,
|
|
3069
|
+
pluginName: () => toValue(pluginName)
|
|
3070
|
+
});
|
|
3071
|
+
function bind(pluginId, cameraId, client) {
|
|
3072
|
+
bindStorage(state, getCameraStorageKey(pluginId, cameraId), client, config, () => {
|
|
3073
|
+
const namespaces = NamespaceManager.pluginCameraNamespaces(pluginId, cameraId);
|
|
3074
|
+
return client.createProxy(namespaces.cameraStorageRpc);
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
2107
3077
|
async function connect(cameraId, name) {
|
|
2108
3078
|
const client = rpc.value;
|
|
2109
3079
|
if (!client || !clientConnected.value) return false;
|
|
2110
3080
|
try {
|
|
2111
3081
|
const pluginId = await resolvePluginId(rpc, name);
|
|
3082
|
+
if (state.disposed) return false;
|
|
2112
3083
|
if (!pluginId) throw new Error(`Plugin "${name}" not found`);
|
|
2113
|
-
|
|
2114
|
-
const namespaces = NamespaceManager.pluginCameraNamespaces(pluginId, cameraId);
|
|
2115
|
-
return client.createProxy(namespaces.cameraStorageRpc);
|
|
2116
|
-
});
|
|
3084
|
+
bind(pluginId, cameraId, client);
|
|
2117
3085
|
isConnected.value = true;
|
|
2118
3086
|
return true;
|
|
2119
3087
|
} catch (err) {
|
|
@@ -2122,6 +3090,18 @@ function useCameraStorage(camera, pluginName) {
|
|
|
2122
3090
|
return false;
|
|
2123
3091
|
}
|
|
2124
3092
|
}
|
|
3093
|
+
async function rebind() {
|
|
3094
|
+
const cameraId = extractCameraId(toValue(camera));
|
|
3095
|
+
const name = toValue(pluginName);
|
|
3096
|
+
const storage = state.cachedStorage;
|
|
3097
|
+
if (!cameraId || !name || !storage) return false;
|
|
3098
|
+
forgetPluginId(name);
|
|
3099
|
+
const pluginId = await resolvePluginId(rpc, name).catch(() => void 0);
|
|
3100
|
+
if (!pluginId || state.disposed || state.cachedStorage !== storage) return false;
|
|
3101
|
+
const key = state.currentStorageKey;
|
|
3102
|
+
bind(pluginId, cameraId, storage.client);
|
|
3103
|
+
return state.currentStorageKey !== key;
|
|
3104
|
+
}
|
|
2125
3105
|
async function getConfig() {
|
|
2126
3106
|
const cameraId = extractCameraId(toValue(camera));
|
|
2127
3107
|
const name = toValue(pluginName);
|
|
@@ -2145,6 +3125,7 @@ function useCameraStorage(camera, pluginName) {
|
|
|
2145
3125
|
], sync, { immediate: true });
|
|
2146
3126
|
cameraUi.on("reconnected", sync);
|
|
2147
3127
|
tryOnScopeDispose(() => {
|
|
3128
|
+
state.disposed = true;
|
|
2148
3129
|
cameraUi.off("reconnected", sync);
|
|
2149
3130
|
cleanupStorage(state, isConnected, config);
|
|
2150
3131
|
});
|
|
@@ -2168,7 +3149,13 @@ function useSensorStorage(sensorId, pluginId) {
|
|
|
2168
3149
|
const error = ref();
|
|
2169
3150
|
const isConnected = ref(false);
|
|
2170
3151
|
const state = createStorageState();
|
|
2171
|
-
const operations = createStorageOperations(state, config, _isLoading, error
|
|
3152
|
+
const operations = createStorageOperations(state, config, _isLoading, error, void 0, {
|
|
3153
|
+
rpc,
|
|
3154
|
+
pluginName: () => {
|
|
3155
|
+
const plugId = toValue(pluginId);
|
|
3156
|
+
return plugId ? pluginNameOf(plugId) : void 0;
|
|
3157
|
+
}
|
|
3158
|
+
});
|
|
2172
3159
|
function connect(senId, plugId) {
|
|
2173
3160
|
const client = rpc.value;
|
|
2174
3161
|
if (!client || !clientConnected.value) return false;
|
|
@@ -2208,6 +3195,7 @@ function useSensorStorage(sensorId, pluginId) {
|
|
|
2208
3195
|
], sync, { immediate: true });
|
|
2209
3196
|
cameraUi.on("reconnected", sync);
|
|
2210
3197
|
tryOnScopeDispose(() => {
|
|
3198
|
+
state.disposed = true;
|
|
2211
3199
|
cameraUi.off("reconnected", sync);
|
|
2212
3200
|
cleanupStorage(state, isConnected, config);
|
|
2213
3201
|
});
|
|
@@ -2230,12 +3218,17 @@ function resetClientState() {
|
|
|
2230
3218
|
clearStorageCache();
|
|
2231
3219
|
clearSensorCache();
|
|
2232
3220
|
clearSnapshotCache();
|
|
3221
|
+
clearOAuthCache();
|
|
2233
3222
|
}
|
|
2234
3223
|
function refreshClientSubscriptions() {
|
|
2235
3224
|
clearPluginCache();
|
|
2236
3225
|
reconnectAllCameraDevices();
|
|
2237
3226
|
reconnectAllSensorManagers();
|
|
2238
3227
|
}
|
|
3228
|
+
function refreshClientStates() {
|
|
3229
|
+
forgetPluginStatus();
|
|
3230
|
+
refreshAllCameraDevices();
|
|
3231
|
+
}
|
|
2239
3232
|
//#endregion
|
|
2240
3233
|
//#region src/plugin.ts
|
|
2241
3234
|
var CAMERA_UI_INJECTION_KEY = Symbol("camera-ui");
|
|
@@ -2245,7 +3238,7 @@ function isContext(input) {
|
|
|
2245
3238
|
}
|
|
2246
3239
|
var DOWN_DEBOUNCE_MS = 4e3;
|
|
2247
3240
|
function makeContextFromTransport(input) {
|
|
2248
|
-
const { natsTransport, target, wsTransport } = input;
|
|
3241
|
+
const { natsTransport, bulkTransport, target, wsTransport } = input;
|
|
2249
3242
|
let hasBeenConnected = natsTransport.getClient() !== null;
|
|
2250
3243
|
let lastClient = natsTransport.getClient();
|
|
2251
3244
|
let downTimer = null;
|
|
@@ -2255,11 +3248,7 @@ function makeContextFromTransport(input) {
|
|
|
2255
3248
|
const error = ref(void 0);
|
|
2256
3249
|
const endpoint = computed(() => target.value?.endpoint.url);
|
|
2257
3250
|
const token = computed(() => target.value?.tokens.access);
|
|
2258
|
-
const extraProxyQuery = computed(() =>
|
|
2259
|
-
const t = target.value;
|
|
2260
|
-
if (!t?.tokens.proxySession) return void 0;
|
|
2261
|
-
return { session: t.tokens.proxySession };
|
|
2262
|
-
});
|
|
3251
|
+
const extraProxyQuery = computed(() => void 0);
|
|
2263
3252
|
const listeners = /* @__PURE__ */ new Map();
|
|
2264
3253
|
function on(event, cb) {
|
|
2265
3254
|
let set = listeners.get(event);
|
|
@@ -2293,11 +3282,15 @@ function makeContextFromTransport(input) {
|
|
|
2293
3282
|
rpc.value = next;
|
|
2294
3283
|
isConnected.value = true;
|
|
2295
3284
|
error.value = void 0;
|
|
3285
|
+
if (hasBeenConnected && (!wasConnected || clientChanged || sawDrop)) markLinkRestored(rpc);
|
|
2296
3286
|
if (!wasConnected || clientChanged) {
|
|
2297
3287
|
refreshClientSubscriptions();
|
|
2298
3288
|
if (hasBeenConnected) emit("reconnected");
|
|
2299
3289
|
hasBeenConnected = true;
|
|
2300
|
-
} else if (sawDrop && hasBeenConnected)
|
|
3290
|
+
} else if (sawDrop && hasBeenConnected) {
|
|
3291
|
+
refreshClientStates();
|
|
3292
|
+
emit("reconnected");
|
|
3293
|
+
}
|
|
2301
3294
|
sawDrop = false;
|
|
2302
3295
|
} else {
|
|
2303
3296
|
sawDrop = true;
|
|
@@ -2319,8 +3312,17 @@ function makeContextFromTransport(input) {
|
|
|
2319
3312
|
natsTransport.on("up", () => {
|
|
2320
3313
|
error.value = void 0;
|
|
2321
3314
|
});
|
|
3315
|
+
let bulkRpc;
|
|
3316
|
+
if (bulkTransport) {
|
|
3317
|
+
const lane = shallowRef();
|
|
3318
|
+
bulkTransport.subscribeClient((next) => {
|
|
3319
|
+
lane.value = next ?? void 0;
|
|
3320
|
+
});
|
|
3321
|
+
bulkRpc = lane;
|
|
3322
|
+
}
|
|
2322
3323
|
return markRaw({
|
|
2323
3324
|
rpc,
|
|
3325
|
+
bulkRpc,
|
|
2324
3326
|
target: readonly(target),
|
|
2325
3327
|
isConnected: readonly(isConnected),
|
|
2326
3328
|
endpoint,
|
|
@@ -2342,7 +3344,8 @@ function createCameraUiPlugin(input) {
|
|
|
2342
3344
|
//#region src/streaming/activityMode.ts
|
|
2343
3345
|
var DEFAULT_CONFIG = {
|
|
2344
3346
|
standbyTimeout: 5e3,
|
|
2345
|
-
activityTimeout: 5e3
|
|
3347
|
+
activityTimeout: 5e3,
|
|
3348
|
+
wakeTimeout: 3e4
|
|
2346
3349
|
};
|
|
2347
3350
|
function createActivityMode(options) {
|
|
2348
3351
|
const { initialMode = "always-on", config = {}, onStreamStart, onStreamStop, isStreamPlaying } = options;
|
|
@@ -2355,6 +3358,8 @@ function createActivityMode(options) {
|
|
|
2355
3358
|
const hasActivity = ref(false);
|
|
2356
3359
|
let activityTimeout;
|
|
2357
3360
|
let idleTimeout;
|
|
3361
|
+
let keepAwake = false;
|
|
3362
|
+
let engaged = false;
|
|
2358
3363
|
function clearActivityTimeout() {
|
|
2359
3364
|
if (activityTimeout) {
|
|
2360
3365
|
clearTimeout(activityTimeout);
|
|
@@ -2406,22 +3411,34 @@ function createActivityMode(options) {
|
|
|
2406
3411
|
}
|
|
2407
3412
|
function startIdleTimer() {
|
|
2408
3413
|
clearIdleTimeout();
|
|
3414
|
+
if (keepAwake) return;
|
|
3415
|
+
const timeout = isStreamPlaying() ? resolvedConfig.standbyTimeout : resolvedConfig.wakeTimeout;
|
|
2409
3416
|
idleTimeout = setTimeout(() => {
|
|
2410
3417
|
if (mode.value === "standby") goToStandby();
|
|
2411
|
-
},
|
|
3418
|
+
}, timeout);
|
|
2412
3419
|
}
|
|
2413
3420
|
function setMode(newMode) {
|
|
2414
3421
|
if (mode.value !== newMode) {
|
|
2415
3422
|
mode.value = newMode;
|
|
2416
|
-
manageStreamState();
|
|
3423
|
+
if (engaged) manageStreamState();
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
function engage() {
|
|
3427
|
+
engaged = true;
|
|
3428
|
+
if ((mode.value === "standby" || mode.value === "activity" && !hasActivity.value) && !isStreamPlaying()) {
|
|
3429
|
+
clearAllTimers();
|
|
3430
|
+
goToStandby();
|
|
3431
|
+
return;
|
|
2417
3432
|
}
|
|
3433
|
+
manageStreamState();
|
|
2418
3434
|
}
|
|
2419
3435
|
function reportActivity(detected) {
|
|
2420
3436
|
if (mode.value !== "activity") return;
|
|
2421
3437
|
if (detected) {
|
|
2422
3438
|
hasActivity.value = true;
|
|
2423
3439
|
clearActivityTimeout();
|
|
2424
|
-
if (inStandby.value
|
|
3440
|
+
if (inStandby.value) exitStandby();
|
|
3441
|
+
else if (!isStreamPlaying()) onStreamStart();
|
|
2425
3442
|
} else {
|
|
2426
3443
|
hasActivity.value = false;
|
|
2427
3444
|
clearActivityTimeout();
|
|
@@ -2445,7 +3462,13 @@ function createActivityMode(options) {
|
|
|
2445
3462
|
}
|
|
2446
3463
|
}
|
|
2447
3464
|
function resetIdleTimer() {
|
|
2448
|
-
if (mode.value === "standby" && !inStandby.value) startIdleTimer();
|
|
3465
|
+
if (engaged && mode.value === "standby" && !inStandby.value) startIdleTimer();
|
|
3466
|
+
}
|
|
3467
|
+
function setKeepAwake(awake) {
|
|
3468
|
+
if (keepAwake === awake) return;
|
|
3469
|
+
keepAwake = awake;
|
|
3470
|
+
if (awake) clearIdleTimeout();
|
|
3471
|
+
else resetIdleTimer();
|
|
2449
3472
|
}
|
|
2450
3473
|
function dispose() {
|
|
2451
3474
|
clearAllTimers();
|
|
@@ -2455,9 +3478,11 @@ function createActivityMode(options) {
|
|
|
2455
3478
|
inStandby,
|
|
2456
3479
|
hasActivity,
|
|
2457
3480
|
setMode,
|
|
3481
|
+
engage,
|
|
2458
3482
|
reportActivity,
|
|
2459
3483
|
resumeFromStandby,
|
|
2460
3484
|
resetIdleTimer,
|
|
3485
|
+
setKeepAwake,
|
|
2461
3486
|
dispose
|
|
2462
3487
|
};
|
|
2463
3488
|
}
|
|
@@ -2510,7 +3535,8 @@ function _init() {
|
|
|
2510
3535
|
} catch (err) {
|
|
2511
3536
|
log$1.error("onTabHidden listener threw:", err);
|
|
2512
3537
|
}
|
|
2513
|
-
for (const entry of _pausedListeners)
|
|
3538
|
+
for (const entry of _pausedListeners) if (entry.delayMs <= 0) _firePausedEntry(entry);
|
|
3539
|
+
else _schedulePausedEntry(entry);
|
|
2514
3540
|
} else if (visible && !_isVisible.value) {
|
|
2515
3541
|
_cancelPausedEntries();
|
|
2516
3542
|
const hiddenMs = _hiddenAt != null ? Date.now() - _hiddenAt : 0;
|
|
@@ -2524,6 +3550,14 @@ function _init() {
|
|
|
2524
3550
|
}
|
|
2525
3551
|
}
|
|
2526
3552
|
});
|
|
3553
|
+
document.addEventListener("freeze", () => {
|
|
3554
|
+
for (const entry of _pausedListeners) {
|
|
3555
|
+
if (entry.timer === null) continue;
|
|
3556
|
+
clearTimeout(entry.timer);
|
|
3557
|
+
entry.timer = null;
|
|
3558
|
+
_firePausedEntry(entry);
|
|
3559
|
+
}
|
|
3560
|
+
});
|
|
2527
3561
|
}
|
|
2528
3562
|
function useTabVisibility() {
|
|
2529
3563
|
_init();
|
|
@@ -2540,7 +3574,7 @@ function useTabVisibility() {
|
|
|
2540
3574
|
function onTabPaused(cb, options = {}) {
|
|
2541
3575
|
const entry = {
|
|
2542
3576
|
cb,
|
|
2543
|
-
delayMs: options.delayMs ??
|
|
3577
|
+
delayMs: options.delayMs ?? 3e4,
|
|
2544
3578
|
timer: null
|
|
2545
3579
|
};
|
|
2546
3580
|
_pausedListeners.add(entry);
|
|
@@ -2581,7 +3615,8 @@ var STREAM_CONFIG = {
|
|
|
2581
3615
|
},
|
|
2582
3616
|
MSE: {
|
|
2583
3617
|
BUFFER_SIZE: 2097152,
|
|
2584
|
-
BUFFER_WINDOW: 5
|
|
3618
|
+
BUFFER_WINDOW: 5,
|
|
3619
|
+
MAX_BUFFER_WINDOW: 60
|
|
2585
3620
|
},
|
|
2586
3621
|
CODECS: {
|
|
2587
3622
|
DEFAULT: [
|
|
@@ -2608,11 +3643,236 @@ var STREAM_CONFIG = {
|
|
|
2608
3643
|
}
|
|
2609
3644
|
};
|
|
2610
3645
|
//#endregion
|
|
3646
|
+
//#region src/streaming/mjpeg.ts
|
|
3647
|
+
function isJpegFrame(data) {
|
|
3648
|
+
if (data.byteLength < 2) return false;
|
|
3649
|
+
const bytes = new Uint8Array(data, 0, 2);
|
|
3650
|
+
return bytes[0] === 255 && bytes[1] === 216;
|
|
3651
|
+
}
|
|
3652
|
+
function createMJPEGHandler(options) {
|
|
3653
|
+
const { signal } = options;
|
|
3654
|
+
const canvas = document.createElement("canvas");
|
|
3655
|
+
const context = canvas.getContext("2d", { alpha: false });
|
|
3656
|
+
const stream = canvas.captureStream();
|
|
3657
|
+
let frames = 0;
|
|
3658
|
+
let decoding = false;
|
|
3659
|
+
let pending;
|
|
3660
|
+
let closed = false;
|
|
3661
|
+
function appendFrame(data) {
|
|
3662
|
+
if (closed || signal.aborted) return;
|
|
3663
|
+
if (decoding) {
|
|
3664
|
+
pending = data;
|
|
3665
|
+
return;
|
|
3666
|
+
}
|
|
3667
|
+
decode(data);
|
|
3668
|
+
}
|
|
3669
|
+
async function decode(data) {
|
|
3670
|
+
decoding = true;
|
|
3671
|
+
try {
|
|
3672
|
+
const bitmap = await createImageBitmap(new Blob([data], { type: "image/jpeg" }));
|
|
3673
|
+
if (!closed) draw(bitmap);
|
|
3674
|
+
bitmap.close();
|
|
3675
|
+
} catch {}
|
|
3676
|
+
decoding = false;
|
|
3677
|
+
const next = pending;
|
|
3678
|
+
pending = void 0;
|
|
3679
|
+
if (next && !closed) decode(next);
|
|
3680
|
+
}
|
|
3681
|
+
function draw(bitmap) {
|
|
3682
|
+
if (!context) return;
|
|
3683
|
+
if (canvas.width !== bitmap.width || canvas.height !== bitmap.height) {
|
|
3684
|
+
canvas.width = bitmap.width;
|
|
3685
|
+
canvas.height = bitmap.height;
|
|
3686
|
+
}
|
|
3687
|
+
context.drawImage(bitmap, 0, 0);
|
|
3688
|
+
frames++;
|
|
3689
|
+
}
|
|
3690
|
+
function close() {
|
|
3691
|
+
closed = true;
|
|
3692
|
+
pending = void 0;
|
|
3693
|
+
for (const track of stream.getTracks()) track.stop();
|
|
3694
|
+
}
|
|
3695
|
+
signal.addEventListener("abort", close, { once: true });
|
|
3696
|
+
return {
|
|
3697
|
+
stream,
|
|
3698
|
+
get frames() {
|
|
3699
|
+
return frames;
|
|
3700
|
+
},
|
|
3701
|
+
appendFrame,
|
|
3702
|
+
close
|
|
3703
|
+
};
|
|
3704
|
+
}
|
|
3705
|
+
//#endregion
|
|
3706
|
+
//#region src/streaming/fmp4.ts
|
|
3707
|
+
var MOOV = 1836019574;
|
|
3708
|
+
var TRAK = 1953653099;
|
|
3709
|
+
var TKHD = 1953196132;
|
|
3710
|
+
var MDIA = 1835297121;
|
|
3711
|
+
var MDHD = 1835296868;
|
|
3712
|
+
var HDLR = 1751411826;
|
|
3713
|
+
var MVEX = 1836475768;
|
|
3714
|
+
var TREX = 1953654136;
|
|
3715
|
+
var MOOF = 1836019558;
|
|
3716
|
+
var TRAF = 1953653094;
|
|
3717
|
+
var TFHD = 1952868452;
|
|
3718
|
+
var TFDT = 1952867444;
|
|
3719
|
+
var TRUN = 1953658222;
|
|
3720
|
+
var VIDE = 1986618469;
|
|
3721
|
+
var SAMPLE_IS_NON_SYNC = 65536;
|
|
3722
|
+
var MAX_RUN_SAMPLES = 4096;
|
|
3723
|
+
function createFmp4KeyframeScanner() {
|
|
3724
|
+
const tracks = /* @__PURE__ */ new Map();
|
|
3725
|
+
let hasVideo;
|
|
3726
|
+
function parseInit(view, start, end) {
|
|
3727
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
3728
|
+
const defaults = /* @__PURE__ */ new Map();
|
|
3729
|
+
forEachBox(view, start, end, (type, s, e) => {
|
|
3730
|
+
if (type === TRAK) {
|
|
3731
|
+
const trak = {
|
|
3732
|
+
id: -1,
|
|
3733
|
+
timescale: 0,
|
|
3734
|
+
video: false
|
|
3735
|
+
};
|
|
3736
|
+
forEachBox(view, s, e, (t, ts, te) => {
|
|
3737
|
+
if (t === TKHD && ts + 24 <= te) trak.id = view.getUint32(ts + (view.getUint8(ts) === 1 ? 20 : 12));
|
|
3738
|
+
else if (t === MDIA) forEachBox(view, ts, te, (m, ms, me) => {
|
|
3739
|
+
if (m === MDHD && ms + 24 <= me) trak.timescale = view.getUint32(ms + (view.getUint8(ms) === 1 ? 20 : 12));
|
|
3740
|
+
else if (m === HDLR && ms + 12 <= me) trak.video = view.getUint32(ms + 8) === VIDE;
|
|
3741
|
+
});
|
|
3742
|
+
});
|
|
3743
|
+
if (trak.id >= 0 && trak.timescale > 0) parsed.set(trak.id, trak);
|
|
3744
|
+
} else if (type === MVEX) forEachBox(view, s, e, (t, ts, te) => {
|
|
3745
|
+
if (t === TREX && ts + 24 <= te) defaults.set(view.getUint32(ts + 4), {
|
|
3746
|
+
duration: view.getUint32(ts + 12),
|
|
3747
|
+
flags: view.getUint32(ts + 20)
|
|
3748
|
+
});
|
|
3749
|
+
});
|
|
3750
|
+
});
|
|
3751
|
+
tracks.clear();
|
|
3752
|
+
for (const [id, { timescale, video }] of parsed) {
|
|
3753
|
+
const trex = defaults.get(id);
|
|
3754
|
+
tracks.set(id, {
|
|
3755
|
+
timescale,
|
|
3756
|
+
video,
|
|
3757
|
+
defaultDuration: trex?.duration ?? 0,
|
|
3758
|
+
defaultFlags: trex?.flags ?? 0
|
|
3759
|
+
});
|
|
3760
|
+
}
|
|
3761
|
+
hasVideo = [...tracks.values()].some((track) => track.video);
|
|
3762
|
+
}
|
|
3763
|
+
function scanTrackFragment(view, start, end, onKeyframe) {
|
|
3764
|
+
const fragment = {
|
|
3765
|
+
duration: 0,
|
|
3766
|
+
flags: 0
|
|
3767
|
+
};
|
|
3768
|
+
forEachBox(view, start, end, (type, s, e) => {
|
|
3769
|
+
if (type === TFHD && s + 8 <= e) {
|
|
3770
|
+
const track = tracks.get(view.getUint32(s + 4));
|
|
3771
|
+
const flags = view.getUint32(s) & 16777215;
|
|
3772
|
+
const length = 8 + (flags & 1 ? 8 : 0) + (flags & 2 ? 4 : 0) + (flags & 8 ? 4 : 0) + (flags & 16 ? 4 : 0) + (flags & 32 ? 4 : 0);
|
|
3773
|
+
if (!track?.video || s + length > e) return;
|
|
3774
|
+
let p = s + 8 + (flags & 1 ? 8 : 0) + (flags & 2 ? 4 : 0);
|
|
3775
|
+
fragment.duration = track.defaultDuration;
|
|
3776
|
+
fragment.flags = track.defaultFlags;
|
|
3777
|
+
if (flags & 8) {
|
|
3778
|
+
fragment.duration = view.getUint32(p);
|
|
3779
|
+
p += 4;
|
|
3780
|
+
}
|
|
3781
|
+
if (flags & 16) p += 4;
|
|
3782
|
+
if (flags & 32) fragment.flags = view.getUint32(p);
|
|
3783
|
+
fragment.track = track;
|
|
3784
|
+
} else if (type === TFDT && s + 8 <= e) {
|
|
3785
|
+
if (view.getUint8(s) === 0) fragment.baseTime = view.getUint32(s + 4);
|
|
3786
|
+
else if (s + 12 <= e) fragment.baseTime = view.getUint32(s + 4) * 2 ** 32 + view.getUint32(s + 8);
|
|
3787
|
+
} else if (type === TRUN && isPlaced(fragment)) fragment.baseTime = scanRun(view, s, e, fragment, onKeyframe);
|
|
3788
|
+
});
|
|
3789
|
+
}
|
|
3790
|
+
function scan(data, onKeyframe) {
|
|
3791
|
+
const view = new DataView(data);
|
|
3792
|
+
forEachBox(view, 0, view.byteLength, (type, start, end) => {
|
|
3793
|
+
if (type === MOOV) parseInit(view, start, end);
|
|
3794
|
+
else if (type === MOOF && hasVideo) forEachBox(view, start, end, (t, s, e) => {
|
|
3795
|
+
if (t === TRAF) scanTrackFragment(view, s, e, onKeyframe);
|
|
3796
|
+
});
|
|
3797
|
+
});
|
|
3798
|
+
}
|
|
3799
|
+
return {
|
|
3800
|
+
get hasVideo() {
|
|
3801
|
+
return hasVideo;
|
|
3802
|
+
},
|
|
3803
|
+
scan
|
|
3804
|
+
};
|
|
3805
|
+
}
|
|
3806
|
+
function forEachBox(view, from, to, visit) {
|
|
3807
|
+
let offset = from;
|
|
3808
|
+
while (offset + 8 <= to) {
|
|
3809
|
+
let size = view.getUint32(offset);
|
|
3810
|
+
let header = 8;
|
|
3811
|
+
if (size === 1) {
|
|
3812
|
+
if (offset + 16 > to) return;
|
|
3813
|
+
size = view.getUint32(offset + 8) * 2 ** 32 + view.getUint32(offset + 12);
|
|
3814
|
+
header = 16;
|
|
3815
|
+
} else if (size === 0) size = to - offset;
|
|
3816
|
+
if (size < header || offset + size > to) return;
|
|
3817
|
+
visit(view.getUint32(offset + 4), offset + header, offset + size);
|
|
3818
|
+
offset += size;
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
function isPlaced(fragment) {
|
|
3822
|
+
return fragment.track !== void 0 && fragment.baseTime !== void 0;
|
|
3823
|
+
}
|
|
3824
|
+
function scanRun(view, start, end, fragment, onKeyframe) {
|
|
3825
|
+
if (start + 8 > end) return fragment.baseTime;
|
|
3826
|
+
const version = view.getUint8(start);
|
|
3827
|
+
const flags = view.getUint32(start) & 16777215;
|
|
3828
|
+
const count = view.getUint32(start + 4);
|
|
3829
|
+
let p = start + 8;
|
|
3830
|
+
if (flags & 1) p += 4;
|
|
3831
|
+
let firstFlags;
|
|
3832
|
+
if (flags & 4) {
|
|
3833
|
+
if (p + 4 > end) return fragment.baseTime;
|
|
3834
|
+
firstFlags = view.getUint32(p);
|
|
3835
|
+
p += 4;
|
|
3836
|
+
}
|
|
3837
|
+
const stride = (flags & 256 ? 4 : 0) + (flags & 512 ? 4 : 0) + (flags & 1024 ? 4 : 0) + (flags & 2048 ? 4 : 0);
|
|
3838
|
+
if (count > MAX_RUN_SAMPLES || p + count * stride > end) return fragment.baseTime;
|
|
3839
|
+
let time = fragment.baseTime;
|
|
3840
|
+
for (let i = 0; i < count; i++) {
|
|
3841
|
+
let duration = fragment.duration;
|
|
3842
|
+
let sampleFlags = i === 0 && firstFlags !== void 0 ? firstFlags : fragment.flags;
|
|
3843
|
+
let offset = 0;
|
|
3844
|
+
if (flags & 256) {
|
|
3845
|
+
duration = view.getUint32(p);
|
|
3846
|
+
p += 4;
|
|
3847
|
+
}
|
|
3848
|
+
if (flags & 512) p += 4;
|
|
3849
|
+
if (flags & 1024) {
|
|
3850
|
+
sampleFlags = view.getUint32(p);
|
|
3851
|
+
p += 4;
|
|
3852
|
+
}
|
|
3853
|
+
if (flags & 2048) {
|
|
3854
|
+
offset = version === 0 ? view.getUint32(p) : view.getInt32(p);
|
|
3855
|
+
p += 4;
|
|
3856
|
+
}
|
|
3857
|
+
if (!(sampleFlags & SAMPLE_IS_NON_SYNC)) onKeyframe((time + offset) / fragment.track.timescale);
|
|
3858
|
+
time += duration;
|
|
3859
|
+
}
|
|
3860
|
+
return time;
|
|
3861
|
+
}
|
|
3862
|
+
//#endregion
|
|
2611
3863
|
//#region src/streaming/utils.ts
|
|
2612
3864
|
var H265_TEST_CODEC = "hvc1.1.6.L153.B0";
|
|
2613
3865
|
function safariVersion() {
|
|
2614
3866
|
return navigator.userAgent.match(/version\/(\d+)/i);
|
|
2615
3867
|
}
|
|
3868
|
+
function isMobile() {
|
|
3869
|
+
return /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
|
3870
|
+
}
|
|
3871
|
+
function suspendsWhenHidden() {
|
|
3872
|
+
if (globalThis.Capacitor?.isNativePlatform?.()) return true;
|
|
3873
|
+
if (typeof navigator === "undefined") return false;
|
|
3874
|
+
return isMobile() || /Macintosh/i.test(navigator.userAgent) && navigator.maxTouchPoints > 1;
|
|
3875
|
+
}
|
|
2616
3876
|
function getSupportedCodecs() {
|
|
2617
3877
|
const codecs = [...STREAM_CONFIG.CODECS.DEFAULT];
|
|
2618
3878
|
const version = safariVersion();
|
|
@@ -2657,6 +3917,9 @@ function canPlayH265(transport) {
|
|
|
2657
3917
|
}
|
|
2658
3918
|
return webrtcH265;
|
|
2659
3919
|
}
|
|
3920
|
+
function canPlayMjpeg() {
|
|
3921
|
+
return typeof createImageBitmap === "function" && typeof HTMLCanvasElement === "function" && typeof HTMLCanvasElement.prototype.captureStream === "function";
|
|
3922
|
+
}
|
|
2660
3923
|
function reportH265DecodeFailure(transport) {
|
|
2661
3924
|
if (transport === "mse") mseH265 = false;
|
|
2662
3925
|
else webrtcH265 = false;
|
|
@@ -2686,6 +3949,11 @@ function getMediaSourceConstructor() {
|
|
|
2686
3949
|
if (hasManagedMediaSource()) return window.ManagedMediaSource;
|
|
2687
3950
|
if ("MediaSource" in window) return window.MediaSource;
|
|
2688
3951
|
}
|
|
3952
|
+
function isVideoInPictureInPicture(video) {
|
|
3953
|
+
if (!video) return false;
|
|
3954
|
+
if (typeof document !== "undefined" && "pictureInPictureElement" in document && document.pictureInPictureElement === video) return true;
|
|
3955
|
+
return video.webkitPresentationMode === "picture-in-picture";
|
|
3956
|
+
}
|
|
2689
3957
|
function abortableSleep(ms, signal) {
|
|
2690
3958
|
return new Promise((resolve, reject) => {
|
|
2691
3959
|
if (signal.aborted) {
|
|
@@ -2701,6 +3969,10 @@ function abortableSleep(ms, signal) {
|
|
|
2701
3969
|
}
|
|
2702
3970
|
//#endregion
|
|
2703
3971
|
//#region src/streaming/mse.ts
|
|
3972
|
+
var CUT_MARGIN = .001;
|
|
3973
|
+
var MAX_KEYFRAMES = 256;
|
|
3974
|
+
var LIVE_EDGE_MARGIN = .3;
|
|
3975
|
+
var PENDING_INITIAL_SIZE = 262144;
|
|
2704
3976
|
function createMSEHandler(options) {
|
|
2705
3977
|
const { videoElement, onReady, onFirstData, onError, signal } = options;
|
|
2706
3978
|
let mediaSource = null;
|
|
@@ -2708,15 +3980,18 @@ function createMSEHandler(options) {
|
|
|
2708
3980
|
let objectUrl = null;
|
|
2709
3981
|
let isReady = false;
|
|
2710
3982
|
let hasFirstData = false;
|
|
2711
|
-
let
|
|
3983
|
+
let failed = false;
|
|
3984
|
+
let pendingBuffer = /* @__PURE__ */ new Uint8Array(0);
|
|
2712
3985
|
let pendingLength = 0;
|
|
2713
3986
|
const earlyDataBuffer = [];
|
|
2714
3987
|
const supportedCodecs = getSupportedCodecs();
|
|
3988
|
+
const scanner = createFmp4KeyframeScanner();
|
|
3989
|
+
const keyframes = [];
|
|
2715
3990
|
function setup() {
|
|
2716
3991
|
if (signal.aborted) return void 0;
|
|
2717
3992
|
const MediaSourceConstructor = getMediaSourceConstructor();
|
|
2718
3993
|
if (!MediaSourceConstructor) {
|
|
2719
|
-
|
|
3994
|
+
fail(/* @__PURE__ */ new Error("MediaSource not supported"), "setup");
|
|
2720
3995
|
return;
|
|
2721
3996
|
}
|
|
2722
3997
|
mediaSource = new MediaSourceConstructor();
|
|
@@ -2734,6 +4009,7 @@ function createMSEHandler(options) {
|
|
|
2734
4009
|
videoElement.src = objectUrl;
|
|
2735
4010
|
videoElement.srcObject = null;
|
|
2736
4011
|
}
|
|
4012
|
+
videoElement.addEventListener("error", handleVideoError);
|
|
2737
4013
|
return filterSupportedCodecs(supportedCodecs, MediaSourceConstructor.isTypeSupported.bind(MediaSourceConstructor));
|
|
2738
4014
|
}
|
|
2739
4015
|
function initializeBuffer(mimeType) {
|
|
@@ -2745,48 +4021,63 @@ function createMSEHandler(options) {
|
|
|
2745
4021
|
return;
|
|
2746
4022
|
}
|
|
2747
4023
|
pendingLength = 0;
|
|
2748
|
-
|
|
4024
|
+
keyframes.length = 0;
|
|
2749
4025
|
const MediaSourceConstructor = getMediaSourceConstructor();
|
|
2750
4026
|
if (MediaSourceConstructor && !MediaSourceConstructor.isTypeSupported(mimeType)) {
|
|
2751
|
-
|
|
4027
|
+
fail(/* @__PURE__ */ new Error(`MIME type not supported: ${mimeType}`), "setup");
|
|
2752
4028
|
return;
|
|
2753
4029
|
}
|
|
2754
4030
|
try {
|
|
2755
4031
|
sourceBuffer = mediaSource.addSourceBuffer(mimeType);
|
|
2756
4032
|
sourceBuffer.mode = "segments";
|
|
2757
4033
|
sourceBuffer.addEventListener("updateend", handleUpdateEnd);
|
|
4034
|
+
sourceBuffer.addEventListener("error", handleSourceBufferError);
|
|
2758
4035
|
isReady = true;
|
|
2759
4036
|
if (earlyDataBuffer.length > 0) {
|
|
2760
4037
|
for (const earlyData of earlyDataBuffer) appendBuffer(earlyData);
|
|
2761
4038
|
earlyDataBuffer.length = 0;
|
|
2762
4039
|
}
|
|
4040
|
+
if (failed || signal.aborted) return;
|
|
2763
4041
|
onReady();
|
|
2764
4042
|
} catch (err) {
|
|
2765
|
-
|
|
4043
|
+
fail(err, isReady ? "runtime" : "setup");
|
|
2766
4044
|
}
|
|
2767
4045
|
}
|
|
2768
4046
|
function handleUpdateEnd() {
|
|
2769
|
-
if (signal.aborted || !sourceBuffer || !mediaSource) return;
|
|
4047
|
+
if (signal.aborted || failed || !sourceBuffer || !mediaSource) return;
|
|
2770
4048
|
if (!sourceBuffer.updating && pendingLength > 0) try {
|
|
2771
|
-
|
|
2772
|
-
sourceBuffer.appendBuffer(data);
|
|
4049
|
+
sourceBuffer.appendBuffer(pendingBuffer.subarray(0, pendingLength));
|
|
2773
4050
|
pendingLength = 0;
|
|
2774
4051
|
} catch (err) {
|
|
2775
|
-
if (!recoverFromQuota(err))
|
|
4052
|
+
if (!recoverFromQuota(err)) {
|
|
4053
|
+
fail(err, "runtime");
|
|
4054
|
+
return;
|
|
4055
|
+
}
|
|
2776
4056
|
}
|
|
2777
4057
|
if (!sourceBuffer.updating && sourceBuffer.buffered?.length) {
|
|
2778
4058
|
const end = sourceBuffer.buffered.end(sourceBuffer.buffered.length - 1);
|
|
2779
|
-
const start = end - STREAM_CONFIG.MSE.BUFFER_WINDOW;
|
|
2780
4059
|
const start0 = sourceBuffer.buffered.start(0);
|
|
4060
|
+
const margin = STREAM_CONFIG.MSE.BUFFER_WINDOW;
|
|
2781
4061
|
if (!hasFirstData) {
|
|
2782
4062
|
hasFirstData = true;
|
|
2783
4063
|
onFirstData();
|
|
2784
4064
|
}
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
4065
|
+
let cut = cutPoint(Math.min(videoElement.currentTime, end) - margin);
|
|
4066
|
+
if (end - start0 > STREAM_CONFIG.MSE.MAX_BUFFER_WINDOW) {
|
|
4067
|
+
const keyframe = cutPoint(end - margin);
|
|
4068
|
+
cut = keyframe !== void 0 && keyframe > start0 + CUT_MARGIN ? keyframe : end - margin;
|
|
4069
|
+
}
|
|
4070
|
+
if (cut !== void 0 && cut > start0 + CUT_MARGIN) {
|
|
4071
|
+
try {
|
|
4072
|
+
sourceBuffer.remove(start0, cut - CUT_MARGIN);
|
|
4073
|
+
mediaSource.setLiveSeekableRange(cut, end);
|
|
4074
|
+
} catch {}
|
|
4075
|
+
forgetKeyframesBefore(cut);
|
|
4076
|
+
}
|
|
4077
|
+
if (videoElement.currentTime < end - margin) {
|
|
4078
|
+
const target = seekPoint(end - margin, end);
|
|
4079
|
+
if (target > videoElement.currentTime) videoElement.currentTime = target;
|
|
4080
|
+
}
|
|
2790
4081
|
const gap = end - videoElement.currentTime;
|
|
2791
4082
|
if (gap > 1) videoElement.playbackRate = 1.1;
|
|
2792
4083
|
else if (gap < .1) videoElement.playbackRate = .9;
|
|
@@ -2794,35 +4085,42 @@ function createMSEHandler(options) {
|
|
|
2794
4085
|
}
|
|
2795
4086
|
}
|
|
2796
4087
|
function appendBuffer(data) {
|
|
2797
|
-
if (signal.aborted) return;
|
|
4088
|
+
if (signal.aborted || failed) return;
|
|
2798
4089
|
if (!sourceBuffer) {
|
|
2799
4090
|
earlyDataBuffer.push(data);
|
|
2800
4091
|
return;
|
|
2801
4092
|
}
|
|
2802
|
-
if (mediaSource?.readyState === "closed") {
|
|
2803
|
-
|
|
4093
|
+
if (mediaSource?.readyState === "closed" || mediaSource?.readyState === "ended") {
|
|
4094
|
+
fail(/* @__PURE__ */ new Error(`MediaSource ${mediaSource.readyState}`), "runtime");
|
|
2804
4095
|
return;
|
|
2805
4096
|
}
|
|
4097
|
+
scanner.scan(data, noteKeyframe);
|
|
2806
4098
|
if (sourceBuffer.updating || pendingLength > 0) {
|
|
2807
|
-
|
|
2808
|
-
if (pendingLength + bytes.byteLength > pendingBuffer.byteLength) {
|
|
4099
|
+
if (!enqueue(new Uint8Array(data))) {
|
|
2809
4100
|
pendingLength = 0;
|
|
2810
|
-
|
|
4101
|
+
fail(/* @__PURE__ */ new Error("MSE pending buffer overflow"), "runtime");
|
|
2811
4102
|
return;
|
|
2812
4103
|
}
|
|
2813
|
-
pendingBuffer.set(bytes, pendingLength);
|
|
2814
|
-
pendingLength += bytes.byteLength;
|
|
2815
4104
|
} else try {
|
|
2816
4105
|
sourceBuffer.appendBuffer(data);
|
|
2817
4106
|
} catch (err) {
|
|
2818
|
-
if (recoverFromQuota(err))
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
4107
|
+
if (recoverFromQuota(err)) enqueue(new Uint8Array(data));
|
|
4108
|
+
else fail(err, "runtime");
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
function enqueue(bytes) {
|
|
4112
|
+
const needed = pendingLength + bytes.byteLength;
|
|
4113
|
+
if (needed > STREAM_CONFIG.MSE.BUFFER_SIZE) return false;
|
|
4114
|
+
if (needed > pendingBuffer.byteLength) {
|
|
4115
|
+
let size = Math.max(pendingBuffer.byteLength, PENDING_INITIAL_SIZE);
|
|
4116
|
+
while (size < needed) size *= 2;
|
|
4117
|
+
const grown = new Uint8Array(Math.min(size, STREAM_CONFIG.MSE.BUFFER_SIZE));
|
|
4118
|
+
grown.set(pendingBuffer.subarray(0, pendingLength));
|
|
4119
|
+
pendingBuffer = grown;
|
|
4120
|
+
}
|
|
4121
|
+
pendingBuffer.set(bytes, pendingLength);
|
|
4122
|
+
pendingLength = needed;
|
|
4123
|
+
return true;
|
|
2826
4124
|
}
|
|
2827
4125
|
function recoverFromQuota(err) {
|
|
2828
4126
|
if (err?.name !== "QuotaExceededError") return false;
|
|
@@ -2830,17 +4128,54 @@ function createMSEHandler(options) {
|
|
|
2830
4128
|
try {
|
|
2831
4129
|
const start0 = sourceBuffer.buffered.start(0);
|
|
2832
4130
|
const end = sourceBuffer.buffered.end(sourceBuffer.buffered.length - 1);
|
|
2833
|
-
const evictTo = Math.max(start0 + 1, end - STREAM_CONFIG.MSE.BUFFER_WINDOW);
|
|
2834
|
-
if (evictTo <= start0) return false;
|
|
2835
|
-
sourceBuffer.remove(start0, evictTo);
|
|
4131
|
+
const evictTo = cutPoint(Math.min(videoElement.currentTime, end)) ?? Math.max(start0 + 1, end - STREAM_CONFIG.MSE.BUFFER_WINDOW);
|
|
4132
|
+
if (evictTo <= start0 + CUT_MARGIN) return false;
|
|
4133
|
+
sourceBuffer.remove(start0, evictTo - CUT_MARGIN);
|
|
4134
|
+
forgetKeyframesBefore(evictTo);
|
|
2836
4135
|
return true;
|
|
2837
4136
|
} catch {
|
|
2838
4137
|
return false;
|
|
2839
4138
|
}
|
|
2840
4139
|
}
|
|
4140
|
+
function noteKeyframe(time) {
|
|
4141
|
+
while (keyframes.length && keyframes[keyframes.length - 1] >= time) keyframes.pop();
|
|
4142
|
+
keyframes.push(time);
|
|
4143
|
+
if (keyframes.length > MAX_KEYFRAMES) keyframes.shift();
|
|
4144
|
+
}
|
|
4145
|
+
function forgetKeyframesBefore(time) {
|
|
4146
|
+
while (keyframes.length && keyframes[0] < time) keyframes.shift();
|
|
4147
|
+
}
|
|
4148
|
+
function cutPoint(time) {
|
|
4149
|
+
if (scanner.hasVideo === false) return time;
|
|
4150
|
+
for (let i = keyframes.length - 1; i >= 0; i--) if (keyframes[i] <= time) return keyframes[i];
|
|
4151
|
+
}
|
|
4152
|
+
function seekPoint(time, end) {
|
|
4153
|
+
if (!scanner.hasVideo) return time;
|
|
4154
|
+
let point = time;
|
|
4155
|
+
for (const keyframe of keyframes) {
|
|
4156
|
+
if (keyframe > end) break;
|
|
4157
|
+
point = keyframe;
|
|
4158
|
+
if (keyframe >= time) break;
|
|
4159
|
+
}
|
|
4160
|
+
return point;
|
|
4161
|
+
}
|
|
4162
|
+
function handleSourceBufferError() {
|
|
4163
|
+
fail(/* @__PURE__ */ new Error("SourceBuffer error"), "runtime");
|
|
4164
|
+
}
|
|
4165
|
+
function handleVideoError() {
|
|
4166
|
+
const error = videoElement.error;
|
|
4167
|
+
fail(new Error(error?.message || `Video element error ${error?.code ?? "unknown"}`), "runtime");
|
|
4168
|
+
}
|
|
4169
|
+
function fail(err, phase) {
|
|
4170
|
+
if (failed || signal.aborted) return;
|
|
4171
|
+
failed = true;
|
|
4172
|
+
onError(err instanceof Error ? err : new Error(String(err)), phase);
|
|
4173
|
+
}
|
|
2841
4174
|
function close() {
|
|
4175
|
+
videoElement.removeEventListener("error", handleVideoError);
|
|
2842
4176
|
if (sourceBuffer) {
|
|
2843
4177
|
sourceBuffer.removeEventListener("updateend", handleUpdateEnd);
|
|
4178
|
+
sourceBuffer.removeEventListener("error", handleSourceBufferError);
|
|
2844
4179
|
if (mediaSource?.readyState === "open") try {
|
|
2845
4180
|
mediaSource.removeSourceBuffer(sourceBuffer);
|
|
2846
4181
|
} catch {}
|
|
@@ -2858,6 +4193,7 @@ function createMSEHandler(options) {
|
|
|
2858
4193
|
hasFirstData = false;
|
|
2859
4194
|
pendingLength = 0;
|
|
2860
4195
|
earlyDataBuffer.length = 0;
|
|
4196
|
+
keyframes.length = 0;
|
|
2861
4197
|
}
|
|
2862
4198
|
signal.addEventListener("abort", close, { once: true });
|
|
2863
4199
|
return {
|
|
@@ -2876,6 +4212,12 @@ function createMSEHandler(options) {
|
|
|
2876
4212
|
close
|
|
2877
4213
|
};
|
|
2878
4214
|
}
|
|
4215
|
+
function chaseLiveEdge(video) {
|
|
4216
|
+
if (video.buffered.length > 0) {
|
|
4217
|
+
const end = video.buffered.end(video.buffered.length - 1);
|
|
4218
|
+
if (end - video.currentTime > 1) video.currentTime = end - LIVE_EDGE_MARGIN;
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
2879
4221
|
async function playVideo(videoElement) {
|
|
2880
4222
|
try {
|
|
2881
4223
|
await videoElement.play();
|
|
@@ -2961,7 +4303,6 @@ function createWebRTCHandler(options) {
|
|
|
2961
4303
|
};
|
|
2962
4304
|
peerConnection.addTransceiver("video", { direction: "recvonly" });
|
|
2963
4305
|
peerConnection.addTransceiver("audio", { direction: "recvonly" });
|
|
2964
|
-
state.micTransceiver = peerConnection.addTransceiver("audio", { direction: "sendonly" });
|
|
2965
4306
|
return peerConnection;
|
|
2966
4307
|
}
|
|
2967
4308
|
async function createOffer() {
|
|
@@ -2979,37 +4320,49 @@ function createWebRTCHandler(options) {
|
|
|
2979
4320
|
get pc() {
|
|
2980
4321
|
return state.pc;
|
|
2981
4322
|
},
|
|
2982
|
-
get micTransceiver() {
|
|
2983
|
-
return state.micTransceiver;
|
|
2984
|
-
},
|
|
2985
4323
|
get isConnected() {
|
|
2986
4324
|
return state.isConnected;
|
|
2987
4325
|
},
|
|
2988
4326
|
createOffer,
|
|
2989
4327
|
handleAnswer: (sdp) => handleSdpAnswer(state.pc, signal, sdp),
|
|
2990
4328
|
handleCandidate: (candidate) => handleIceCandidate(state.pc, signal, candidate, filterUdp),
|
|
2991
|
-
setMicrophoneTrack: (track) => setMicTrack(state.micTransceiver, signal, track),
|
|
2992
4329
|
close
|
|
2993
4330
|
};
|
|
2994
4331
|
}
|
|
2995
4332
|
function createBackchannelHandler(options) {
|
|
2996
|
-
const { onConnected,
|
|
4333
|
+
const { filterUdp, onConnected, onFailed, onCandidate, signal } = options;
|
|
2997
4334
|
const state = {
|
|
2998
4335
|
pc: null,
|
|
2999
4336
|
micTransceiver: null,
|
|
3000
4337
|
isConnected: false
|
|
3001
4338
|
};
|
|
4339
|
+
let answered = false;
|
|
4340
|
+
const heldCandidates = [];
|
|
4341
|
+
function sendCandidate(candidate) {
|
|
4342
|
+
if (answered) onCandidate(candidate);
|
|
4343
|
+
else heldCandidates.push(candidate);
|
|
4344
|
+
}
|
|
4345
|
+
async function handleAnswer(sdp) {
|
|
4346
|
+
if (signal.aborted || !state.pc) return;
|
|
4347
|
+
answered = true;
|
|
4348
|
+
for (const candidate of heldCandidates.splice(0)) onCandidate(candidate);
|
|
4349
|
+
await state.pc.setRemoteDescription({
|
|
4350
|
+
type: "answer",
|
|
4351
|
+
sdp
|
|
4352
|
+
});
|
|
4353
|
+
}
|
|
3002
4354
|
function createPeerConnection() {
|
|
3003
|
-
const peerConnection = createBasePeerConnection(signal,
|
|
4355
|
+
const peerConnection = createBasePeerConnection(signal, sendCandidate, filterUdp);
|
|
3004
4356
|
peerConnection.onconnectionstatechange = () => {
|
|
3005
4357
|
if (signal.aborted) return;
|
|
3006
4358
|
const connectionState = peerConnection.connectionState;
|
|
3007
4359
|
if (connectionState === "connected") {
|
|
3008
4360
|
state.isConnected = true;
|
|
3009
4361
|
onConnected();
|
|
3010
|
-
} else if (connectionState === "disconnected"
|
|
4362
|
+
} else if (connectionState === "disconnected") state.isConnected = false;
|
|
4363
|
+
else if (connectionState === "failed") {
|
|
3011
4364
|
state.isConnected = false;
|
|
3012
|
-
|
|
4365
|
+
onFailed();
|
|
3013
4366
|
}
|
|
3014
4367
|
};
|
|
3015
4368
|
state.micTransceiver = peerConnection.addTransceiver("audio", { direction: "sendonly" });
|
|
@@ -3031,8 +4384,8 @@ function createBackchannelHandler(options) {
|
|
|
3031
4384
|
return state.isConnected;
|
|
3032
4385
|
},
|
|
3033
4386
|
createOffer,
|
|
3034
|
-
handleAnswer
|
|
3035
|
-
handleCandidate: (candidate) => handleIceCandidate(state.pc, signal, candidate,
|
|
4387
|
+
handleAnswer,
|
|
4388
|
+
handleCandidate: (candidate) => handleIceCandidate(state.pc, signal, candidate, filterUdp),
|
|
3036
4389
|
setMicrophoneTrack: (track) => setMicTrack(state.micTransceiver, signal, track),
|
|
3037
4390
|
close
|
|
3038
4391
|
};
|
|
@@ -3049,10 +4402,23 @@ async function processWebRTCMessage(handler, msg) {
|
|
|
3049
4402
|
//#region src/streaming/streamConnection.ts
|
|
3050
4403
|
var log = new Logger("StreamConnection");
|
|
3051
4404
|
var MSE_AUDIO_CODEC = /mp4a|opus|flac|mp3/i;
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
4405
|
+
var RETRY_BASE_DELAY = 1e3;
|
|
4406
|
+
var RETRY_MAX_DELAY = 3e4;
|
|
4407
|
+
var RETRY_RESET_AFTER = 3e4;
|
|
4408
|
+
var WS_RESOLVE_TIMEOUT = 15e3;
|
|
4409
|
+
var HEALTH_CHECK_INTERVAL = 1e3;
|
|
4410
|
+
var STALL_AFTER = 3e3;
|
|
4411
|
+
var STALL_RECONNECT_AFTER = 15e3;
|
|
4412
|
+
var HAVE_CURRENT_DATA$1 = 2;
|
|
4413
|
+
var PROBE_WAIT_MS = 2e3;
|
|
4414
|
+
function isPageHidden() {
|
|
4415
|
+
return typeof document !== "undefined" && document.visibilityState === "hidden";
|
|
4416
|
+
}
|
|
4417
|
+
function decodedFrames(video) {
|
|
4418
|
+
return typeof video.getVideoPlaybackQuality === "function" ? video.getVideoPlaybackQuality().totalVideoFrames : void 0;
|
|
4419
|
+
}
|
|
4420
|
+
function withJitter(delayMs) {
|
|
4421
|
+
return delayMs * (1 - Math.random() / 2);
|
|
3056
4422
|
}
|
|
3057
4423
|
var StreamConnection = class {
|
|
3058
4424
|
status;
|
|
@@ -3067,6 +4433,7 @@ var StreamConnection = class {
|
|
|
3067
4433
|
hasAudio;
|
|
3068
4434
|
hasBackchannel;
|
|
3069
4435
|
isPlaying;
|
|
4436
|
+
stalled;
|
|
3070
4437
|
error;
|
|
3071
4438
|
probeInfo;
|
|
3072
4439
|
muted;
|
|
@@ -3082,20 +4449,43 @@ var StreamConnection = class {
|
|
|
3082
4449
|
options;
|
|
3083
4450
|
wsTransport;
|
|
3084
4451
|
scope = effectScope(true);
|
|
4452
|
+
stopsWhenHidden = suspendsWhenHidden();
|
|
3085
4453
|
connectionGeneration = 0;
|
|
3086
4454
|
abortController = new AbortController();
|
|
3087
4455
|
probedSourceId;
|
|
4456
|
+
probeInFlight;
|
|
4457
|
+
probing;
|
|
3088
4458
|
wasPausedByVisibility = false;
|
|
4459
|
+
hiddenPauseRan = false;
|
|
3089
4460
|
disarmPipWatch = null;
|
|
3090
4461
|
wsHandle;
|
|
4462
|
+
wsOpenedAt = 0;
|
|
3091
4463
|
webrtcHandler;
|
|
3092
4464
|
mseHandler;
|
|
3093
|
-
|
|
4465
|
+
mjpegHandler;
|
|
4466
|
+
audioHandler;
|
|
4467
|
+
audioElement;
|
|
4468
|
+
cancelFirstFrameWatch;
|
|
4469
|
+
awaitingWebRTCFrame = false;
|
|
3094
4470
|
backchannelHandler;
|
|
3095
4471
|
pendingMicTrack = null;
|
|
4472
|
+
offerPeer;
|
|
4473
|
+
answeredPeer;
|
|
3096
4474
|
lastMediaStream = null;
|
|
3097
4475
|
stopWatchers = [];
|
|
3098
|
-
|
|
4476
|
+
failures = 0;
|
|
4477
|
+
playingSince;
|
|
4478
|
+
connectToken;
|
|
4479
|
+
rejectedToken;
|
|
4480
|
+
lastError;
|
|
4481
|
+
mseRejected = false;
|
|
4482
|
+
mseAnswerPending = false;
|
|
4483
|
+
mseStopPending = false;
|
|
4484
|
+
healthInterval;
|
|
4485
|
+
lastProgressAt = 0;
|
|
4486
|
+
lastMediaTime = 0;
|
|
4487
|
+
lastFrameCount;
|
|
4488
|
+
frameCounterLive = false;
|
|
3099
4489
|
offTabVisible;
|
|
3100
4490
|
offTabPaused;
|
|
3101
4491
|
onVideoPauseBound;
|
|
@@ -3107,8 +4497,6 @@ var StreamConnection = class {
|
|
|
3107
4497
|
stopConnectTimeout;
|
|
3108
4498
|
startMseConnectTimeout;
|
|
3109
4499
|
stopMseConnectTimeout;
|
|
3110
|
-
startReconnectTimeout;
|
|
3111
|
-
stopReconnectTimeout;
|
|
3112
4500
|
constructor(options) {
|
|
3113
4501
|
this.options = options;
|
|
3114
4502
|
const ctx = useCameraUi();
|
|
@@ -3126,6 +4514,7 @@ var StreamConnection = class {
|
|
|
3126
4514
|
this.hasAudio = ref(false);
|
|
3127
4515
|
this.hasBackchannel = ref(false);
|
|
3128
4516
|
this.isPlaying = ref(false);
|
|
4517
|
+
this.stalled = ref(false);
|
|
3129
4518
|
this.error = ref();
|
|
3130
4519
|
this.probeInfo = shallowRef();
|
|
3131
4520
|
this.muted = ref(true);
|
|
@@ -3140,47 +4529,44 @@ var StreamConnection = class {
|
|
|
3140
4529
|
this.isReady = computed(() => !!this.camera.value && !!this.videoElement.value && !!this.target.value);
|
|
3141
4530
|
this.scope.run(() => {
|
|
3142
4531
|
const { onTabPaused, onTabVisible } = useTabVisibility();
|
|
4532
|
+
const hiddenStopDelay = this.stopsWhenHidden ? 0 : DEFAULT_TAB_PAUSE_MS;
|
|
3143
4533
|
const wsConnectTimeout = useTimeoutFn(() => {
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
4534
|
+
const handle = this.wsHandle;
|
|
4535
|
+
if (!handle || handle.readyState === WebSocket.OPEN) return;
|
|
4536
|
+
if (handle.readyState === WebSocket.CLOSED && performance.now() - this.wsOpenedAt < WS_RESOLVE_TIMEOUT) {
|
|
4537
|
+
this.startWsConnectTimeout();
|
|
4538
|
+
return;
|
|
3147
4539
|
}
|
|
4540
|
+
this.disconnectWebSocket();
|
|
4541
|
+
if (!this.abortController.signal.aborted && this.status.value !== "closed") this.retryLater();
|
|
3148
4542
|
}, STREAM_CONFIG.WEBRTC.WS_CONNECT_TIMEOUT, { immediate: false });
|
|
3149
4543
|
const connectTimeout = useTimeoutFn(() => {
|
|
4544
|
+
if (this.abortController.signal.aborted) return;
|
|
3150
4545
|
if (this.webrtcHandler && !this.webrtcHandler.isConnected) {
|
|
3151
|
-
if (this.requestedMode.value === "auto")
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
} else {
|
|
3158
|
-
this.markSourceUndecodable();
|
|
3159
|
-
this.restart();
|
|
4546
|
+
if (this.requestedMode.value === "auto") this.fallBackToMSE();
|
|
4547
|
+
else this.retryAfterNoMedia();
|
|
4548
|
+
} else if (this.webrtcHandler && this.awaitingWebRTCFrame || this.mjpegHandler && !this.isPlaying.value || this.status.value === "connected" && !this.isPlaying.value) {
|
|
4549
|
+
if (isPageHidden()) {
|
|
4550
|
+
this.startConnectTimeout();
|
|
4551
|
+
return;
|
|
3160
4552
|
}
|
|
3161
|
-
|
|
3162
|
-
|
|
4553
|
+
this.handleNoFramesWhileConnected();
|
|
4554
|
+
}
|
|
4555
|
+
}, () => this.webrtcTookOver() ? STALL_RECONNECT_AFTER : STREAM_CONFIG.WEBRTC.CONNECT_TIMEOUT, { immediate: false });
|
|
3163
4556
|
const mseConnectTimeout = useTimeoutFn(() => {
|
|
3164
4557
|
if (this.abortController.signal.aborted || this.status.value === "connected" || this.status.value === "closed") return;
|
|
3165
|
-
if (this.mseHandler && !this.mseHandler.isReady)
|
|
3166
|
-
this.markSourceUndecodable();
|
|
3167
|
-
this.restart();
|
|
3168
|
-
}
|
|
4558
|
+
if (this.mseHandler && !this.mseHandler.isReady) this.retryAfterNoMedia();
|
|
3169
4559
|
}, STREAM_CONFIG.WEBRTC.CONNECT_TIMEOUT, { immediate: false });
|
|
3170
|
-
const reconnectTimeout = useTimeoutFn(() => {
|
|
3171
|
-
if (!this.abortController.signal.aborted) this.restart();
|
|
3172
|
-
}, STREAM_CONFIG.WEBRTC.RECONNECT_DELAY, { immediate: false });
|
|
3173
4560
|
this.startWsConnectTimeout = wsConnectTimeout.start;
|
|
3174
4561
|
this.stopWsConnectTimeout = wsConnectTimeout.stop;
|
|
3175
4562
|
this.startConnectTimeout = connectTimeout.start;
|
|
3176
4563
|
this.stopConnectTimeout = connectTimeout.stop;
|
|
3177
4564
|
this.startMseConnectTimeout = mseConnectTimeout.start;
|
|
3178
4565
|
this.stopMseConnectTimeout = mseConnectTimeout.stop;
|
|
3179
|
-
this.startReconnectTimeout = reconnectTimeout.start;
|
|
3180
|
-
this.stopReconnectTimeout = reconnectTimeout.stop;
|
|
3181
4566
|
this.setupWatchers();
|
|
3182
4567
|
this.offTabPaused = onTabPaused(() => {
|
|
3183
4568
|
log.debug(`onTabPaused fired — status=${this.status.value}, isReady=${this.isReady.value}, target=${!!this.target.value}`);
|
|
4569
|
+
this.hiddenPauseRan = true;
|
|
3184
4570
|
if (this.status.value === "idle" || this.status.value === "closed") {
|
|
3185
4571
|
log.debug(`onTabPaused — already in ${this.status.value}, skipping stop()`);
|
|
3186
4572
|
return;
|
|
@@ -3193,16 +4579,23 @@ var StreamConnection = class {
|
|
|
3193
4579
|
this.wasPausedByVisibility = true;
|
|
3194
4580
|
this.stop();
|
|
3195
4581
|
log.debug("onTabPaused — stop() done, wasPausedByVisibility=true");
|
|
3196
|
-
});
|
|
3197
|
-
this.offTabVisible = onTabVisible(() => {
|
|
4582
|
+
}, { delayMs: hiddenStopDelay });
|
|
4583
|
+
this.offTabVisible = onTabVisible(({ hiddenMs }) => {
|
|
3198
4584
|
log.debug(`onTabVisible fired — wasPausedByVisibility=${this.wasPausedByVisibility}, status=${this.status.value}, isReady=${this.isReady.value}, target=${!!this.target.value}`);
|
|
3199
4585
|
this.disarmPipWatch?.();
|
|
3200
|
-
|
|
3201
|
-
|
|
4586
|
+
const pauseRan = this.hiddenPauseRan;
|
|
4587
|
+
this.hiddenPauseRan = false;
|
|
4588
|
+
if (this.wasPausedByVisibility) {
|
|
4589
|
+
this.wasPausedByVisibility = false;
|
|
4590
|
+
this.startWhenReady();
|
|
4591
|
+
return;
|
|
4592
|
+
}
|
|
4593
|
+
if (!pauseRan && hiddenMs >= hiddenStopDelay && this.status.value === "connected") {
|
|
4594
|
+
log.debug(`onTabVisible: hidden for ${hiddenMs}ms without a stop, reconnecting`);
|
|
4595
|
+
this.restart();
|
|
3202
4596
|
return;
|
|
3203
4597
|
}
|
|
3204
|
-
|
|
3205
|
-
this.startWhenReady();
|
|
4598
|
+
log.debug("onTabVisible — not paused by visibility, no-op");
|
|
3206
4599
|
});
|
|
3207
4600
|
if (autoStart) whenever(this.isReady, () => {
|
|
3208
4601
|
if (this.status.value === "idle" || this.status.value === "closed") this.start();
|
|
@@ -3240,10 +4633,17 @@ var StreamConnection = class {
|
|
|
3240
4633
|
log.debug("start() — skipped, isReady=false");
|
|
3241
4634
|
return;
|
|
3242
4635
|
}
|
|
4636
|
+
if (this.stopsWhenHidden && isPageHidden()) {
|
|
4637
|
+
log.debug("start() — page hidden, starting once it is visible");
|
|
4638
|
+
this.wasPausedByVisibility = true;
|
|
4639
|
+
return;
|
|
4640
|
+
}
|
|
3243
4641
|
this.cleanup();
|
|
3244
4642
|
this.abortController = new AbortController();
|
|
3245
4643
|
const gen = ++this.connectionGeneration;
|
|
3246
4644
|
this.triedSourceIds.clear();
|
|
4645
|
+
this.failures = 0;
|
|
4646
|
+
this.playingSince = void 0;
|
|
3247
4647
|
this.status.value = "connecting";
|
|
3248
4648
|
this.error.value = void 0;
|
|
3249
4649
|
this.isPlaying.value = false;
|
|
@@ -3260,9 +4660,12 @@ var StreamConnection = class {
|
|
|
3260
4660
|
this.status.value = "error";
|
|
3261
4661
|
return;
|
|
3262
4662
|
}
|
|
4663
|
+
let dialedSourceId;
|
|
3263
4664
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
3264
4665
|
const probedSourceId = this.source.value?._id;
|
|
3265
|
-
|
|
4666
|
+
dialedSourceId = probedSourceId;
|
|
4667
|
+
log.debug("start(): connecting WebSocket");
|
|
4668
|
+
const probe = await this.dialWithProbe();
|
|
3266
4669
|
if (gen !== this.connectionGeneration) {
|
|
3267
4670
|
log.debug("start() — generation stale after probe, aborting");
|
|
3268
4671
|
return;
|
|
@@ -3270,18 +4673,19 @@ var StreamConnection = class {
|
|
|
3270
4673
|
if (probe && probedSourceId && this.probeSaysUnplayable(probe)) this.triedSourceIds.add(probedSourceId);
|
|
3271
4674
|
const reinit = this.initializeSource();
|
|
3272
4675
|
if (reinit === "unsupported") {
|
|
4676
|
+
this.disconnectWebSocket();
|
|
3273
4677
|
this.setUnsupported();
|
|
3274
4678
|
return;
|
|
3275
4679
|
}
|
|
3276
4680
|
if (!reinit) {
|
|
4681
|
+
this.disconnectWebSocket();
|
|
3277
4682
|
this.error.value = /* @__PURE__ */ new Error("No streaming source available");
|
|
3278
4683
|
this.status.value = "error";
|
|
3279
4684
|
return;
|
|
3280
4685
|
}
|
|
3281
4686
|
if (!probe || this.source.value?._id === probedSourceId) break;
|
|
3282
4687
|
}
|
|
3283
|
-
|
|
3284
|
-
this.connectWebSocket();
|
|
4688
|
+
if (this.source.value?._id !== dialedSourceId) this.dialWithProbe();
|
|
3285
4689
|
} catch (err) {
|
|
3286
4690
|
if (gen !== this.connectionGeneration) return;
|
|
3287
4691
|
log.debug("start() — error:", err);
|
|
@@ -3297,6 +4701,7 @@ var StreamConnection = class {
|
|
|
3297
4701
|
log.debug(`stop() — was ${this.status.value}, transitioning to closed`);
|
|
3298
4702
|
++this.connectionGeneration;
|
|
3299
4703
|
this.status.value = "closed";
|
|
4704
|
+
this.isPlaying.value = false;
|
|
3300
4705
|
this.cleanup();
|
|
3301
4706
|
}
|
|
3302
4707
|
async setMode(mode) {
|
|
@@ -3328,21 +4733,20 @@ var StreamConnection = class {
|
|
|
3328
4733
|
}
|
|
3329
4734
|
}
|
|
3330
4735
|
async setMicrophone(track) {
|
|
3331
|
-
if (
|
|
3332
|
-
|
|
4736
|
+
if (!track) {
|
|
4737
|
+
this.closeBackchannel();
|
|
3333
4738
|
return;
|
|
3334
4739
|
}
|
|
3335
|
-
if (this.
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
else if (this.backchannelHandler?.isConnected) await this.backchannelHandler.setMicrophoneTrack(track);
|
|
3340
|
-
}
|
|
4740
|
+
if (!this.hasBackchannel.value) return;
|
|
4741
|
+
this.pendingMicTrack = track;
|
|
4742
|
+
if (!this.backchannelHandler) await this.startBackchannel();
|
|
4743
|
+
else if (this.backchannelHandler.isConnected) await this.backchannelHandler.setMicrophoneTrack(track);
|
|
3341
4744
|
}
|
|
3342
4745
|
setMuted(muted) {
|
|
3343
4746
|
this.muted.value = muted;
|
|
3344
4747
|
const video = this.videoElement.value;
|
|
3345
4748
|
if (video) video.muted = muted;
|
|
4749
|
+
if (this.audioElement) this.audioElement.muted = muted;
|
|
3346
4750
|
}
|
|
3347
4751
|
async play() {
|
|
3348
4752
|
this.paused.value = false;
|
|
@@ -3359,11 +4763,17 @@ var StreamConnection = class {
|
|
|
3359
4763
|
} catch {}
|
|
3360
4764
|
}
|
|
3361
4765
|
}
|
|
4766
|
+
const audio = this.audioElement;
|
|
4767
|
+
if (audio) {
|
|
4768
|
+
audio.muted = this.muted.value;
|
|
4769
|
+
audio.play().catch(() => {});
|
|
4770
|
+
}
|
|
3362
4771
|
}
|
|
3363
4772
|
pause() {
|
|
3364
4773
|
this.paused.value = true;
|
|
3365
4774
|
const video = this.videoElement.value;
|
|
3366
4775
|
if (video) video.pause();
|
|
4776
|
+
this.audioElement?.pause();
|
|
3367
4777
|
}
|
|
3368
4778
|
destroy() {
|
|
3369
4779
|
for (const stopWatcher of this.stopWatchers) stopWatcher();
|
|
@@ -3382,14 +4792,21 @@ var StreamConnection = class {
|
|
|
3382
4792
|
this.stop();
|
|
3383
4793
|
this.scope.stop();
|
|
3384
4794
|
}
|
|
3385
|
-
|
|
3386
|
-
this.
|
|
4795
|
+
restart() {
|
|
4796
|
+
return this.reconnect(1e3);
|
|
4797
|
+
}
|
|
4798
|
+
invalidateProbe() {
|
|
4799
|
+
this.probeInfo.value = void 0;
|
|
4800
|
+
this.probeInFlight = void 0;
|
|
4801
|
+
}
|
|
4802
|
+
async reconnect(delayMs, waitStatus = "reconnecting") {
|
|
4803
|
+
this.status.value = waitStatus;
|
|
3387
4804
|
this.isPlaying.value = false;
|
|
3388
4805
|
this.cleanup();
|
|
3389
4806
|
this.abortController = new AbortController();
|
|
3390
4807
|
const gen = ++this.connectionGeneration;
|
|
3391
4808
|
try {
|
|
3392
|
-
await abortableSleep(
|
|
4809
|
+
await abortableSleep(delayMs, this.abortController.signal);
|
|
3393
4810
|
} catch {
|
|
3394
4811
|
return;
|
|
3395
4812
|
}
|
|
@@ -3398,6 +4815,7 @@ var StreamConnection = class {
|
|
|
3398
4815
|
this.status.value = "idle";
|
|
3399
4816
|
return;
|
|
3400
4817
|
}
|
|
4818
|
+
this.status.value = "reconnecting";
|
|
3401
4819
|
try {
|
|
3402
4820
|
const init = this.initializeSource();
|
|
3403
4821
|
if (init === "unsupported") {
|
|
@@ -3409,13 +4827,32 @@ var StreamConnection = class {
|
|
|
3409
4827
|
this.status.value = "error";
|
|
3410
4828
|
return;
|
|
3411
4829
|
}
|
|
3412
|
-
this.
|
|
4830
|
+
this.dialWithProbe();
|
|
3413
4831
|
} catch (err) {
|
|
3414
4832
|
if (gen !== this.connectionGeneration) return;
|
|
3415
4833
|
this.error.value = err instanceof Error ? err : new Error(String(err));
|
|
3416
4834
|
this.status.value = "error";
|
|
3417
4835
|
}
|
|
3418
4836
|
}
|
|
4837
|
+
retryLater(err) {
|
|
4838
|
+
const delay = this.nextRetryDelay();
|
|
4839
|
+
if (err) this.error.value = err;
|
|
4840
|
+
this.reconnect(delay, err ? "error" : "reconnecting");
|
|
4841
|
+
}
|
|
4842
|
+
nextRetryDelay() {
|
|
4843
|
+
if (this.playingSince !== void 0 && performance.now() - this.playingSince >= RETRY_RESET_AFTER) this.failures = 0;
|
|
4844
|
+
this.playingSince = void 0;
|
|
4845
|
+
const base = Math.min(RETRY_BASE_DELAY * 2 ** this.failures, RETRY_MAX_DELAY);
|
|
4846
|
+
this.failures++;
|
|
4847
|
+
return withJitter(base);
|
|
4848
|
+
}
|
|
4849
|
+
retryAfterNoMedia() {
|
|
4850
|
+
if (this.markSourceUndecodable()) {
|
|
4851
|
+
this.restart();
|
|
4852
|
+
return;
|
|
4853
|
+
}
|
|
4854
|
+
this.retryLater(this.lastError);
|
|
4855
|
+
}
|
|
3419
4856
|
startWhenReady() {
|
|
3420
4857
|
if (this.isReady.value) {
|
|
3421
4858
|
log.debug("startWhenReady — already ready, calling start() now");
|
|
@@ -3438,6 +4875,13 @@ var StreamConnection = class {
|
|
|
3438
4875
|
this.restart();
|
|
3439
4876
|
});
|
|
3440
4877
|
this.stopWatchers.push(stopAuthWatch);
|
|
4878
|
+
const stopTokenWatch = watch(() => this.target.value?.tokens.access, (token) => {
|
|
4879
|
+
if (this.rejectedToken === void 0 || !token || token === this.rejectedToken) return;
|
|
4880
|
+
this.rejectedToken = void 0;
|
|
4881
|
+
if (this.status.value === "idle" || this.status.value === "closed") return;
|
|
4882
|
+
this.reconnect(0);
|
|
4883
|
+
});
|
|
4884
|
+
this.stopWatchers.push(stopTokenWatch);
|
|
3441
4885
|
const stopSourcesWatch = watch(() => this.camera.value?.sources.value, () => {
|
|
3442
4886
|
if (this.status.value !== "unsupported") return;
|
|
3443
4887
|
this.stop();
|
|
@@ -3481,11 +4925,11 @@ var StreamConnection = class {
|
|
|
3481
4925
|
}
|
|
3482
4926
|
}, { immediate: true });
|
|
3483
4927
|
this.stopWatchers.push(stopVideoElWatch);
|
|
3484
|
-
const
|
|
3485
|
-
this.
|
|
3486
|
-
if (
|
|
4928
|
+
const stopHealthWatch = watch(() => this.status.value === "connected" && this.isPlaying.value, (active) => {
|
|
4929
|
+
this.stopHealthCheck();
|
|
4930
|
+
if (active) this.startHealthCheck();
|
|
3487
4931
|
}, { immediate: true });
|
|
3488
|
-
this.stopWatchers.push(
|
|
4932
|
+
this.stopWatchers.push(stopHealthWatch);
|
|
3489
4933
|
const stopCameraWatch = watch([
|
|
3490
4934
|
this.camera,
|
|
3491
4935
|
this.videoElement,
|
|
@@ -3536,6 +4980,7 @@ var StreamConnection = class {
|
|
|
3536
4980
|
}
|
|
3537
4981
|
isSourcePlayable(source) {
|
|
3538
4982
|
if (this.triedSourceIds.has(source._id)) return false;
|
|
4983
|
+
if (source.videoCodec === "JPEG") return canPlayMjpeg();
|
|
3539
4984
|
if (source.videoCodec !== "H265") return true;
|
|
3540
4985
|
const mode = this.requestedMode.value;
|
|
3541
4986
|
if (mode === "auto") return canPlayH265("auto");
|
|
@@ -3598,6 +5043,7 @@ var StreamConnection = class {
|
|
|
3598
5043
|
probeSaysUnplayable(probe) {
|
|
3599
5044
|
const video = probe.video.filter((v) => v.direction === "sendonly");
|
|
3600
5045
|
if (!video.length) return false;
|
|
5046
|
+
if (video.every((v) => v.codec === "JPEG")) return !canPlayMjpeg();
|
|
3601
5047
|
if (!video.every((v) => v.codec === "H265")) return false;
|
|
3602
5048
|
const mode = this.requestedMode.value;
|
|
3603
5049
|
return !canPlayH265(mode === "auto" ? "auto" : mode === "mse" ? "mse" : "webrtc");
|
|
@@ -3612,38 +5058,55 @@ var StreamConnection = class {
|
|
|
3612
5058
|
}
|
|
3613
5059
|
markSourceUndecodable() {
|
|
3614
5060
|
const src = this.source.value;
|
|
3615
|
-
if (!src || !this.sourceLikelyH265()) return;
|
|
5061
|
+
if (!src || !this.sourceLikelyH265()) return false;
|
|
3616
5062
|
this.triedSourceIds.add(src._id);
|
|
5063
|
+
this.invalidateProbe();
|
|
5064
|
+
return true;
|
|
3617
5065
|
}
|
|
3618
5066
|
handleNoFramesWhileConnected() {
|
|
3619
|
-
if (this.sourceLikelyH265()) reportH265DecodeFailure(this.activeMode.value === "mse" ? "mse" : "webrtc");
|
|
3620
|
-
if (this.requestedMode.value === "auto" && this.activeMode.value !== "mse") {
|
|
5067
|
+
if (this.sourceLikelyH265() && !this.webrtcTookOver()) reportH265DecodeFailure(this.activeMode.value === "mse" ? "mse" : "webrtc");
|
|
5068
|
+
if (this.requestedMode.value === "auto" && this.activeMode.value !== "mse" && !this.mjpegHandler && !this.mseRejected) {
|
|
3621
5069
|
this.webrtcHandler?.close();
|
|
3622
5070
|
this.webrtcHandler = void 0;
|
|
5071
|
+
this.awaitingWebRTCFrame = false;
|
|
3623
5072
|
this.activeMode.value = "mse";
|
|
5073
|
+
this.isPlaying.value = false;
|
|
3624
5074
|
this.status.value = "reconnecting";
|
|
3625
5075
|
this.startMSE();
|
|
5076
|
+
this.resumeBackchannel();
|
|
3626
5077
|
return;
|
|
3627
5078
|
}
|
|
3628
|
-
this.
|
|
3629
|
-
|
|
5079
|
+
this.retryAfterNoMedia();
|
|
5080
|
+
}
|
|
5081
|
+
webrtcTookOver() {
|
|
5082
|
+
return this.awaitingWebRTCFrame && this.isPlaying.value;
|
|
3630
5083
|
}
|
|
3631
5084
|
noteDeliveredAudio(delivered) {
|
|
3632
5085
|
if (delivered && !this.hasAudio.value) this.hasAudio.value = true;
|
|
3633
5086
|
}
|
|
3634
|
-
|
|
5087
|
+
probeStream() {
|
|
3635
5088
|
const cam = this.camera.value;
|
|
3636
5089
|
const source = this.source.value;
|
|
3637
|
-
if (!cam || !source || this.abortController.signal.aborted) return void 0;
|
|
5090
|
+
if (!cam || !source || this.abortController.signal.aborted) return Promise.resolve(void 0);
|
|
5091
|
+
if (this.probeInFlight?.sourceId === source._id) return this.probeInFlight.result;
|
|
5092
|
+
const entry = {
|
|
5093
|
+
sourceId: source._id,
|
|
5094
|
+
result: Promise.resolve(void 0)
|
|
5095
|
+
};
|
|
5096
|
+
this.probeInFlight = entry;
|
|
5097
|
+
entry.result = this.askProbe(cam, entry);
|
|
5098
|
+
return entry.result;
|
|
5099
|
+
}
|
|
5100
|
+
async askProbe(cam, entry) {
|
|
3638
5101
|
try {
|
|
3639
|
-
const probe = await cam.probeStream(
|
|
5102
|
+
const probe = await cam.probeStream(entry.sourceId, {
|
|
3640
5103
|
video: true,
|
|
3641
5104
|
audio: ["pcma", "opus"],
|
|
3642
5105
|
microphone: true
|
|
3643
5106
|
});
|
|
3644
|
-
if (probe && !this.abortController.signal.aborted) {
|
|
5107
|
+
if (probe && this.probeInFlight === entry && !this.abortController.signal.aborted) {
|
|
3645
5108
|
this.probeInfo.value = probe;
|
|
3646
|
-
this.probedSourceId =
|
|
5109
|
+
this.probedSourceId = entry.sourceId;
|
|
3647
5110
|
this.hasBackchannel.value = probe.audio.some((a) => a.direction === "recvonly");
|
|
3648
5111
|
this.hasAudio.value = probe.audio.filter((a) => a.direction === "sendonly").length > 0;
|
|
3649
5112
|
this.hasVideo.value = probe.video.filter((v) => v.direction === "sendonly").length > 0;
|
|
@@ -3651,14 +5114,46 @@ var StreamConnection = class {
|
|
|
3651
5114
|
return probe;
|
|
3652
5115
|
} catch {
|
|
3653
5116
|
return;
|
|
5117
|
+
} finally {
|
|
5118
|
+
if (this.probeInFlight === entry) this.probeInFlight = void 0;
|
|
3654
5119
|
}
|
|
3655
5120
|
}
|
|
5121
|
+
dialWithProbe() {
|
|
5122
|
+
const probing = this.settledProbe(this.probeForSource());
|
|
5123
|
+
this.probing = probing;
|
|
5124
|
+
this.connectWebSocket();
|
|
5125
|
+
return probing;
|
|
5126
|
+
}
|
|
5127
|
+
probeForSource() {
|
|
5128
|
+
const source = this.source.value;
|
|
5129
|
+
const known = this.probeInfo.value;
|
|
5130
|
+
return known && source && this.probedSourceId === source._id ? Promise.resolve(known) : this.probeStream();
|
|
5131
|
+
}
|
|
5132
|
+
settledProbe(probe) {
|
|
5133
|
+
return new Promise((resolve) => {
|
|
5134
|
+
const timer = setTimeout(() => resolve(void 0), PROBE_WAIT_MS);
|
|
5135
|
+
const settle = (result) => {
|
|
5136
|
+
clearTimeout(timer);
|
|
5137
|
+
resolve(result);
|
|
5138
|
+
};
|
|
5139
|
+
probe.then(settle, () => settle(void 0));
|
|
5140
|
+
});
|
|
5141
|
+
}
|
|
3656
5142
|
connectWebSocket() {
|
|
3657
5143
|
this.disconnectWebSocket();
|
|
3658
5144
|
const cam = this.camera.value;
|
|
3659
5145
|
if (this.abortController.signal.aborted || !cam || !this.source.value || !this.target.value) return;
|
|
3660
5146
|
const sourceNamePart = this.source.value.name ?? this.source.value.role ?? this.source.value._id;
|
|
3661
5147
|
const sourceName = createSourceName(cam.name.value, sourceNamePart);
|
|
5148
|
+
this.connectToken = this.target.value.tokens.access;
|
|
5149
|
+
this.rejectedToken = void 0;
|
|
5150
|
+
this.lastError = void 0;
|
|
5151
|
+
this.mseRejected = false;
|
|
5152
|
+
this.mseAnswerPending = false;
|
|
5153
|
+
this.mseStopPending = false;
|
|
5154
|
+
this.offerPeer = void 0;
|
|
5155
|
+
this.answeredPeer = void 0;
|
|
5156
|
+
this.wsOpenedAt = performance.now();
|
|
3662
5157
|
this.startWsConnectTimeout();
|
|
3663
5158
|
this.wsHandle = this.wsTransport.open({
|
|
3664
5159
|
path: "/api/go2rtc",
|
|
@@ -3666,7 +5161,7 @@ var StreamConnection = class {
|
|
|
3666
5161
|
binaryType: "arraybuffer"
|
|
3667
5162
|
});
|
|
3668
5163
|
this.wsHandle.on("open", () => this.handleWsOpen());
|
|
3669
|
-
this.wsHandle.on("close", () => this.handleWsClose());
|
|
5164
|
+
this.wsHandle.on("close", (info) => this.handleWsClose(info));
|
|
3670
5165
|
this.wsHandle.on("message", (ev) => this.handleWsMessage(ev));
|
|
3671
5166
|
this.wsHandle.on("error", () => {});
|
|
3672
5167
|
}
|
|
@@ -3684,13 +5179,45 @@ var StreamConnection = class {
|
|
|
3684
5179
|
if (this.abortController.signal.aborted) return;
|
|
3685
5180
|
this.stopWsConnectTimeout();
|
|
3686
5181
|
this.status.value = "connecting";
|
|
5182
|
+
const handle = this.wsHandle;
|
|
5183
|
+
(this.probing ?? Promise.resolve(void 0)).then(() => {
|
|
5184
|
+
if (this.abortController.signal.aborted || this.wsHandle !== handle) return;
|
|
5185
|
+
this.requestStream();
|
|
5186
|
+
});
|
|
5187
|
+
}
|
|
5188
|
+
requestStream() {
|
|
5189
|
+
if (this.sourceIsMjpeg()) {
|
|
5190
|
+
this.startMjpeg();
|
|
5191
|
+
return;
|
|
5192
|
+
}
|
|
3687
5193
|
const mode = this.requestedMode.value;
|
|
3688
5194
|
if (mode === "auto") this.startAutoMode();
|
|
3689
5195
|
else if (mode === "webrtc" || mode === "webrtc/tcp") this.startWebRTC(mode);
|
|
3690
5196
|
else if (mode === "mse") this.startMSE();
|
|
3691
5197
|
}
|
|
3692
|
-
handleWsClose() {
|
|
3693
|
-
if (this.status.value
|
|
5198
|
+
handleWsClose(info) {
|
|
5199
|
+
if (this.status.value === "closed" || this.abortController.signal.aborted) return;
|
|
5200
|
+
const kind = info ? classifyClose(info) : "other";
|
|
5201
|
+
if (kind === "other") {
|
|
5202
|
+
this.retryLater();
|
|
5203
|
+
return;
|
|
5204
|
+
}
|
|
5205
|
+
const rejected = this.connectToken;
|
|
5206
|
+
log.debug(`go2rtc socket closed (${info?.code}): ${info?.reason}`);
|
|
5207
|
+
if (kind === "forbidden") {
|
|
5208
|
+
this.error.value = new Error(info?.reason || `Stream refused (${info?.code})`);
|
|
5209
|
+
this.status.value = "error";
|
|
5210
|
+
this.isPlaying.value = false;
|
|
5211
|
+
this.cleanup();
|
|
5212
|
+
this.rejectedToken = rejected;
|
|
5213
|
+
return;
|
|
5214
|
+
}
|
|
5215
|
+
if (this.target.value?.tokens.access !== rejected) {
|
|
5216
|
+
this.reconnect(0);
|
|
5217
|
+
return;
|
|
5218
|
+
}
|
|
5219
|
+
this.reconnect(withJitter(RETRY_MAX_DELAY));
|
|
5220
|
+
this.rejectedToken = rejected;
|
|
3694
5221
|
}
|
|
3695
5222
|
handleWsMessage(ev) {
|
|
3696
5223
|
if (this.abortController.signal.aborted) return;
|
|
@@ -3702,34 +5229,82 @@ var StreamConnection = class {
|
|
|
3702
5229
|
return;
|
|
3703
5230
|
}
|
|
3704
5231
|
this.handleMessage(msg);
|
|
3705
|
-
} else if (this.
|
|
3706
|
-
|
|
3707
|
-
this.handleFirstFrame();
|
|
3708
|
-
}
|
|
5232
|
+
} else if (this.mjpegHandler && isJpegFrame(ev.data)) this.mjpegHandler.appendFrame(ev.data);
|
|
5233
|
+
else (this.mseHandler ?? this.audioHandler)?.appendBuffer(ev.data);
|
|
3709
5234
|
}
|
|
3710
5235
|
handleMessage(msg) {
|
|
3711
5236
|
switch (msg.type) {
|
|
3712
5237
|
case "webrtc/answer":
|
|
5238
|
+
this.answeredPeer = this.takeOfferReply();
|
|
5239
|
+
if (this.answeredPeer) processWebRTCMessage(this.answeredPeer, msg);
|
|
5240
|
+
this.resumeBackchannel();
|
|
5241
|
+
break;
|
|
3713
5242
|
case "webrtc/candidate":
|
|
3714
|
-
if (this.
|
|
3715
|
-
else if (this.backchannelHandler) processWebRTCMessage(this.backchannelHandler, msg);
|
|
5243
|
+
if (this.answeredPeer && this.isCurrentPeer(this.answeredPeer)) processWebRTCMessage(this.answeredPeer, msg);
|
|
3716
5244
|
break;
|
|
3717
5245
|
case "mse":
|
|
3718
|
-
|
|
5246
|
+
this.mseAnswerPending = false;
|
|
5247
|
+
if (this.mseStopPending) {
|
|
5248
|
+
this.mseStopPending = false;
|
|
5249
|
+
this.sendWsMessage({ type: "mse/stop" });
|
|
5250
|
+
}
|
|
5251
|
+
if (typeof msg.value === "string") {
|
|
5252
|
+
const handler = this.mseHandler ?? this.audioHandler;
|
|
5253
|
+
if (!handler) break;
|
|
3719
5254
|
this.noteDeliveredAudio(MSE_AUDIO_CODEC.test(msg.value));
|
|
3720
|
-
|
|
5255
|
+
handler.initializeBuffer(msg.value);
|
|
3721
5256
|
}
|
|
3722
5257
|
break;
|
|
3723
|
-
case "
|
|
3724
|
-
this.
|
|
3725
|
-
|
|
5258
|
+
case "mjpeg":
|
|
5259
|
+
if (this.mjpegHandler) this.status.value = "connected";
|
|
5260
|
+
break;
|
|
5261
|
+
case "error": this.handleGo2rtcError(String(msg.value));
|
|
5262
|
+
}
|
|
5263
|
+
}
|
|
5264
|
+
handleGo2rtcError(message) {
|
|
5265
|
+
const request = message.slice(0, Math.max(0, message.indexOf(":")));
|
|
5266
|
+
const err = new Error(message);
|
|
5267
|
+
log.debug(`go2rtc error: ${message}`);
|
|
5268
|
+
if (request === "webrtc/offer") {
|
|
5269
|
+
const peer = this.takeOfferReply();
|
|
5270
|
+
if (peer && peer === this.webrtcHandler && !peer.isConnected) {
|
|
5271
|
+
this.lastError = err;
|
|
5272
|
+
this.stopConnectTimeout();
|
|
5273
|
+
if (this.requestedMode.value === "auto") this.fallBackToMSE();
|
|
5274
|
+
else this.retryLater(err);
|
|
5275
|
+
return;
|
|
5276
|
+
}
|
|
5277
|
+
if (peer && peer === this.backchannelHandler) this.closeBackchannel();
|
|
5278
|
+
this.resumeBackchannel();
|
|
5279
|
+
return;
|
|
5280
|
+
}
|
|
5281
|
+
if (request === "mjpeg") {
|
|
5282
|
+
if (this.mjpegHandler) this.retryLater(err);
|
|
5283
|
+
return;
|
|
5284
|
+
}
|
|
5285
|
+
if (request === "mse") {
|
|
5286
|
+
this.mseAnswerPending = false;
|
|
5287
|
+
this.mseStopPending = false;
|
|
5288
|
+
if (this.audioHandler) {
|
|
5289
|
+
this.closeAudio();
|
|
5290
|
+
return;
|
|
5291
|
+
}
|
|
5292
|
+
if (!this.mseHandler) return;
|
|
5293
|
+
this.lastError = err;
|
|
5294
|
+
this.mseRejected = true;
|
|
5295
|
+
if (this.requestedMode.value === "auto" && this.webrtcHandler) {
|
|
5296
|
+
this.dropMSE();
|
|
5297
|
+
return;
|
|
3726
5298
|
}
|
|
5299
|
+
this.retryLater(err);
|
|
5300
|
+
return;
|
|
3727
5301
|
}
|
|
5302
|
+
if (!request.startsWith("webrtc")) this.lastError = err;
|
|
3728
5303
|
}
|
|
3729
5304
|
async startWebRTC(mode) {
|
|
3730
5305
|
if (this.abortController.signal.aborted) return;
|
|
3731
5306
|
this.startConnectTimeout();
|
|
3732
|
-
|
|
5307
|
+
const handler = createWebRTCHandler({
|
|
3733
5308
|
mode,
|
|
3734
5309
|
signal: this.abortController.signal,
|
|
3735
5310
|
onConnected: (stream) => this.handleWebRTCConnected(stream),
|
|
@@ -3740,11 +5315,9 @@ var StreamConnection = class {
|
|
|
3740
5315
|
value: candidate
|
|
3741
5316
|
})
|
|
3742
5317
|
});
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
value: offer
|
|
3747
|
-
});
|
|
5318
|
+
this.webrtcHandler = handler;
|
|
5319
|
+
const offer = await handler.createOffer();
|
|
5320
|
+
if (offer && !this.abortController.signal.aborted && this.webrtcHandler === handler) this.sendOffer(handler, offer);
|
|
3748
5321
|
}
|
|
3749
5322
|
handleWebRTCConnected(stream) {
|
|
3750
5323
|
if (this.abortController.signal.aborted) return;
|
|
@@ -3761,13 +5334,13 @@ var StreamConnection = class {
|
|
|
3761
5334
|
video.muted = this.muted.value;
|
|
3762
5335
|
playVideo(video);
|
|
3763
5336
|
this.status.value = "connected";
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
this.
|
|
3767
|
-
this.
|
|
3768
|
-
this.
|
|
3769
|
-
|
|
3770
|
-
|
|
5337
|
+
if (stream.getVideoTracks().length > 0) {
|
|
5338
|
+
this.awaitingWebRTCFrame = true;
|
|
5339
|
+
if (this.webrtcTookOver()) this.startConnectTimeout();
|
|
5340
|
+
this.resetHealthBaseline(video);
|
|
5341
|
+
this.watchFirstFrame(video);
|
|
5342
|
+
} else this.handleFirstFrame();
|
|
5343
|
+
if (this.requestedMode.value === "auto" && this.mseHandler) this.dropMSE();
|
|
3771
5344
|
}
|
|
3772
5345
|
handleWebRTCDisconnected() {
|
|
3773
5346
|
if (this.status.value !== "closed" && !this.abortController.signal.aborted) {
|
|
@@ -3777,22 +5350,28 @@ var StreamConnection = class {
|
|
|
3777
5350
|
this.webrtcHandler = void 0;
|
|
3778
5351
|
this.activeMode.value = "mse";
|
|
3779
5352
|
this.status.value = "connected";
|
|
3780
|
-
|
|
5353
|
+
this.resumeBackchannel();
|
|
5354
|
+
} else this.retryLater();
|
|
3781
5355
|
}
|
|
3782
5356
|
}
|
|
3783
5357
|
handleWebRTCFailed() {
|
|
3784
5358
|
if (this.abortController.signal.aborted) return;
|
|
3785
5359
|
this.stopConnectTimeout();
|
|
3786
|
-
if (this.requestedMode.value === "auto")
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
this.
|
|
5360
|
+
if (this.requestedMode.value === "auto") this.fallBackToMSE();
|
|
5361
|
+
else this.retryLater(this.lastError ?? /* @__PURE__ */ new Error("WebRTC connection failed"));
|
|
5362
|
+
}
|
|
5363
|
+
fallBackToMSE() {
|
|
5364
|
+
this.webrtcHandler?.close();
|
|
5365
|
+
this.webrtcHandler = void 0;
|
|
5366
|
+
this.awaitingWebRTCFrame = false;
|
|
5367
|
+
if (this.mseRejected) {
|
|
5368
|
+
this.retryLater(this.lastError);
|
|
5369
|
+
return;
|
|
3795
5370
|
}
|
|
5371
|
+
this.activeMode.value = "mse";
|
|
5372
|
+
if (this.mseHandler?.isReady) this.handleMSEReady();
|
|
5373
|
+
else this.startMSE();
|
|
5374
|
+
this.resumeBackchannel();
|
|
3796
5375
|
}
|
|
3797
5376
|
startMSE() {
|
|
3798
5377
|
if (this.mseHandler) return;
|
|
@@ -3803,22 +5382,19 @@ var StreamConnection = class {
|
|
|
3803
5382
|
signal: this.abortController.signal,
|
|
3804
5383
|
onReady: () => this.handleMSEReady(),
|
|
3805
5384
|
onFirstData: () => this.handleMSEFirstData(),
|
|
3806
|
-
onError: (err) =>
|
|
3807
|
-
if (this.requestedMode.value !== "auto") {
|
|
3808
|
-
this.error.value = err;
|
|
3809
|
-
this.status.value = "error";
|
|
3810
|
-
} else if (this.activeMode.value === "mse" && this.status.value === "connected") {
|
|
3811
|
-
log.debug("MSE error while active in auto mode — restarting:", err);
|
|
3812
|
-
this.restart();
|
|
3813
|
-
}
|
|
3814
|
-
}
|
|
5385
|
+
onError: (err, phase) => this.handleMSEError(err, phase)
|
|
3815
5386
|
});
|
|
3816
5387
|
const codecs = this.mseHandler.setup();
|
|
3817
5388
|
if (codecs) {
|
|
5389
|
+
if (this.mseStopPending) {
|
|
5390
|
+
this.mseStopPending = false;
|
|
5391
|
+
this.sendWsMessage({ type: "mse/stop" });
|
|
5392
|
+
}
|
|
3818
5393
|
this.sendWsMessage({
|
|
3819
5394
|
type: "mse",
|
|
3820
5395
|
value: codecs
|
|
3821
5396
|
});
|
|
5397
|
+
this.mseAnswerPending = true;
|
|
3822
5398
|
this.startMseConnectTimeout();
|
|
3823
5399
|
}
|
|
3824
5400
|
}
|
|
@@ -3834,15 +5410,54 @@ var StreamConnection = class {
|
|
|
3834
5410
|
this.activeMode.value = "mse";
|
|
3835
5411
|
this.status.value = "connected";
|
|
3836
5412
|
}
|
|
5413
|
+
if (!this.webrtcHandler && !this.isPlaying.value) this.startConnectTimeout();
|
|
3837
5414
|
}
|
|
3838
5415
|
handleMSEFirstData() {
|
|
3839
5416
|
if (this.abortController.signal.aborted) return;
|
|
3840
5417
|
const video = this.videoElement.value;
|
|
3841
|
-
if (video)
|
|
5418
|
+
if (video) {
|
|
5419
|
+
this.watchFirstFrame(video);
|
|
5420
|
+
playVideo(video);
|
|
5421
|
+
}
|
|
5422
|
+
}
|
|
5423
|
+
handleMSEError(err, phase) {
|
|
5424
|
+
if (this.abortController.signal.aborted) return;
|
|
5425
|
+
log.debug(`MSE ${phase} error:`, err);
|
|
5426
|
+
if (this.requestedMode.value === "auto" && this.webrtcHandler) {
|
|
5427
|
+
const tookOver = this.isPlaying.value || this.status.value === "connected";
|
|
5428
|
+
this.dropMSE();
|
|
5429
|
+
if (tookOver) {
|
|
5430
|
+
this.isPlaying.value = false;
|
|
5431
|
+
this.status.value = "reconnecting";
|
|
5432
|
+
}
|
|
5433
|
+
return;
|
|
5434
|
+
}
|
|
5435
|
+
const video = this.videoElement.value;
|
|
5436
|
+
const beforeFirstFrame = !video || video.readyState < HAVE_CURRENT_DATA$1;
|
|
5437
|
+
if ((phase === "setup" || beforeFirstFrame) && this.sourceLikelyH265()) {
|
|
5438
|
+
reportH265DecodeFailure("mse");
|
|
5439
|
+
this.markSourceUndecodable();
|
|
5440
|
+
this.restart();
|
|
5441
|
+
return;
|
|
5442
|
+
}
|
|
5443
|
+
if (phase === "setup") {
|
|
5444
|
+
this.error.value = err;
|
|
5445
|
+
this.status.value = "error";
|
|
5446
|
+
this.cleanup();
|
|
5447
|
+
return;
|
|
5448
|
+
}
|
|
5449
|
+
this.retryLater();
|
|
5450
|
+
}
|
|
5451
|
+
dropMSE() {
|
|
5452
|
+
this.mseHandler?.close();
|
|
5453
|
+
this.mseHandler = void 0;
|
|
5454
|
+
this.stopMseConnectTimeout();
|
|
5455
|
+
if (this.mseAnswerPending) this.mseStopPending = true;
|
|
5456
|
+
else this.sendWsMessage({ type: "mse/stop" });
|
|
3842
5457
|
}
|
|
3843
|
-
|
|
5458
|
+
startAutoMode() {
|
|
3844
5459
|
if (this.abortController.signal.aborted) return;
|
|
3845
|
-
const probe = this.probeInfo.value
|
|
5460
|
+
const probe = this.probeInfo.value;
|
|
3846
5461
|
if (probe) {
|
|
3847
5462
|
if (!checkWebRTCCompatibility(probe).compatible) {
|
|
3848
5463
|
this.activeMode.value = "mse";
|
|
@@ -3850,69 +5465,171 @@ var StreamConnection = class {
|
|
|
3850
5465
|
return;
|
|
3851
5466
|
}
|
|
3852
5467
|
}
|
|
3853
|
-
this.startMSE();
|
|
3854
5468
|
this.startWebRTC("webrtc");
|
|
5469
|
+
this.startMSE();
|
|
5470
|
+
}
|
|
5471
|
+
sourceIsMjpeg() {
|
|
5472
|
+
const video = this.probeInfo.value?.video.filter((v) => v.direction === "sendonly") ?? [];
|
|
5473
|
+
if (video.length) return video.every((v) => v.codec === "JPEG");
|
|
5474
|
+
return this.source.value?.videoCodec === "JPEG";
|
|
5475
|
+
}
|
|
5476
|
+
startMjpeg() {
|
|
5477
|
+
const video = this.videoElement.value;
|
|
5478
|
+
if (this.abortController.signal.aborted || !video) return;
|
|
5479
|
+
const handler = createMJPEGHandler({ signal: this.abortController.signal });
|
|
5480
|
+
this.mjpegHandler = handler;
|
|
5481
|
+
this.activeMode.value = "mjpeg";
|
|
5482
|
+
this.lastMediaStream = handler.stream;
|
|
5483
|
+
video.srcObject = handler.stream;
|
|
5484
|
+
playVideo(video);
|
|
5485
|
+
this.sendWsMessage({ type: "mjpeg" });
|
|
5486
|
+
this.startConnectTimeout();
|
|
5487
|
+
this.watchFirstFrame(video);
|
|
5488
|
+
if (this.hasAudio.value) this.startAudio();
|
|
5489
|
+
}
|
|
5490
|
+
startAudio() {
|
|
5491
|
+
if (this.audioHandler || this.abortController.signal.aborted) return;
|
|
5492
|
+
const audio = document.createElement("audio");
|
|
5493
|
+
audio.muted = this.muted.value;
|
|
5494
|
+
const handler = createMSEHandler({
|
|
5495
|
+
videoElement: audio,
|
|
5496
|
+
signal: this.abortController.signal,
|
|
5497
|
+
onReady: () => {},
|
|
5498
|
+
onFirstData: () => playVideo(audio),
|
|
5499
|
+
onError: (err) => {
|
|
5500
|
+
log.debug("MJPEG audio error:", err);
|
|
5501
|
+
this.dropAudio();
|
|
5502
|
+
}
|
|
5503
|
+
});
|
|
5504
|
+
const codecs = handler.setup()?.split(",").filter((codec) => MSE_AUDIO_CODEC.test(codec)).join(",");
|
|
5505
|
+
if (!codecs) {
|
|
5506
|
+
handler.close();
|
|
5507
|
+
return;
|
|
5508
|
+
}
|
|
5509
|
+
this.audioElement = audio;
|
|
5510
|
+
this.audioHandler = handler;
|
|
5511
|
+
this.sendWsMessage({
|
|
5512
|
+
type: "mse",
|
|
5513
|
+
value: codecs
|
|
5514
|
+
});
|
|
5515
|
+
this.mseAnswerPending = true;
|
|
5516
|
+
}
|
|
5517
|
+
dropAudio() {
|
|
5518
|
+
if (!this.audioHandler) return;
|
|
5519
|
+
this.closeAudio();
|
|
5520
|
+
if (this.mseAnswerPending) this.mseStopPending = true;
|
|
5521
|
+
else this.sendWsMessage({ type: "mse/stop" });
|
|
5522
|
+
}
|
|
5523
|
+
closeAudio() {
|
|
5524
|
+
this.audioHandler?.close();
|
|
5525
|
+
this.audioHandler = void 0;
|
|
5526
|
+
const audio = this.audioElement;
|
|
5527
|
+
if (!audio) return;
|
|
5528
|
+
this.audioElement = void 0;
|
|
5529
|
+
audio.pause();
|
|
5530
|
+
audio.removeAttribute("src");
|
|
5531
|
+
audio.load();
|
|
3855
5532
|
}
|
|
3856
5533
|
handleFirstFrame() {
|
|
3857
|
-
if (
|
|
5534
|
+
if (this.abortController.signal.aborted) return;
|
|
5535
|
+
if (!this.webrtcHandler || this.webrtcHandler.isConnected) {
|
|
3858
5536
|
this.stopConnectTimeout();
|
|
5537
|
+
this.awaitingWebRTCFrame = false;
|
|
5538
|
+
this.resumeBackchannel();
|
|
5539
|
+
}
|
|
5540
|
+
this.error.value = void 0;
|
|
5541
|
+
if (!this.isPlaying.value) {
|
|
3859
5542
|
this.isPlaying.value = true;
|
|
5543
|
+
this.playingSince = performance.now();
|
|
3860
5544
|
}
|
|
3861
5545
|
}
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
if (
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
if (end - video.currentTime > 1) video.currentTime = end;
|
|
5546
|
+
watchFirstFrame(video) {
|
|
5547
|
+
this.cancelFirstFrameWatch?.();
|
|
5548
|
+
if (video.readyState >= HAVE_CURRENT_DATA$1) {
|
|
5549
|
+
this.handleFirstFrame();
|
|
5550
|
+
return;
|
|
3868
5551
|
}
|
|
5552
|
+
const target = video;
|
|
5553
|
+
const onFrame = () => {
|
|
5554
|
+
this.cancelFirstFrameWatch?.();
|
|
5555
|
+
this.handleFirstFrame();
|
|
5556
|
+
};
|
|
5557
|
+
const callbackId = target.requestVideoFrameCallback?.(onFrame);
|
|
5558
|
+
video.addEventListener("loadeddata", onFrame);
|
|
5559
|
+
this.cancelFirstFrameWatch = () => {
|
|
5560
|
+
this.cancelFirstFrameWatch = void 0;
|
|
5561
|
+
video.removeEventListener("loadeddata", onFrame);
|
|
5562
|
+
if (callbackId !== void 0) target.cancelVideoFrameCallback?.(callbackId);
|
|
5563
|
+
};
|
|
5564
|
+
}
|
|
5565
|
+
startHealthCheck() {
|
|
5566
|
+
const video = this.videoElement.value;
|
|
5567
|
+
if (video) this.resetHealthBaseline(video);
|
|
5568
|
+
this.healthInterval = setInterval(() => this.checkHealth(), HEALTH_CHECK_INTERVAL);
|
|
3869
5569
|
}
|
|
3870
|
-
|
|
3871
|
-
if (this.
|
|
3872
|
-
clearInterval(this.
|
|
3873
|
-
this.
|
|
5570
|
+
stopHealthCheck() {
|
|
5571
|
+
if (this.healthInterval !== void 0) {
|
|
5572
|
+
clearInterval(this.healthInterval);
|
|
5573
|
+
this.healthInterval = void 0;
|
|
3874
5574
|
}
|
|
5575
|
+
this.stalled.value = false;
|
|
3875
5576
|
}
|
|
3876
|
-
|
|
5577
|
+
resetHealthBaseline(video) {
|
|
5578
|
+
this.lastProgressAt = performance.now();
|
|
5579
|
+
this.lastMediaTime = video.currentTime;
|
|
5580
|
+
this.lastFrameCount = this.frameCount(video);
|
|
5581
|
+
this.frameCounterLive = false;
|
|
5582
|
+
this.stalled.value = false;
|
|
5583
|
+
}
|
|
5584
|
+
checkHealth() {
|
|
3877
5585
|
const video = this.videoElement.value;
|
|
3878
|
-
if (
|
|
3879
|
-
|
|
3880
|
-
|
|
5586
|
+
if (!video) return;
|
|
5587
|
+
const mse = this.activeMode.value === "mse";
|
|
5588
|
+
const frames = this.frameCount(video);
|
|
5589
|
+
if (frames !== void 0 && frames > 0) this.frameCounterLive = true;
|
|
5590
|
+
const progressed = frames !== this.lastFrameCount || mse && video.currentTime !== this.lastMediaTime;
|
|
5591
|
+
if (mse) chaseLiveEdge(video);
|
|
5592
|
+
if (this.audioElement) chaseLiveEdge(this.audioElement);
|
|
5593
|
+
this.lastMediaTime = video.currentTime;
|
|
5594
|
+
this.lastFrameCount = frames;
|
|
5595
|
+
const now = performance.now();
|
|
5596
|
+
if (progressed || !this.expectsFrames(video)) {
|
|
5597
|
+
this.lastProgressAt = now;
|
|
5598
|
+
this.stalled.value = false;
|
|
3881
5599
|
return;
|
|
3882
5600
|
}
|
|
3883
|
-
const
|
|
3884
|
-
if (
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
else
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
5601
|
+
const silentFor = now - this.lastProgressAt;
|
|
5602
|
+
if (silentFor >= STALL_RECONNECT_AFTER) {
|
|
5603
|
+
log.debug(`no frame for ${Math.round(silentFor)}ms, reconnecting`);
|
|
5604
|
+
this.retryLater();
|
|
5605
|
+
} else if (silentFor >= STALL_AFTER) this.stalled.value = true;
|
|
5606
|
+
}
|
|
5607
|
+
frameCount(video) {
|
|
5608
|
+
return this.mjpegHandler ? this.mjpegHandler.frames : decodedFrames(video);
|
|
5609
|
+
}
|
|
5610
|
+
expectsFrames(video) {
|
|
5611
|
+
if (this.paused.value || video.paused || isVideoInPictureInPicture(video) || isPageHidden()) return false;
|
|
5612
|
+
return this.activeMode.value === "mse" || this.frameCounterLive;
|
|
3895
5613
|
}
|
|
3896
5614
|
cleanup() {
|
|
3897
5615
|
this.abortController.abort();
|
|
3898
5616
|
this.stopConnectTimeout();
|
|
3899
5617
|
this.stopWsConnectTimeout();
|
|
3900
5618
|
this.stopMseConnectTimeout();
|
|
3901
|
-
this.
|
|
3902
|
-
this.
|
|
5619
|
+
this.stopHealthCheck();
|
|
5620
|
+
this.cancelFirstFrameWatch?.();
|
|
5621
|
+
this.awaitingWebRTCFrame = false;
|
|
3903
5622
|
this.lastMediaStream = null;
|
|
3904
5623
|
this.webrtcHandler?.close();
|
|
3905
5624
|
this.webrtcHandler = void 0;
|
|
3906
5625
|
this.mseHandler?.close();
|
|
3907
5626
|
this.mseHandler = void 0;
|
|
5627
|
+
this.mjpegHandler?.close();
|
|
5628
|
+
this.mjpegHandler = void 0;
|
|
5629
|
+
this.closeAudio();
|
|
3908
5630
|
this.closeBackchannel();
|
|
3909
5631
|
this.disconnectWebSocket();
|
|
3910
5632
|
const video = this.videoElement.value;
|
|
3911
|
-
if (this.firstFrameCallbackId !== void 0) {
|
|
3912
|
-
if (video && "cancelVideoFrameCallback" in video) video.cancelVideoFrameCallback(this.firstFrameCallbackId);
|
|
3913
|
-
else cancelAnimationFrame(this.firstFrameCallbackId);
|
|
3914
|
-
this.firstFrameCallbackId = void 0;
|
|
3915
|
-
}
|
|
3916
5633
|
if (video) {
|
|
3917
5634
|
video.style.display = "";
|
|
3918
5635
|
video.pause();
|
|
@@ -3923,27 +5640,52 @@ var StreamConnection = class {
|
|
|
3923
5640
|
}
|
|
3924
5641
|
async startBackchannel() {
|
|
3925
5642
|
if (this.abortController.signal.aborted || !this.wsHandle || this.wsHandle.readyState !== WebSocket.OPEN) return;
|
|
3926
|
-
this.
|
|
5643
|
+
if (this.offerPeer) return;
|
|
5644
|
+
if (this.webrtcHandler && (!this.webrtcHandler.isConnected || this.awaitingWebRTCFrame)) return;
|
|
5645
|
+
const handler = createBackchannelHandler({
|
|
5646
|
+
filterUdp: this.requestedMode.value === "webrtc/tcp",
|
|
3927
5647
|
signal: this.abortController.signal,
|
|
3928
5648
|
onCandidate: (candidate) => this.sendWsMessage({
|
|
3929
5649
|
type: "webrtc/candidate",
|
|
3930
5650
|
value: candidate
|
|
3931
5651
|
}),
|
|
3932
5652
|
onConnected: async () => {
|
|
3933
|
-
if (this.pendingMicTrack && this.backchannelHandler) await
|
|
5653
|
+
if (this.pendingMicTrack && this.backchannelHandler === handler) await handler.setMicrophoneTrack(this.pendingMicTrack);
|
|
3934
5654
|
},
|
|
3935
|
-
|
|
3936
|
-
this.backchannelHandler = void 0;
|
|
3937
|
-
}
|
|
5655
|
+
onFailed: () => this.closeBackchannel(handler)
|
|
3938
5656
|
});
|
|
3939
|
-
|
|
3940
|
-
|
|
5657
|
+
this.backchannelHandler = handler;
|
|
5658
|
+
let offer;
|
|
5659
|
+
try {
|
|
5660
|
+
offer = await handler.createOffer();
|
|
5661
|
+
} catch {
|
|
5662
|
+
this.closeBackchannel(handler);
|
|
5663
|
+
return;
|
|
5664
|
+
}
|
|
5665
|
+
if (offer && !this.abortController.signal.aborted && this.backchannelHandler === handler) this.sendOffer(handler, offer);
|
|
5666
|
+
}
|
|
5667
|
+
resumeBackchannel() {
|
|
5668
|
+
if (this.pendingMicTrack && !this.backchannelHandler) this.startBackchannel();
|
|
5669
|
+
}
|
|
5670
|
+
sendOffer(peer, sdp) {
|
|
5671
|
+
if (this.wsHandle?.readyState !== WebSocket.OPEN) return;
|
|
5672
|
+
this.offerPeer = peer;
|
|
5673
|
+
this.wsHandle.send(JSON.stringify({
|
|
3941
5674
|
type: "webrtc/offer",
|
|
3942
|
-
value:
|
|
3943
|
-
});
|
|
5675
|
+
value: sdp
|
|
5676
|
+
}));
|
|
3944
5677
|
}
|
|
3945
|
-
|
|
3946
|
-
this.
|
|
5678
|
+
takeOfferReply() {
|
|
5679
|
+
const peer = this.offerPeer;
|
|
5680
|
+
this.offerPeer = void 0;
|
|
5681
|
+
return peer && this.isCurrentPeer(peer) ? peer : void 0;
|
|
5682
|
+
}
|
|
5683
|
+
isCurrentPeer(peer) {
|
|
5684
|
+
return peer === this.webrtcHandler || peer === this.backchannelHandler;
|
|
5685
|
+
}
|
|
5686
|
+
closeBackchannel(handler = this.backchannelHandler) {
|
|
5687
|
+
handler?.close();
|
|
5688
|
+
if (handler !== this.backchannelHandler) return;
|
|
3947
5689
|
this.backchannelHandler = void 0;
|
|
3948
5690
|
this.pendingMicTrack = null;
|
|
3949
5691
|
}
|
|
@@ -3955,6 +5697,7 @@ function createStreamConnection(options) {
|
|
|
3955
5697
|
//#region src/streaming/streamManager.ts
|
|
3956
5698
|
var StreamManager = class {
|
|
3957
5699
|
streams = /* @__PURE__ */ new Map();
|
|
5700
|
+
standalone = /* @__PURE__ */ new Set();
|
|
3958
5701
|
releaseTimers = /* @__PURE__ */ new Map();
|
|
3959
5702
|
releaseDelay;
|
|
3960
5703
|
DEFAULT_RELEASE_DELAY = 2e3;
|
|
@@ -4033,6 +5776,23 @@ var StreamManager = class {
|
|
|
4033
5776
|
this.releaseTimers.set(cameraName, timer);
|
|
4034
5777
|
}
|
|
4035
5778
|
}
|
|
5779
|
+
track(stream) {
|
|
5780
|
+
this.standalone.add(stream);
|
|
5781
|
+
return () => {
|
|
5782
|
+
this.standalone.delete(stream);
|
|
5783
|
+
};
|
|
5784
|
+
}
|
|
5785
|
+
restartConnected() {
|
|
5786
|
+
const streams = new Set(this.standalone);
|
|
5787
|
+
for (const entry of this.streams.values()) streams.add(entry.stream);
|
|
5788
|
+
let restarted = 0;
|
|
5789
|
+
for (const stream of streams) {
|
|
5790
|
+
if (stream.status.value !== "connected" || stream.paused.value) continue;
|
|
5791
|
+
stream.restart().catch(() => {});
|
|
5792
|
+
restarted++;
|
|
5793
|
+
}
|
|
5794
|
+
return restarted;
|
|
5795
|
+
}
|
|
4036
5796
|
forceRelease(cameraName) {
|
|
4037
5797
|
this.cancelRelease(cameraName);
|
|
4038
5798
|
this.doRelease(cameraName);
|
|
@@ -4092,6 +5852,9 @@ function destroyStream(stream) {
|
|
|
4092
5852
|
else s.stop();
|
|
4093
5853
|
}
|
|
4094
5854
|
var streamManager = new StreamManager();
|
|
5855
|
+
function restartConnectedStreams() {
|
|
5856
|
+
return streamManager.restartConnected();
|
|
5857
|
+
}
|
|
4095
5858
|
//#endregion
|
|
4096
5859
|
//#region src/composables/useFullscreen.ts
|
|
4097
5860
|
var fsRegistry = /* @__PURE__ */ new WeakMap();
|
|
@@ -4220,6 +5983,9 @@ function applyWrapperStyles(wrapper, mode) {
|
|
|
4220
5983
|
//#region src/composables/useCameraStream.ts
|
|
4221
5984
|
var AUTO_STAGGER_STEP_MS = 75;
|
|
4222
5985
|
var AUTO_STAGGER_RESET_MS = 500;
|
|
5986
|
+
var OFFSCREEN_STOP_DELAY_MS = 1e4;
|
|
5987
|
+
var HAVE_CURRENT_DATA = 2;
|
|
5988
|
+
var EMPTY_POSTER = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
|
|
4223
5989
|
var autoStaggerIndex = 0;
|
|
4224
5990
|
var autoStaggerLastTouch = 0;
|
|
4225
5991
|
function acquireAutoStaggerDelay() {
|
|
@@ -4269,7 +6035,13 @@ function useCameraStream(options) {
|
|
|
4269
6035
|
const nativeWidth = ref(0);
|
|
4270
6036
|
const nativeHeight = ref(0);
|
|
4271
6037
|
const isPip = ref(false);
|
|
6038
|
+
const keepAwake = ref(false);
|
|
6039
|
+
const offscreen = ref(false);
|
|
4272
6040
|
const autoStartReady = ref(startDelay <= 0);
|
|
6041
|
+
let offscreenTimer;
|
|
6042
|
+
let resumeTimer;
|
|
6043
|
+
let stoppedOffscreen = false;
|
|
6044
|
+
let seenInViewport = false;
|
|
4273
6045
|
if (startDelay > 0) startDelayTimer = setTimeout(() => {
|
|
4274
6046
|
autoStartReady.value = true;
|
|
4275
6047
|
}, startDelay);
|
|
@@ -4279,6 +6051,7 @@ function useCameraStream(options) {
|
|
|
4279
6051
|
const isCameraDisabled = computed(() => cameraDeviceRef.value?.disabled.value === true);
|
|
4280
6052
|
const status = computed(() => currentStream.value?.status.value ?? "idle");
|
|
4281
6053
|
const isPlaying = computed(() => currentStream.value?.isPlaying.value ?? false);
|
|
6054
|
+
const stalled = computed(() => currentStream.value?.stalled?.value ?? false);
|
|
4282
6055
|
const activeMode = computed(() => currentStream.value?.activeMode.value ?? "webrtc");
|
|
4283
6056
|
const activeResolution = computed(() => currentStream.value?.activeResolution.value ?? "low-resolution");
|
|
4284
6057
|
const degradedFrom = computed(() => currentStream.value?.degradedFrom.value ?? null);
|
|
@@ -4299,7 +6072,7 @@ function useCameraStream(options) {
|
|
|
4299
6072
|
initialMode: toValue(options.activityMode) ?? "always-on",
|
|
4300
6073
|
config: activityConfig,
|
|
4301
6074
|
onStreamStart: () => {
|
|
4302
|
-
if (!isCameraDisabled.value) currentStream.value?.start();
|
|
6075
|
+
if (!isCameraDisabled.value && !deferUntilSeen()) currentStream.value?.start();
|
|
4303
6076
|
},
|
|
4304
6077
|
onStreamStop: () => {
|
|
4305
6078
|
if (isUsingCachedStream.value) {
|
|
@@ -4330,7 +6103,7 @@ function useCameraStream(options) {
|
|
|
4330
6103
|
video.muted = true;
|
|
4331
6104
|
video.disablePictureInPicture = false;
|
|
4332
6105
|
video.preload = "auto";
|
|
4333
|
-
video.poster =
|
|
6106
|
+
video.poster = EMPTY_POSTER;
|
|
4334
6107
|
video.style.position = "absolute";
|
|
4335
6108
|
video.style.width = "100%";
|
|
4336
6109
|
video.style.height = "100%";
|
|
@@ -4360,6 +6133,74 @@ function useCameraStream(options) {
|
|
|
4360
6133
|
function onLeavePip() {
|
|
4361
6134
|
isPip.value = false;
|
|
4362
6135
|
}
|
|
6136
|
+
function onPresentationModeChange() {
|
|
6137
|
+
isPip.value = isVideoInPictureInPicture(videoElement.value);
|
|
6138
|
+
}
|
|
6139
|
+
function isRunning() {
|
|
6140
|
+
return status.value !== "idle" && status.value !== "closed" && status.value !== "unsupported";
|
|
6141
|
+
}
|
|
6142
|
+
function capturePoster() {
|
|
6143
|
+
const video = videoElement.value;
|
|
6144
|
+
if (!video || video.readyState < HAVE_CURRENT_DATA || !video.videoWidth || !video.videoHeight) return;
|
|
6145
|
+
const tileWidth = Math.round((containerElement.value?.clientWidth || video.videoWidth) * (globalThis.devicePixelRatio || 1));
|
|
6146
|
+
const canvas = document.createElement("canvas");
|
|
6147
|
+
canvas.width = Math.min(video.videoWidth, tileWidth);
|
|
6148
|
+
canvas.height = Math.round(video.videoHeight * canvas.width / video.videoWidth);
|
|
6149
|
+
try {
|
|
6150
|
+
canvas.getContext("2d")?.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
6151
|
+
video.poster = canvas.toDataURL("image/jpeg", .8);
|
|
6152
|
+
} catch {}
|
|
6153
|
+
}
|
|
6154
|
+
function clearOffscreenTimer() {
|
|
6155
|
+
clearTimeout(offscreenTimer);
|
|
6156
|
+
offscreenTimer = void 0;
|
|
6157
|
+
}
|
|
6158
|
+
function clearResumeTimer() {
|
|
6159
|
+
clearTimeout(resumeTimer);
|
|
6160
|
+
resumeTimer = void 0;
|
|
6161
|
+
}
|
|
6162
|
+
function syncOffscreen() {
|
|
6163
|
+
if (!offscreen.value) {
|
|
6164
|
+
clearOffscreenTimer();
|
|
6165
|
+
if (stoppedOffscreen) scheduleResume();
|
|
6166
|
+
return;
|
|
6167
|
+
}
|
|
6168
|
+
clearResumeTimer();
|
|
6169
|
+
if (isPip.value || keepAwake.value || paused.value || !isRunning()) {
|
|
6170
|
+
clearOffscreenTimer();
|
|
6171
|
+
return;
|
|
6172
|
+
}
|
|
6173
|
+
offscreenTimer ??= setTimeout(stopOffscreen, seenInViewport ? OFFSCREEN_STOP_DELAY_MS : 0);
|
|
6174
|
+
}
|
|
6175
|
+
function deferUntilSeen() {
|
|
6176
|
+
if (!offscreen.value || seenInViewport) return false;
|
|
6177
|
+
stoppedOffscreen = true;
|
|
6178
|
+
return true;
|
|
6179
|
+
}
|
|
6180
|
+
function stopOffscreen() {
|
|
6181
|
+
offscreenTimer = void 0;
|
|
6182
|
+
if (!offscreen.value || isPip.value || keepAwake.value || paused.value || activityModeManager.inStandby.value || !isRunning()) return;
|
|
6183
|
+
if (isVideoInPictureInPicture(videoElement.value)) {
|
|
6184
|
+
isPip.value = true;
|
|
6185
|
+
return;
|
|
6186
|
+
}
|
|
6187
|
+
if (registeredCamName && streamManager.getRefCount(registeredCamName) > 1) {
|
|
6188
|
+
offscreenTimer = setTimeout(stopOffscreen, OFFSCREEN_STOP_DELAY_MS);
|
|
6189
|
+
return;
|
|
6190
|
+
}
|
|
6191
|
+
capturePoster();
|
|
6192
|
+
currentStream.value?.stop();
|
|
6193
|
+
stoppedOffscreen = true;
|
|
6194
|
+
}
|
|
6195
|
+
function scheduleResume() {
|
|
6196
|
+
resumeTimer ??= setTimeout(() => {
|
|
6197
|
+
resumeTimer = void 0;
|
|
6198
|
+
if (offscreen.value || !stoppedOffscreen) return;
|
|
6199
|
+
stoppedOffscreen = false;
|
|
6200
|
+
if (isCameraDisabled.value || !shouldAutoStart() || activityModeManager.inStandby.value) return;
|
|
6201
|
+
if (status.value === "idle" || status.value === "closed") currentStream.value?.start();
|
|
6202
|
+
}, acquireAutoStaggerDelay());
|
|
6203
|
+
}
|
|
4363
6204
|
function initializeIsolated() {
|
|
4364
6205
|
if (initialized.value) return;
|
|
4365
6206
|
const container = containerElement.value;
|
|
@@ -4368,10 +6209,11 @@ function useCameraStream(options) {
|
|
|
4368
6209
|
initialized.value = true;
|
|
4369
6210
|
isUsingCachedStream.value = false;
|
|
4370
6211
|
currentStream.value = ownedStream;
|
|
6212
|
+
cleanupFns.push(streamManager.track(ownedStream));
|
|
4371
6213
|
const video = createVideoElement();
|
|
4372
6214
|
videoElement.value = video;
|
|
4373
6215
|
streamVideoElementRef.value = video;
|
|
4374
|
-
if (shouldAutoStart() && autoStartReady.value && activityModeManager.mode.value === "always-on" && !isCameraDisabled.value) ownedStream.start();
|
|
6216
|
+
if (shouldAutoStart() && autoStartReady.value && activityModeManager.mode.value === "always-on" && !isCameraDisabled.value && !deferUntilSeen()) ownedStream.start();
|
|
4375
6217
|
}
|
|
4376
6218
|
function initialize() {
|
|
4377
6219
|
if (initialized.value) return;
|
|
@@ -4422,7 +6264,7 @@ function useCameraStream(options) {
|
|
|
4422
6264
|
videoElement.value = video;
|
|
4423
6265
|
cached.videoElementRef.value = video;
|
|
4424
6266
|
setupCachedStreamWatchers(cached.stream, video, camName);
|
|
4425
|
-
if (shouldAutoStart() && autoStartReady.value && activityModeManager.mode.value === "always-on" && !isCameraDisabled.value) {
|
|
6267
|
+
if (shouldAutoStart() && autoStartReady.value && activityModeManager.mode.value === "always-on" && !isCameraDisabled.value && !deferUntilSeen()) {
|
|
4426
6268
|
if (cached.stream.activeMode.value !== "mse") attachCachedStream(cached, video, camName);
|
|
4427
6269
|
else video.play().catch(() => {});
|
|
4428
6270
|
}
|
|
@@ -4435,7 +6277,7 @@ function useCameraStream(options) {
|
|
|
4435
6277
|
videoElement.value = video;
|
|
4436
6278
|
streamVideoElementRef.value = video;
|
|
4437
6279
|
streamManager.register(camName, ownedStream, streamVideoElementRef, cameraDeviceRef, containerElement, video);
|
|
4438
|
-
if (shouldAutoStart() && autoStartReady.value && activityModeManager.mode.value === "always-on" && !isCameraDisabled.value) ownedStream.start();
|
|
6280
|
+
if (shouldAutoStart() && autoStartReady.value && activityModeManager.mode.value === "always-on" && !isCameraDisabled.value && !deferUntilSeen()) ownedStream.start();
|
|
4439
6281
|
const stopPlayingWatch = watch(() => ownedStream.isPlaying.value, (playing) => {
|
|
4440
6282
|
if (playing && video.srcObject instanceof MediaStream) streamManager.updateMediaStream(camName, video.srcObject);
|
|
4441
6283
|
}, { immediate: true });
|
|
@@ -4539,9 +6381,20 @@ function useCameraStream(options) {
|
|
|
4539
6381
|
function setActivityMode(mode) {
|
|
4540
6382
|
activityModeManager.setMode(mode);
|
|
4541
6383
|
}
|
|
6384
|
+
function syncActivityMode() {
|
|
6385
|
+
const mode = toValue(options.activityMode);
|
|
6386
|
+
if (mode && mode !== activityModeManager.mode.value) activityModeManager.setMode(mode);
|
|
6387
|
+
}
|
|
4542
6388
|
function reportActivity(detected) {
|
|
4543
6389
|
activityModeManager.reportActivity(detected);
|
|
4544
6390
|
}
|
|
6391
|
+
function resetIdleTimer() {
|
|
6392
|
+
activityModeManager.resetIdleTimer();
|
|
6393
|
+
}
|
|
6394
|
+
function setKeepAwake(awake) {
|
|
6395
|
+
keepAwake.value = awake;
|
|
6396
|
+
activityModeManager.setKeepAwake(awake);
|
|
6397
|
+
}
|
|
4545
6398
|
function setMuted(newMuted) {
|
|
4546
6399
|
currentStream.value?.setMuted(newMuted);
|
|
4547
6400
|
const video = videoElement.value;
|
|
@@ -4573,6 +6426,8 @@ function useCameraStream(options) {
|
|
|
4573
6426
|
clearTimeout(startDelayTimer);
|
|
4574
6427
|
startDelayTimer = void 0;
|
|
4575
6428
|
}
|
|
6429
|
+
clearOffscreenTimer();
|
|
6430
|
+
clearResumeTimer();
|
|
4576
6431
|
currentStream.value?.setMuted(true);
|
|
4577
6432
|
const video = videoElement.value;
|
|
4578
6433
|
if (video) video.muted = true;
|
|
@@ -4603,19 +6458,37 @@ function useCameraStream(options) {
|
|
|
4603
6458
|
else for (const [name, active] of Object.entries(cls)) canvas.classList.toggle(name, !!active);
|
|
4604
6459
|
}
|
|
4605
6460
|
}
|
|
4606
|
-
if (
|
|
6461
|
+
if (typeof IntersectionObserver !== "undefined") {
|
|
4607
6462
|
let visibilityObserver;
|
|
4608
6463
|
const stopVisibilityWatch = watch(containerElement, (container) => {
|
|
4609
6464
|
visibilityObserver?.disconnect();
|
|
4610
6465
|
visibilityObserver = void 0;
|
|
4611
6466
|
if (!container) return;
|
|
6467
|
+
const tileHeight = Math.round(container.getBoundingClientRect().height);
|
|
6468
|
+
const margin = tileHeight > 0 ? `${tileHeight}px 0px` : "25% 0px";
|
|
4612
6469
|
visibilityObserver = new IntersectionObserver((entries) => {
|
|
4613
|
-
|
|
6470
|
+
const entry = entries[entries.length - 1];
|
|
6471
|
+
if (!entry) return;
|
|
6472
|
+
offscreen.value = !entry.isIntersecting;
|
|
6473
|
+
if (!entry.isIntersecting) return;
|
|
6474
|
+
seenInViewport = true;
|
|
6475
|
+
reclaimSharedVideo();
|
|
6476
|
+
}, {
|
|
6477
|
+
rootMargin: margin,
|
|
6478
|
+
scrollMargin: margin
|
|
4614
6479
|
});
|
|
4615
6480
|
visibilityObserver.observe(container);
|
|
4616
6481
|
}, { immediate: true });
|
|
6482
|
+
const stopOffscreenWatch = watch([
|
|
6483
|
+
offscreen,
|
|
6484
|
+
status,
|
|
6485
|
+
isPip,
|
|
6486
|
+
keepAwake,
|
|
6487
|
+
paused
|
|
6488
|
+
], syncOffscreen);
|
|
4617
6489
|
cleanupFns.push(() => {
|
|
4618
6490
|
stopVisibilityWatch();
|
|
6491
|
+
stopOffscreenWatch();
|
|
4619
6492
|
visibilityObserver?.disconnect();
|
|
4620
6493
|
});
|
|
4621
6494
|
}
|
|
@@ -4676,19 +6549,19 @@ function useCameraStream(options) {
|
|
|
4676
6549
|
if (oldVideo) {
|
|
4677
6550
|
oldVideo.removeEventListener("enterpictureinpicture", onEnterPip);
|
|
4678
6551
|
oldVideo.removeEventListener("leavepictureinpicture", onLeavePip);
|
|
6552
|
+
oldVideo.removeEventListener("webkitpresentationmodechanged", onPresentationModeChange);
|
|
4679
6553
|
}
|
|
4680
6554
|
if (video) {
|
|
4681
6555
|
video.addEventListener("enterpictureinpicture", onEnterPip);
|
|
4682
6556
|
video.addEventListener("leavepictureinpicture", onLeavePip);
|
|
6557
|
+
video.addEventListener("webkitpresentationmodechanged", onPresentationModeChange);
|
|
4683
6558
|
}
|
|
4684
6559
|
}, { immediate: true });
|
|
4685
6560
|
cleanupFns.push(stopPipWatch);
|
|
4686
6561
|
watch([containerElement, videoElement], ([container, video]) => {
|
|
4687
6562
|
if (container && video) insertVideoIntoContainer(video, container);
|
|
4688
6563
|
}, { immediate: true });
|
|
4689
|
-
watch(() => toValue(options.activityMode),
|
|
4690
|
-
if (newMode && newMode !== activityModeManager.mode.value) activityModeManager.setMode(newMode);
|
|
4691
|
-
});
|
|
6564
|
+
watch(() => toValue(options.activityMode), syncActivityMode);
|
|
4692
6565
|
watch([
|
|
4693
6566
|
containerElement,
|
|
4694
6567
|
resolvedCameraDevice,
|
|
@@ -4696,18 +6569,32 @@ function useCameraStream(options) {
|
|
|
4696
6569
|
autoStartReady
|
|
4697
6570
|
], () => {
|
|
4698
6571
|
if (!initialized.value) initialize();
|
|
4699
|
-
else if (autoStartReady.value && shouldAutoStart() && activityModeManager.mode.value === "always-on" && !isPlaying.value && status.value === "idle" && !isCameraDisabled.value) currentStream.value?.start();
|
|
6572
|
+
else if (autoStartReady.value && shouldAutoStart() && activityModeManager.mode.value === "always-on" && !isPlaying.value && status.value === "idle" && !isCameraDisabled.value && !deferUntilSeen()) currentStream.value?.start();
|
|
6573
|
+
}, { immediate: true });
|
|
6574
|
+
let activityEngaged = false;
|
|
6575
|
+
watch(() => initialized.value && autoStartReady.value && shouldAutoStart() && !isCameraDisabled.value, (ready) => {
|
|
6576
|
+
if (!ready || activityEngaged) return;
|
|
6577
|
+
activityEngaged = true;
|
|
6578
|
+
syncActivityMode();
|
|
6579
|
+
activityModeManager.engage();
|
|
4700
6580
|
}, { immediate: true });
|
|
6581
|
+
watch(isPlaying, (playing) => {
|
|
6582
|
+
if (!playing) return;
|
|
6583
|
+
activityModeManager.resetIdleTimer();
|
|
6584
|
+
const video = videoElement.value;
|
|
6585
|
+
if (video && video.poster !== EMPTY_POSTER) video.poster = EMPTY_POSTER;
|
|
6586
|
+
});
|
|
4701
6587
|
watch(isCameraDisabled, (disabled, wasDisabled) => {
|
|
4702
6588
|
if (!initialized.value) return;
|
|
4703
6589
|
if (!wasDisabled && disabled) currentStream.value?.stop();
|
|
4704
|
-
else if (wasDisabled && !disabled && shouldAutoStart()) currentStream.value?.restart();
|
|
6590
|
+
else if (wasDisabled && !disabled && shouldAutoStart() && !activityModeManager.inStandby.value) currentStream.value?.restart();
|
|
4705
6591
|
});
|
|
4706
6592
|
onBeforeUnmount(cleanup);
|
|
4707
6593
|
tryOnScopeDispose(cleanup);
|
|
4708
6594
|
return {
|
|
4709
6595
|
status,
|
|
4710
6596
|
isPlaying,
|
|
6597
|
+
stalled,
|
|
4711
6598
|
activeMode,
|
|
4712
6599
|
activeResolution,
|
|
4713
6600
|
degradedFrom,
|
|
@@ -4743,6 +6630,8 @@ function useCameraStream(options) {
|
|
|
4743
6630
|
setMicrophone,
|
|
4744
6631
|
setActivityMode,
|
|
4745
6632
|
reportActivity,
|
|
6633
|
+
resetIdleTimer,
|
|
6634
|
+
setKeepAwake,
|
|
4746
6635
|
setMuted,
|
|
4747
6636
|
play: playStream,
|
|
4748
6637
|
pause: pauseStream,
|
|
@@ -4762,73 +6651,9 @@ function useCoreManager() {
|
|
|
4762
6651
|
getFFmpegPath: () => rpcCall(ctx.rpc, (rpc) => proxy(rpc).getFFmpegPath()),
|
|
4763
6652
|
getServerAddresses: () => rpcCall(ctx.rpc, (rpc) => proxy(rpc).getServerAddresses()),
|
|
4764
6653
|
getPluginsByInterface: (interfaceName) => rpcCall(ctx.rpc, (rpc) => proxy(rpc).getPluginsByInterface(interfaceName)),
|
|
4765
|
-
getCloudServerId: () => rpcCall(ctx.rpc, (rpc) => proxy(rpc).getCloudServerId())
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
//#endregion
|
|
4769
|
-
//#region src/composables/useOAuth.ts
|
|
4770
|
-
var POLL_ACTIVE_MS = 1500;
|
|
4771
|
-
var POLL_IDLE_MS = 3e4;
|
|
4772
|
-
var oauthStates = /* @__PURE__ */ new Map();
|
|
4773
|
-
function clearOAuthCache() {
|
|
4774
|
-
oauthStates.clear();
|
|
4775
|
-
}
|
|
4776
|
-
function createOAuthState(pluginName) {
|
|
4777
|
-
const ctx = useCameraUi();
|
|
4778
|
-
const state = ref({ status: "disconnected" });
|
|
4779
|
-
const metadata = ref(null);
|
|
4780
|
-
let timer = null;
|
|
4781
|
-
const call = (fn) => rpcCall(ctx.rpc, async (rpc) => {
|
|
4782
|
-
const id = await resolvePluginId(ctx.rpc, pluginName);
|
|
4783
|
-
if (!id) throw new Error(`Plugin "${pluginName}" not found`);
|
|
4784
|
-
return fn(rpc.createProxy(NamespaceManager.pluginNamespaces(id).pluginChildRpc));
|
|
4785
|
-
});
|
|
4786
|
-
async function refresh() {
|
|
4787
|
-
state.value = await call((proxy) => proxy.getOAuthState());
|
|
4788
|
-
if (!metadata.value) metadata.value = await call((proxy) => proxy.getOAuthMetadata());
|
|
4789
|
-
}
|
|
4790
|
-
function startPolling(intervalMs) {
|
|
4791
|
-
if (timer) clearInterval(timer);
|
|
4792
|
-
timer = setInterval(() => {
|
|
4793
|
-
refresh().catch(() => void 0);
|
|
4794
|
-
}, intervalMs);
|
|
4795
|
-
}
|
|
4796
|
-
watch(() => state.value.status, (status) => {
|
|
4797
|
-
startPolling(status === "awaiting_user" || status === "polling" ? POLL_ACTIVE_MS : POLL_IDLE_MS);
|
|
4798
|
-
}, { immediate: true });
|
|
4799
|
-
refresh().catch(() => void 0);
|
|
4800
|
-
return {
|
|
4801
|
-
state,
|
|
4802
|
-
metadata,
|
|
4803
|
-
call,
|
|
4804
|
-
refresh
|
|
4805
|
-
};
|
|
4806
|
-
}
|
|
4807
|
-
function useOAuth(pluginName) {
|
|
4808
|
-
if (!oauthStates.has(pluginName)) oauthStates.set(pluginName, createOAuthState(pluginName));
|
|
4809
|
-
const { state, metadata, call, refresh } = oauthStates.get(pluginName);
|
|
4810
|
-
async function startDeviceFlow(scope) {
|
|
4811
|
-
state.value = await call((proxy) => proxy.startDeviceFlow(scope));
|
|
4812
|
-
}
|
|
4813
|
-
async function startAuthCodeFlow(scope) {
|
|
4814
|
-
state.value = await call((proxy) => proxy.startAuthCodeFlow(scope));
|
|
4815
|
-
}
|
|
4816
|
-
async function cancel() {
|
|
4817
|
-
if (state.value.status === "awaiting_user" || state.value.status === "polling") await call((proxy) => proxy.cancelDeviceFlow?.() ?? proxy.cancelAuthCodeFlow?.() ?? Promise.resolve());
|
|
4818
|
-
await refresh();
|
|
4819
|
-
}
|
|
4820
|
-
async function disconnect() {
|
|
4821
|
-
await call((proxy) => proxy.disconnect());
|
|
4822
|
-
await refresh();
|
|
4823
|
-
}
|
|
4824
|
-
return {
|
|
4825
|
-
state,
|
|
4826
|
-
metadata,
|
|
4827
|
-
startDeviceFlow,
|
|
4828
|
-
startAuthCodeFlow,
|
|
4829
|
-
cancel,
|
|
4830
|
-
disconnect,
|
|
4831
|
-
refresh
|
|
6654
|
+
getCloudServerId: () => rpcCall(ctx.rpc, (rpc) => proxy(rpc).getCloudServerId()),
|
|
6655
|
+
getRestoreGeneration: () => rpcCall(ctx.rpc, (rpc) => proxy(rpc).getRestoreGeneration()),
|
|
6656
|
+
getInstanceId: () => rpcCall(ctx.rpc, (rpc) => proxy(rpc).getInstanceId())
|
|
4832
6657
|
};
|
|
4833
6658
|
}
|
|
4834
6659
|
//#endregion
|
|
@@ -4848,6 +6673,10 @@ function useTerminal() {
|
|
|
4848
6673
|
let streamAbortController;
|
|
4849
6674
|
let currentOptions;
|
|
4850
6675
|
let streamGen = 0;
|
|
6676
|
+
let stream;
|
|
6677
|
+
let streamClient;
|
|
6678
|
+
let wentOffline = false;
|
|
6679
|
+
let keptAcrossBlip = false;
|
|
4851
6680
|
const proxy = (r) => r.createProxy(namespaces.terminalManagerRpc);
|
|
4852
6681
|
async function connect(options) {
|
|
4853
6682
|
if (isConnecting.value || isConnected.value) return;
|
|
@@ -4861,11 +6690,13 @@ function useTerminal() {
|
|
|
4861
6690
|
cwd: options?.cwd,
|
|
4862
6691
|
shell: options?.shell,
|
|
4863
6692
|
env: options?.env
|
|
4864
|
-
}));
|
|
6693
|
+
}), { maxRetries: 0 });
|
|
4865
6694
|
sessionId.value = session.sessionId;
|
|
4866
6695
|
dimensions.value = session.dimensions;
|
|
4867
6696
|
isConnected.value = true;
|
|
4868
6697
|
streamAbortController = new AbortController();
|
|
6698
|
+
wentOffline = false;
|
|
6699
|
+
keptAcrossBlip = false;
|
|
4869
6700
|
const gen = ++streamGen;
|
|
4870
6701
|
consumeOutputStream(session.sessionId, options, gen);
|
|
4871
6702
|
} catch (err) {
|
|
@@ -4879,18 +6710,26 @@ function useTerminal() {
|
|
|
4879
6710
|
try {
|
|
4880
6711
|
const current = rpc.value;
|
|
4881
6712
|
if (!current) return;
|
|
4882
|
-
|
|
6713
|
+
const output = proxy(current).generateOutput(sid);
|
|
6714
|
+
stream = output;
|
|
6715
|
+
streamClient = current;
|
|
6716
|
+
for await (const chunk of output) {
|
|
4883
6717
|
if (gen !== streamGen || streamAbortController?.signal.aborted) break;
|
|
4884
6718
|
options?.onData?.(chunk);
|
|
4885
6719
|
}
|
|
4886
6720
|
} catch (err) {
|
|
4887
6721
|
if (gen === streamGen && !streamAbortController?.signal.aborted) {
|
|
6722
|
+
if (keptAcrossBlip) {
|
|
6723
|
+
restart();
|
|
6724
|
+
return;
|
|
6725
|
+
}
|
|
4888
6726
|
error.value = err instanceof Error ? err : new Error(String(err));
|
|
4889
6727
|
options?.onError?.(error.value);
|
|
4890
6728
|
}
|
|
4891
6729
|
} finally {
|
|
4892
6730
|
if (gen === streamGen && !streamAbortController?.signal.aborted) {
|
|
4893
6731
|
isConnected.value = false;
|
|
6732
|
+
closeRemote(sid);
|
|
4894
6733
|
options?.onClose?.();
|
|
4895
6734
|
}
|
|
4896
6735
|
}
|
|
@@ -4915,32 +6754,53 @@ function useTerminal() {
|
|
|
4915
6754
|
}
|
|
4916
6755
|
}
|
|
4917
6756
|
async function close() {
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
if (sessionId.value) try {
|
|
4921
|
-
await rpcCall(rpc, (r) => proxy(r).closeSession(sessionId.value), {
|
|
4922
|
-
awaitConnect: false,
|
|
4923
|
-
maxRetries: 0
|
|
4924
|
-
});
|
|
4925
|
-
} catch {}
|
|
6757
|
+
stopStream();
|
|
6758
|
+
if (sessionId.value) await closeRemote(sessionId.value);
|
|
4926
6759
|
sessionId.value = void 0;
|
|
4927
6760
|
isConnected.value = false;
|
|
4928
6761
|
currentOptions = void 0;
|
|
4929
6762
|
}
|
|
4930
|
-
function
|
|
4931
|
-
if (!sessionId.value && !isConnected.value) return;
|
|
4932
|
-
const savedOptions = currentOptions;
|
|
6763
|
+
function stopStream() {
|
|
4933
6764
|
streamAbortController?.abort();
|
|
4934
6765
|
streamGen++;
|
|
6766
|
+
stream?.return(void 0).catch(() => void 0);
|
|
6767
|
+
stream = void 0;
|
|
6768
|
+
streamClient = void 0;
|
|
6769
|
+
keptAcrossBlip = false;
|
|
6770
|
+
}
|
|
6771
|
+
function closeRemote(sid) {
|
|
6772
|
+
return rpcCall(rpc, (r) => proxy(r).closeSession(sid), {
|
|
6773
|
+
awaitConnect: false,
|
|
6774
|
+
maxRetries: 0
|
|
6775
|
+
}).catch(() => void 0);
|
|
6776
|
+
}
|
|
6777
|
+
function restart() {
|
|
6778
|
+
const savedOptions = currentOptions;
|
|
6779
|
+
const sid = sessionId.value;
|
|
6780
|
+
stopStream();
|
|
6781
|
+
if (sid) closeRemote(sid);
|
|
4935
6782
|
sessionId.value = void 0;
|
|
4936
6783
|
isConnected.value = false;
|
|
4937
6784
|
currentOptions = void 0;
|
|
4938
6785
|
savedOptions?.onClose?.();
|
|
4939
6786
|
if (savedOptions) connect(savedOptions);
|
|
4940
6787
|
}
|
|
6788
|
+
function handleReconnected() {
|
|
6789
|
+
if (!sessionId.value && !isConnected.value) return;
|
|
6790
|
+
if (isConnected.value && !wentOffline && rpc.value === streamClient) {
|
|
6791
|
+
keptAcrossBlip = true;
|
|
6792
|
+
return;
|
|
6793
|
+
}
|
|
6794
|
+
restart();
|
|
6795
|
+
}
|
|
6796
|
+
function handleDisconnected() {
|
|
6797
|
+
wentOffline = true;
|
|
6798
|
+
}
|
|
4941
6799
|
cameraUi.on("reconnected", handleReconnected);
|
|
6800
|
+
cameraUi.on("disconnected", handleDisconnected);
|
|
4942
6801
|
tryOnScopeDispose(() => {
|
|
4943
6802
|
cameraUi.off("reconnected", handleReconnected);
|
|
6803
|
+
cameraUi.off("disconnected", handleDisconnected);
|
|
4944
6804
|
close();
|
|
4945
6805
|
});
|
|
4946
6806
|
return {
|
|
@@ -4957,4 +6817,4 @@ function useTerminal() {
|
|
|
4957
6817
|
};
|
|
4958
6818
|
}
|
|
4959
6819
|
//#endregion
|
|
4960
|
-
export { CAMERA_UI_INJECTION_KEY, NamespaceManager, STREAM_CONFIG, acquireSensorManager, clearCameraCache, clearOAuthCache, clearPluginCache, clearSensorCache, clearSnapshotCache, clearStorageCache, createActivityMode, createCameraUiPlugin, createReactiveCameraDevice, createSensorManager, getSnapshotUrl, isReactiveAudioSensor, isReactiveBatteryInfo, isReactiveCarbonDioxideInfo, isReactiveCarbonMonoxideSensor, isReactiveClassifierSensor, isReactiveColdSensor, isReactiveContactSensor, isReactiveDoorbellTrigger, isReactiveFaceSensor, isReactiveGarageControl, isReactiveGasSensor, isReactiveHeatSensor, isReactiveHumidityInfo, isReactiveIlluminanceInfo, isReactiveLeakSensor, isReactiveLicensePlateSensor, isReactiveLightControl, isReactiveLockControl, isReactiveMotionSensor, isReactiveObjectSensor, isReactiveOccupancySensor, isReactivePTZControl, isReactivePowerSensor, isReactiveProblemSensor, isReactiveSecuritySystem, isReactiveSirenControl, isReactiveSmokeSensor, isReactiveSwitchControl, isReactiveTamperSensor, isReactiveTemperatureInfo, isReactiveVibrationSensor, refreshClientSubscriptions, releaseSensorManager, resetClientState, rpcCall, setFullscreenRoot, useAllSensors, useAudioSensor, useCameraById, useCameraStorage, useCameraStream, useCameraUi, useClassifierSensors, useCoreManager, useCuiFullscreen, useDeviceManager, useFaceSensor, useLicensePlateSensor, useMotionSensor, useOAuth, useObjectSensor, usePTZControl, usePlugin, usePluginStorage, useRpcCall, useRpcSubscription, useSensorById, useSensorByType, useSensorStorage, useSensors, useSensorsByType, useSnapshot, useTabVisibility, useTerminal, useTopmostFullscreenElement };
|
|
6820
|
+
export { CAMERA_UI_INJECTION_KEY, NamespaceManager, STREAM_CONFIG, acquireSensorManager, clearCameraCache, clearOAuthCache, clearPluginCache, clearSensorCache, clearSnapshotCache, clearStorageCache, createActivityMode, createCameraUiPlugin, createReactiveCameraDevice, createSensorManager, getSnapshotUrl, isReactiveAudioSensor, isReactiveBatteryInfo, isReactiveCarbonDioxideInfo, isReactiveCarbonMonoxideSensor, isReactiveClassifierSensor, isReactiveColdSensor, isReactiveContactSensor, isReactiveDoorbellTrigger, isReactiveFaceSensor, isReactiveGarageControl, isReactiveGasSensor, isReactiveHeatSensor, isReactiveHumidityInfo, isReactiveIlluminanceInfo, isReactiveLeakSensor, isReactiveLicensePlateSensor, isReactiveLightControl, isReactiveLockControl, isReactiveMotionSensor, isReactiveObjectSensor, isReactiveOccupancySensor, isReactivePTZControl, isReactivePowerSensor, isReactiveProblemSensor, isReactiveSecuritySystem, isReactiveSirenControl, isReactiveSmokeSensor, isReactiveSwitchControl, isReactiveTamperSensor, isReactiveTemperatureInfo, isReactiveVibrationSensor, refreshClientSubscriptions, releaseSensorManager, resetClientState, restartConnectedStreams, rpcCall, setFullscreenRoot, useAllSensors, useAudioSensor, useCameraById, useCameraStorage, useCameraStream, useCameraUi, useClassifierSensors, useCoreManager, useCuiFullscreen, useDeviceManager, useFaceSensor, useLicensePlateSensor, useMotionSensor, useOAuth, useObjectSensor, usePTZControl, usePlugin, usePluginStorage, useRpcCall, useRpcSubscription, useSensorById, useSensorByType, useSensorStorage, useSensors, useSensorsByType, useSnapshot, useTabVisibility, useTerminal, useTopmostFullscreenElement };
|