@camera.ui/browser 0.0.202 → 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.
Files changed (3) hide show
  1. package/dist/index.d.ts +71 -21
  2. package/dist/index.js +2507 -647
  3. 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, reactive, readonly, ref, shallowRef, toValue, watch } from "vue";
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 <= 0) {
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 { sensorSubject: `sensor.${sensorId}.subject` };
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 workerDisconnect() {
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 ?? defaultShouldRetry;
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
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
220
- if (signal?.aborted) throw makeAbortError();
221
- let rpc = rpcRef.value;
222
- if (!rpc) {
223
- if (!awaitConnect) throw new Error("rpc: not connected");
224
- rpc = await waitForRpc(rpcRef, connectTimeoutMs, signal);
225
- }
226
- try {
227
- return await fn(rpc);
228
- } catch (err) {
229
- lastError = err;
230
- if (attempt >= maxRetries) throw err;
231
- if (!shouldRetry(err, attempt)) throw err;
232
- await sleep$1(retryDelayMs(attempt), signal);
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
- unsubscribe = await subscribeFn(rpc);
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$1(retryDelayMs(attempt));
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 !== rpc) bind(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, prevRpc) => {
339
- if (rpc && rpc !== prevRpc) bind(rpc);
340
- else if (!rpc && prevRpc) unbind();
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 defaultShouldRetry(err) {
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 === "TIMEOUT" || code === "CONNECTION_CLOSED") return true;
359
- const msg = err.message.toLowerCase();
360
- if (msg.includes("not connected")) return true;
361
- if (msg.includes("connection closed")) return true;
362
- if (msg.includes("no responders")) return true;
363
- if (msg.includes("connection refused")) return true;
364
- if (msg.includes("socket")) return true;
365
- if (msg.includes("timed out") || msg.includes("timeout")) return true;
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 = watch(rpcRef, (next) => {
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$1(ms, signal) {
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(id);
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 reconnectHooked = false;
585
+ var hookedContexts = /* @__PURE__ */ new WeakSet();
442
586
  function hookReconnectRefresh(cameraUi, deviceManager) {
443
- if (reconnectHooked) return;
444
- reconnectHooked = true;
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 setSnapshot(cameraId, data, fetchedAt) {
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
- if (!await acquireCameraDevice(deviceManager, id)) return;
538
- const idOrName = toValue(cameraIdOrName);
539
- if ((typeof idOrName === "string" ? idOrName : idOrName._id) !== id || heldDeviceId === id) {
540
- releaseCameraDevice(id);
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
- } catch {} finally {
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(heldDeviceId);
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
- try {
679
- const result = await rpcCall(rpcRef, (client) => client.createProxy(cameraNamespaces.cameraControllerRpc).snapshotWithMeta(sourceId, forceNew));
680
- if (result && result.data.byteLength > 0) {
681
- setSnapshot(initialCamera._id, result.data, Date.now() - result.ageMs);
682
- return result.data;
683
- }
684
- return;
685
- } catch {
686
- const result = await rpcCall(rpcRef, (client) => client.createProxy(cameraNamespaces.cameraControllerRpc).snapshot(sourceId, forceNew));
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
- async function refreshStates() {
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": setSnapshot(initialCamera._id, event.data.snapshot, event.data.fetchedAt);
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
- const deletedEvent = event.data;
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) refreshStates().catch(() => {});
743
- else {
744
- const newStates = { ...sensorStates.value };
745
- delete newStates[connectedEvent.sensorId];
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) refreshStates().catch(() => {});
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
- closeSubscription = await rpcCall(rpcRef, (client) => client.subscribe(cameraNamespaces.cameraSubject, handleCameraEvent));
763
- closeSensorSubscription = await rpcCall(rpcRef, (client) => client.subscribe(sensorNamespaces.sensorsSubject, handleSensorEvent));
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
- await init();
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: init,
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
- async function acquireCameraDevice(deviceManager, id) {
845
- if (cameraCache.has(id)) return cameraCache.acquire(id, () => {
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
- const pending = pendingLoads$1.get(id);
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
- return device ? cameraCache.acquire(id, () => device) : void 0;
1250
+ const joined = acquireCached(key);
1251
+ if (joined || !device) return joined;
852
1252
  }
853
- const loadPromise = deviceManager.getCamera(id);
854
- pendingLoads$1.set(id, loadPromise);
1253
+ const loadPromise = deviceManager.getCamera(key);
1254
+ pendingLoads$1.set(key, loadPromise);
855
1255
  try {
856
1256
  const device = await loadPromise;
857
- return device ? cameraCache.acquire(id, () => device) : void 0;
1257
+ return device ? cacheLoadedDevice(device) : void 0;
858
1258
  } finally {
859
- pendingLoads$1.delete(id);
1259
+ if (pendingLoads$1.get(key) === loadPromise) pendingLoads$1.delete(key);
860
1260
  }
861
1261
  }
862
- function releaseCameraDevice(id) {
863
- cameraCache.release(id);
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 currentCameraId;
878
- async function loadCamera(id) {
879
- if (!isConnected.value || !id) return;
880
- if (currentCameraId && currentCameraId !== id) {
881
- cameraCache.release(currentCameraId);
882
- camera.value = void 0;
883
- currentCameraId = void 0;
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 (cameraCache.has(id)) {
886
- const cached = cameraCache.acquire(id, () => {
887
- throw new Error("Should not create - already cached");
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
- const pending = pendingLoads$1.get(id);
895
- if (pending) {
1331
+ if (!retry) {
896
1332
  _isLoading.value = true;
897
- try {
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 loadPromise;
918
- if (device) {
919
- cameraCache.acquire(id, () => device);
920
- if (toValue(cameraIdOrName) !== id) {
921
- cameraCache.release(id);
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
- error.value = err instanceof Error ? err : new Error(String(err));
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 id = toValue(cameraIdOrName);
937
- if (!id) return;
938
- if (currentCameraId) {
939
- if (cameraCache.getRefCount(currentCameraId) <= 1) cameraCache.forceRelease(currentCameraId);
940
- else cameraCache.release(currentCameraId);
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
- currentCameraId = void 0;
1358
+ boundKey = void 0;
943
1359
  }
944
- await loadCamera(id);
1360
+ await loadCamera(key);
945
1361
  }
946
- watch([isConnected, () => toValue(cameraIdOrName)], async ([connected, id]) => {
947
- if (connected && id) await loadCamera(id);
948
- else if (!connected && currentCameraId) {
949
- cameraCache.release(currentCameraId);
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
- if (currentCameraId) {
956
- cameraCache.release(currentCameraId);
957
- camera.value = void 0;
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 handleStatusMessage(message) {
1007
- if (message?.type !== "pluginStatusChanged") return;
1008
- const { pluginName, running } = message.data;
1009
- if (!running) pluginCache.forceRelease(pluginName);
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 { rpc, isConnected } = useCameraUi();
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
- plugin.value = cached.proxy;
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 && !pluginCache.has(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 pluginNamespaces = NamespaceManager.pluginNamespaces(pluginInfo.id);
1762
+ const namespace = NamespaceManager.pluginNamespaces(pluginInfo.id).pluginChildRpc;
1132
1763
  return {
1133
- proxy: client.createProxy(pluginNamespaces.pluginChildRpc),
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, result);
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.getRefCount(currentPluginName) <= 1) pluginCache.forceRelease(currentPluginName);
1162
- else pluginCache.release(currentPluginName);
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], oldValues) => {
1181
- const prevClient = oldValues?.[2];
1182
- if (currentPluginName && prevClient && prevClient !== currentClient) {
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 ((!connected || !currentClient) && currentPluginName) {
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 = reactive({ ...state.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
- for (const callback of capabilityCallbacks) callback(capabilities.value);
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
- async function subscribeToSensorEvents(sensorId) {
1331
- if (sensorSubscriptions.has(sensorId)) return;
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
- const unsubscribe = await rpcCall(rpcRef, (c) => c.subscribe(namespace.sensorSubject, (msg) => handlePerSensorEvent(sensorId, msg)));
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
- subscribeToSensorEvents(event.sensor.id);
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) sensor.connected.value = event.connected;
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
- async function doInit() {
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
- sensorMap.value = /* @__PURE__ */ new Map();
1382
- let pendingEvents = [];
1383
- globalUnsubscribe = await rpcCall(rpcRef, (c) => c.subscribe(sensorsSubject, (msg) => {
1384
- if (pendingEvents) pendingEvents.push(msg);
1385
- else handleGlobalSensorEvent(msg);
1386
- }));
1387
- const [sensors, states] = await rpcCall(rpcRef, async (c) => {
1388
- const proxy = c.createProxy(sensorsRpc);
1389
- return Promise.all([proxy.getSensors(), proxy.getSensorStates()]);
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
- const newMap = /* @__PURE__ */ new Map();
1392
- for (const sensor of sensors) {
1393
- const reactiveSensor = createReactiveSensor(sensor, states[sensor.id] ?? {
1394
- properties: {},
1395
- capabilities: [],
1396
- type: sensor.type
1397
- }, rpcRef);
1398
- newMap.set(sensor.id, reactiveSensor);
1399
- }
1400
- sensorMap.value = newMap;
1401
- for (const sensorId of newMap.keys()) await subscribeToSensorEvents(sensorId);
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 p = initPromise.catch(() => {}).then(() => doInit()).finally(() => {
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
- globalUnsubscribe?.();
1432
- globalUnsubscribe = void 0;
1433
- for (const unsubscribe of sensorSubscriptions.values()) unsubscribe();
1434
- sensorSubscriptions.clear();
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.initPromise = cached.manager.reconnect();
2326
+ cached.manager.reconnect().catch(() => {});
1470
2327
  });
1471
2328
  }
1472
2329
  var GLOBAL_SENSOR_MANAGER_KEY = "sensors";
1473
2330
  function acquireSensorManager(rpcOrContext) {
1474
- return sensorManagerCache.acquire(GLOBAL_SENSOR_MANAGER_KEY, () => ({ manager: createSensorManager(rpcOrContext) }));
2331
+ const cached = acquireSharedManager(rpcOrContext);
2332
+ cached.manager.retainLive(ALL_LIVE_SCOPE);
2333
+ return cached;
1475
2334
  }
1476
2335
  function releaseSensorManager() {
1477
- sensorManagerCache.release(GLOBAL_SENSOR_MANAGER_KEY);
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 (!cameraUi.rpc.value || !isConnected) return void 0;
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
- const cached = acquireSensorManager(cameraUi);
1494
- state.acquired = true;
1495
- state.cachedManager = cached.manager;
1496
- if (!cached.initPromise) cached.initPromise = cached.manager.ensureInitialized();
1497
- await cached.initPromise;
1498
- return cached.manager;
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.acquired) {
1508
- releaseSensorManager();
1509
- state.acquired = false;
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
- const manager = await ensureSensorManager(state, cameraUi, connected, _isLoading, error);
1528
- sensor.value = manager?.getSensor(senId);
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, type]) => {
2460
+ ], async ([connected, camId]) => {
1558
2461
  if (connected && camId) {
1559
- const manager = await ensureSensorManager(state, cameraUi, connected, _isLoading, error);
1560
- sensor.value = manager ? sensorsForCamera(manager, camId).find((s) => s.type === type) : void 0;
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, preferred]) => {
2620
+ ], async ([connected, camId]) => {
1705
2621
  if (connected && camId) {
1706
- const manager = await ensureSensorManager(state, cameraUi, connected, _isLoading, error);
1707
- cachedManager.value = manager;
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
- if (!cachedManager.value) return void 0;
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 sleep(ms) {
1947
- return new Promise((resolve) => setTimeout(resolve, ms));
1948
- }
1949
- function createStorageOperations(state, config, isLoading, error) {
1950
- let inflightGetConfig;
1951
- let inflightStorage;
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 (!inflightGetConfig || inflightStorage !== state.cachedStorage) {
1954
- inflightStorage = state.cachedStorage;
1955
- const request = getConfigWithRetry().finally(() => {
1956
- if (inflightGetConfig === request) {
1957
- inflightGetConfig = void 0;
1958
- inflightStorage = void 0;
1959
- }
1960
- });
1961
- inflightGetConfig = request;
1962
- }
1963
- return inflightGetConfig;
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 getConfigWithRetry() {
2881
+ async function attempt(load, visible) {
1966
2882
  const cached = state.cachedStorage;
1967
- if (!cached) return void 0;
1968
- isLoading.value = true;
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
- let result = await cached.getConfig();
1971
- for (const delay of GET_CONFIG_RETRY_DELAYS) {
1972
- if (!cached.error.value || state.cachedStorage !== cached) break;
1973
- await sleep(delay);
1974
- if (state.cachedStorage !== cached) break;
1975
- result = await cached.getConfig();
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
- return result;
2907
+ if (cached.error.value) scheduleRetry(load);
2908
+ else endLoad(state, result);
1981
2909
  } finally {
1982
- isLoading.value = false;
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
- bindStorage(state, getPluginStorageKey(pluginId), client, config, () => {
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
- bindStorage(state, getCameraStorageKey(pluginId, cameraId), client, config, () => {
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) emit("reconnected");
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
- }, resolvedConfig.standbyTimeout);
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 || !isStreamPlaying()) exitStandby();
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) _schedulePausedEntry(entry);
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 ?? DEFAULT_TAB_PAUSE_MS,
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 pendingBuffer = new Uint8Array(STREAM_CONFIG.MSE.BUFFER_SIZE);
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
- onError(/* @__PURE__ */ new Error("MediaSource not supported"));
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
- pendingBuffer = new Uint8Array(STREAM_CONFIG.MSE.BUFFER_SIZE);
4024
+ keyframes.length = 0;
2749
4025
  const MediaSourceConstructor = getMediaSourceConstructor();
2750
4026
  if (MediaSourceConstructor && !MediaSourceConstructor.isTypeSupported(mimeType)) {
2751
- onError(/* @__PURE__ */ new Error(`MIME type not supported: ${mimeType}`));
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
- onError(err instanceof Error ? err : new Error(String(err)));
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
- const data = pendingBuffer.slice(0, pendingLength);
2772
- sourceBuffer.appendBuffer(data);
4049
+ sourceBuffer.appendBuffer(pendingBuffer.subarray(0, pendingLength));
2773
4050
  pendingLength = 0;
2774
4051
  } catch (err) {
2775
- if (!recoverFromQuota(err)) onError(err instanceof Error ? err : new Error(String(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
- if (start > start0) try {
2786
- sourceBuffer.remove(start0, start);
2787
- mediaSource.setLiveSeekableRange(start, end);
2788
- } catch {}
2789
- if (videoElement.currentTime < start) videoElement.currentTime = start;
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
- onError(/* @__PURE__ */ new Error("MediaSource closed"));
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
- const bytes = new Uint8Array(data);
2808
- if (pendingLength + bytes.byteLength > pendingBuffer.byteLength) {
4099
+ if (!enqueue(new Uint8Array(data))) {
2809
4100
  pendingLength = 0;
2810
- onError(/* @__PURE__ */ new Error("MSE pending buffer overflow"));
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
- const bytes = new Uint8Array(data);
2820
- if (bytes.byteLength <= pendingBuffer.byteLength) {
2821
- pendingBuffer.set(bytes, 0);
2822
- pendingLength = bytes.byteLength;
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, onDisconnected, onCandidate, signal } = options;
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, onCandidate, false);
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" || connectionState === "failed") {
4362
+ } else if (connectionState === "disconnected") state.isConnected = false;
4363
+ else if (connectionState === "failed") {
3011
4364
  state.isConnected = false;
3012
- onDisconnected();
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: (sdp) => handleSdpAnswer(state.pc, signal, sdp),
3035
- handleCandidate: (candidate) => handleIceCandidate(state.pc, signal, candidate, false),
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
- function isVideoInPictureInPicture(video) {
3053
- if (!video) return false;
3054
- if (typeof document !== "undefined" && "pictureInPictureElement" in document && document.pictureInPictureElement === video) return true;
3055
- return video.webkitPresentationMode === "picture-in-picture";
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
- firstFrameCallbackId;
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
- mseMonitorInterval;
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
- if (this.wsHandle?.readyState === WebSocket.CONNECTING) {
3145
- this.disconnectWebSocket();
3146
- if (!this.abortController.signal.aborted && this.status.value !== "closed") this.restart();
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
- this.webrtcHandler.close();
3153
- this.webrtcHandler = void 0;
3154
- this.activeMode.value = "mse";
3155
- if (this.mseHandler?.isReady) this.status.value = "connected";
3156
- else this.startMSE();
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
- } else if (this.status.value === "connected" && !this.isPlaying.value && !this.abortController.signal.aborted) this.handleNoFramesWhileConnected();
3162
- }, STREAM_CONFIG.WEBRTC.CONNECT_TIMEOUT, { immediate: false });
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
- if (!this.wasPausedByVisibility) {
3201
- log.debug("onTabVisible — not paused by visibility, no-op");
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
- this.wasPausedByVisibility = false;
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
- const probe = await this.probeStream();
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
- log.debug("start() — connecting WebSocket");
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 (this.webrtcHandler) {
3332
- await this.webrtcHandler.setMicrophoneTrack(track);
4736
+ if (!track) {
4737
+ this.closeBackchannel();
3333
4738
  return;
3334
4739
  }
3335
- if (this.activeMode.value === "mse" && this.hasBackchannel.value) {
3336
- this.pendingMicTrack = track;
3337
- if (track && !this.backchannelHandler) await this.startBackchannel();
3338
- else if (!track && this.backchannelHandler) this.closeBackchannel();
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
- async restart() {
3386
- this.status.value = "reconnecting";
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(1e3, this.abortController.signal);
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.connectWebSocket();
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 stopMseWatch = watch(this.activeMode, (mode) => {
3485
- this.stopMseMonitor();
3486
- if (mode === "mse") this.mseMonitorInterval = setInterval(() => this.monitorMseBuffer(), 1e3);
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(stopMseWatch);
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.markSourceUndecodable();
3629
- this.restart();
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
- async probeStream() {
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(source._id, {
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 = source._id;
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 !== "closed" && !this.abortController.signal.aborted) this.startReconnectTimeout();
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.mseHandler) {
3706
- this.mseHandler.appendBuffer(ev.data);
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.webrtcHandler) processWebRTCMessage(this.webrtcHandler, msg);
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
- if (this.mseHandler && typeof msg.value === "string") {
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
- this.mseHandler.initializeBuffer(msg.value);
5255
+ handler.initializeBuffer(msg.value);
3721
5256
  }
3722
5257
  break;
3723
- case "error": if (this.requestedMode.value !== "auto") {
3724
- this.error.value = new Error(msg.value);
3725
- this.status.value = "error";
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
- this.webrtcHandler = createWebRTCHandler({
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
- const offer = await this.webrtcHandler.createOffer();
3744
- if (offer && !this.abortController.signal.aborted) this.sendWsMessage({
3745
- type: "webrtc/offer",
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
- this.handleFirstFrameWebRTC(stream);
3765
- if (this.requestedMode.value === "auto" && this.mseHandler) {
3766
- this.mseHandler.close();
3767
- this.mseHandler = void 0;
3768
- this.stopMseConnectTimeout();
3769
- this.sendWsMessage({ type: "mse/stop" });
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
- } else this.restart();
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
- this.webrtcHandler?.close();
3788
- this.webrtcHandler = void 0;
3789
- this.activeMode.value = "mse";
3790
- if (!this.mseHandler?.isReady) this.startMSE();
3791
- else this.status.value = "connected";
3792
- } else {
3793
- this.error.value = /* @__PURE__ */ new Error("WebRTC connection failed");
3794
- this.status.value = "error";
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) playVideo(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
- async startAutoMode() {
5458
+ startAutoMode() {
3844
5459
  if (this.abortController.signal.aborted) return;
3845
- const probe = this.probeInfo.value ?? await this.probeStream();
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 (!this.isPlaying.value && !this.abortController.signal.aborted) {
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
- monitorMseBuffer() {
3863
- const video = this.videoElement.value;
3864
- if (!video || this.activeMode.value !== "mse" || !this.isPlaying.value) return;
3865
- if (video.buffered.length > 0) {
3866
- const end = video.buffered.end(video.buffered.length - 1);
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
- stopMseMonitor() {
3871
- if (this.mseMonitorInterval !== void 0) {
3872
- clearInterval(this.mseMonitorInterval);
3873
- this.mseMonitorInterval = void 0;
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
- handleFirstFrameWebRTC(stream) {
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 (this.isPlaying.value || this.abortController.signal.aborted || !video) return;
3879
- if (!stream.getVideoTracks()[0]) {
3880
- this.handleFirstFrame();
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 videoWithCallback = video;
3884
- if (typeof videoWithCallback.requestVideoFrameCallback === "function") this.firstFrameCallbackId = videoWithCallback.requestVideoFrameCallback(() => {
3885
- if (!this.abortController.signal.aborted) this.handleFirstFrame();
3886
- });
3887
- else {
3888
- const checkFrame = () => {
3889
- if (this.abortController.signal.aborted) return;
3890
- if (video.readyState >= 2 && video.videoWidth > 0) this.handleFirstFrame();
3891
- else this.firstFrameCallbackId = requestAnimationFrame(checkFrame);
3892
- };
3893
- this.firstFrameCallbackId = requestAnimationFrame(checkFrame);
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.stopReconnectTimeout();
3902
- this.stopMseMonitor();
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.backchannelHandler = createBackchannelHandler({
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 this.backchannelHandler.setMicrophoneTrack(this.pendingMicTrack);
5653
+ if (this.pendingMicTrack && this.backchannelHandler === handler) await handler.setMicrophoneTrack(this.pendingMicTrack);
3934
5654
  },
3935
- onDisconnected: () => {
3936
- this.backchannelHandler = void 0;
3937
- }
5655
+ onFailed: () => this.closeBackchannel(handler)
3938
5656
  });
3939
- const offer = await this.backchannelHandler.createOffer();
3940
- if (offer && !this.abortController.signal.aborted) this.sendWsMessage({
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: offer
3943
- });
5675
+ value: sdp
5676
+ }));
3944
5677
  }
3945
- closeBackchannel() {
3946
- this.backchannelHandler?.close();
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 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
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 (!isolated && typeof IntersectionObserver !== "undefined") {
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
- if (entries.some((e) => e.isIntersecting)) reclaimSharedVideo();
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), (newMode) => {
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
- for await (const chunk of proxy(current).generateOutput(sid)) {
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
- streamAbortController?.abort();
4919
- streamGen++;
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 handleReconnected() {
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 };