@camera.ui/browser 0.0.111 → 0.0.113

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 (51) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +10 -1
  3. package/dist/index.d.ts +1269 -0
  4. package/dist/index.js +4179 -0
  5. package/package.json +55 -39
  6. package/CHANGELOG.md +0 -8
  7. package/dist/bundle.js +0 -2
  8. package/dist/bundle.js.LICENSE.txt +0 -14
  9. package/dist/types/packages/client/browser/src/api.d.ts +0 -8
  10. package/dist/types/packages/client/browser/src/client.d.ts +0 -21
  11. package/dist/types/packages/client/browser/src/index.d.ts +0 -2
  12. package/dist/types/packages/client/browser/src/proxy/cameraDevice.d.ts +0 -26
  13. package/dist/types/packages/client/browser/src/proxy/coreManager.d.ts +0 -14
  14. package/dist/types/packages/client/browser/src/proxy/deviceManager.d.ts +0 -16
  15. package/dist/types/packages/client/browser/src/proxy/index.d.ts +0 -3
  16. package/dist/types/packages/client/browser/src/proxy.d.ts +0 -7
  17. package/dist/types/packages/client/browser/src/streaming/config.d.ts +0 -21
  18. package/dist/types/packages/client/browser/src/streaming/go2rts-session.d.ts +0 -286
  19. package/dist/types/packages/client/browser/src/streaming/types.d.ts +0 -38
  20. package/dist/types/packages/client/browser/src/types.d.ts +0 -56
  21. package/dist/types/packages/client/browser/src/utils.d.ts +0 -12
  22. package/dist/types/packages/common/src/utils/subscribed.d.ts +0 -18
  23. package/dist/types/packages/types/src/index.d.ts +0 -812
  24. package/dist/types/server/src/api/database/types.d.ts +0 -80
  25. package/dist/types/server/src/api/go2rtc/types.d.ts +0 -147
  26. package/dist/types/server/src/api/schemas/backup.schema.d.ts +0 -211
  27. package/dist/types/server/src/api/schemas/cameras.schema.d.ts +0 -1864
  28. package/dist/types/server/src/api/schemas/config.schema.d.ts +0 -102
  29. package/dist/types/server/src/api/schemas/go2rtc.schema.d.ts +0 -735
  30. package/dist/types/server/src/api/schemas/plugins.schema.d.ts +0 -72
  31. package/dist/types/server/src/api/schemas/storage.schema.d.ts +0 -22
  32. package/dist/types/server/src/api/schemas/system.schema.d.ts +0 -185
  33. package/dist/types/server/src/api/schemas/users.schema.d.ts +0 -857
  34. package/dist/types/server/src/api/types/index.d.ts +0 -612
  35. package/dist/types/server/src/api/websocket/types.d.ts +0 -43
  36. package/dist/types/server/src/camera/classes.d.ts +0 -2
  37. package/dist/types/server/src/camera/index.d.ts +0 -92
  38. package/dist/types/server/src/camera/interfaces.d.ts +0 -17
  39. package/dist/types/server/src/camera/iou.d.ts +0 -2
  40. package/dist/types/server/src/camera/polygon.d.ts +0 -4
  41. package/dist/types/server/src/camera/types.d.ts +0 -17
  42. package/dist/types/server/src/go2rtc/types.d.ts +0 -15
  43. package/dist/types/server/src/manager/types.d.ts +0 -15
  44. package/dist/types/server/src/plugins/schema.d.ts +0 -27
  45. package/dist/types/server/src/plugins/types.d.ts +0 -49
  46. package/dist/types/server/src/rpc/namespaces.d.ts +0 -35
  47. package/dist/types/server/src/rpc/types.d.ts +0 -111
  48. package/dist/types/server/src/services/config/defaults.d.ts +0 -12
  49. package/dist/types/server/src/services/config/types.d.ts +0 -162
  50. package/dist/types/server/src/types.d.ts +0 -34
  51. package/dist/types/shared/types/index.d.ts +0 -22
package/dist/index.js ADDED
@@ -0,0 +1,4179 @@
1
+ import { computed, inject, markRaw, onBeforeUnmount, reactive, readonly, ref, shallowRef, toValue, watch } from "vue";
2
+ import { tryOnScopeDispose, useTimeoutFn, whenever } from "@vueuse/core";
3
+ import { SensorType } from "@camera.ui/sdk";
4
+ export * from "@camera.ui/sdk";
5
+ export * from "@camera.ui/sdk/internal";
6
+ //#region src/utils/createDebouncedCache.ts
7
+ function createDebouncedCache(options = {}) {
8
+ const { releaseDelay = 1e3, onRelease } = options;
9
+ const cache = /* @__PURE__ */ new Map();
10
+ const releaseTimers = /* @__PURE__ */ new Map();
11
+ function cancelTimer(key) {
12
+ const timer = releaseTimers.get(key);
13
+ if (timer) {
14
+ clearTimeout(timer);
15
+ releaseTimers.delete(key);
16
+ }
17
+ }
18
+ function doRelease(key) {
19
+ const entry = cache.get(key);
20
+ if (entry) {
21
+ try {
22
+ onRelease?.(key, entry.value);
23
+ } catch {}
24
+ cache.delete(key);
25
+ }
26
+ releaseTimers.delete(key);
27
+ }
28
+ function acquire(key, create) {
29
+ cancelTimer(key);
30
+ const cached = cache.get(key);
31
+ if (cached) {
32
+ cached.refCount++;
33
+ return cached.value;
34
+ }
35
+ const value = create();
36
+ cache.set(key, {
37
+ value,
38
+ refCount: 1
39
+ });
40
+ return value;
41
+ }
42
+ function release(key) {
43
+ const cached = cache.get(key);
44
+ if (!cached) return;
45
+ cached.refCount--;
46
+ if (cached.refCount <= 0) {
47
+ const timer = setTimeout(() => {
48
+ const stillCached = cache.get(key);
49
+ if (stillCached && stillCached.refCount <= 0) doRelease(key);
50
+ else releaseTimers.delete(key);
51
+ }, releaseDelay);
52
+ releaseTimers.set(key, timer);
53
+ }
54
+ }
55
+ function get(key) {
56
+ return cache.get(key)?.value;
57
+ }
58
+ function has(key) {
59
+ return cache.has(key);
60
+ }
61
+ function forceRelease(key) {
62
+ cancelTimer(key);
63
+ doRelease(key);
64
+ }
65
+ function clear() {
66
+ for (const key of releaseTimers.keys()) cancelTimer(key);
67
+ for (const key of cache.keys()) doRelease(key);
68
+ }
69
+ function getRefCount(key) {
70
+ return cache.get(key)?.refCount ?? 0;
71
+ }
72
+ function forEachValue(cb) {
73
+ for (const [key, entry] of cache) cb(entry.value, key);
74
+ }
75
+ return {
76
+ acquire,
77
+ release,
78
+ get,
79
+ has,
80
+ forceRelease,
81
+ clear,
82
+ getRefCount,
83
+ forEachValue
84
+ };
85
+ }
86
+ //#endregion
87
+ //#region src/composables/useCameraUi.ts
88
+ function useCameraUi() {
89
+ const context = inject(CAMERA_UI_INJECTION_KEY);
90
+ if (!context) throw new Error("[camera.ui] useCameraUi() called without CameraUiPlugin installed. Make sure to call app.use(createCameraUiPlugin({ ... })) before using this composable.");
91
+ return context;
92
+ }
93
+ //#endregion
94
+ //#region ../../externals/camera.ui/server/src/utils/camera.ts
95
+ function createSourceName(cameraName, sourceName) {
96
+ return `cui_${cameraName.replace(/ /g, "_").toLowerCase()}_${sourceName.replace(/ /g, "_").toLowerCase()}`;
97
+ }
98
+ //#endregion
99
+ //#region ../../externals/camera.ui/server/src/rpc/namespaces.ts
100
+ var NamespaceManager = class {
101
+ static coreManagerNamespaces() {
102
+ return {
103
+ coreManagerSubject: "coreManager.subscriber",
104
+ coreManagerRpc: "coreManager.rpc"
105
+ };
106
+ }
107
+ static deviceManagerNamespaces() {
108
+ return {
109
+ deviceManagerSubject: "deviceManager.subscriber",
110
+ deviceManagerRpc: "deviceManager.rpc"
111
+ };
112
+ }
113
+ static discoveryManagerNamespaces() {
114
+ return {
115
+ discoveryManagerSubject: "discoveryManager.subscriber",
116
+ discoveryManagerRpc: "discoveryManager.rpc"
117
+ };
118
+ }
119
+ static pluginNamespaces(pluginId) {
120
+ return {
121
+ pluginDeviceManagerSubject: `plugin.${pluginId}.deviceManager.subscriber`,
122
+ pluginChildRpc: `plugin.${pluginId}.child.rpc`,
123
+ pluginChild: `plugin.${pluginId}.child`,
124
+ pluginStorageRpc: `plugin.${pluginId}.storage.rpc`
125
+ };
126
+ }
127
+ static cameraNamespaces(cameraId) {
128
+ return {
129
+ cameraSubject: `camera.${cameraId}.subscriber`,
130
+ cameraControllerRpc: `camera.${cameraId}.controller.rpc`
131
+ };
132
+ }
133
+ static frameWorkerNamespaces(cameraId) {
134
+ return {
135
+ frameWorkerChildRpc: `camera.${cameraId}.frameWorker.child.rpc`,
136
+ frameWorkerChild: `camera.${cameraId}.frameWorker.child`
137
+ };
138
+ }
139
+ static pluginCameraNamespaces(pluginId, cameraId) {
140
+ return {
141
+ cameraInterfacesRpc: `plugin.${pluginId}.camera.${cameraId}.cameraInterfaces.rpc`,
142
+ cameraStorageRpc: `plugin.${pluginId}.camera.${cameraId}.cameraStorage.rpc`,
143
+ cameraImplRpc: `plugin.${pluginId}.camera.${cameraId}.impl.rpc`
144
+ };
145
+ }
146
+ static pluginSensorNamespaces(pluginId, cameraId, sensorId) {
147
+ return { sensorStorageRpc: `plugin.${pluginId}.camera.${cameraId}.sensor.${sensorId}.storage.rpc` };
148
+ }
149
+ static sensorControllerNamespaces(cameraId) {
150
+ return {
151
+ sensorSubject: `camera.${cameraId}.sensors.subject`,
152
+ sensorRpc: `camera.${cameraId}.sensors.rpc`,
153
+ sensorWriteSubject: `camera.${cameraId}.sensors.writes`
154
+ };
155
+ }
156
+ static sensorEventNamespaces(cameraId, sensorId) {
157
+ return { sensorSubject: `camera.${cameraId}.sensor.${sensorId}.subject` };
158
+ }
159
+ static sensorProviderNamespaces(pluginId, cameraId, sensorId) {
160
+ return { sensorRpc: `plugin.${pluginId}.camera.${cameraId}.sensor.${sensorId}.rpc` };
161
+ }
162
+ static frameWorkerDetectionNamespaces(cameraId) {
163
+ return { detectionRpc: `camera.${cameraId}.frameWorker.detection.rpc` };
164
+ }
165
+ static detectionEventNamespaces(cameraId) {
166
+ return { detectionEventSubject: `camera.${cameraId}.events.subject` };
167
+ }
168
+ static sensorControllerRpc(cameraId) {
169
+ return `camera.${cameraId}.sensors.controller.rpc`;
170
+ }
171
+ static terminalManagerNamespaces() {
172
+ return {
173
+ terminalManagerSubject: "terminalManager.subscriber",
174
+ terminalManagerRpc: "terminalManager.rpc"
175
+ };
176
+ }
177
+ static downloadManagerNamespaces() {
178
+ return { downloadManagerRpc: "downloadManager.rpc" };
179
+ }
180
+ static notificationManagerNamespaces() {
181
+ return { notificationsPublishSubject: "notifications.publish" };
182
+ }
183
+ static workerAgentRpc(agentId) {
184
+ return `worker.${agentId}.rpc`;
185
+ }
186
+ static workerHeartbeat() {
187
+ return "workers.heartbeat";
188
+ }
189
+ static workerDisconnect() {
190
+ return "workers.disconnect";
191
+ }
192
+ };
193
+ //#endregion
194
+ //#region src/composables/useRpc.ts
195
+ var DEFAULT_MAX_RETRIES = 3;
196
+ var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
197
+ async function rpcCall(rpcRef, fn, options = {}) {
198
+ const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
199
+ const awaitConnect = options.awaitConnect ?? true;
200
+ const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
201
+ const retryDelayMs = options.retryDelayMs ?? defaultRetryDelayMs;
202
+ const shouldRetry = options.shouldRetry ?? defaultShouldRetry;
203
+ const signal = options.signal;
204
+ let lastError;
205
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
206
+ if (signal?.aborted) throw makeAbortError();
207
+ let rpc = rpcRef.value;
208
+ if (!rpc) {
209
+ if (!awaitConnect) throw new Error("rpc: not connected");
210
+ rpc = await waitForRpc(rpcRef, connectTimeoutMs, signal);
211
+ }
212
+ try {
213
+ return await fn(rpc);
214
+ } catch (err) {
215
+ lastError = err;
216
+ if (attempt >= maxRetries) throw err;
217
+ if (!shouldRetry(err, attempt)) throw err;
218
+ await sleep(retryDelayMs(attempt), signal);
219
+ }
220
+ }
221
+ throw lastError ?? /* @__PURE__ */ new Error("rpc: max retries exceeded");
222
+ }
223
+ function useRpcCall(fn, options = {}) {
224
+ const ctx = useCameraUi();
225
+ const data = shallowRef();
226
+ const loading = ref(false);
227
+ const error = ref();
228
+ let abortCtrl;
229
+ async function execute() {
230
+ abortCtrl?.abort();
231
+ abortCtrl = new AbortController();
232
+ const localCtrl = abortCtrl;
233
+ loading.value = true;
234
+ error.value = void 0;
235
+ try {
236
+ const result = await rpcCall(ctx.rpc, fn, {
237
+ ...options,
238
+ signal: localCtrl.signal
239
+ });
240
+ if (localCtrl.signal.aborted) return void 0;
241
+ data.value = result;
242
+ return result;
243
+ } catch (err) {
244
+ if (localCtrl.signal.aborted) return void 0;
245
+ error.value = err instanceof Error ? err : new Error(String(err));
246
+ return;
247
+ } finally {
248
+ if (!localCtrl.signal.aborted) loading.value = false;
249
+ }
250
+ }
251
+ if (options.immediate) execute();
252
+ if (options.watch?.length) watch(options.watch, () => {
253
+ execute();
254
+ });
255
+ if (options.refetchOnReconnect !== false) {
256
+ const handleReconnected = () => {
257
+ execute();
258
+ };
259
+ ctx.on("reconnected", handleReconnected);
260
+ tryOnScopeDispose(() => ctx.off("reconnected", handleReconnected));
261
+ }
262
+ tryOnScopeDispose(() => abortCtrl?.abort());
263
+ return {
264
+ data,
265
+ loading,
266
+ error,
267
+ execute,
268
+ refresh: execute
269
+ };
270
+ }
271
+ function useRpcSubscription(subscribeFn, options = {}) {
272
+ const ctx = useCameraUi();
273
+ const isActive = ref(false);
274
+ const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
275
+ const retryDelayMs = options.retryDelayMs ?? defaultRetryDelayMs;
276
+ let unsubscribe;
277
+ let pending = false;
278
+ let disposed = false;
279
+ async function bind(rpc) {
280
+ if (pending || disposed) return;
281
+ pending = true;
282
+ try {
283
+ if (unsubscribe) {
284
+ try {
285
+ unsubscribe();
286
+ } catch {}
287
+ unsubscribe = void 0;
288
+ }
289
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
290
+ if (disposed) return;
291
+ try {
292
+ unsubscribe = await subscribeFn(rpc);
293
+ isActive.value = true;
294
+ return;
295
+ } catch (err) {
296
+ if (attempt >= maxRetries) {
297
+ isActive.value = false;
298
+ options.onError?.(err);
299
+ return;
300
+ }
301
+ await sleep(retryDelayMs(attempt));
302
+ }
303
+ }
304
+ } finally {
305
+ pending = false;
306
+ }
307
+ }
308
+ function unbind() {
309
+ if (unsubscribe) {
310
+ try {
311
+ unsubscribe();
312
+ } catch {}
313
+ unsubscribe = void 0;
314
+ }
315
+ isActive.value = false;
316
+ }
317
+ async function resubscribe() {
318
+ const rpc = ctx.rpc.value;
319
+ if (rpc) await bind(rpc);
320
+ }
321
+ if (ctx.rpc.value) bind(ctx.rpc.value);
322
+ const stopWatch = watch(ctx.rpc, (rpc, prevRpc) => {
323
+ if (rpc && rpc !== prevRpc) bind(rpc);
324
+ else if (!rpc && prevRpc) unbind();
325
+ });
326
+ tryOnScopeDispose(() => {
327
+ disposed = true;
328
+ stopWatch();
329
+ unbind();
330
+ });
331
+ return {
332
+ isActive,
333
+ resubscribe
334
+ };
335
+ }
336
+ function defaultRetryDelayMs(attempt) {
337
+ return Math.min(100 * Math.pow(2, attempt), 1e3);
338
+ }
339
+ function defaultShouldRetry(err) {
340
+ if (!(err instanceof Error)) return false;
341
+ const msg = err.message.toLowerCase();
342
+ if (msg.includes("not connected")) return true;
343
+ if (msg.includes("connection closed")) return true;
344
+ if (msg.includes("no responders")) return true;
345
+ if (msg.includes("connection refused")) return true;
346
+ if (msg.includes("socket")) return true;
347
+ if (msg.includes("timeout")) return true;
348
+ return false;
349
+ }
350
+ async function waitForRpc(rpcRef, timeoutMs, signal) {
351
+ if (rpcRef.value) return rpcRef.value;
352
+ return new Promise((resolve, reject) => {
353
+ const timer = setTimeout(() => {
354
+ stopWatch();
355
+ signal?.removeEventListener("abort", onAbort);
356
+ reject(/* @__PURE__ */ new Error("rpc: connect timeout"));
357
+ }, timeoutMs);
358
+ const onAbort = () => {
359
+ clearTimeout(timer);
360
+ stopWatch();
361
+ reject(makeAbortError());
362
+ };
363
+ signal?.addEventListener("abort", onAbort, { once: true });
364
+ const stopWatch = watch(rpcRef, (next) => {
365
+ if (next) {
366
+ clearTimeout(timer);
367
+ stopWatch();
368
+ signal?.removeEventListener("abort", onAbort);
369
+ resolve(next);
370
+ }
371
+ });
372
+ });
373
+ }
374
+ async function sleep(ms, signal) {
375
+ return new Promise((resolve, reject) => {
376
+ if (signal?.aborted) {
377
+ reject(makeAbortError());
378
+ return;
379
+ }
380
+ const timer = setTimeout(() => {
381
+ signal?.removeEventListener("abort", onAbort);
382
+ resolve();
383
+ }, ms);
384
+ const onAbort = () => {
385
+ clearTimeout(timer);
386
+ reject(makeAbortError());
387
+ };
388
+ signal?.addEventListener("abort", onAbort, { once: true });
389
+ });
390
+ }
391
+ function makeAbortError() {
392
+ if (typeof DOMException !== "undefined") return new DOMException("Aborted", "AbortError");
393
+ const err = /* @__PURE__ */ new Error("Aborted");
394
+ err.name = "AbortError";
395
+ return err;
396
+ }
397
+ //#endregion
398
+ //#region src/composables/useSnapshot.ts
399
+ var REVOKE_DELAY_MS = 5e3;
400
+ var snapshotCache = /* @__PURE__ */ new Map();
401
+ var urlCache = /* @__PURE__ */ new Map();
402
+ var subscribers = /* @__PURE__ */ new Map();
403
+ function notify(cameraId) {
404
+ subscribers.get(cameraId)?.forEach((cb) => cb());
405
+ }
406
+ function deferRevoke(url) {
407
+ setTimeout(() => URL.revokeObjectURL(url), REVOKE_DELAY_MS);
408
+ }
409
+ function setSnapshot(cameraId, data) {
410
+ const oldUrl = urlCache.get(cameraId);
411
+ if (oldUrl) {
412
+ deferRevoke(oldUrl);
413
+ urlCache.delete(cameraId);
414
+ }
415
+ snapshotCache.set(cameraId, data);
416
+ notify(cameraId);
417
+ }
418
+ function getSnapshot(cameraId) {
419
+ return snapshotCache.get(cameraId);
420
+ }
421
+ function getSnapshotUrl(cameraId) {
422
+ const buffer = snapshotCache.get(cameraId);
423
+ if (!buffer) return void 0;
424
+ let url = urlCache.get(cameraId);
425
+ if (!url) {
426
+ url = URL.createObjectURL(new Blob([buffer], { type: "image/jpeg" }));
427
+ urlCache.set(cameraId, url);
428
+ }
429
+ return url;
430
+ }
431
+ function clearSnapshotCache() {
432
+ for (const url of urlCache.values()) URL.revokeObjectURL(url);
433
+ urlCache.clear();
434
+ snapshotCache.clear();
435
+ }
436
+ function useSnapshot(cameraIdOrName) {
437
+ const { isConnected } = useCameraUi();
438
+ const deviceManager = useDeviceManager();
439
+ const snapshot = shallowRef();
440
+ const _isLoading = ref(false);
441
+ const initialLoadDone = ref(false);
442
+ const snapshotSrc = computed(() => {
443
+ if (!snapshot.value) return void 0;
444
+ const idOrName = toValue(cameraIdOrName);
445
+ return getSnapshotUrl(typeof idOrName === "string" ? idOrName : idOrName._id);
446
+ });
447
+ function subscribe(cameraId) {
448
+ if (!subscribers.has(cameraId)) subscribers.set(cameraId, /* @__PURE__ */ new Set());
449
+ const updateFn = () => {
450
+ snapshot.value = snapshotCache.get(cameraId);
451
+ };
452
+ subscribers.get(cameraId).add(updateFn);
453
+ return () => {
454
+ subscribers.get(cameraId)?.delete(updateFn);
455
+ };
456
+ }
457
+ let unsubscribe;
458
+ async function loadSnapshot(id) {
459
+ if (!isConnected.value || !id) return;
460
+ const cached = snapshotCache.get(id);
461
+ if (cached) {
462
+ snapshot.value = cached;
463
+ initialLoadDone.value = true;
464
+ return;
465
+ }
466
+ _isLoading.value = true;
467
+ try {
468
+ const device = await deviceManager.getCamera(id);
469
+ if (device) {
470
+ const result = await device.fetchSnapshot();
471
+ if (result) {
472
+ setSnapshot(id, result);
473
+ snapshot.value = result;
474
+ }
475
+ }
476
+ } catch {} finally {
477
+ _isLoading.value = false;
478
+ initialLoadDone.value = true;
479
+ }
480
+ }
481
+ function isCameraDisabled(camOrId) {
482
+ return typeof camOrId === "object" && camOrId?.disabled === true;
483
+ }
484
+ async function refresh() {
485
+ const cameraIdName = toValue(cameraIdOrName);
486
+ const id = typeof cameraIdName === "string" ? cameraIdName : cameraIdName._id;
487
+ if (!id || !isConnected.value) return;
488
+ if (isCameraDisabled(cameraIdName)) return;
489
+ _isLoading.value = true;
490
+ try {
491
+ const device = await deviceManager.getCamera(id);
492
+ if (device) {
493
+ const result = await device.fetchSnapshot(void 0, true);
494
+ if (result) {
495
+ setSnapshot(id, result);
496
+ snapshot.value = result;
497
+ }
498
+ }
499
+ } catch {} finally {
500
+ _isLoading.value = false;
501
+ initialLoadDone.value = true;
502
+ }
503
+ }
504
+ watch([
505
+ isConnected,
506
+ () => toValue(cameraIdOrName),
507
+ () => isCameraDisabled(toValue(cameraIdOrName))
508
+ ], async ([connected, cameraIdName, disabled]) => {
509
+ unsubscribe?.();
510
+ unsubscribe = void 0;
511
+ const id = typeof cameraIdName === "string" ? cameraIdName : cameraIdName._id;
512
+ if (connected && id && !disabled) {
513
+ unsubscribe = subscribe(id);
514
+ await loadSnapshot(id);
515
+ } else if (id) {
516
+ snapshot.value = snapshotCache.get(id);
517
+ initialLoadDone.value = true;
518
+ } else snapshot.value = void 0;
519
+ }, { immediate: true });
520
+ tryOnScopeDispose(() => {
521
+ unsubscribe?.();
522
+ });
523
+ return {
524
+ snapshot,
525
+ snapshotSrc,
526
+ isLoading: computed(() => _isLoading.value || !initialLoadDone.value),
527
+ refresh
528
+ };
529
+ }
530
+ //#endregion
531
+ //#region src/composables/useCamera.ts
532
+ async function createReactiveCameraDevice(rpcOrContext, initialCamera) {
533
+ const cameraNamespaces = NamespaceManager.cameraNamespaces(initialCamera._id);
534
+ const sensorNamespaces = NamespaceManager.sensorControllerNamespaces(initialCamera._id);
535
+ const ctx = "rpc" in rpcOrContext && "value" in rpcOrContext.rpc ? rpcOrContext : void 0;
536
+ const rpcRef = ctx ? ctx.rpc : shallowRef(rpcOrContext);
537
+ let closeSubscription;
538
+ let closeSensorSubscription;
539
+ const camera = shallowRef(initialCamera);
540
+ const connected = ref(false);
541
+ const frameWorkerConnected = ref(false);
542
+ const snapshot = computed(() => getSnapshot(initialCamera._id));
543
+ const snapshotLoading = ref(false);
544
+ const sensorStates = ref({});
545
+ const name = computed(() => camera.value.name);
546
+ const room = computed(() => camera.value.room);
547
+ const nativeId = computed(() => camera.value.nativeId);
548
+ const disabled = computed(() => camera.value.disabled);
549
+ const snooze = computed(() => camera.value.detectionSettings?.snooze ?? false);
550
+ const isCloud = computed(() => camera.value.isCloud);
551
+ const sources = computed(() => {
552
+ return JSON.parse(JSON.stringify(camera.value.sources)).map((source) => ({
553
+ ...source,
554
+ snapshot: async (forceNew) => fetchSnapshot(source._id, forceNew),
555
+ probeStream: async (probeConfig, refresh = false) => probeStream(source._id, probeConfig, refresh)
556
+ }));
557
+ });
558
+ const highResolutionSource = computed(() => sources.value.find((s) => s.role === "high-resolution"));
559
+ const midResolutionSource = computed(() => sources.value.find((s) => s.role === "mid-resolution"));
560
+ const lowResolutionSource = computed(() => sources.value.find((s) => s.role === "low-resolution"));
561
+ const streamSource = computed(() => highResolutionSource.value ?? midResolutionSource.value ?? lowResolutionSource.value);
562
+ const snapshotSource = computed(() => sources.value.find((s) => s.role === "snapshot") ?? sources.value.find((s) => s.useForSnapshot));
563
+ const existingSensorTypes = computed(() => {
564
+ const types = /* @__PURE__ */ new Set();
565
+ for (const state of Object.values(sensorStates.value)) types.add(state.type);
566
+ return types;
567
+ });
568
+ const capabilities = computed(() => Array.from(existingSensorTypes.value));
569
+ const hasLight = computed(() => existingSensorTypes.value.has(SensorType.Light));
570
+ const hasSiren = computed(() => existingSensorTypes.value.has(SensorType.Siren));
571
+ const hasDoorbell = computed(() => existingSensorTypes.value.has(SensorType.Doorbell));
572
+ const hasBattery = computed(() => existingSensorTypes.value.has(SensorType.Battery));
573
+ const hasAudioSensor = computed(() => existingSensorTypes.value.has(SensorType.Audio));
574
+ const hasMotionSensor = computed(() => existingSensorTypes.value.has(SensorType.Motion));
575
+ const hasObjectSensor = computed(() => existingSensorTypes.value.has(SensorType.Object));
576
+ const hasPtz = computed(() => existingSensorTypes.value.has(SensorType.PTZ));
577
+ async function fetchSnapshot(sourceId, forceNew) {
578
+ snapshotLoading.value = true;
579
+ try {
580
+ const result = await rpcCall(rpcRef, (client) => client.createProxy(cameraNamespaces.cameraControllerRpc).snapshot(sourceId, forceNew));
581
+ if (result && result.byteLength > 0) {
582
+ setSnapshot(initialCamera._id, result);
583
+ return result;
584
+ }
585
+ } finally {
586
+ snapshotLoading.value = false;
587
+ }
588
+ }
589
+ async function probeStream(sourceId, probeConfig, refresh = false) {
590
+ return rpcCall(rpcRef, (client) => client.createProxy(cameraNamespaces.cameraControllerRpc).probeStream(sourceId, probeConfig, refresh));
591
+ }
592
+ async function streamUrl(sourceId) {
593
+ return rpcCall(rpcRef, (client) => client.createProxy(cameraNamespaces.cameraControllerRpc).streamUrl(sourceId));
594
+ }
595
+ async function refreshStates() {
596
+ const response = await rpcCall(rpcRef, (client) => client.createProxy(cameraNamespaces.cameraControllerRpc).refreshStates());
597
+ camera.value = response.camera;
598
+ connected.value = response.cameraState;
599
+ frameWorkerConnected.value = response.frameWorkerState;
600
+ sensorStates.value = response.sensorStates;
601
+ }
602
+ async function handleCameraEvent(event) {
603
+ switch (event.type) {
604
+ case "removed":
605
+ await close();
606
+ break;
607
+ case "updated":
608
+ camera.value = event.data;
609
+ break;
610
+ case "cameraState":
611
+ connected.value = event.data;
612
+ break;
613
+ case "frameWorkerState":
614
+ frameWorkerConnected.value = event.data;
615
+ break;
616
+ case "snapshot:updated":
617
+ setSnapshot(initialCamera._id, event.data.snapshot);
618
+ break;
619
+ }
620
+ }
621
+ function handleSensorEvent(event) {
622
+ if (event.type === "sensor:added") {
623
+ const addedEvent = event.data;
624
+ sensorStates.value = {
625
+ ...sensorStates.value,
626
+ [addedEvent.sensor.id]: addedEvent.state
627
+ };
628
+ } else if (event.type === "sensor:removed") {
629
+ const removedEvent = event.data;
630
+ const newStates = { ...sensorStates.value };
631
+ delete newStates[removedEvent.sensorId];
632
+ sensorStates.value = newStates;
633
+ }
634
+ }
635
+ async function init() {
636
+ closeSubscription?.();
637
+ closeSensorSubscription?.();
638
+ closeSubscription = await rpcCall(rpcRef, (client) => client.subscribe(cameraNamespaces.cameraSubject, handleCameraEvent));
639
+ closeSensorSubscription = await rpcCall(rpcRef, (client) => client.subscribe(sensorNamespaces.sensorSubject, handleSensorEvent));
640
+ await refreshStates();
641
+ }
642
+ async function close() {
643
+ closeSubscription?.();
644
+ closeSubscription = void 0;
645
+ closeSensorSubscription?.();
646
+ closeSensorSubscription = void 0;
647
+ }
648
+ async function fetchSnapshotFn(sourceId, forceNew) {
649
+ const useForSnapshotSource = sources.value.find((s) => s.useForSnapshot);
650
+ const dedicatedSnapshotSource = sources.value.find((s) => s.role === "snapshot");
651
+ const id = sourceId ?? dedicatedSnapshotSource?._id ?? useForSnapshotSource?._id ?? lowResolutionSource.value?._id ?? midResolutionSource.value?._id ?? highResolutionSource.value?._id ?? streamSource.value?._id;
652
+ if (!id) return Promise.resolve(void 0);
653
+ return fetchSnapshot(id, forceNew);
654
+ }
655
+ async function probeStreamFn(sourceId, probeConfig, refresh = false) {
656
+ const id = sourceId ?? streamSource.value?._id;
657
+ if (!id) return Promise.resolve(void 0);
658
+ return probeStream(id, probeConfig, refresh);
659
+ }
660
+ await init();
661
+ return {
662
+ id: initialCamera._id,
663
+ name,
664
+ room,
665
+ nativeId,
666
+ disabled,
667
+ snooze,
668
+ isCloud,
669
+ connected,
670
+ frameWorkerConnected,
671
+ sources,
672
+ streamSource,
673
+ snapshotSource,
674
+ highResolutionSource,
675
+ midResolutionSource,
676
+ lowResolutionSource,
677
+ capabilities,
678
+ hasLight,
679
+ hasSiren,
680
+ hasDoorbell,
681
+ hasBattery,
682
+ hasAudioSensor,
683
+ hasMotionSensor,
684
+ hasObjectSensor,
685
+ hasPtz,
686
+ camera,
687
+ snapshot,
688
+ snapshotLoading,
689
+ fetchSnapshot: fetchSnapshotFn,
690
+ probeStream: probeStreamFn,
691
+ streamUrl,
692
+ refreshStates,
693
+ reconnect: init,
694
+ close
695
+ };
696
+ }
697
+ //#endregion
698
+ //#region src/composables/useDeviceManager.ts
699
+ function useDeviceManager() {
700
+ const ctx = useCameraUi();
701
+ const namespaces = NamespaceManager.deviceManagerNamespaces();
702
+ async function getCamera(cameraIdOrName) {
703
+ const camera = await rpcCall(ctx.rpc, (rpc) => rpc.createProxy(namespaces.deviceManagerRpc).getCamera(cameraIdOrName, "@camera.ui/browser"));
704
+ if (camera) return createReactiveCameraDevice(ctx, camera);
705
+ }
706
+ return { getCamera };
707
+ }
708
+ //#endregion
709
+ //#region src/composables/useCameraById.ts
710
+ var cameraCache = createDebouncedCache({
711
+ releaseDelay: 1e3,
712
+ onRelease: (_key, device) => device.close()
713
+ });
714
+ var pendingLoads$1 = /* @__PURE__ */ new Map();
715
+ function clearCameraCache() {
716
+ cameraCache.clear();
717
+ pendingLoads$1.clear();
718
+ }
719
+ function reconnectAllCameraDevices() {
720
+ cameraCache.forEachValue((device) => {
721
+ device.reconnect().catch(() => {});
722
+ });
723
+ }
724
+ function useCameraById(cameraIdOrName) {
725
+ const { isConnected } = useCameraUi();
726
+ const deviceManager = useDeviceManager();
727
+ const camera = shallowRef();
728
+ const _isLoading = ref(false);
729
+ const initialLoadDone = ref(false);
730
+ const error = ref();
731
+ let currentCameraId;
732
+ async function loadCamera(id) {
733
+ if (!isConnected.value || !id) return;
734
+ if (currentCameraId && currentCameraId !== id) {
735
+ cameraCache.release(currentCameraId);
736
+ camera.value = void 0;
737
+ currentCameraId = void 0;
738
+ }
739
+ if (cameraCache.has(id)) {
740
+ const cached = cameraCache.acquire(id, () => {
741
+ throw new Error("Should not create - already cached");
742
+ });
743
+ currentCameraId = id;
744
+ camera.value = cached;
745
+ initialLoadDone.value = true;
746
+ return;
747
+ }
748
+ const pending = pendingLoads$1.get(id);
749
+ if (pending) {
750
+ _isLoading.value = true;
751
+ try {
752
+ const device = await pending;
753
+ if (device && cameraCache.has(id)) {
754
+ const cached = cameraCache.acquire(id, () => device);
755
+ currentCameraId = id;
756
+ camera.value = cached;
757
+ }
758
+ } catch (err) {
759
+ error.value = err instanceof Error ? err : new Error(String(err));
760
+ } finally {
761
+ _isLoading.value = false;
762
+ initialLoadDone.value = true;
763
+ }
764
+ return;
765
+ }
766
+ _isLoading.value = true;
767
+ error.value = void 0;
768
+ const loadPromise = deviceManager.getCamera(id);
769
+ pendingLoads$1.set(id, loadPromise);
770
+ try {
771
+ const device = await loadPromise;
772
+ if (device) {
773
+ cameraCache.acquire(id, () => device);
774
+ currentCameraId = id;
775
+ camera.value = device;
776
+ }
777
+ } catch (err) {
778
+ error.value = err instanceof Error ? err : new Error(String(err));
779
+ } finally {
780
+ pendingLoads$1.delete(id);
781
+ _isLoading.value = false;
782
+ initialLoadDone.value = true;
783
+ }
784
+ }
785
+ async function refresh() {
786
+ const id = toValue(cameraIdOrName);
787
+ if (!id) return;
788
+ if (currentCameraId) {
789
+ if (cameraCache.getRefCount(currentCameraId) <= 1) cameraCache.forceRelease(currentCameraId);
790
+ else cameraCache.release(currentCameraId);
791
+ camera.value = void 0;
792
+ currentCameraId = void 0;
793
+ }
794
+ await loadCamera(id);
795
+ }
796
+ watch([isConnected, () => toValue(cameraIdOrName)], async ([connected, id]) => {
797
+ if (connected && id) await loadCamera(id);
798
+ else if (!connected && currentCameraId) {
799
+ cameraCache.release(currentCameraId);
800
+ camera.value = void 0;
801
+ currentCameraId = void 0;
802
+ }
803
+ }, { immediate: true });
804
+ tryOnScopeDispose(() => {
805
+ if (currentCameraId) {
806
+ cameraCache.release(currentCameraId);
807
+ camera.value = void 0;
808
+ currentCameraId = void 0;
809
+ }
810
+ });
811
+ return {
812
+ camera,
813
+ isLoading: computed(() => _isLoading.value || !initialLoadDone.value),
814
+ error,
815
+ refresh
816
+ };
817
+ }
818
+ //#endregion
819
+ //#region src/composables/usePlugin.ts
820
+ var pluginCache = createDebouncedCache({ releaseDelay: 1e3 });
821
+ var pendingLoads = /* @__PURE__ */ new Map();
822
+ var instances = /* @__PURE__ */ new Set();
823
+ function clearPluginCache() {
824
+ pluginCache.clear();
825
+ pendingLoads.clear();
826
+ for (const reset of instances) try {
827
+ reset();
828
+ } catch {}
829
+ }
830
+ function usePlugin(pluginName) {
831
+ const { rpc, isConnected } = useCameraUi();
832
+ const plugin = shallowRef();
833
+ const contract = ref();
834
+ const _isLoading = ref(false);
835
+ const initialLoadDone = ref(false);
836
+ const error = ref();
837
+ let currentPluginName;
838
+ function acquirePlugin(name) {
839
+ if (pluginCache.has(name)) return pluginCache.acquire(name, () => {
840
+ throw new Error("Should not create - already cached");
841
+ });
842
+ }
843
+ function releasePlugin(name) {
844
+ pluginCache.release(name);
845
+ }
846
+ async function loadPlugin(name) {
847
+ if (!isConnected.value || !name) return;
848
+ if (currentPluginName && currentPluginName !== name) {
849
+ releasePlugin(currentPluginName);
850
+ plugin.value = void 0;
851
+ contract.value = void 0;
852
+ currentPluginName = void 0;
853
+ }
854
+ const cached = acquirePlugin(name);
855
+ if (cached) {
856
+ currentPluginName = name;
857
+ plugin.value = cached.proxy;
858
+ contract.value = cached.contract;
859
+ initialLoadDone.value = true;
860
+ return;
861
+ }
862
+ const pending = pendingLoads.get(name);
863
+ if (pending) {
864
+ _isLoading.value = true;
865
+ try {
866
+ if (await pending) {
867
+ const cachedAfterPending = acquirePlugin(name);
868
+ if (cachedAfterPending) {
869
+ currentPluginName = name;
870
+ plugin.value = cachedAfterPending.proxy;
871
+ contract.value = cachedAfterPending.contract;
872
+ }
873
+ }
874
+ } catch (err) {
875
+ error.value = err instanceof Error ? err : new Error(String(err));
876
+ } finally {
877
+ _isLoading.value = false;
878
+ initialLoadDone.value = true;
879
+ }
880
+ return;
881
+ }
882
+ _isLoading.value = true;
883
+ error.value = void 0;
884
+ const loadPromise = rpcCall(rpc, async (client) => {
885
+ const coreNamespaces = NamespaceManager.coreManagerNamespaces();
886
+ const pluginInfo = await client.createProxy(coreNamespaces.coreManagerRpc).getPlugin(name);
887
+ if (!pluginInfo) throw new Error(`Plugin "${name}" not found`);
888
+ const pluginNamespaces = NamespaceManager.pluginNamespaces(pluginInfo.id);
889
+ return {
890
+ proxy: client.createProxy(pluginNamespaces.pluginChildRpc),
891
+ contract: pluginInfo.contract
892
+ };
893
+ });
894
+ pendingLoads.set(name, loadPromise);
895
+ try {
896
+ const result = await loadPromise;
897
+ if (result) {
898
+ pluginCache.acquire(name, () => result);
899
+ currentPluginName = name;
900
+ plugin.value = result.proxy;
901
+ contract.value = result.contract;
902
+ }
903
+ } catch (err) {
904
+ error.value = err instanceof Error ? err : new Error(String(err));
905
+ plugin.value = void 0;
906
+ contract.value = void 0;
907
+ } finally {
908
+ pendingLoads.delete(name);
909
+ _isLoading.value = false;
910
+ initialLoadDone.value = true;
911
+ }
912
+ }
913
+ async function refresh() {
914
+ const name = toValue(pluginName);
915
+ if (!name) return;
916
+ if (currentPluginName) {
917
+ if (pluginCache.getRefCount(currentPluginName) <= 1) pluginCache.forceRelease(currentPluginName);
918
+ else pluginCache.release(currentPluginName);
919
+ plugin.value = void 0;
920
+ contract.value = void 0;
921
+ currentPluginName = void 0;
922
+ }
923
+ await loadPlugin(name);
924
+ }
925
+ const resetInstance = () => {
926
+ if (!currentPluginName) return;
927
+ plugin.value = void 0;
928
+ contract.value = void 0;
929
+ currentPluginName = void 0;
930
+ };
931
+ instances.add(resetInstance);
932
+ watch([
933
+ isConnected,
934
+ () => toValue(pluginName),
935
+ rpc
936
+ ], async ([connected, name, currentClient], oldValues) => {
937
+ const prevClient = oldValues?.[2];
938
+ if (currentPluginName && prevClient && prevClient !== currentClient) {
939
+ releasePlugin(currentPluginName);
940
+ plugin.value = void 0;
941
+ contract.value = void 0;
942
+ currentPluginName = void 0;
943
+ }
944
+ if (connected && name && currentClient) await loadPlugin(name);
945
+ else if ((!connected || !currentClient) && currentPluginName) {
946
+ releasePlugin(currentPluginName);
947
+ plugin.value = void 0;
948
+ contract.value = void 0;
949
+ currentPluginName = void 0;
950
+ }
951
+ }, { immediate: true });
952
+ tryOnScopeDispose(() => {
953
+ instances.delete(resetInstance);
954
+ if (currentPluginName) {
955
+ releasePlugin(currentPluginName);
956
+ plugin.value = void 0;
957
+ contract.value = void 0;
958
+ currentPluginName = void 0;
959
+ }
960
+ });
961
+ return {
962
+ plugin,
963
+ contract,
964
+ isLoading: computed(() => _isLoading.value || !initialLoadDone.value),
965
+ error,
966
+ refresh
967
+ };
968
+ }
969
+ //#endregion
970
+ //#region src/composables/utils.ts
971
+ function extractCameraId(camera) {
972
+ if (!camera) return void 0;
973
+ if (typeof camera === "string") return camera;
974
+ return camera.id;
975
+ }
976
+ //#endregion
977
+ //#region src/composables/useSensor.ts
978
+ function createSensorTypeGuard(sensorType) {
979
+ return (sensor) => sensor.type === sensorType;
980
+ }
981
+ var isReactiveLightControl = createSensorTypeGuard(SensorType.Light);
982
+ var isReactiveSirenControl = createSensorTypeGuard(SensorType.Siren);
983
+ var isReactiveBatteryInfo = createSensorTypeGuard(SensorType.Battery);
984
+ var isReactiveDoorbellTrigger = createSensorTypeGuard(SensorType.Doorbell);
985
+ var isReactiveContactSensor = createSensorTypeGuard(SensorType.Contact);
986
+ var isReactiveMotionSensor = createSensorTypeGuard(SensorType.Motion);
987
+ var isReactiveObjectSensor = createSensorTypeGuard(SensorType.Object);
988
+ var isReactiveAudioSensor = createSensorTypeGuard(SensorType.Audio);
989
+ var isReactiveFaceSensor = createSensorTypeGuard(SensorType.Face);
990
+ var isReactiveLicensePlateSensor = createSensorTypeGuard(SensorType.LicensePlate);
991
+ var isReactiveClassifierSensor = createSensorTypeGuard(SensorType.Classifier);
992
+ var isReactivePTZControl = createSensorTypeGuard(SensorType.PTZ);
993
+ var isReactiveSwitchControl = createSensorTypeGuard(SensorType.Switch);
994
+ var isReactiveLockControl = createSensorTypeGuard(SensorType.Lock);
995
+ var isReactiveSecuritySystem = createSensorTypeGuard(SensorType.SecuritySystem);
996
+ var isReactiveTemperatureInfo = createSensorTypeGuard(SensorType.Temperature);
997
+ var isReactiveHumidityInfo = createSensorTypeGuard(SensorType.Humidity);
998
+ var isReactiveOccupancySensor = createSensorTypeGuard(SensorType.Occupancy);
999
+ var isReactiveSmokeSensor = createSensorTypeGuard(SensorType.Smoke);
1000
+ var isReactiveLeakSensor = createSensorTypeGuard(SensorType.Leak);
1001
+ var isReactiveGarageControl = createSensorTypeGuard(SensorType.Garage);
1002
+ function createReactiveSensor(data, state, rpcRef, cameraId) {
1003
+ const displayName = ref(state.displayName ?? data.displayName ?? data.name);
1004
+ const capabilities = ref(state.capabilities ?? data.capabilities ?? []);
1005
+ const properties = reactive({ ...state.properties });
1006
+ const capabilityCallbacks = [];
1007
+ return {
1008
+ id: data.id,
1009
+ type: data.type,
1010
+ name: data.name,
1011
+ displayName,
1012
+ pluginId: data.pluginId,
1013
+ capabilities,
1014
+ properties,
1015
+ getProperty(property) {
1016
+ return properties[property];
1017
+ },
1018
+ async setProperty(property, value) {
1019
+ const sensorNamespace = `plugin.${data.pluginId}.camera.${cameraId}.sensor.${data.id}.rpc`;
1020
+ await rpcCall(rpcRef, (client) => client.createProxy(sensorNamespace).updateValue(property, value));
1021
+ },
1022
+ async setDisplayName(newDisplayName) {
1023
+ const namespace = NamespaceManager.sensorControllerNamespaces(cameraId);
1024
+ await rpcCall(rpcRef, (client) => client.createProxy(namespace.sensorRpc).setDisplayName(data.id, newDisplayName));
1025
+ },
1026
+ hasCapability(capability) {
1027
+ return capabilities.value.includes(capability);
1028
+ },
1029
+ onCapabilitiesChanged(callback) {
1030
+ capabilityCallbacks.push(callback);
1031
+ return () => {
1032
+ const index = capabilityCallbacks.indexOf(callback);
1033
+ if (index !== -1) capabilityCallbacks.splice(index, 1);
1034
+ };
1035
+ },
1036
+ _notifyCapabilitiesChanged() {
1037
+ for (const callback of capabilityCallbacks) callback(capabilities.value);
1038
+ }
1039
+ };
1040
+ }
1041
+ function createSensorManager(rpcOrContext, cameraId, sensorSubjectNamespace, sensorRpcNamespace) {
1042
+ const ctx = "rpc" in rpcOrContext && "value" in rpcOrContext.rpc ? rpcOrContext : void 0;
1043
+ const rpcRef = ctx ? ctx.rpc : shallowRef(rpcOrContext);
1044
+ const sensorMap = shallowRef(/* @__PURE__ */ new Map());
1045
+ let globalUnsubscribe;
1046
+ const sensorSubscriptions = /* @__PURE__ */ new Map();
1047
+ const initialized = ref(false);
1048
+ let initPromise;
1049
+ function handlePerSensorEvent(sensorId, message) {
1050
+ const sensor = sensorMap.value.get(sensorId);
1051
+ if (!sensor) return;
1052
+ if (message.type === "property:changed") {
1053
+ const event = message.data;
1054
+ sensor.properties[event.property] = event.value;
1055
+ } else if (message.type === "sensor:displayName:changed") {
1056
+ const event = message.data;
1057
+ sensor.displayName.value = event.displayName;
1058
+ } else if (message.type === "sensor:capabilities:changed") {
1059
+ const event = message.data;
1060
+ sensor.capabilities.value = event.capabilities;
1061
+ sensor._notifyCapabilitiesChanged();
1062
+ }
1063
+ }
1064
+ async function subscribeToSensorEvents(sensorId) {
1065
+ if (sensorSubscriptions.has(sensorId)) return;
1066
+ const namespace = NamespaceManager.sensorEventNamespaces(cameraId, sensorId);
1067
+ const unsubscribe = await rpcCall(rpcRef, (c) => c.subscribe(namespace.sensorSubject, (msg) => handlePerSensorEvent(sensorId, msg)));
1068
+ sensorSubscriptions.set(sensorId, unsubscribe);
1069
+ }
1070
+ function unsubscribeFromSensorEvents(sensorId) {
1071
+ const unsubscribe = sensorSubscriptions.get(sensorId);
1072
+ if (unsubscribe) {
1073
+ unsubscribe();
1074
+ sensorSubscriptions.delete(sensorId);
1075
+ }
1076
+ }
1077
+ function handleGlobalSensorEvent(message) {
1078
+ if (message.type === "sensor:added") {
1079
+ const event = message.data;
1080
+ if (sensorMap.value.has(event.sensor.id)) return;
1081
+ const reactiveSensor = createReactiveSensor(event.sensor, event.state, rpcRef, cameraId);
1082
+ const newMap = new Map(sensorMap.value);
1083
+ newMap.set(event.sensor.id, reactiveSensor);
1084
+ sensorMap.value = newMap;
1085
+ subscribeToSensorEvents(event.sensor.id);
1086
+ } else if (message.type === "sensor:removed") {
1087
+ const event = message.data;
1088
+ unsubscribeFromSensorEvents(event.sensorId);
1089
+ const newMap = new Map(sensorMap.value);
1090
+ newMap.delete(event.sensorId);
1091
+ sensorMap.value = newMap;
1092
+ }
1093
+ }
1094
+ async function doInit() {
1095
+ globalUnsubscribe?.();
1096
+ globalUnsubscribe = void 0;
1097
+ for (const unsubscribe of sensorSubscriptions.values()) unsubscribe();
1098
+ sensorSubscriptions.clear();
1099
+ sensorMap.value = /* @__PURE__ */ new Map();
1100
+ let pendingEvents = [];
1101
+ globalUnsubscribe = await rpcCall(rpcRef, (c) => c.subscribe(sensorSubjectNamespace, (msg) => {
1102
+ if (pendingEvents) pendingEvents.push(msg);
1103
+ else handleGlobalSensorEvent(msg);
1104
+ }));
1105
+ const [sensors, states] = await rpcCall(rpcRef, async (c) => {
1106
+ const proxy = c.createProxy(sensorRpcNamespace);
1107
+ return Promise.all([proxy.getSensors(), proxy.getSensorStates()]);
1108
+ });
1109
+ const newMap = /* @__PURE__ */ new Map();
1110
+ for (const sensor of sensors) {
1111
+ const reactiveSensor = createReactiveSensor(sensor, states[sensor.id] ?? {
1112
+ properties: {},
1113
+ capabilities: [],
1114
+ type: sensor.type
1115
+ }, rpcRef, cameraId);
1116
+ newMap.set(sensor.id, reactiveSensor);
1117
+ }
1118
+ sensorMap.value = newMap;
1119
+ for (const sensorId of newMap.keys()) await subscribeToSensorEvents(sensorId);
1120
+ const queued = pendingEvents;
1121
+ pendingEvents = void 0;
1122
+ for (const msg of queued) handleGlobalSensorEvent(msg);
1123
+ initialized.value = true;
1124
+ }
1125
+ async function ensureInitialized() {
1126
+ if (initialized.value) return;
1127
+ if (!initPromise) initPromise = doInit().finally(() => {
1128
+ initPromise = void 0;
1129
+ });
1130
+ return initPromise;
1131
+ }
1132
+ async function reconnect() {
1133
+ initialized.value = false;
1134
+ if (!initPromise) initPromise = doInit().finally(() => {
1135
+ initPromise = void 0;
1136
+ });
1137
+ return initPromise;
1138
+ }
1139
+ function close() {
1140
+ globalUnsubscribe?.();
1141
+ globalUnsubscribe = void 0;
1142
+ for (const unsubscribe of sensorSubscriptions.values()) unsubscribe();
1143
+ sensorSubscriptions.clear();
1144
+ sensorMap.value.clear();
1145
+ initialized.value = false;
1146
+ initPromise = void 0;
1147
+ }
1148
+ const sensors = computed(() => Array.from(sensorMap.value.values()));
1149
+ return {
1150
+ sensors,
1151
+ isInitialized: computed(() => initialized.value),
1152
+ getSensor(sensorId) {
1153
+ return sensorMap.value.get(sensorId);
1154
+ },
1155
+ getSensorsByType(type) {
1156
+ return sensors.value.filter((s) => s.type === type);
1157
+ },
1158
+ hasSensorType(type) {
1159
+ return sensors.value.some((s) => s.type === type);
1160
+ },
1161
+ async setDisplayName(sensorId, displayName) {
1162
+ await rpcCall(rpcRef, (c) => c.createProxy(sensorRpcNamespace).setDisplayName(sensorId, displayName));
1163
+ },
1164
+ ensureInitialized,
1165
+ reconnect,
1166
+ close
1167
+ };
1168
+ }
1169
+ var sensorManagerCache = createDebouncedCache({
1170
+ releaseDelay: 1e3,
1171
+ onRelease: (_key, cached) => cached.manager.close()
1172
+ });
1173
+ function clearSensorCache() {
1174
+ sensorManagerCache.clear();
1175
+ }
1176
+ function reconnectAllSensorManagers() {
1177
+ sensorManagerCache.forEachValue((cached) => {
1178
+ cached.initPromise = cached.manager.reconnect();
1179
+ });
1180
+ }
1181
+ function acquireSensorManager(cameraId, rpcOrContext) {
1182
+ return sensorManagerCache.acquire(cameraId, () => {
1183
+ const namespaces = NamespaceManager.sensorControllerNamespaces(cameraId);
1184
+ return { manager: createSensorManager(rpcOrContext, cameraId, namespaces.sensorSubject, namespaces.sensorRpc) };
1185
+ });
1186
+ }
1187
+ function releaseSensorManager(cameraId) {
1188
+ sensorManagerCache.release(cameraId);
1189
+ }
1190
+ function createSensorComposableState() {
1191
+ return {
1192
+ currentCameraId: void 0,
1193
+ cachedManager: void 0
1194
+ };
1195
+ }
1196
+ async function ensureSensorManager(state, cameraUi, isConnected, cameraId, isLoading, error) {
1197
+ if (!cameraUi.rpc.value || !isConnected) return void 0;
1198
+ if (state.currentCameraId && state.currentCameraId !== cameraId) {
1199
+ releaseSensorManager(state.currentCameraId);
1200
+ state.currentCameraId = void 0;
1201
+ state.cachedManager = void 0;
1202
+ }
1203
+ isLoading.value = true;
1204
+ error.value = void 0;
1205
+ try {
1206
+ const cached = acquireSensorManager(cameraId, cameraUi);
1207
+ state.currentCameraId = cameraId;
1208
+ state.cachedManager = cached.manager;
1209
+ if (!cached.initPromise) cached.initPromise = cached.manager.ensureInitialized();
1210
+ await cached.initPromise;
1211
+ return cached.manager;
1212
+ } catch (err) {
1213
+ error.value = err instanceof Error ? err : new Error(String(err));
1214
+ return;
1215
+ } finally {
1216
+ isLoading.value = false;
1217
+ }
1218
+ }
1219
+ function cleanupSensorComposable(state) {
1220
+ if (state.currentCameraId) {
1221
+ releaseSensorManager(state.currentCameraId);
1222
+ state.currentCameraId = void 0;
1223
+ state.cachedManager = void 0;
1224
+ }
1225
+ }
1226
+ function useSensorById(camera, sensorId) {
1227
+ const cameraUi = useCameraUi();
1228
+ const { isConnected } = cameraUi;
1229
+ const sensor = shallowRef();
1230
+ const _isLoading = ref(false);
1231
+ const initialLoadDone = ref(false);
1232
+ const error = ref();
1233
+ const state = createSensorComposableState();
1234
+ watch([
1235
+ isConnected,
1236
+ () => extractCameraId(toValue(camera)),
1237
+ () => toValue(sensorId)
1238
+ ], async ([connected, camId, senId]) => {
1239
+ if (connected && camId && senId) {
1240
+ sensor.value = (await ensureSensorManager(state, cameraUi, connected, camId, _isLoading, error))?.getSensor(senId);
1241
+ initialLoadDone.value = true;
1242
+ } else {
1243
+ cleanupSensorComposable(state);
1244
+ sensor.value = void 0;
1245
+ }
1246
+ }, { immediate: true });
1247
+ tryOnScopeDispose(() => {
1248
+ cleanupSensorComposable(state);
1249
+ sensor.value = void 0;
1250
+ });
1251
+ return {
1252
+ sensor,
1253
+ isLoading: computed(() => _isLoading.value || !initialLoadDone.value),
1254
+ error
1255
+ };
1256
+ }
1257
+ function useSensorByType(camera, sensorType) {
1258
+ const cameraUi = useCameraUi();
1259
+ const { isConnected } = cameraUi;
1260
+ const sensor = shallowRef();
1261
+ const _isLoading = ref(false);
1262
+ const initialLoadDone = ref(false);
1263
+ const error = ref();
1264
+ const state = createSensorComposableState();
1265
+ watch([
1266
+ isConnected,
1267
+ () => extractCameraId(toValue(camera)),
1268
+ () => toValue(sensorType)
1269
+ ], async ([connected, camId, type]) => {
1270
+ if (connected && camId) {
1271
+ sensor.value = (await ensureSensorManager(state, cameraUi, connected, camId, _isLoading, error))?.getSensorsByType(type)[0];
1272
+ initialLoadDone.value = true;
1273
+ } else {
1274
+ cleanupSensorComposable(state);
1275
+ sensor.value = void 0;
1276
+ }
1277
+ }, { immediate: true });
1278
+ tryOnScopeDispose(() => {
1279
+ cleanupSensorComposable(state);
1280
+ sensor.value = void 0;
1281
+ });
1282
+ return {
1283
+ sensor,
1284
+ isLoading: computed(() => _isLoading.value || !initialLoadDone.value),
1285
+ error
1286
+ };
1287
+ }
1288
+ function useSensorsByType(camera, sensorType) {
1289
+ const cameraUi = useCameraUi();
1290
+ const { isConnected } = cameraUi;
1291
+ const sensorsRef = shallowRef([]);
1292
+ const cachedManager = shallowRef();
1293
+ const _isLoading = ref(false);
1294
+ const initialLoadDone = ref(false);
1295
+ const error = ref();
1296
+ const state = createSensorComposableState();
1297
+ watch([
1298
+ isConnected,
1299
+ () => extractCameraId(toValue(camera)),
1300
+ () => toValue(sensorType)
1301
+ ], async ([connected, camId, type]) => {
1302
+ if (connected && camId) {
1303
+ const manager = await ensureSensorManager(state, cameraUi, connected, camId, _isLoading, error);
1304
+ cachedManager.value = manager;
1305
+ sensorsRef.value = manager?.getSensorsByType(type) ?? [];
1306
+ initialLoadDone.value = true;
1307
+ } else {
1308
+ cleanupSensorComposable(state);
1309
+ sensorsRef.value = [];
1310
+ cachedManager.value = void 0;
1311
+ }
1312
+ }, { immediate: true });
1313
+ const sensors = computed(() => {
1314
+ if (!cachedManager.value) return [];
1315
+ const type = toValue(sensorType);
1316
+ return cachedManager.value.sensors.value.filter((s) => s.type === type);
1317
+ });
1318
+ tryOnScopeDispose(() => {
1319
+ cleanupSensorComposable(state);
1320
+ sensorsRef.value = [];
1321
+ });
1322
+ return {
1323
+ sensors,
1324
+ isLoading: computed(() => _isLoading.value || !initialLoadDone.value),
1325
+ error
1326
+ };
1327
+ }
1328
+ function useSensors(camera) {
1329
+ const cameraUi = useCameraUi();
1330
+ const { isConnected } = cameraUi;
1331
+ const sensorsRef = shallowRef([]);
1332
+ const cachedManager = shallowRef();
1333
+ const _isLoading = ref(false);
1334
+ const initialLoadDone = ref(false);
1335
+ const error = ref();
1336
+ const state = createSensorComposableState();
1337
+ watch([isConnected, () => extractCameraId(toValue(camera))], async ([connected, camId]) => {
1338
+ if (connected && camId) {
1339
+ const manager = await ensureSensorManager(state, cameraUi, connected, camId, _isLoading, error);
1340
+ cachedManager.value = manager;
1341
+ sensorsRef.value = manager?.sensors.value ?? [];
1342
+ initialLoadDone.value = true;
1343
+ } else {
1344
+ cleanupSensorComposable(state);
1345
+ sensorsRef.value = [];
1346
+ cachedManager.value = void 0;
1347
+ }
1348
+ }, { immediate: true });
1349
+ const sensors = computed(() => cachedManager.value?.sensors.value ?? []);
1350
+ tryOnScopeDispose(() => {
1351
+ cleanupSensorComposable(state);
1352
+ sensorsRef.value = [];
1353
+ });
1354
+ return {
1355
+ sensors,
1356
+ isLoading: computed(() => _isLoading.value || !initialLoadDone.value),
1357
+ error
1358
+ };
1359
+ }
1360
+ function useTypedSensor(camera, sensorType) {
1361
+ const cameraUi = useCameraUi();
1362
+ const { isConnected } = cameraUi;
1363
+ const sensor = shallowRef();
1364
+ const cachedManager = shallowRef();
1365
+ const _isLoading = ref(false);
1366
+ const initialLoadDone = ref(false);
1367
+ const error = ref();
1368
+ const state = createSensorComposableState();
1369
+ watch([isConnected, () => extractCameraId(toValue(camera))], async ([connected, camId]) => {
1370
+ if (connected && camId) {
1371
+ const manager = await ensureSensorManager(state, cameraUi, connected, camId, _isLoading, error);
1372
+ cachedManager.value = manager;
1373
+ sensor.value = manager?.getSensorsByType(sensorType)[0];
1374
+ initialLoadDone.value = true;
1375
+ } else {
1376
+ cleanupSensorComposable(state);
1377
+ sensor.value = void 0;
1378
+ }
1379
+ }, { immediate: true });
1380
+ watch(() => {
1381
+ if (!cachedManager.value) return void 0;
1382
+ return cachedManager.value.getSensorsByType(sensorType)[0]?.id;
1383
+ }, () => {
1384
+ if (cachedManager.value) sensor.value = cachedManager.value.getSensorsByType(sensorType)[0];
1385
+ });
1386
+ tryOnScopeDispose(() => {
1387
+ cleanupSensorComposable(state);
1388
+ sensor.value = void 0;
1389
+ });
1390
+ return {
1391
+ sensor,
1392
+ isLoading: computed(() => _isLoading.value || !initialLoadDone.value),
1393
+ error
1394
+ };
1395
+ }
1396
+ function useMotionSensor(camera) {
1397
+ return useTypedSensor(camera, SensorType.Motion);
1398
+ }
1399
+ function useObjectSensor(camera) {
1400
+ return useTypedSensor(camera, SensorType.Object);
1401
+ }
1402
+ function useFaceSensor(camera) {
1403
+ return useTypedSensor(camera, SensorType.Face);
1404
+ }
1405
+ function useLicensePlateSensor(camera) {
1406
+ return useTypedSensor(camera, SensorType.LicensePlate);
1407
+ }
1408
+ function useAudioSensor(camera) {
1409
+ return useTypedSensor(camera, SensorType.Audio);
1410
+ }
1411
+ function usePTZControl(camera) {
1412
+ return useTypedSensor(camera, SensorType.PTZ);
1413
+ }
1414
+ function useClassifierSensors(camera) {
1415
+ const cameraUi = useCameraUi();
1416
+ const { isConnected } = cameraUi;
1417
+ const sensorsRef = shallowRef([]);
1418
+ const cachedManager = shallowRef();
1419
+ const _isLoading = ref(false);
1420
+ const initialLoadDone = ref(false);
1421
+ const error = ref();
1422
+ const state = createSensorComposableState();
1423
+ watch([isConnected, () => extractCameraId(toValue(camera))], async ([connected, camId]) => {
1424
+ if (connected && camId) {
1425
+ const manager = await ensureSensorManager(state, cameraUi, connected, camId, _isLoading, error);
1426
+ cachedManager.value = manager;
1427
+ sensorsRef.value = manager?.getSensorsByType(SensorType.Classifier) ?? [];
1428
+ initialLoadDone.value = true;
1429
+ } else {
1430
+ cleanupSensorComposable(state);
1431
+ sensorsRef.value = [];
1432
+ cachedManager.value = void 0;
1433
+ }
1434
+ }, { immediate: true });
1435
+ const sensors = computed(() => cachedManager.value?.getSensorsByType(SensorType.Classifier) ?? []);
1436
+ tryOnScopeDispose(() => {
1437
+ cleanupSensorComposable(state);
1438
+ sensorsRef.value = [];
1439
+ });
1440
+ return {
1441
+ sensors,
1442
+ isLoading: computed(() => _isLoading.value || !initialLoadDone.value),
1443
+ error
1444
+ };
1445
+ }
1446
+ //#endregion
1447
+ //#region src/composables/resolvePluginId.ts
1448
+ var pluginIdCache = /* @__PURE__ */ new Map();
1449
+ function clearPluginIdCache() {
1450
+ pluginIdCache.clear();
1451
+ }
1452
+ async function resolvePluginId(rpcRef, pluginName) {
1453
+ const cached = pluginIdCache.get(pluginName);
1454
+ if (cached) return cached;
1455
+ const namespaces = NamespaceManager.coreManagerNamespaces();
1456
+ const pluginInfo = await rpcCall(rpcRef, (rpc) => rpc.createProxy(namespaces.coreManagerRpc).getPlugin(pluginName));
1457
+ if (pluginInfo) {
1458
+ pluginIdCache.set(pluginName, pluginInfo.id);
1459
+ return pluginInfo.id;
1460
+ }
1461
+ }
1462
+ //#endregion
1463
+ //#region src/composables/useStorage.ts
1464
+ function createReactiveStorage(proxy) {
1465
+ const config = shallowRef();
1466
+ const isLoading = ref(false);
1467
+ const error = ref();
1468
+ async function getConfig() {
1469
+ isLoading.value = true;
1470
+ error.value = void 0;
1471
+ try {
1472
+ config.value = await proxy.getConfig();
1473
+ return config.value;
1474
+ } catch (err) {
1475
+ error.value = err instanceof Error ? err : new Error(String(err));
1476
+ return;
1477
+ } finally {
1478
+ isLoading.value = false;
1479
+ }
1480
+ }
1481
+ async function setValue(key, value) {
1482
+ isLoading.value = true;
1483
+ error.value = void 0;
1484
+ try {
1485
+ await proxy.setValue(key, value);
1486
+ await getConfig();
1487
+ } catch (err) {
1488
+ error.value = err instanceof Error ? err : new Error(String(err));
1489
+ throw err;
1490
+ } finally {
1491
+ isLoading.value = false;
1492
+ }
1493
+ }
1494
+ async function setConfig(newConfig) {
1495
+ isLoading.value = true;
1496
+ error.value = void 0;
1497
+ try {
1498
+ await proxy.setConfig(newConfig);
1499
+ await getConfig();
1500
+ } catch (err) {
1501
+ error.value = err instanceof Error ? err : new Error(String(err));
1502
+ throw err;
1503
+ } finally {
1504
+ isLoading.value = false;
1505
+ }
1506
+ }
1507
+ async function submitValue(key, value) {
1508
+ isLoading.value = true;
1509
+ error.value = void 0;
1510
+ try {
1511
+ const response = await proxy.submitValue(key, value);
1512
+ if (!response?.toast || response.toast.type !== "error") await getConfig();
1513
+ return response;
1514
+ } catch (err) {
1515
+ error.value = err instanceof Error ? err : new Error(String(err));
1516
+ throw err;
1517
+ } finally {
1518
+ isLoading.value = false;
1519
+ }
1520
+ }
1521
+ return {
1522
+ proxy,
1523
+ config,
1524
+ isLoading,
1525
+ error,
1526
+ getConfig,
1527
+ setValue,
1528
+ setConfig,
1529
+ submitValue
1530
+ };
1531
+ }
1532
+ var storageCache = createDebouncedCache({ releaseDelay: 1e3 });
1533
+ function getPluginStorageKey(pluginId) {
1534
+ return `plugin:${pluginId}`;
1535
+ }
1536
+ function getCameraStorageKey(pluginId, cameraId) {
1537
+ return `plugin:${pluginId}:camera:${cameraId}`;
1538
+ }
1539
+ function getSensorStorageKey(pluginId, cameraId, sensorId) {
1540
+ return `plugin:${pluginId}:camera:${cameraId}:sensor:${sensorId}`;
1541
+ }
1542
+ function acquireStorage(key, createProxy) {
1543
+ return storageCache.acquire(key, () => {
1544
+ return createReactiveStorage(createProxy());
1545
+ });
1546
+ }
1547
+ function releaseStorage(key) {
1548
+ storageCache.release(key);
1549
+ }
1550
+ function clearStorageCache() {
1551
+ storageCache.clear();
1552
+ clearPluginIdCache();
1553
+ }
1554
+ function createStorageState() {
1555
+ return {
1556
+ currentStorageKey: void 0,
1557
+ cachedStorage: void 0
1558
+ };
1559
+ }
1560
+ function cleanupStorage(state, isConnected, config) {
1561
+ if (state.currentStorageKey) {
1562
+ releaseStorage(state.currentStorageKey);
1563
+ state.currentStorageKey = void 0;
1564
+ }
1565
+ isConnected.value = false;
1566
+ state.cachedStorage = void 0;
1567
+ config.value = void 0;
1568
+ }
1569
+ function createStorageOperations(state, config, isLoading, error) {
1570
+ async function getConfig() {
1571
+ const cached = state.cachedStorage;
1572
+ if (!cached) return void 0;
1573
+ const result = await cached.getConfig();
1574
+ if (state.cachedStorage !== cached) return result;
1575
+ config.value = cached.config.value;
1576
+ isLoading.value = cached.isLoading.value;
1577
+ error.value = cached.error.value;
1578
+ return result;
1579
+ }
1580
+ async function setValue(key, value) {
1581
+ const cached = state.cachedStorage;
1582
+ if (!cached) throw new Error("Storage not connected");
1583
+ isLoading.value = true;
1584
+ error.value = void 0;
1585
+ try {
1586
+ await cached.setValue(key, value);
1587
+ if (state.cachedStorage === cached) config.value = cached.config.value;
1588
+ } catch (err) {
1589
+ error.value = err instanceof Error ? err : new Error(String(err));
1590
+ throw err;
1591
+ } finally {
1592
+ isLoading.value = false;
1593
+ }
1594
+ }
1595
+ async function setConfig(newConfig) {
1596
+ const cached = state.cachedStorage;
1597
+ if (!cached) throw new Error("Storage not connected");
1598
+ isLoading.value = true;
1599
+ error.value = void 0;
1600
+ try {
1601
+ await cached.setConfig(newConfig);
1602
+ if (state.cachedStorage === cached) config.value = cached.config.value;
1603
+ } catch (err) {
1604
+ error.value = err instanceof Error ? err : new Error(String(err));
1605
+ throw err;
1606
+ } finally {
1607
+ isLoading.value = false;
1608
+ }
1609
+ }
1610
+ async function submitValue(key, value) {
1611
+ const cached = state.cachedStorage;
1612
+ if (!cached) throw new Error("Storage not connected");
1613
+ isLoading.value = true;
1614
+ error.value = void 0;
1615
+ try {
1616
+ const result = await cached.submitValue(key, value);
1617
+ if (state.cachedStorage === cached) config.value = cached.config.value;
1618
+ return result;
1619
+ } catch (err) {
1620
+ error.value = err instanceof Error ? err : new Error(String(err));
1621
+ throw err;
1622
+ } finally {
1623
+ isLoading.value = false;
1624
+ }
1625
+ }
1626
+ return {
1627
+ getConfig,
1628
+ setValue,
1629
+ setConfig,
1630
+ submitValue
1631
+ };
1632
+ }
1633
+ function usePluginStorage(pluginName) {
1634
+ const { rpc, isConnected: clientConnected } = useCameraUi();
1635
+ const config = shallowRef();
1636
+ const _isLoading = ref(false);
1637
+ const initialSetupDone = ref(false);
1638
+ const error = ref();
1639
+ const isConnected = ref(false);
1640
+ const state = createStorageState();
1641
+ const operations = createStorageOperations(state, config, _isLoading, error);
1642
+ async function connect(name) {
1643
+ if (!rpc.value || !clientConnected.value) return false;
1644
+ try {
1645
+ const pluginId = await resolvePluginId(rpc, name);
1646
+ if (!pluginId) throw new Error(`Plugin "${name}" not found`);
1647
+ const storageKey = getPluginStorageKey(pluginId);
1648
+ if (state.currentStorageKey && state.currentStorageKey !== storageKey) releaseStorage(state.currentStorageKey);
1649
+ state.currentStorageKey = storageKey;
1650
+ state.cachedStorage = acquireStorage(storageKey, () => {
1651
+ const namespaces = NamespaceManager.pluginNamespaces(pluginId);
1652
+ return rpc.value.createProxy(namespaces.pluginStorageRpc);
1653
+ });
1654
+ isConnected.value = true;
1655
+ return true;
1656
+ } catch (err) {
1657
+ error.value = err instanceof Error ? err : new Error(String(err));
1658
+ isConnected.value = false;
1659
+ return false;
1660
+ }
1661
+ }
1662
+ async function getConfig() {
1663
+ if (!state.cachedStorage) {
1664
+ const name = toValue(pluginName);
1665
+ if (!name) return void 0;
1666
+ if (!await connect(name)) return void 0;
1667
+ }
1668
+ return operations.getConfig();
1669
+ }
1670
+ watch([clientConnected, () => toValue(pluginName)], async ([connected, name]) => {
1671
+ if (connected && name) {
1672
+ await connect(name);
1673
+ operations.getConfig();
1674
+ } else cleanupStorage(state, isConnected, config);
1675
+ initialSetupDone.value = true;
1676
+ }, { immediate: true });
1677
+ tryOnScopeDispose(() => cleanupStorage(state, isConnected, config));
1678
+ return {
1679
+ config,
1680
+ isLoading: computed(() => _isLoading.value || !initialSetupDone.value),
1681
+ error,
1682
+ isConnected,
1683
+ getConfig,
1684
+ setValue: operations.setValue,
1685
+ setConfig: operations.setConfig,
1686
+ submitValue: operations.submitValue
1687
+ };
1688
+ }
1689
+ function useCameraStorage(camera, pluginName) {
1690
+ const { rpc, isConnected: clientConnected } = useCameraUi();
1691
+ const config = shallowRef();
1692
+ const _isLoading = ref(false);
1693
+ const initialSetupDone = ref(false);
1694
+ const error = ref();
1695
+ const isConnected = ref(false);
1696
+ const state = createStorageState();
1697
+ const operations = createStorageOperations(state, config, _isLoading, error);
1698
+ async function connect(cameraId, name) {
1699
+ if (!rpc.value || !clientConnected.value) return false;
1700
+ try {
1701
+ const pluginId = await resolvePluginId(rpc, name);
1702
+ if (!pluginId) throw new Error(`Plugin "${name}" not found`);
1703
+ const storageKey = getCameraStorageKey(pluginId, cameraId);
1704
+ if (state.currentStorageKey && state.currentStorageKey !== storageKey) releaseStorage(state.currentStorageKey);
1705
+ state.currentStorageKey = storageKey;
1706
+ state.cachedStorage = acquireStorage(storageKey, () => {
1707
+ const namespaces = NamespaceManager.pluginCameraNamespaces(pluginId, cameraId);
1708
+ return rpc.value.createProxy(namespaces.cameraStorageRpc);
1709
+ });
1710
+ isConnected.value = true;
1711
+ return true;
1712
+ } catch (err) {
1713
+ error.value = err instanceof Error ? err : new Error(String(err));
1714
+ isConnected.value = false;
1715
+ return false;
1716
+ }
1717
+ }
1718
+ async function getConfig() {
1719
+ const cameraId = extractCameraId(toValue(camera));
1720
+ const name = toValue(pluginName);
1721
+ if (!cameraId || !name) return void 0;
1722
+ if (!state.cachedStorage) {
1723
+ if (!await connect(cameraId, name)) return void 0;
1724
+ }
1725
+ return operations.getConfig();
1726
+ }
1727
+ watch([
1728
+ clientConnected,
1729
+ () => extractCameraId(toValue(camera)),
1730
+ () => toValue(pluginName)
1731
+ ], async ([connected, cameraId, name]) => {
1732
+ if (connected && cameraId && name) {
1733
+ await connect(cameraId, name);
1734
+ operations.getConfig();
1735
+ } else cleanupStorage(state, isConnected, config);
1736
+ initialSetupDone.value = true;
1737
+ }, { immediate: true });
1738
+ tryOnScopeDispose(() => cleanupStorage(state, isConnected, config));
1739
+ return {
1740
+ config,
1741
+ isLoading: computed(() => _isLoading.value || !initialSetupDone.value),
1742
+ error,
1743
+ isConnected,
1744
+ getConfig,
1745
+ setValue: operations.setValue,
1746
+ setConfig: operations.setConfig,
1747
+ submitValue: operations.submitValue
1748
+ };
1749
+ }
1750
+ function useSensorStorage(camera, sensorId, pluginId) {
1751
+ const { rpc, isConnected: clientConnected } = useCameraUi();
1752
+ const config = shallowRef();
1753
+ const _isLoading = ref(false);
1754
+ const initialSetupDone = ref(false);
1755
+ const error = ref();
1756
+ const isConnected = ref(false);
1757
+ const state = createStorageState();
1758
+ const operations = createStorageOperations(state, config, _isLoading, error);
1759
+ function connect(cameraId, senId, plugId) {
1760
+ if (!rpc.value || !clientConnected.value) return false;
1761
+ try {
1762
+ const storageKey = getSensorStorageKey(plugId, cameraId, senId);
1763
+ if (state.currentStorageKey && state.currentStorageKey !== storageKey) releaseStorage(state.currentStorageKey);
1764
+ state.currentStorageKey = storageKey;
1765
+ state.cachedStorage = acquireStorage(storageKey, () => {
1766
+ const namespaces = NamespaceManager.pluginSensorNamespaces(plugId, cameraId, senId);
1767
+ return rpc.value.createProxy(namespaces.sensorStorageRpc);
1768
+ });
1769
+ isConnected.value = true;
1770
+ return true;
1771
+ } catch (err) {
1772
+ error.value = err instanceof Error ? err : new Error(String(err));
1773
+ isConnected.value = false;
1774
+ return false;
1775
+ }
1776
+ }
1777
+ async function getConfig() {
1778
+ const cameraId = extractCameraId(toValue(camera));
1779
+ const senId = toValue(sensorId);
1780
+ const plugId = toValue(pluginId);
1781
+ if (!cameraId || !senId || !plugId) return void 0;
1782
+ if (!state.cachedStorage) {
1783
+ if (!connect(cameraId, senId, plugId)) return void 0;
1784
+ }
1785
+ return operations.getConfig();
1786
+ }
1787
+ watch([
1788
+ clientConnected,
1789
+ () => extractCameraId(toValue(camera)),
1790
+ () => toValue(sensorId),
1791
+ () => toValue(pluginId)
1792
+ ], ([connected, cameraId, senId, plugId]) => {
1793
+ if (connected && cameraId && senId && plugId) {
1794
+ connect(cameraId, senId, plugId);
1795
+ operations.getConfig();
1796
+ } else cleanupStorage(state, isConnected, config);
1797
+ initialSetupDone.value = true;
1798
+ }, { immediate: true });
1799
+ tryOnScopeDispose(() => cleanupStorage(state, isConnected, config));
1800
+ return {
1801
+ config,
1802
+ isLoading: computed(() => _isLoading.value || !initialSetupDone.value),
1803
+ error,
1804
+ isConnected,
1805
+ getConfig,
1806
+ setValue: operations.setValue,
1807
+ setConfig: operations.setConfig,
1808
+ submitValue: operations.submitValue
1809
+ };
1810
+ }
1811
+ //#endregion
1812
+ //#region src/composables/resetClientState.ts
1813
+ function resetClientState() {
1814
+ clearPluginCache();
1815
+ clearCameraCache();
1816
+ clearStorageCache();
1817
+ clearSensorCache();
1818
+ clearSnapshotCache();
1819
+ }
1820
+ function refreshClientSubscriptions() {
1821
+ clearPluginCache();
1822
+ reconnectAllCameraDevices();
1823
+ reconnectAllSensorManagers();
1824
+ }
1825
+ //#endregion
1826
+ //#region src/plugin.ts
1827
+ var CAMERA_UI_INJECTION_KEY = Symbol("camera-ui");
1828
+ function isContext(input) {
1829
+ return "rpc" in input && "isConnected" in input;
1830
+ }
1831
+ function makeContextFromTransport(input) {
1832
+ const { natsTransport, target, wsTransport } = input;
1833
+ let hasBeenConnected = false;
1834
+ const rpc = shallowRef(natsTransport.getClient() ?? void 0);
1835
+ const isConnected = ref(natsTransport.getClient()?.isConnected ?? false);
1836
+ const error = ref(void 0);
1837
+ const endpoint = computed(() => target.value?.endpoint.url);
1838
+ const token = computed(() => target.value?.tokens.access);
1839
+ const extraProxyQuery = computed(() => {
1840
+ const t = target.value;
1841
+ if (!t?.tokens.proxySession) return void 0;
1842
+ return { session: t.tokens.proxySession };
1843
+ });
1844
+ const listeners = /* @__PURE__ */ new Map();
1845
+ function on(event, cb) {
1846
+ let set = listeners.get(event);
1847
+ if (!set) {
1848
+ set = /* @__PURE__ */ new Set();
1849
+ listeners.set(event, set);
1850
+ }
1851
+ set.add(cb);
1852
+ }
1853
+ function off(event, cb) {
1854
+ listeners.get(event)?.delete(cb);
1855
+ }
1856
+ function emit(event) {
1857
+ const set = listeners.get(event);
1858
+ if (!set) return;
1859
+ for (const cb of set) try {
1860
+ cb();
1861
+ } catch (err) {
1862
+ console.warn("[camera.ui] context listener threw:", err);
1863
+ }
1864
+ }
1865
+ natsTransport.subscribeClient((next) => {
1866
+ const wasConnected = isConnected.value;
1867
+ if (next) {
1868
+ rpc.value = next;
1869
+ isConnected.value = true;
1870
+ error.value = void 0;
1871
+ if (!wasConnected) {
1872
+ refreshClientSubscriptions();
1873
+ if (hasBeenConnected) emit("reconnected");
1874
+ hasBeenConnected = true;
1875
+ }
1876
+ } else {
1877
+ rpc.value = void 0;
1878
+ isConnected.value = false;
1879
+ if (wasConnected) emit("disconnected");
1880
+ }
1881
+ });
1882
+ natsTransport.on("auth-error", (payload) => {
1883
+ error.value = new Error(payload.message ?? "auth-error");
1884
+ });
1885
+ natsTransport.on("down", (payload) => {
1886
+ if (!error.value) error.value = new Error(payload.reason);
1887
+ });
1888
+ natsTransport.on("up", () => {
1889
+ error.value = void 0;
1890
+ });
1891
+ return markRaw({
1892
+ rpc,
1893
+ target: readonly(target),
1894
+ isConnected: readonly(isConnected),
1895
+ endpoint,
1896
+ token,
1897
+ extraProxyQuery,
1898
+ error: readonly(error),
1899
+ wsTransport,
1900
+ on,
1901
+ off
1902
+ });
1903
+ }
1904
+ function createCameraUiPlugin(input) {
1905
+ return { install(app) {
1906
+ const ctx = isContext(input) ? input : makeContextFromTransport(input);
1907
+ app.provide(CAMERA_UI_INJECTION_KEY, ctx);
1908
+ } };
1909
+ }
1910
+ //#endregion
1911
+ //#region src/streaming/activityMode.ts
1912
+ var DEFAULT_CONFIG = {
1913
+ standbyTimeout: 5e3,
1914
+ activityTimeout: 5e3
1915
+ };
1916
+ function createActivityMode(options) {
1917
+ const { initialMode = "always-on", config = {}, onStreamStart, onStreamStop, isStreamPlaying } = options;
1918
+ const resolvedConfig = {
1919
+ ...DEFAULT_CONFIG,
1920
+ ...config
1921
+ };
1922
+ const mode = ref(initialMode);
1923
+ const inStandby = ref(false);
1924
+ const hasActivity = ref(false);
1925
+ let activityTimeout;
1926
+ let idleTimeout;
1927
+ function clearActivityTimeout() {
1928
+ if (activityTimeout) {
1929
+ clearTimeout(activityTimeout);
1930
+ activityTimeout = void 0;
1931
+ }
1932
+ }
1933
+ function clearIdleTimeout() {
1934
+ if (idleTimeout) {
1935
+ clearTimeout(idleTimeout);
1936
+ idleTimeout = void 0;
1937
+ }
1938
+ }
1939
+ function clearAllTimers() {
1940
+ clearActivityTimeout();
1941
+ clearIdleTimeout();
1942
+ }
1943
+ function goToStandby() {
1944
+ if (!inStandby.value) {
1945
+ inStandby.value = true;
1946
+ onStreamStop();
1947
+ }
1948
+ }
1949
+ function exitStandby() {
1950
+ if (inStandby.value) {
1951
+ inStandby.value = false;
1952
+ onStreamStart();
1953
+ }
1954
+ }
1955
+ function manageStreamState() {
1956
+ clearAllTimers();
1957
+ switch (mode.value) {
1958
+ case "always-on":
1959
+ inStandby.value = false;
1960
+ if (!isStreamPlaying()) onStreamStart();
1961
+ break;
1962
+ case "standby":
1963
+ if (!inStandby.value) {
1964
+ if (!isStreamPlaying()) onStreamStart();
1965
+ startIdleTimer();
1966
+ }
1967
+ break;
1968
+ case "activity":
1969
+ if (hasActivity.value) {
1970
+ inStandby.value = false;
1971
+ if (!isStreamPlaying()) onStreamStart();
1972
+ } else activityTimeout = setTimeout(() => {
1973
+ if (mode.value === "activity" && !hasActivity.value) goToStandby();
1974
+ }, resolvedConfig.activityTimeout);
1975
+ break;
1976
+ }
1977
+ }
1978
+ function startIdleTimer() {
1979
+ clearIdleTimeout();
1980
+ idleTimeout = setTimeout(() => {
1981
+ if (mode.value === "standby") goToStandby();
1982
+ }, resolvedConfig.standbyTimeout);
1983
+ }
1984
+ function setMode(newMode) {
1985
+ if (mode.value !== newMode) {
1986
+ mode.value = newMode;
1987
+ manageStreamState();
1988
+ }
1989
+ }
1990
+ function reportActivity(detected) {
1991
+ if (mode.value !== "activity") return;
1992
+ if (detected) {
1993
+ hasActivity.value = true;
1994
+ clearActivityTimeout();
1995
+ if (inStandby.value || !isStreamPlaying()) exitStandby();
1996
+ } else {
1997
+ hasActivity.value = false;
1998
+ clearActivityTimeout();
1999
+ activityTimeout = setTimeout(() => {
2000
+ if (!hasActivity.value) goToStandby();
2001
+ }, resolvedConfig.activityTimeout);
2002
+ }
2003
+ }
2004
+ function resumeFromStandby() {
2005
+ if (!inStandby.value) return;
2006
+ exitStandby();
2007
+ switch (mode.value) {
2008
+ case "standby":
2009
+ startIdleTimer();
2010
+ break;
2011
+ case "activity":
2012
+ clearActivityTimeout();
2013
+ if (!hasActivity.value) activityTimeout = setTimeout(() => {
2014
+ goToStandby();
2015
+ }, resolvedConfig.activityTimeout);
2016
+ break;
2017
+ }
2018
+ }
2019
+ function resetIdleTimer() {
2020
+ if (mode.value === "standby" && !inStandby.value) startIdleTimer();
2021
+ }
2022
+ function dispose() {
2023
+ clearAllTimers();
2024
+ }
2025
+ return {
2026
+ mode,
2027
+ inStandby,
2028
+ hasActivity,
2029
+ setMode,
2030
+ reportActivity,
2031
+ resumeFromStandby,
2032
+ resetIdleTimer,
2033
+ dispose
2034
+ };
2035
+ }
2036
+ //#endregion
2037
+ //#region src/composables/useTabVisibility.ts
2038
+ var DEFAULT_TAB_PAUSE_MS = 3e4;
2039
+ function log$1(...args) {
2040
+ const d = /* @__PURE__ */ new Date();
2041
+ const hh = String(d.getHours()).padStart(2, "0");
2042
+ const mm = String(d.getMinutes()).padStart(2, "0");
2043
+ const ss = String(d.getSeconds()).padStart(2, "0");
2044
+ const ms = String(d.getMilliseconds()).padStart(3, "0");
2045
+ console.log(`[TabVisibility ${hh}:${mm}:${ss}.${ms}]`, ...args);
2046
+ }
2047
+ var _isVisible = ref(typeof document === "undefined" ? true : document.visibilityState === "visible");
2048
+ var _hiddenAt = null;
2049
+ var _hiddenListeners = /* @__PURE__ */ new Set();
2050
+ var _pausedListeners = /* @__PURE__ */ new Set();
2051
+ var _visibleListeners = /* @__PURE__ */ new Set();
2052
+ var _initialized = false;
2053
+ function _firePausedEntry(entry) {
2054
+ log$1(`paused listener fired (delay ${entry.delayMs}ms reached)`);
2055
+ try {
2056
+ entry.cb();
2057
+ } catch (err) {
2058
+ console.error("[TabVisibility] onTabPaused listener threw:", err);
2059
+ }
2060
+ }
2061
+ function _schedulePausedEntry(entry) {
2062
+ log$1(`paused listener scheduled in ${entry.delayMs}ms`);
2063
+ entry.timer = setTimeout(() => {
2064
+ entry.timer = null;
2065
+ if (_isVisible.value) {
2066
+ log$1("paused listener skipped — tab visible before delay");
2067
+ return;
2068
+ }
2069
+ _firePausedEntry(entry);
2070
+ }, entry.delayMs);
2071
+ }
2072
+ function _cancelPausedEntries() {
2073
+ for (const entry of _pausedListeners) if (entry.timer !== null) {
2074
+ clearTimeout(entry.timer);
2075
+ entry.timer = null;
2076
+ }
2077
+ }
2078
+ function _init() {
2079
+ if (_initialized || typeof document === "undefined") return;
2080
+ _initialized = true;
2081
+ document.addEventListener("visibilitychange", () => {
2082
+ const visible = document.visibilityState === "visible";
2083
+ if (!visible && _isVisible.value) {
2084
+ _isVisible.value = false;
2085
+ _hiddenAt = Date.now();
2086
+ log$1(`tab → hidden (hidden=${_hiddenListeners.size} paused=${_pausedListeners.size})`);
2087
+ for (const entry of _hiddenListeners) try {
2088
+ entry.cb();
2089
+ } catch (err) {
2090
+ console.error("[TabVisibility] onTabHidden listener threw:", err);
2091
+ }
2092
+ for (const entry of _pausedListeners) _schedulePausedEntry(entry);
2093
+ } else if (visible && !_isVisible.value) {
2094
+ _cancelPausedEntries();
2095
+ const hiddenMs = _hiddenAt != null ? Date.now() - _hiddenAt : 0;
2096
+ _hiddenAt = null;
2097
+ _isVisible.value = true;
2098
+ log$1(`tab → visible (hiddenMs=${hiddenMs}, visible-listeners=${_visibleListeners.size})`);
2099
+ for (const { cb } of _visibleListeners) try {
2100
+ cb({ hiddenMs });
2101
+ } catch (err) {
2102
+ console.error("[TabVisibility] onTabVisible listener threw:", err);
2103
+ }
2104
+ }
2105
+ });
2106
+ }
2107
+ function useTabVisibility() {
2108
+ _init();
2109
+ const isVisible = computed(() => _isVisible.value);
2110
+ function onTabHidden(cb) {
2111
+ const entry = { cb };
2112
+ _hiddenListeners.add(entry);
2113
+ const off = () => {
2114
+ _hiddenListeners.delete(entry);
2115
+ };
2116
+ tryOnScopeDispose(off);
2117
+ return off;
2118
+ }
2119
+ function onTabPaused(cb, options = {}) {
2120
+ const entry = {
2121
+ cb,
2122
+ delayMs: options.delayMs ?? DEFAULT_TAB_PAUSE_MS,
2123
+ timer: null
2124
+ };
2125
+ _pausedListeners.add(entry);
2126
+ const off = () => {
2127
+ if (entry.timer !== null) {
2128
+ clearTimeout(entry.timer);
2129
+ entry.timer = null;
2130
+ }
2131
+ _pausedListeners.delete(entry);
2132
+ };
2133
+ tryOnScopeDispose(off);
2134
+ return off;
2135
+ }
2136
+ function onTabVisible(cb) {
2137
+ const entry = { cb };
2138
+ _visibleListeners.add(entry);
2139
+ const off = () => {
2140
+ _visibleListeners.delete(entry);
2141
+ };
2142
+ tryOnScopeDispose(off);
2143
+ return off;
2144
+ }
2145
+ return {
2146
+ isVisible,
2147
+ onTabHidden,
2148
+ onTabPaused,
2149
+ onTabVisible
2150
+ };
2151
+ }
2152
+ //#endregion
2153
+ //#region src/streaming/config.ts
2154
+ var STREAM_CONFIG = {
2155
+ WEBRTC: {
2156
+ RECONNECT_DELAY: 1e3,
2157
+ CONNECT_TIMEOUT: 1e4,
2158
+ WS_CONNECT_TIMEOUT: 5e3,
2159
+ ICE_SERVERS: [{ urls: "stun:stun.l.google.com:19302" }]
2160
+ },
2161
+ MSE: {
2162
+ BUFFER_SIZE: 2 * 1024 * 1024,
2163
+ BUFFER_WINDOW: 5
2164
+ },
2165
+ CODECS: {
2166
+ DEFAULT: [
2167
+ "avc1.640029",
2168
+ "avc1.64002A",
2169
+ "avc1.640033",
2170
+ "hvc1.1.6.L153.B0",
2171
+ "mp4a.40.2",
2172
+ "mp4a.40.5",
2173
+ "flac",
2174
+ "opus"
2175
+ ],
2176
+ WEBRTC_VIDEO: [
2177
+ "H264",
2178
+ "VP8",
2179
+ "VP9"
2180
+ ],
2181
+ WEBRTC_AUDIO: [
2182
+ "opus",
2183
+ "G722",
2184
+ "PCMU",
2185
+ "PCMA"
2186
+ ]
2187
+ }
2188
+ };
2189
+ //#endregion
2190
+ //#region src/streaming/utils.ts
2191
+ function safariVersion() {
2192
+ return navigator.userAgent.match(/version\/(\d+)/i);
2193
+ }
2194
+ function getSupportedCodecs() {
2195
+ const codecs = [...STREAM_CONFIG.CODECS.DEFAULT];
2196
+ const version = safariVersion();
2197
+ if (version?.[1]) {
2198
+ const majorVersion = parseInt(version[1], 10);
2199
+ if (majorVersion < 13) {
2200
+ const index = codecs.indexOf("mp4a.40.2");
2201
+ if (index > -1) codecs.splice(index);
2202
+ } else if (majorVersion < 14) {
2203
+ const index = codecs.indexOf("flac");
2204
+ if (index > -1) codecs.splice(index);
2205
+ } else {
2206
+ const index = codecs.indexOf("opus");
2207
+ if (index > -1) codecs.splice(index);
2208
+ }
2209
+ }
2210
+ return codecs;
2211
+ }
2212
+ function filterSupportedCodecs(codecs, isTypeSupported) {
2213
+ return codecs.filter((codec) => isTypeSupported(`video/mp4; codecs="${codec}"`)).join(",");
2214
+ }
2215
+ function isWebRTCCompatibleAudio(codec) {
2216
+ return STREAM_CONFIG.CODECS.WEBRTC_AUDIO.includes(codec);
2217
+ }
2218
+ function isWebRTCCompatibleVideo(codec) {
2219
+ if (STREAM_CONFIG.CODECS.WEBRTC_VIDEO.includes(codec)) return true;
2220
+ if (codec === "H265") return isH265Supported();
2221
+ return false;
2222
+ }
2223
+ function isH265Supported() {
2224
+ try {
2225
+ const videoCodecs = RTCRtpSender?.getCapabilities("video")?.codecs;
2226
+ if (!videoCodecs) return false;
2227
+ return videoCodecs.some((c) => c.mimeType.toLowerCase().includes("h265") || c.mimeType.toLowerCase().includes("hevc"));
2228
+ } catch {
2229
+ return false;
2230
+ }
2231
+ }
2232
+ function checkWebRTCCompatibility(probe) {
2233
+ const audioStreams = probe.audio.filter((s) => s.direction === "sendonly");
2234
+ const videoStreams = probe.video.filter((s) => s.direction === "sendonly");
2235
+ const audioCompatible = audioStreams.length === 0 || audioStreams.some((s) => isWebRTCCompatibleAudio(s.codec));
2236
+ const videoCompatible = videoStreams.length === 0 || videoStreams.some((s) => isWebRTCCompatibleVideo(s.codec));
2237
+ const incompatibleCodecs = [];
2238
+ if (!audioCompatible) incompatibleCodecs.push(...audioStreams.map((s) => s.codec));
2239
+ if (!videoCompatible) incompatibleCodecs.push(...videoStreams.map((s) => s.codec));
2240
+ return {
2241
+ compatible: audioCompatible && videoCompatible,
2242
+ audioCompatible,
2243
+ videoCompatible,
2244
+ incompatibleCodecs
2245
+ };
2246
+ }
2247
+ function hasManagedMediaSource() {
2248
+ return "ManagedMediaSource" in window;
2249
+ }
2250
+ function getMediaSourceConstructor() {
2251
+ if (hasManagedMediaSource()) return window.ManagedMediaSource;
2252
+ if ("MediaSource" in window) return window.MediaSource;
2253
+ }
2254
+ function abortableSleep(ms, signal) {
2255
+ return new Promise((resolve, reject) => {
2256
+ if (signal.aborted) {
2257
+ reject(new DOMException("Aborted", "AbortError"));
2258
+ return;
2259
+ }
2260
+ const timer = setTimeout(resolve, ms);
2261
+ signal.addEventListener("abort", () => {
2262
+ clearTimeout(timer);
2263
+ reject(new DOMException("Aborted", "AbortError"));
2264
+ }, { once: true });
2265
+ });
2266
+ }
2267
+ //#endregion
2268
+ //#region src/streaming/mse.ts
2269
+ function createMSEHandler(options) {
2270
+ const { videoElement, onReady, onFirstData, onError, signal } = options;
2271
+ let mediaSource = null;
2272
+ let sourceBuffer = null;
2273
+ let isReady = false;
2274
+ let hasFirstData = false;
2275
+ let pendingBuffer = new Uint8Array(STREAM_CONFIG.MSE.BUFFER_SIZE);
2276
+ let pendingLength = 0;
2277
+ const earlyDataBuffer = [];
2278
+ const supportedCodecs = getSupportedCodecs();
2279
+ function setup() {
2280
+ if (signal.aborted) return void 0;
2281
+ const MediaSourceConstructor = getMediaSourceConstructor();
2282
+ if (!MediaSourceConstructor) {
2283
+ onError(/* @__PURE__ */ new Error("MediaSource not supported"));
2284
+ return;
2285
+ }
2286
+ mediaSource = new MediaSourceConstructor();
2287
+ const isMMS = hasManagedMediaSource();
2288
+ const oldSrc = videoElement.src;
2289
+ mediaSource.addEventListener("sourceopen", () => {
2290
+ if (signal.aborted) return;
2291
+ if (!isMMS && oldSrc && oldSrc.startsWith("blob:") && oldSrc !== videoElement.src) URL.revokeObjectURL(oldSrc);
2292
+ }, { once: true });
2293
+ if (isMMS) {
2294
+ videoElement.disableRemotePlayback = true;
2295
+ videoElement.srcObject = mediaSource;
2296
+ } else {
2297
+ videoElement.src = URL.createObjectURL(mediaSource);
2298
+ videoElement.srcObject = null;
2299
+ }
2300
+ return filterSupportedCodecs(supportedCodecs, MediaSourceConstructor.isTypeSupported.bind(MediaSourceConstructor));
2301
+ }
2302
+ function initializeBuffer(mimeType) {
2303
+ if (signal.aborted || !mediaSource) return;
2304
+ if (mediaSource.readyState !== "open") {
2305
+ mediaSource.addEventListener("sourceopen", () => {
2306
+ if (!signal.aborted) initializeBuffer(mimeType);
2307
+ }, { once: true });
2308
+ return;
2309
+ }
2310
+ pendingLength = 0;
2311
+ pendingBuffer = new Uint8Array(STREAM_CONFIG.MSE.BUFFER_SIZE);
2312
+ const MediaSourceConstructor = getMediaSourceConstructor();
2313
+ if (MediaSourceConstructor && !MediaSourceConstructor.isTypeSupported(mimeType)) {
2314
+ onError(/* @__PURE__ */ new Error(`MIME type not supported: ${mimeType}`));
2315
+ return;
2316
+ }
2317
+ try {
2318
+ sourceBuffer = mediaSource.addSourceBuffer(mimeType);
2319
+ sourceBuffer.mode = "segments";
2320
+ sourceBuffer.addEventListener("updateend", handleUpdateEnd);
2321
+ isReady = true;
2322
+ if (earlyDataBuffer.length > 0) {
2323
+ for (const earlyData of earlyDataBuffer) appendBuffer(earlyData);
2324
+ earlyDataBuffer.length = 0;
2325
+ }
2326
+ onReady();
2327
+ } catch (err) {
2328
+ onError(err instanceof Error ? err : new Error(String(err)));
2329
+ }
2330
+ }
2331
+ function handleUpdateEnd() {
2332
+ if (signal.aborted || !sourceBuffer || !mediaSource) return;
2333
+ if (!sourceBuffer.updating && pendingLength > 0) try {
2334
+ const data = pendingBuffer.slice(0, pendingLength);
2335
+ sourceBuffer.appendBuffer(data);
2336
+ pendingLength = 0;
2337
+ } catch {}
2338
+ if (!sourceBuffer.updating && sourceBuffer.buffered?.length) {
2339
+ const end = sourceBuffer.buffered.end(sourceBuffer.buffered.length - 1);
2340
+ const start = end - STREAM_CONFIG.MSE.BUFFER_WINDOW;
2341
+ const start0 = sourceBuffer.buffered.start(0);
2342
+ if (!hasFirstData) {
2343
+ hasFirstData = true;
2344
+ onFirstData();
2345
+ }
2346
+ if (start > start0) try {
2347
+ sourceBuffer.remove(start0, start);
2348
+ mediaSource.setLiveSeekableRange(start, end);
2349
+ } catch {}
2350
+ if (videoElement.currentTime < start) videoElement.currentTime = start;
2351
+ const gap = end - videoElement.currentTime;
2352
+ if (gap > 1) videoElement.playbackRate = 1.1;
2353
+ else if (gap < .1) videoElement.playbackRate = .9;
2354
+ else videoElement.playbackRate = 1;
2355
+ }
2356
+ }
2357
+ function appendBuffer(data) {
2358
+ if (signal.aborted) return;
2359
+ if (!sourceBuffer) {
2360
+ earlyDataBuffer.push(data);
2361
+ return;
2362
+ }
2363
+ if (mediaSource?.readyState === "closed") {
2364
+ onError(/* @__PURE__ */ new Error("MediaSource closed"));
2365
+ return;
2366
+ }
2367
+ if (sourceBuffer.updating || pendingLength > 0) {
2368
+ const bytes = new Uint8Array(data);
2369
+ pendingBuffer.set(bytes, pendingLength);
2370
+ pendingLength += bytes.byteLength;
2371
+ } else try {
2372
+ sourceBuffer.appendBuffer(data);
2373
+ } catch {}
2374
+ }
2375
+ function close() {
2376
+ if (sourceBuffer) {
2377
+ sourceBuffer.removeEventListener("updateend", handleUpdateEnd);
2378
+ if (mediaSource?.readyState === "open") try {
2379
+ mediaSource.removeSourceBuffer(sourceBuffer);
2380
+ } catch {}
2381
+ sourceBuffer = null;
2382
+ }
2383
+ if (mediaSource?.readyState === "open") try {
2384
+ mediaSource.endOfStream();
2385
+ } catch {}
2386
+ mediaSource = null;
2387
+ isReady = false;
2388
+ hasFirstData = false;
2389
+ pendingLength = 0;
2390
+ earlyDataBuffer.length = 0;
2391
+ }
2392
+ signal.addEventListener("abort", close, { once: true });
2393
+ return {
2394
+ get mediaSource() {
2395
+ return mediaSource;
2396
+ },
2397
+ get sourceBuffer() {
2398
+ return sourceBuffer;
2399
+ },
2400
+ get isReady() {
2401
+ return isReady;
2402
+ },
2403
+ setup,
2404
+ initializeBuffer,
2405
+ appendBuffer,
2406
+ close
2407
+ };
2408
+ }
2409
+ async function playVideo(videoElement) {
2410
+ try {
2411
+ await videoElement.play();
2412
+ } catch {
2413
+ if (!videoElement.muted) {
2414
+ videoElement.muted = true;
2415
+ await playVideo(videoElement);
2416
+ }
2417
+ }
2418
+ }
2419
+ //#endregion
2420
+ //#region src/streaming/webrtc.ts
2421
+ function createBasePeerConnection(signal, onCandidate, filterUdp) {
2422
+ const peerConnection = new RTCPeerConnection({
2423
+ bundlePolicy: "max-bundle",
2424
+ iceServers: [...STREAM_CONFIG.WEBRTC.ICE_SERVERS]
2425
+ });
2426
+ peerConnection.onicecandidate = (ev) => {
2427
+ if (signal.aborted) return;
2428
+ if (filterUdp && ev.candidate?.protocol === "udp") return;
2429
+ const candidate = ev.candidate?.toJSON().candidate ?? "";
2430
+ if (candidate) onCandidate(candidate);
2431
+ };
2432
+ return peerConnection;
2433
+ }
2434
+ async function handleSdpAnswer(pc, signal, sdp) {
2435
+ if (signal.aborted || !pc) return;
2436
+ await pc.setRemoteDescription({
2437
+ type: "answer",
2438
+ sdp
2439
+ });
2440
+ }
2441
+ async function handleIceCandidate(pc, signal, candidate, filterUdp) {
2442
+ if (signal.aborted || !pc) return;
2443
+ if (filterUdp && candidate.includes(" udp ")) return;
2444
+ await pc.addIceCandidate({
2445
+ candidate,
2446
+ sdpMid: "0"
2447
+ });
2448
+ }
2449
+ async function setMicTrack(micTransceiver, signal, track) {
2450
+ if (signal.aborted || !micTransceiver) return;
2451
+ try {
2452
+ await micTransceiver.sender.replaceTrack(track);
2453
+ } catch {}
2454
+ }
2455
+ function closePeerConnection(state) {
2456
+ if (state.micTransceiver) {
2457
+ state.micTransceiver.sender.replaceTrack(null).catch(() => {});
2458
+ state.micTransceiver = null;
2459
+ }
2460
+ if (state.pc) {
2461
+ state.pc.getTransceivers().forEach((t) => t.sender?.track?.stop());
2462
+ state.pc.close();
2463
+ state.pc = null;
2464
+ }
2465
+ state.isConnected = false;
2466
+ }
2467
+ function createWebRTCHandler(options) {
2468
+ const { mode, onConnected, onDisconnected, onFailed, onCandidate, signal } = options;
2469
+ const state = {
2470
+ pc: null,
2471
+ micTransceiver: null,
2472
+ isConnected: false
2473
+ };
2474
+ const filterUdp = mode === "webrtc/tcp";
2475
+ function createPeerConnection() {
2476
+ const peerConnection = createBasePeerConnection(signal, onCandidate, filterUdp);
2477
+ peerConnection.onconnectionstatechange = () => {
2478
+ if (signal.aborted) return;
2479
+ const connectionState = peerConnection.connectionState;
2480
+ if (connectionState === "connected") {
2481
+ state.isConnected = true;
2482
+ onConnected(new MediaStream(peerConnection.getTransceivers().filter((tr) => tr.currentDirection === "recvonly").map((tr) => tr.receiver.track)));
2483
+ } else if (connectionState === "failed") {
2484
+ state.isConnected = false;
2485
+ onFailed();
2486
+ } else if (connectionState === "disconnected") {
2487
+ state.isConnected = false;
2488
+ onDisconnected();
2489
+ }
2490
+ };
2491
+ peerConnection.addTransceiver("video", { direction: "recvonly" });
2492
+ peerConnection.addTransceiver("audio", { direction: "recvonly" });
2493
+ state.micTransceiver = peerConnection.addTransceiver("audio", { direction: "sendonly" });
2494
+ return peerConnection;
2495
+ }
2496
+ async function createOffer() {
2497
+ if (signal.aborted) return void 0;
2498
+ state.pc = createPeerConnection();
2499
+ const offer = await state.pc.createOffer();
2500
+ await state.pc.setLocalDescription(offer);
2501
+ return offer.sdp;
2502
+ }
2503
+ function close() {
2504
+ closePeerConnection(state);
2505
+ }
2506
+ signal.addEventListener("abort", close, { once: true });
2507
+ return {
2508
+ get pc() {
2509
+ return state.pc;
2510
+ },
2511
+ get micTransceiver() {
2512
+ return state.micTransceiver;
2513
+ },
2514
+ get isConnected() {
2515
+ return state.isConnected;
2516
+ },
2517
+ createOffer,
2518
+ handleAnswer: (sdp) => handleSdpAnswer(state.pc, signal, sdp),
2519
+ handleCandidate: (candidate) => handleIceCandidate(state.pc, signal, candidate, filterUdp),
2520
+ setMicrophoneTrack: (track) => setMicTrack(state.micTransceiver, signal, track),
2521
+ close
2522
+ };
2523
+ }
2524
+ function createBackchannelHandler(options) {
2525
+ const { onConnected, onDisconnected, onCandidate, signal } = options;
2526
+ const state = {
2527
+ pc: null,
2528
+ micTransceiver: null,
2529
+ isConnected: false
2530
+ };
2531
+ function createPeerConnection() {
2532
+ const peerConnection = createBasePeerConnection(signal, onCandidate, false);
2533
+ peerConnection.onconnectionstatechange = () => {
2534
+ if (signal.aborted) return;
2535
+ const connectionState = peerConnection.connectionState;
2536
+ if (connectionState === "connected") {
2537
+ state.isConnected = true;
2538
+ onConnected();
2539
+ } else if (connectionState === "disconnected" || connectionState === "failed") {
2540
+ state.isConnected = false;
2541
+ onDisconnected();
2542
+ }
2543
+ };
2544
+ state.micTransceiver = peerConnection.addTransceiver("audio", { direction: "sendonly" });
2545
+ return peerConnection;
2546
+ }
2547
+ async function createOffer() {
2548
+ if (signal.aborted) return void 0;
2549
+ state.pc = createPeerConnection();
2550
+ const offer = await state.pc.createOffer();
2551
+ await state.pc.setLocalDescription(offer);
2552
+ return offer.sdp;
2553
+ }
2554
+ function close() {
2555
+ closePeerConnection(state);
2556
+ }
2557
+ signal.addEventListener("abort", close, { once: true });
2558
+ return {
2559
+ get isConnected() {
2560
+ return state.isConnected;
2561
+ },
2562
+ createOffer,
2563
+ handleAnswer: (sdp) => handleSdpAnswer(state.pc, signal, sdp),
2564
+ handleCandidate: (candidate) => handleIceCandidate(state.pc, signal, candidate, false),
2565
+ setMicrophoneTrack: (track) => setMicTrack(state.micTransceiver, signal, track),
2566
+ close
2567
+ };
2568
+ }
2569
+ async function processWebRTCMessage(handler, msg) {
2570
+ switch (msg.type) {
2571
+ case "webrtc/answer":
2572
+ await handler.handleAnswer(msg.value);
2573
+ break;
2574
+ case "webrtc/candidate":
2575
+ if (msg.value) await handler.handleCandidate(msg.value);
2576
+ break;
2577
+ }
2578
+ }
2579
+ //#endregion
2580
+ //#region src/streaming/streamConnection.ts
2581
+ function log(...args) {
2582
+ const d = /* @__PURE__ */ new Date();
2583
+ const hh = String(d.getHours()).padStart(2, "0");
2584
+ const mm = String(d.getMinutes()).padStart(2, "0");
2585
+ const ss = String(d.getSeconds()).padStart(2, "0");
2586
+ const ms = String(d.getMilliseconds()).padStart(3, "0");
2587
+ console.log(`[StreamConnection ${hh}:${mm}:${ss}.${ms}]`, ...args);
2588
+ }
2589
+ var StreamConnection = class {
2590
+ status;
2591
+ activeMode;
2592
+ requestedMode;
2593
+ activeResolution;
2594
+ requestedResolution;
2595
+ source;
2596
+ hasVideo;
2597
+ hasAudio;
2598
+ hasBackchannel;
2599
+ isPlaying;
2600
+ error;
2601
+ probeInfo;
2602
+ muted;
2603
+ paused;
2604
+ nativeWidth;
2605
+ nativeHeight;
2606
+ options;
2607
+ connectionGeneration = 0;
2608
+ abortController = new AbortController();
2609
+ offTabVisible;
2610
+ offTabPaused;
2611
+ wasPausedByVisibility = false;
2612
+ wsTransport;
2613
+ wsHandle;
2614
+ webrtcHandler;
2615
+ mseHandler;
2616
+ firstFrameCallbackId;
2617
+ backchannelHandler;
2618
+ pendingMicTrack = null;
2619
+ lastMediaStream = null;
2620
+ stopWatchers = [];
2621
+ mseMonitorInterval;
2622
+ onVideoPauseBound;
2623
+ onVideoPlayBound;
2624
+ onVideoResizeBound;
2625
+ camera;
2626
+ videoElement;
2627
+ containerElement;
2628
+ target;
2629
+ isReady;
2630
+ effectiveMode;
2631
+ startWsConnectTimeout;
2632
+ stopWsConnectTimeout;
2633
+ startConnectTimeout;
2634
+ stopConnectTimeout;
2635
+ startReconnectTimeout;
2636
+ stopReconnectTimeout;
2637
+ constructor(options) {
2638
+ this.options = options;
2639
+ const ctx = useCameraUi();
2640
+ if (!ctx.wsTransport) throw new Error("[camera.ui] StreamConnection requires a wsTransport on CameraUiContext — pass `wsTransport` to createCameraUiPlugin.");
2641
+ this.target = ctx.target;
2642
+ this.wsTransport = ctx.wsTransport;
2643
+ const { autoStart = true } = options;
2644
+ this.status = ref("idle");
2645
+ this.activeMode = ref("webrtc");
2646
+ this.activeResolution = ref("low-resolution");
2647
+ this.source = shallowRef();
2648
+ this.hasVideo = ref(false);
2649
+ this.hasAudio = ref(false);
2650
+ this.hasBackchannel = ref(false);
2651
+ this.isPlaying = ref(false);
2652
+ this.error = ref();
2653
+ this.probeInfo = shallowRef();
2654
+ this.muted = ref(true);
2655
+ this.paused = ref(false);
2656
+ this.nativeWidth = ref(0);
2657
+ this.nativeHeight = ref(0);
2658
+ this.requestedMode = ref(toValue(options.mode) ?? "auto");
2659
+ this.requestedResolution = ref(toValue(options.resolution) ?? "high-resolution");
2660
+ this.camera = computed(() => toValue(options.camera));
2661
+ this.videoElement = computed(() => toValue(options.videoElement));
2662
+ this.containerElement = computed(() => toValue(options.containerElement));
2663
+ this.isReady = computed(() => !!this.camera.value && !!this.videoElement.value && !!this.target.value);
2664
+ this.effectiveMode = computed(() => {
2665
+ if (this.requestedMode.value === "auto") return this.activeMode.value;
2666
+ return this.requestedMode.value;
2667
+ });
2668
+ const { onTabPaused, onTabVisible } = useTabVisibility();
2669
+ const { start: startWsConnectTimeout, stop: stopWsConnectTimeout } = useTimeoutFn(() => {
2670
+ if (this.wsHandle?.readyState === WebSocket.CONNECTING) {
2671
+ this.disconnectWebSocket();
2672
+ if (!this.abortController.signal.aborted && this.status.value !== "closed") this.restart();
2673
+ }
2674
+ }, STREAM_CONFIG.WEBRTC.WS_CONNECT_TIMEOUT, { immediate: false });
2675
+ this.startWsConnectTimeout = startWsConnectTimeout;
2676
+ this.stopWsConnectTimeout = stopWsConnectTimeout;
2677
+ const { start: startConnectTimeout, stop: stopConnectTimeout } = useTimeoutFn(() => {
2678
+ if (this.webrtcHandler && !this.webrtcHandler.isConnected) if (this.requestedMode.value === "auto") if (this.mseHandler?.isReady) {
2679
+ this.activeMode.value = "mse";
2680
+ this.status.value = "connected";
2681
+ } else {
2682
+ this.activeMode.value = "mse";
2683
+ this.startMSE();
2684
+ }
2685
+ else this.restart();
2686
+ }, STREAM_CONFIG.WEBRTC.CONNECT_TIMEOUT, { immediate: false });
2687
+ this.startConnectTimeout = startConnectTimeout;
2688
+ this.stopConnectTimeout = stopConnectTimeout;
2689
+ const { start: startReconnectTimeout, stop: stopReconnectTimeout } = useTimeoutFn(() => {
2690
+ if (!this.abortController.signal.aborted) this.restart();
2691
+ }, STREAM_CONFIG.WEBRTC.RECONNECT_DELAY, { immediate: false });
2692
+ this.startReconnectTimeout = startReconnectTimeout;
2693
+ this.stopReconnectTimeout = stopReconnectTimeout;
2694
+ this.setupWatchers();
2695
+ this.offTabPaused = onTabPaused(() => {
2696
+ log(`onTabPaused fired — status=${this.status.value}, isReady=${this.isReady.value}, target=${!!this.target.value}`);
2697
+ if (this.status.value === "idle" || this.status.value === "closed") {
2698
+ log(`onTabPaused — already in ${this.status.value}, skipping stop()`);
2699
+ return;
2700
+ }
2701
+ this.wasPausedByVisibility = true;
2702
+ this.stop();
2703
+ log("onTabPaused — stop() done, wasPausedByVisibility=true");
2704
+ });
2705
+ this.offTabVisible = onTabVisible(() => {
2706
+ log(`onTabVisible fired — wasPausedByVisibility=${this.wasPausedByVisibility}, status=${this.status.value}, isReady=${this.isReady.value}, target=${!!this.target.value}`);
2707
+ if (!this.wasPausedByVisibility) {
2708
+ log("onTabVisible — not paused by visibility, no-op");
2709
+ return;
2710
+ }
2711
+ this.wasPausedByVisibility = false;
2712
+ this.startWhenReady();
2713
+ });
2714
+ if (autoStart) whenever(this.isReady, () => {
2715
+ if (this.status.value === "idle" || this.status.value === "closed") this.start();
2716
+ }, { immediate: true });
2717
+ }
2718
+ async start() {
2719
+ log(`start() called — status=${this.status.value}, isReady=${this.isReady.value}`);
2720
+ if (this.status.value !== "idle" && this.status.value !== "closed") {
2721
+ log(`start() — skipped, status is ${this.status.value}`);
2722
+ return;
2723
+ }
2724
+ if (!this.isReady.value) {
2725
+ log("start() — skipped, isReady=false");
2726
+ return;
2727
+ }
2728
+ this.cleanup();
2729
+ this.abortController = new AbortController();
2730
+ const gen = ++this.connectionGeneration;
2731
+ this.status.value = "connecting";
2732
+ this.error.value = void 0;
2733
+ this.isPlaying.value = false;
2734
+ try {
2735
+ if (!this.initializeSource()) {
2736
+ log("start() — no streaming source available");
2737
+ this.error.value = /* @__PURE__ */ new Error("No streaming source available");
2738
+ this.status.value = "error";
2739
+ return;
2740
+ }
2741
+ await this.probeStream();
2742
+ if (gen !== this.connectionGeneration) {
2743
+ log("start() — generation stale after probe, aborting");
2744
+ return;
2745
+ }
2746
+ this.initializeSource();
2747
+ log("start() — connecting WebSocket");
2748
+ this.connectWebSocket();
2749
+ } catch (err) {
2750
+ if (gen !== this.connectionGeneration) return;
2751
+ log("start() — error:", err);
2752
+ this.error.value = err instanceof Error ? err : new Error(String(err));
2753
+ this.status.value = "error";
2754
+ }
2755
+ }
2756
+ stop() {
2757
+ if (this.status.value === "closed") {
2758
+ log("stop() — already closed, no-op");
2759
+ return;
2760
+ }
2761
+ log(`stop() — was ${this.status.value}, transitioning to closed`);
2762
+ ++this.connectionGeneration;
2763
+ this.status.value = "closed";
2764
+ this.cleanup();
2765
+ }
2766
+ async setMode(mode) {
2767
+ if (this.requestedMode.value === mode) return;
2768
+ this.requestedMode.value = mode;
2769
+ this.activeMode.value = mode === "auto" ? "webrtc" : mode;
2770
+ if (this.status.value === "connected") await this.restart();
2771
+ }
2772
+ async setResolution(resolution) {
2773
+ if (this.requestedResolution.value === resolution) return;
2774
+ this.requestedResolution.value = resolution;
2775
+ const result = this.getSourceForResolution(resolution);
2776
+ if (result && result.source._id !== this.source.value?._id) {
2777
+ this.source.value = result.source;
2778
+ this.activeResolution.value = result.effectiveResolution;
2779
+ if (this.status.value === "connected") await this.restart();
2780
+ }
2781
+ }
2782
+ async setMicrophone(track) {
2783
+ if (this.webrtcHandler) {
2784
+ await this.webrtcHandler.setMicrophoneTrack(track);
2785
+ return;
2786
+ }
2787
+ if (this.activeMode.value === "mse" && this.hasBackchannel.value) {
2788
+ this.pendingMicTrack = track;
2789
+ if (track && !this.backchannelHandler) await this.startBackchannel();
2790
+ else if (!track && this.backchannelHandler) this.closeBackchannel();
2791
+ else if (this.backchannelHandler?.isConnected) await this.backchannelHandler.setMicrophoneTrack(track);
2792
+ }
2793
+ }
2794
+ setMuted(muted) {
2795
+ this.muted.value = muted;
2796
+ const video = this.videoElement.value;
2797
+ if (video) video.muted = muted;
2798
+ }
2799
+ async play() {
2800
+ this.paused.value = false;
2801
+ const video = this.videoElement.value;
2802
+ if (!video) return;
2803
+ try {
2804
+ await video.play();
2805
+ } catch {
2806
+ if (!video.muted) {
2807
+ video.muted = true;
2808
+ this.muted.value = true;
2809
+ try {
2810
+ await video.play();
2811
+ } catch {}
2812
+ }
2813
+ }
2814
+ }
2815
+ pause() {
2816
+ this.paused.value = true;
2817
+ const video = this.videoElement.value;
2818
+ if (video) video.pause();
2819
+ }
2820
+ destroy() {
2821
+ for (const stopWatcher of this.stopWatchers) stopWatcher();
2822
+ this.stopWatchers = [];
2823
+ this.offTabVisible?.();
2824
+ this.offTabVisible = void 0;
2825
+ this.offTabPaused?.();
2826
+ this.offTabPaused = void 0;
2827
+ this.stop();
2828
+ }
2829
+ async restart() {
2830
+ this.status.value = "reconnecting";
2831
+ this.isPlaying.value = false;
2832
+ this.cleanup();
2833
+ this.abortController = new AbortController();
2834
+ const gen = ++this.connectionGeneration;
2835
+ try {
2836
+ await abortableSleep(1e3, this.abortController.signal);
2837
+ } catch {
2838
+ return;
2839
+ }
2840
+ if (gen !== this.connectionGeneration) return;
2841
+ if (!this.isReady.value) {
2842
+ this.status.value = "idle";
2843
+ return;
2844
+ }
2845
+ try {
2846
+ if (!this.initializeSource()) {
2847
+ this.error.value = /* @__PURE__ */ new Error("No streaming source available");
2848
+ this.status.value = "error";
2849
+ return;
2850
+ }
2851
+ this.connectWebSocket();
2852
+ } catch (err) {
2853
+ if (gen !== this.connectionGeneration) return;
2854
+ this.error.value = err instanceof Error ? err : new Error(String(err));
2855
+ this.status.value = "error";
2856
+ }
2857
+ }
2858
+ startWhenReady() {
2859
+ if (this.isReady.value) {
2860
+ log("startWhenReady — already ready, calling start() now");
2861
+ this.start();
2862
+ return;
2863
+ }
2864
+ log(`startWhenReady — not ready (camera=${!!this.camera.value}, video=${!!this.videoElement.value}, target=${!!this.target.value}), watching isReady`);
2865
+ const stop = watch(this.isReady, (ready) => {
2866
+ if (!ready) return;
2867
+ log("startWhenReady — isReady → true, calling start()");
2868
+ stop();
2869
+ this.start();
2870
+ });
2871
+ this.stopWatchers.push(stop);
2872
+ }
2873
+ setupWatchers() {
2874
+ const stopAuthWatch = watch(() => this.target.value?.endpoint.url ?? null, (next, prev) => {
2875
+ if (!next || !prev || next === prev) return;
2876
+ if (this.status.value === "idle" || this.status.value === "closed") return;
2877
+ this.restart();
2878
+ });
2879
+ this.stopWatchers.push(stopAuthWatch);
2880
+ const stopModeWatch = watch(() => toValue(this.options.mode), (newMode) => {
2881
+ if (newMode !== void 0 && newMode !== this.requestedMode.value) this.setMode(newMode);
2882
+ });
2883
+ this.stopWatchers.push(stopModeWatch);
2884
+ const stopResolutionWatch = watch(() => toValue(this.options.resolution), (newResolution) => {
2885
+ if (newResolution !== void 0 && newResolution !== this.requestedResolution.value) this.setResolution(newResolution);
2886
+ });
2887
+ this.stopWatchers.push(stopResolutionWatch);
2888
+ const stopVideoElWatch = watch(this.videoElement, (video, oldVideo) => {
2889
+ if (oldVideo) {
2890
+ if (this.onVideoPauseBound) oldVideo.removeEventListener("pause", this.onVideoPauseBound);
2891
+ if (this.onVideoPlayBound) oldVideo.removeEventListener("play", this.onVideoPlayBound);
2892
+ if (this.onVideoResizeBound) oldVideo.removeEventListener("resize", this.onVideoResizeBound);
2893
+ }
2894
+ if (video) {
2895
+ this.onVideoPauseBound = () => {
2896
+ if (!this.paused.value) video.play().catch(() => {});
2897
+ };
2898
+ this.onVideoPlayBound = () => {
2899
+ this.paused.value = false;
2900
+ };
2901
+ this.onVideoResizeBound = () => {
2902
+ if (video.videoWidth > 0 && video.videoHeight > 0) {
2903
+ this.nativeWidth.value = video.videoWidth;
2904
+ this.nativeHeight.value = video.videoHeight;
2905
+ }
2906
+ };
2907
+ video.addEventListener("pause", this.onVideoPauseBound);
2908
+ video.addEventListener("play", this.onVideoPlayBound);
2909
+ video.addEventListener("resize", this.onVideoResizeBound);
2910
+ if (video.videoWidth > 0 && video.videoHeight > 0) {
2911
+ this.nativeWidth.value = video.videoWidth;
2912
+ this.nativeHeight.value = video.videoHeight;
2913
+ }
2914
+ }
2915
+ }, { immediate: true });
2916
+ this.stopWatchers.push(stopVideoElWatch);
2917
+ const stopMseWatch = watch(this.activeMode, (mode) => {
2918
+ this.stopMseMonitor();
2919
+ if (mode === "mse") this.mseMonitorInterval = setInterval(() => this.monitorMseBuffer(), 1e3);
2920
+ }, { immediate: true });
2921
+ this.stopWatchers.push(stopMseWatch);
2922
+ const stopCameraWatch = watch([
2923
+ this.camera,
2924
+ this.videoElement,
2925
+ () => this.target.value
2926
+ ], ([newCamera, newVideo, newClient], [oldCamera, oldVideo]) => {
2927
+ if (newCamera !== oldCamera) {
2928
+ if (this.status.value === "connected") {
2929
+ if (newCamera && newVideo && newClient) this.restart();
2930
+ }
2931
+ } else if (newVideo !== oldVideo && newVideo) {
2932
+ if (newVideo.srcObject instanceof MediaStream) {
2933
+ const existingStream = newVideo.srcObject;
2934
+ if (existingStream.active && existingStream.getTracks().some((t) => t.readyState === "live")) {
2935
+ playVideo(newVideo);
2936
+ this.lastMediaStream = existingStream;
2937
+ return;
2938
+ }
2939
+ }
2940
+ if (this.status.value === "connected" && this.activeMode.value !== "mse") {
2941
+ const mediaStream = this.lastMediaStream;
2942
+ if (mediaStream?.active && mediaStream.getTracks().some((t) => t.readyState === "live")) {
2943
+ newVideo.srcObject = mediaStream;
2944
+ playVideo(newVideo);
2945
+ } else this.restart();
2946
+ } else if (this.status.value === "connected" && this.activeMode.value === "mse") this.restart();
2947
+ else if (this.status.value === "connecting" || this.status.value === "reconnecting") {
2948
+ const stopConnectionWatch = watch(() => this.isPlaying.value, (playing) => {
2949
+ const currentVideo = this.videoElement.value;
2950
+ if (playing && currentVideo?.srcObject instanceof MediaStream) {
2951
+ playVideo(currentVideo);
2952
+ stopConnectionWatch();
2953
+ } else if (playing && currentVideo && !currentVideo.srcObject) {
2954
+ if (this.lastMediaStream?.active) {
2955
+ currentVideo.srcObject = this.lastMediaStream;
2956
+ playVideo(currentVideo);
2957
+ stopConnectionWatch();
2958
+ }
2959
+ }
2960
+ }, { immediate: true });
2961
+ this.stopWatchers.push(stopConnectionWatch);
2962
+ }
2963
+ }
2964
+ });
2965
+ this.stopWatchers.push(stopCameraWatch);
2966
+ }
2967
+ getSourceForResolution(resolution) {
2968
+ const cam = this.camera.value;
2969
+ if (!cam) return void 0;
2970
+ const resolutionOrder = [
2971
+ "high-resolution",
2972
+ "mid-resolution",
2973
+ "low-resolution"
2974
+ ];
2975
+ const exactSource = cam.sources.value.find((s) => s.role === resolution);
2976
+ if (exactSource) return {
2977
+ source: exactSource,
2978
+ effectiveResolution: resolution
2979
+ };
2980
+ const startIndex = resolutionOrder.indexOf(resolution);
2981
+ for (let i = startIndex; i < resolutionOrder.length; i++) {
2982
+ const src = cam.sources.value.find((s) => s.role === resolutionOrder[i]);
2983
+ if (src) return {
2984
+ source: src,
2985
+ effectiveResolution: resolutionOrder[i]
2986
+ };
2987
+ }
2988
+ const fallback = cam.streamSource.value;
2989
+ if (fallback) return {
2990
+ source: fallback,
2991
+ effectiveResolution: fallback.role ?? "low-resolution"
2992
+ };
2993
+ }
2994
+ initializeSource() {
2995
+ const result = this.getSourceForResolution(this.requestedResolution.value);
2996
+ if (!result) return false;
2997
+ this.source.value = result.source;
2998
+ this.activeResolution.value = result.effectiveResolution;
2999
+ return true;
3000
+ }
3001
+ async probeStream() {
3002
+ const cam = this.camera.value;
3003
+ if (!cam || !this.source.value || this.abortController.signal.aborted) return void 0;
3004
+ try {
3005
+ const probe = await cam.probeStream(this.source.value._id, {
3006
+ video: true,
3007
+ audio: ["pcma", "opus"],
3008
+ microphone: true
3009
+ });
3010
+ if (probe && !this.abortController.signal.aborted) {
3011
+ this.probeInfo.value = probe;
3012
+ this.hasBackchannel.value = probe.audio.some((a) => a.direction === "recvonly");
3013
+ this.hasAudio.value = probe.audio.filter((a) => a.direction === "sendonly").length > 0;
3014
+ this.hasVideo.value = probe.video.filter((v) => v.direction === "sendonly").length > 0;
3015
+ }
3016
+ return probe;
3017
+ } catch {
3018
+ return;
3019
+ }
3020
+ }
3021
+ connectWebSocket() {
3022
+ this.disconnectWebSocket();
3023
+ const cam = this.camera.value;
3024
+ if (this.abortController.signal.aborted || !cam || !this.source.value || !this.target.value) return;
3025
+ const sourceNamePart = this.source.value.name ?? this.source.value.role ?? this.source.value._id;
3026
+ const sourceName = createSourceName(cam.name.value, sourceNamePart);
3027
+ this.startWsConnectTimeout();
3028
+ this.wsHandle = this.wsTransport.open({
3029
+ path: "/api/go2rtc",
3030
+ query: { src: sourceName },
3031
+ binaryType: "arraybuffer"
3032
+ });
3033
+ this.wsHandle.on("open", () => this.handleWsOpen());
3034
+ this.wsHandle.on("close", () => this.handleWsClose());
3035
+ this.wsHandle.on("message", (ev) => this.handleWsMessage(ev));
3036
+ this.wsHandle.on("error", () => {});
3037
+ }
3038
+ disconnectWebSocket() {
3039
+ this.stopWsConnectTimeout();
3040
+ if (this.wsHandle) {
3041
+ this.wsHandle.dispose();
3042
+ this.wsHandle = void 0;
3043
+ }
3044
+ }
3045
+ sendWsMessage(msg) {
3046
+ if (this.wsHandle?.readyState === WebSocket.OPEN) this.wsHandle.send(JSON.stringify(msg));
3047
+ }
3048
+ handleWsOpen() {
3049
+ if (this.abortController.signal.aborted) return;
3050
+ this.stopWsConnectTimeout();
3051
+ this.status.value = "connecting";
3052
+ const mode = this.effectiveMode.value;
3053
+ if (mode === "webrtc" || mode === "webrtc/tcp") this.startWebRTC(mode);
3054
+ else if (mode === "mse") this.startMSE();
3055
+ else if (this.requestedMode.value === "auto") this.startAutoMode();
3056
+ }
3057
+ handleWsClose() {
3058
+ if (this.status.value !== "closed" && !this.abortController.signal.aborted) this.startReconnectTimeout();
3059
+ }
3060
+ handleWsMessage(ev) {
3061
+ if (this.abortController.signal.aborted) return;
3062
+ if (typeof ev.data === "string") {
3063
+ const msg = JSON.parse(ev.data);
3064
+ this.handleMessage(msg);
3065
+ } else if (this.mseHandler) {
3066
+ this.mseHandler.appendBuffer(ev.data);
3067
+ this.handleFirstFrame();
3068
+ }
3069
+ }
3070
+ handleMessage(msg) {
3071
+ switch (msg.type) {
3072
+ case "webrtc/answer":
3073
+ case "webrtc/candidate":
3074
+ if (this.webrtcHandler) processWebRTCMessage(this.webrtcHandler, msg);
3075
+ else if (this.backchannelHandler) processWebRTCMessage(this.backchannelHandler, msg);
3076
+ break;
3077
+ case "mse":
3078
+ if (this.mseHandler && typeof msg.value === "string") this.mseHandler.initializeBuffer(msg.value);
3079
+ break;
3080
+ case "error":
3081
+ if (this.requestedMode.value !== "auto") {
3082
+ this.error.value = new Error(msg.value);
3083
+ this.status.value = "error";
3084
+ }
3085
+ break;
3086
+ }
3087
+ }
3088
+ async startWebRTC(mode) {
3089
+ if (this.abortController.signal.aborted) return;
3090
+ this.startConnectTimeout();
3091
+ this.webrtcHandler = createWebRTCHandler({
3092
+ mode,
3093
+ signal: this.abortController.signal,
3094
+ onConnected: (stream) => this.handleWebRTCConnected(stream),
3095
+ onDisconnected: () => this.handleWebRTCDisconnected(),
3096
+ onFailed: () => this.handleWebRTCFailed(),
3097
+ onCandidate: (candidate) => this.sendWsMessage({
3098
+ type: "webrtc/candidate",
3099
+ value: candidate
3100
+ })
3101
+ });
3102
+ const offer = await this.webrtcHandler.createOffer();
3103
+ if (offer && !this.abortController.signal.aborted) this.sendWsMessage({
3104
+ type: "webrtc/offer",
3105
+ value: offer
3106
+ });
3107
+ }
3108
+ handleWebRTCConnected(stream) {
3109
+ if (this.abortController.signal.aborted) return;
3110
+ const video = this.videoElement.value;
3111
+ if (!video) return;
3112
+ this.stopConnectTimeout();
3113
+ this.activeMode.value = this.requestedMode.value === "auto" ? "webrtc" : this.requestedMode.value;
3114
+ this.lastMediaStream = stream;
3115
+ video.srcObject = stream;
3116
+ video.muted = this.muted.value;
3117
+ playVideo(video);
3118
+ this.status.value = "connected";
3119
+ this.handleFirstFrameWebRTC(stream);
3120
+ if (this.requestedMode.value === "auto" && this.mseHandler) {
3121
+ this.mseHandler.close();
3122
+ this.mseHandler = void 0;
3123
+ }
3124
+ }
3125
+ handleWebRTCDisconnected() {
3126
+ if (this.status.value !== "closed" && !this.abortController.signal.aborted) {
3127
+ this.status.value = "reconnecting";
3128
+ if (this.requestedMode.value === "auto" && this.mseHandler?.isReady) {
3129
+ this.activeMode.value = "mse";
3130
+ this.status.value = "connected";
3131
+ } else this.restart();
3132
+ }
3133
+ }
3134
+ handleWebRTCFailed() {
3135
+ if (this.abortController.signal.aborted) return;
3136
+ this.stopConnectTimeout();
3137
+ if (this.requestedMode.value === "auto") {
3138
+ this.activeMode.value = "mse";
3139
+ if (!this.mseHandler?.isReady) this.startMSE();
3140
+ else this.status.value = "connected";
3141
+ } else {
3142
+ this.error.value = /* @__PURE__ */ new Error("WebRTC connection failed");
3143
+ this.status.value = "error";
3144
+ }
3145
+ }
3146
+ startMSE() {
3147
+ const video = this.videoElement.value;
3148
+ if (this.abortController.signal.aborted || !video) return;
3149
+ this.mseHandler = createMSEHandler({
3150
+ videoElement: video,
3151
+ signal: this.abortController.signal,
3152
+ onReady: () => this.handleMSEReady(),
3153
+ onFirstData: () => this.handleMSEFirstData(),
3154
+ onError: (err) => {
3155
+ if (this.requestedMode.value !== "auto") {
3156
+ this.error.value = err;
3157
+ this.status.value = "error";
3158
+ }
3159
+ }
3160
+ });
3161
+ const codecs = this.mseHandler.setup();
3162
+ if (codecs) this.sendWsMessage({
3163
+ type: "mse",
3164
+ value: codecs
3165
+ });
3166
+ }
3167
+ handleMSEReady() {
3168
+ if (this.abortController.signal.aborted) return;
3169
+ if (this.requestedMode.value === "auto") {
3170
+ if (!this.webrtcHandler?.isConnected) {
3171
+ this.activeMode.value = "mse";
3172
+ this.status.value = "connected";
3173
+ }
3174
+ } else {
3175
+ this.activeMode.value = "mse";
3176
+ this.status.value = "connected";
3177
+ }
3178
+ }
3179
+ handleMSEFirstData() {
3180
+ if (this.abortController.signal.aborted) return;
3181
+ const video = this.videoElement.value;
3182
+ if (video) playVideo(video);
3183
+ }
3184
+ async startAutoMode() {
3185
+ if (this.abortController.signal.aborted) return;
3186
+ const probe = this.probeInfo.value ?? await this.probeStream();
3187
+ if (probe) {
3188
+ if (!checkWebRTCCompatibility(probe).compatible) {
3189
+ this.activeMode.value = "mse";
3190
+ this.startMSE();
3191
+ return;
3192
+ }
3193
+ }
3194
+ this.startMSE();
3195
+ this.startWebRTC("webrtc");
3196
+ }
3197
+ handleFirstFrame() {
3198
+ if (!this.isPlaying.value && !this.abortController.signal.aborted) this.isPlaying.value = true;
3199
+ }
3200
+ monitorMseBuffer() {
3201
+ const video = this.videoElement.value;
3202
+ if (!video || this.activeMode.value !== "mse" || !this.isPlaying.value) return;
3203
+ if (video.buffered.length > 0) {
3204
+ const end = video.buffered.end(video.buffered.length - 1);
3205
+ if (end - video.currentTime > 1) video.currentTime = end;
3206
+ }
3207
+ }
3208
+ stopMseMonitor() {
3209
+ if (this.mseMonitorInterval !== void 0) {
3210
+ clearInterval(this.mseMonitorInterval);
3211
+ this.mseMonitorInterval = void 0;
3212
+ }
3213
+ }
3214
+ handleFirstFrameWebRTC(stream) {
3215
+ const video = this.videoElement.value;
3216
+ if (this.isPlaying.value || this.abortController.signal.aborted || !video) return;
3217
+ if (!stream.getVideoTracks()[0]) {
3218
+ this.handleFirstFrame();
3219
+ return;
3220
+ }
3221
+ const videoWithCallback = video;
3222
+ if (typeof videoWithCallback.requestVideoFrameCallback === "function") this.firstFrameCallbackId = videoWithCallback.requestVideoFrameCallback(() => {
3223
+ if (!this.abortController.signal.aborted) this.handleFirstFrame();
3224
+ });
3225
+ else {
3226
+ const checkFrame = () => {
3227
+ if (this.abortController.signal.aborted) return;
3228
+ if (video.readyState >= 2 && video.videoWidth > 0) this.handleFirstFrame();
3229
+ else this.firstFrameCallbackId = requestAnimationFrame(checkFrame);
3230
+ };
3231
+ this.firstFrameCallbackId = requestAnimationFrame(checkFrame);
3232
+ }
3233
+ }
3234
+ cleanup() {
3235
+ this.abortController.abort();
3236
+ this.stopConnectTimeout();
3237
+ this.stopWsConnectTimeout();
3238
+ this.stopReconnectTimeout();
3239
+ this.stopMseMonitor();
3240
+ const videoEl = this.videoElement.value;
3241
+ if (videoEl) {
3242
+ if (this.onVideoPauseBound) videoEl.removeEventListener("pause", this.onVideoPauseBound);
3243
+ if (this.onVideoPlayBound) videoEl.removeEventListener("play", this.onVideoPlayBound);
3244
+ if (this.onVideoResizeBound) videoEl.removeEventListener("resize", this.onVideoResizeBound);
3245
+ }
3246
+ this.lastMediaStream = null;
3247
+ this.webrtcHandler?.close();
3248
+ this.webrtcHandler = void 0;
3249
+ this.mseHandler?.close();
3250
+ this.mseHandler = void 0;
3251
+ this.closeBackchannel();
3252
+ this.disconnectWebSocket();
3253
+ const video = this.videoElement.value;
3254
+ if (this.firstFrameCallbackId !== void 0) {
3255
+ if (video && "cancelVideoFrameCallback" in video) video.cancelVideoFrameCallback(this.firstFrameCallbackId);
3256
+ else cancelAnimationFrame(this.firstFrameCallbackId);
3257
+ this.firstFrameCallbackId = void 0;
3258
+ }
3259
+ if (video) {
3260
+ video.style.display = "";
3261
+ video.pause();
3262
+ video.srcObject = null;
3263
+ video.removeAttribute("src");
3264
+ video.load();
3265
+ }
3266
+ }
3267
+ async startBackchannel() {
3268
+ if (this.abortController.signal.aborted || !this.wsHandle || this.wsHandle.readyState !== WebSocket.OPEN) return;
3269
+ this.backchannelHandler = createBackchannelHandler({
3270
+ signal: this.abortController.signal,
3271
+ onCandidate: (candidate) => this.sendWsMessage({
3272
+ type: "webrtc/candidate",
3273
+ value: candidate
3274
+ }),
3275
+ onConnected: async () => {
3276
+ if (this.pendingMicTrack && this.backchannelHandler) await this.backchannelHandler.setMicrophoneTrack(this.pendingMicTrack);
3277
+ },
3278
+ onDisconnected: () => {
3279
+ this.backchannelHandler = void 0;
3280
+ }
3281
+ });
3282
+ const offer = await this.backchannelHandler.createOffer();
3283
+ if (offer && !this.abortController.signal.aborted) this.sendWsMessage({
3284
+ type: "webrtc/offer",
3285
+ value: offer
3286
+ });
3287
+ }
3288
+ closeBackchannel() {
3289
+ this.backchannelHandler?.close();
3290
+ this.backchannelHandler = void 0;
3291
+ this.pendingMicTrack = null;
3292
+ }
3293
+ };
3294
+ function createStreamConnection(options) {
3295
+ return new StreamConnection(options);
3296
+ }
3297
+ //#endregion
3298
+ //#region src/streaming/streamManager.ts
3299
+ var StreamManager = class {
3300
+ streams = /* @__PURE__ */ new Map();
3301
+ releaseTimers = /* @__PURE__ */ new Map();
3302
+ releaseDelay;
3303
+ DEFAULT_RELEASE_DELAY = 2e3;
3304
+ constructor(config = {}) {
3305
+ this.releaseDelay = config.releaseDelay ?? this.DEFAULT_RELEASE_DELAY;
3306
+ }
3307
+ has(cameraName) {
3308
+ return this.streams.has(cameraName);
3309
+ }
3310
+ get(cameraName) {
3311
+ return this.streams.get(cameraName);
3312
+ }
3313
+ acquire(cameraName, consumerContainerRef) {
3314
+ this.cancelRelease(cameraName);
3315
+ const entry = this.streams.get(cameraName);
3316
+ if (entry) {
3317
+ entry.refCount++;
3318
+ if (consumerContainerRef) entry.consumerContainerRefs.add(consumerContainerRef);
3319
+ return entry;
3320
+ }
3321
+ }
3322
+ register(cameraName, stream, videoElementRef, cameraDeviceRef, containerElementRef, sharedVideoElement) {
3323
+ this.cancelRelease(cameraName);
3324
+ const entry = {
3325
+ stream,
3326
+ videoElementRef,
3327
+ cameraDeviceRef,
3328
+ containerElementRef,
3329
+ consumerContainerRefs: new Set([containerElementRef]),
3330
+ sharedVideoElement,
3331
+ mediaStream: null,
3332
+ refCount: 1
3333
+ };
3334
+ this.streams.set(cameraName, entry);
3335
+ return entry;
3336
+ }
3337
+ updateContainerElement(cameraName, container) {
3338
+ const entry = this.streams.get(cameraName);
3339
+ if (entry) entry.containerElementRef.value = container;
3340
+ }
3341
+ updateSharedVideoElement(cameraName, videoElement) {
3342
+ const entry = this.streams.get(cameraName);
3343
+ if (entry) entry.sharedVideoElement = videoElement;
3344
+ }
3345
+ updateMediaStream(cameraName, mediaStream) {
3346
+ const entry = this.streams.get(cameraName);
3347
+ if (entry) entry.mediaStream = mediaStream;
3348
+ }
3349
+ release(cameraName, _videoElement, consumerContainerRef) {
3350
+ const entry = this.streams.get(cameraName);
3351
+ if (!entry) return;
3352
+ entry.refCount--;
3353
+ if (consumerContainerRef) {
3354
+ entry.consumerContainerRefs.delete(consumerContainerRef);
3355
+ if (entry.refCount > 0 && consumerContainerRef.value === entry.containerElementRef.value) for (const ref of entry.consumerContainerRefs) {
3356
+ const candidate = ref.value;
3357
+ if (candidate && candidate !== consumerContainerRef.value) {
3358
+ entry.containerElementRef = ref;
3359
+ if (entry.sharedVideoElement && entry.sharedVideoElement.parentElement !== candidate) {
3360
+ candidate.appendChild(entry.sharedVideoElement);
3361
+ entry.sharedVideoElement.play().catch(() => {});
3362
+ }
3363
+ break;
3364
+ }
3365
+ }
3366
+ }
3367
+ if (entry.refCount <= 0) {
3368
+ const timer = setTimeout(() => {
3369
+ this.doRelease(cameraName);
3370
+ }, this.releaseDelay);
3371
+ this.releaseTimers.set(cameraName, timer);
3372
+ }
3373
+ }
3374
+ forceRelease(cameraName) {
3375
+ this.cancelRelease(cameraName);
3376
+ this.doRelease(cameraName);
3377
+ }
3378
+ clear() {
3379
+ for (const timer of this.releaseTimers.values()) clearTimeout(timer);
3380
+ this.releaseTimers.clear();
3381
+ for (const [cameraName, entry] of this.streams) {
3382
+ entry.stream.stop();
3383
+ this.streams.delete(cameraName);
3384
+ }
3385
+ }
3386
+ getDebugInfo() {
3387
+ return Array.from(this.streams.entries()).map(([cameraName, entry]) => ({
3388
+ cameraName,
3389
+ refCount: entry.refCount,
3390
+ mode: entry.stream.activeMode.value,
3391
+ resolution: entry.stream.activeResolution.value
3392
+ }));
3393
+ }
3394
+ cancelRelease(cameraName) {
3395
+ const timer = this.releaseTimers.get(cameraName);
3396
+ if (timer) {
3397
+ clearTimeout(timer);
3398
+ this.releaseTimers.delete(cameraName);
3399
+ }
3400
+ }
3401
+ doRelease(cameraName) {
3402
+ const entry = this.streams.get(cameraName);
3403
+ if (!entry) return;
3404
+ if (entry.refCount <= 0) {
3405
+ entry.stream.stop();
3406
+ this.streams.delete(cameraName);
3407
+ }
3408
+ this.releaseTimers.delete(cameraName);
3409
+ }
3410
+ };
3411
+ var streamManager = new StreamManager();
3412
+ //#endregion
3413
+ //#region src/composables/useFullscreen.ts
3414
+ var fsRegistry = /* @__PURE__ */ new WeakMap();
3415
+ var fullscreenStack = [];
3416
+ var topmostFullscreenWrapper = shallowRef(null);
3417
+ function refreshTopmost() {
3418
+ topmostFullscreenWrapper.value = fullscreenStack[fullscreenStack.length - 1] ?? null;
3419
+ }
3420
+ function useTopmostFullscreenElement() {
3421
+ return computed(() => topmostFullscreenWrapper.value);
3422
+ }
3423
+ function useCuiFullscreen(target, options = {}) {
3424
+ const mode = options.mode ?? "fit";
3425
+ const isFullscreen = ref(false);
3426
+ const currentEl = shallowRef(null);
3427
+ watch(() => toValue(target), (el) => {
3428
+ currentEl.value = el ?? null;
3429
+ }, { immediate: true });
3430
+ function onEscape(e) {
3431
+ if (e.key === "Escape" && isFullscreen.value) exit();
3432
+ }
3433
+ async function enter() {
3434
+ const el = currentEl.value;
3435
+ if (!el || isFullscreen.value) return;
3436
+ const wrapper = document.createElement("div");
3437
+ wrapper.className = `cui-fullscreen cui-fullscreen-${mode}`;
3438
+ applyWrapperStyles(wrapper, mode);
3439
+ const state = {
3440
+ wrapper,
3441
+ parent: el.parentElement,
3442
+ next: el.nextSibling,
3443
+ bodyOverflow: document.body.style.overflow,
3444
+ htmlOverflow: document.documentElement.style.overflow
3445
+ };
3446
+ fsRegistry.set(el, state);
3447
+ el.setAttribute("data-cui-fullscreen", mode);
3448
+ document.body.appendChild(wrapper);
3449
+ wrapper.appendChild(el);
3450
+ document.body.style.overflow = "hidden";
3451
+ document.documentElement.style.overflow = "hidden";
3452
+ document.addEventListener("keydown", onEscape);
3453
+ fullscreenStack.push(wrapper);
3454
+ refreshTopmost();
3455
+ isFullscreen.value = true;
3456
+ }
3457
+ async function exit() {
3458
+ const el = currentEl.value;
3459
+ if (!el || !isFullscreen.value) return;
3460
+ const state = fsRegistry.get(el);
3461
+ if (!state) return;
3462
+ fsRegistry.delete(el);
3463
+ el.removeAttribute("data-cui-fullscreen");
3464
+ if (state.parent) if (state.next && state.next.parentNode === state.parent) state.parent.insertBefore(el, state.next);
3465
+ else state.parent.appendChild(el);
3466
+ state.wrapper.remove();
3467
+ document.body.style.overflow = state.bodyOverflow;
3468
+ document.documentElement.style.overflow = state.htmlOverflow;
3469
+ document.removeEventListener("keydown", onEscape);
3470
+ const stackIndex = fullscreenStack.indexOf(state.wrapper);
3471
+ if (stackIndex !== -1) fullscreenStack.splice(stackIndex, 1);
3472
+ refreshTopmost();
3473
+ isFullscreen.value = false;
3474
+ }
3475
+ async function toggle() {
3476
+ if (isFullscreen.value) await exit();
3477
+ else await enter();
3478
+ }
3479
+ tryOnScopeDispose(() => {
3480
+ if (isFullscreen.value && currentEl.value) exit();
3481
+ document.removeEventListener("keydown", onEscape);
3482
+ });
3483
+ return {
3484
+ isFullscreen: computed(() => isFullscreen.value),
3485
+ isSupported: computed(() => true),
3486
+ enter,
3487
+ exit,
3488
+ toggle
3489
+ };
3490
+ }
3491
+ function applyWrapperStyles(wrapper, mode) {
3492
+ const common = {
3493
+ position: "fixed",
3494
+ top: "0",
3495
+ right: "0",
3496
+ bottom: "0",
3497
+ left: "0",
3498
+ zIndex: "2147483647",
3499
+ margin: "0",
3500
+ background: "black",
3501
+ paddingTop: "env(safe-area-inset-top)",
3502
+ paddingRight: "env(safe-area-inset-right)",
3503
+ paddingBottom: "env(safe-area-inset-bottom)",
3504
+ paddingLeft: "env(safe-area-inset-left)",
3505
+ boxSizing: "border-box"
3506
+ };
3507
+ if (mode === "fit") Object.assign(wrapper.style, common, {
3508
+ display: "flex",
3509
+ alignItems: "center",
3510
+ justifyContent: "center",
3511
+ overflow: "hidden"
3512
+ });
3513
+ else Object.assign(wrapper.style, common, {
3514
+ display: "block",
3515
+ overflowY: "auto",
3516
+ overflowX: "hidden",
3517
+ WebkitOverflowScrolling: "touch",
3518
+ overscrollBehavior: "contain"
3519
+ });
3520
+ }
3521
+ //#endregion
3522
+ //#region src/composables/useCameraStream.ts
3523
+ var AUTO_STAGGER_STEP_MS = 75;
3524
+ var AUTO_STAGGER_RESET_MS = 500;
3525
+ var autoStaggerIndex = 0;
3526
+ var autoStaggerLastTouch = 0;
3527
+ function acquireAutoStaggerDelay() {
3528
+ const now = performance.now();
3529
+ if (now - autoStaggerLastTouch > AUTO_STAGGER_RESET_MS) autoStaggerIndex = 0;
3530
+ const delay = autoStaggerIndex * AUTO_STAGGER_STEP_MS;
3531
+ autoStaggerIndex++;
3532
+ autoStaggerLastTouch = now;
3533
+ return delay;
3534
+ }
3535
+ function useCameraStream(options) {
3536
+ const { activityConfig, autoStart: autoStartOption = true, isolated = false } = options;
3537
+ const shouldAutoStart = () => toValue(autoStartOption);
3538
+ const startDelay = options.startDelay ?? acquireAutoStaggerDelay();
3539
+ const autoStartReady = ref(startDelay <= 0);
3540
+ let startDelayTimer;
3541
+ if (startDelay > 0) startDelayTimer = setTimeout(() => {
3542
+ autoStartReady.value = true;
3543
+ }, startDelay);
3544
+ const { isConnected } = useCameraUi();
3545
+ const cameraGetter = computed(() => toValue(options.camera));
3546
+ const isCameraString = computed(() => typeof cameraGetter.value === "string");
3547
+ const cameraName = computed(() => {
3548
+ const cam = cameraGetter.value;
3549
+ return typeof cam === "string" ? cam : cam.name.value;
3550
+ });
3551
+ const { camera: cameraDeviceFromLookup, isLoading: cameraDeviceLoading } = useCameraById(cameraName);
3552
+ const resolvedCameraDevice = computed(() => {
3553
+ if (isCameraString.value) return cameraDeviceFromLookup.value;
3554
+ return cameraGetter.value;
3555
+ });
3556
+ const containerElement = shallowRef();
3557
+ const fullscreenElement = shallowRef();
3558
+ const videoElement = shallowRef();
3559
+ const streamVideoElementRef = shallowRef();
3560
+ const currentStream = shallowRef();
3561
+ const isUsingCachedStream = ref(false);
3562
+ const initialized = ref(false);
3563
+ const cleanedUp = ref(false);
3564
+ const cameraDeviceRef = shallowRef();
3565
+ watch(resolvedCameraDevice, (device) => {
3566
+ cameraDeviceRef.value = device;
3567
+ }, { immediate: true });
3568
+ const isCameraDisabled = computed(() => cameraDeviceRef.value?.disabled.value === true);
3569
+ const cleanupFns = [];
3570
+ let ownedConnection;
3571
+ function createOwnedStream() {
3572
+ ownedConnection = createStreamConnection({
3573
+ camera: cameraDeviceRef,
3574
+ videoElement: streamVideoElementRef,
3575
+ containerElement,
3576
+ mode: computed(() => toValue(options.mode) ?? "auto"),
3577
+ resolution: computed(() => toValue(options.resolution) ?? "high-resolution"),
3578
+ autoStart: false
3579
+ });
3580
+ return ownedConnection;
3581
+ }
3582
+ const ownedStream = createOwnedStream();
3583
+ function createVideoElement() {
3584
+ const video = document.createElement("video");
3585
+ video.autoplay = true;
3586
+ video.playsInline = true;
3587
+ video.muted = true;
3588
+ video.disablePictureInPicture = false;
3589
+ video.preload = "auto";
3590
+ video.style.position = "absolute";
3591
+ video.style.width = "100%";
3592
+ video.style.height = "100%";
3593
+ video.style.inset = "0";
3594
+ video.style.objectFit = "fill";
3595
+ return video;
3596
+ }
3597
+ function insertVideoIntoContainer(video, container) {
3598
+ if (video.parentElement && video.parentElement !== container) video.parentElement.removeChild(video);
3599
+ if (video.parentElement !== container) container.appendChild(video);
3600
+ }
3601
+ watch([containerElement, videoElement], ([container, video]) => {
3602
+ if (container && video) insertVideoIntoContainer(video, container);
3603
+ }, { immediate: true });
3604
+ const status = computed(() => currentStream.value?.status.value ?? "idle");
3605
+ const isPlaying = computed(() => currentStream.value?.isPlaying.value ?? false);
3606
+ const activeMode = computed(() => currentStream.value?.activeMode.value ?? "webrtc");
3607
+ const activeResolution = computed(() => currentStream.value?.activeResolution.value ?? "low-resolution");
3608
+ const hasAudio = computed(() => currentStream.value?.hasAudio.value ?? false);
3609
+ const hasBackchannel = computed(() => currentStream.value?.hasBackchannel.value ?? false);
3610
+ const error = computed(() => currentStream.value?.error.value);
3611
+ const isReconnecting = computed(() => status.value === "reconnecting");
3612
+ const isBusy = computed(() => cameraDeviceLoading.value || !isPlaying.value && status.value !== "error");
3613
+ const hasSound = computed(() => hasAudio.value && status.value === "connected");
3614
+ const hasIntercom = computed(() => Boolean(typeof navigator !== "undefined" && navigator.mediaDevices) && hasBackchannel.value);
3615
+ const muted = computed(() => currentStream.value?.muted.value ?? true);
3616
+ const paused = computed(() => currentStream.value?.paused.value ?? false);
3617
+ const nativeWidth = ref(0);
3618
+ const nativeHeight = ref(0);
3619
+ const stopDimensionSync = watch([() => currentStream.value?.nativeWidth.value, () => currentStream.value?.nativeHeight.value], ([w, h]) => {
3620
+ if (w && w > 0) nativeWidth.value = w;
3621
+ if (h && h > 0) nativeHeight.value = h;
3622
+ });
3623
+ cleanupFns.push(stopDimensionSync);
3624
+ if (options.canvasStyle !== void 0 || options.canvasClass !== void 0) {
3625
+ let stopCanvasListener;
3626
+ function applyCanvasStyles(canvas) {
3627
+ const style = toValue(options.canvasStyle);
3628
+ const cls = toValue(options.canvasClass);
3629
+ if (style) if (typeof style === "string") canvas.style.cssText += ";" + style;
3630
+ else if (Array.isArray(style)) {
3631
+ for (const s of style) if (typeof s === "string") canvas.style.cssText += ";" + s;
3632
+ else if (s) Object.assign(canvas.style, s);
3633
+ } else Object.assign(canvas.style, style);
3634
+ if (cls) if (typeof cls === "string") canvas.className = cls;
3635
+ else if (Array.isArray(cls)) canvas.className = cls.filter(Boolean).join(" ");
3636
+ else for (const [name, active] of Object.entries(cls)) canvas.classList.toggle(name, !!active);
3637
+ }
3638
+ const stopContainerWatch = watch(containerElement, (container) => {
3639
+ stopCanvasListener?.();
3640
+ stopCanvasListener = void 0;
3641
+ if (!container) return;
3642
+ Array.from(container.querySelectorAll("canvas")).forEach((node) => {
3643
+ applyCanvasStyles(node);
3644
+ });
3645
+ const observer = new MutationObserver((records) => {
3646
+ for (const r of records) Array.from(r.addedNodes).forEach((added) => {
3647
+ if (added instanceof HTMLCanvasElement) applyCanvasStyles(added);
3648
+ else if (added instanceof HTMLElement) Array.from(added.querySelectorAll("canvas")).forEach((c) => applyCanvasStyles(c));
3649
+ });
3650
+ });
3651
+ observer.observe(container, {
3652
+ childList: true,
3653
+ subtree: true
3654
+ });
3655
+ stopCanvasListener = () => observer.disconnect();
3656
+ }, { immediate: true });
3657
+ cleanupFns.push(() => {
3658
+ stopContainerWatch();
3659
+ stopCanvasListener?.();
3660
+ });
3661
+ }
3662
+ if (options.videoStyle !== void 0 || options.videoClass !== void 0) {
3663
+ const stopVideoStyleWatch = watch([
3664
+ videoElement,
3665
+ () => toValue(options.videoStyle),
3666
+ () => toValue(options.videoClass)
3667
+ ], ([video, style, cls]) => {
3668
+ if (!video) return;
3669
+ if (style) if (typeof style === "string") video.style.cssText += ";" + style;
3670
+ else if (Array.isArray(style)) {
3671
+ for (const s of style) if (typeof s === "string") video.style.cssText += ";" + s;
3672
+ else if (s) Object.assign(video.style, s);
3673
+ } else Object.assign(video.style, style);
3674
+ if (cls) if (typeof cls === "string") video.className = cls;
3675
+ else if (Array.isArray(cls)) video.className = cls.filter(Boolean).join(" ");
3676
+ else for (const [name, active] of Object.entries(cls)) video.classList.toggle(name, active);
3677
+ }, { immediate: true });
3678
+ cleanupFns.push(stopVideoStyleWatch);
3679
+ }
3680
+ const isPip = ref(false);
3681
+ const supportsPip = computed(() => typeof document !== "undefined" && document.pictureInPictureEnabled && !!videoElement.value);
3682
+ function onEnterPip() {
3683
+ isPip.value = true;
3684
+ }
3685
+ function onLeavePip() {
3686
+ isPip.value = false;
3687
+ }
3688
+ const stopPipWatch = watch(videoElement, (video, oldVideo) => {
3689
+ if (oldVideo) {
3690
+ oldVideo.removeEventListener("enterpictureinpicture", onEnterPip);
3691
+ oldVideo.removeEventListener("leavepictureinpicture", onLeavePip);
3692
+ }
3693
+ if (video) {
3694
+ video.addEventListener("enterpictureinpicture", onEnterPip);
3695
+ video.addEventListener("leavepictureinpicture", onLeavePip);
3696
+ }
3697
+ }, { immediate: true });
3698
+ cleanupFns.push(stopPipWatch);
3699
+ const { isFullscreen, toggle: toggleFullscreen } = useCuiFullscreen(computed(() => fullscreenElement.value ?? containerElement.value));
3700
+ const activityModeManager = createActivityMode({
3701
+ initialMode: toValue(options.activityMode) ?? "always-on",
3702
+ config: activityConfig,
3703
+ onStreamStart: () => {
3704
+ if (!isCameraDisabled.value) currentStream.value?.start();
3705
+ },
3706
+ onStreamStop: () => currentStream.value?.stop(),
3707
+ isStreamPlaying: () => isPlaying.value
3708
+ });
3709
+ watch(() => toValue(options.activityMode), (newMode) => {
3710
+ if (newMode && newMode !== activityModeManager.mode.value) activityModeManager.setMode(newMode);
3711
+ });
3712
+ function initializeIsolated() {
3713
+ if (initialized.value) return;
3714
+ const container = containerElement.value;
3715
+ const camDevice = resolvedCameraDevice.value;
3716
+ if (!container || !camDevice || !isConnected.value) return;
3717
+ initialized.value = true;
3718
+ isUsingCachedStream.value = false;
3719
+ currentStream.value = ownedStream;
3720
+ const video = createVideoElement();
3721
+ videoElement.value = video;
3722
+ streamVideoElementRef.value = video;
3723
+ if (shouldAutoStart() && autoStartReady.value && activityModeManager.mode.value === "always-on" && !isCameraDisabled.value) ownedStream.start();
3724
+ }
3725
+ function initialize() {
3726
+ if (initialized.value) return;
3727
+ if (isolated) {
3728
+ initializeIsolated();
3729
+ return;
3730
+ }
3731
+ const container = containerElement.value;
3732
+ const camName = cameraName.value;
3733
+ const camDevice = resolvedCameraDevice.value;
3734
+ if (!container || !camName || !isConnected.value) return;
3735
+ const cached = streamManager.acquire(camName, containerElement);
3736
+ if (cached) {
3737
+ initialized.value = true;
3738
+ isUsingCachedStream.value = true;
3739
+ currentStream.value = cached.stream;
3740
+ if (camDevice && cached.cameraDeviceRef) cached.cameraDeviceRef.value = camDevice;
3741
+ let video = cached.sharedVideoElement;
3742
+ if (!video) {
3743
+ video = createVideoElement();
3744
+ cached.sharedVideoElement = video;
3745
+ streamManager.updateSharedVideoElement(camName, video);
3746
+ }
3747
+ if (cached.mediaStream?.active) video.srcObject = cached.mediaStream;
3748
+ if (container) cached.containerElementRef = containerElement;
3749
+ videoElement.value = video;
3750
+ cached.videoElementRef.value = video;
3751
+ setupCachedStreamWatchers(cached.stream, video, camName);
3752
+ if (shouldAutoStart() && autoStartReady.value && !isCameraDisabled.value) if (cached.stream.activeMode.value !== "mse") attachCachedStream(cached, video, camName);
3753
+ else video.play().catch(() => {});
3754
+ } else if (camDevice) {
3755
+ initialized.value = true;
3756
+ isUsingCachedStream.value = false;
3757
+ currentStream.value = ownedStream;
3758
+ const video = createVideoElement();
3759
+ videoElement.value = video;
3760
+ streamVideoElementRef.value = video;
3761
+ streamManager.register(camName, ownedStream, streamVideoElementRef, cameraDeviceRef, containerElement, video);
3762
+ if (shouldAutoStart() && autoStartReady.value && activityModeManager.mode.value === "always-on" && !isCameraDisabled.value) ownedStream.start();
3763
+ const stopPlayingWatch = watch(() => ownedStream.isPlaying.value, (playing) => {
3764
+ if (playing && video.srcObject instanceof MediaStream) streamManager.updateMediaStream(camName, video.srcObject);
3765
+ }, { immediate: true });
3766
+ cleanupFns.push(stopPlayingWatch);
3767
+ const stopStatusWatch = watch(() => ownedStream.status.value, (status) => {
3768
+ if (status === "connected") {
3769
+ if (video.srcObject instanceof MediaStream) streamManager.updateMediaStream(camName, video.srcObject);
3770
+ }
3771
+ });
3772
+ cleanupFns.push(stopStatusWatch);
3773
+ }
3774
+ }
3775
+ function attachCachedStream(cached, video, camName) {
3776
+ if (!cached) return;
3777
+ const stream = cached.stream;
3778
+ const streamMode = stream.activeMode.value;
3779
+ const streamStatus = stream.status.value;
3780
+ const mediaStreamValid = cached.mediaStream?.active && cached.mediaStream.getTracks().some((t) => t.readyState === "live");
3781
+ if (streamMode === "mse") {
3782
+ streamManager.updateMediaStream(camName, null);
3783
+ stream.restart();
3784
+ } else if (mediaStreamValid && cached.mediaStream) {
3785
+ video.srcObject = cached.mediaStream;
3786
+ video.play().catch(() => {});
3787
+ } else if (streamStatus === "idle" || streamStatus === "closed") {
3788
+ stream.start();
3789
+ watchForMediaStream(stream, video, camName);
3790
+ } else if (streamStatus === "connected") {
3791
+ stream.restart();
3792
+ watchForMediaStream(stream, video, camName);
3793
+ } else watchForMediaStream(stream, video, camName);
3794
+ }
3795
+ function watchForMediaStream(stream, video, camName) {
3796
+ const stopWatch = watch([() => stream.isPlaying.value, () => stream.status.value], ([playing, status]) => {
3797
+ if (status === "connected" || playing) {
3798
+ const mediaStream = streamManager.get(camName)?.mediaStream;
3799
+ if (mediaStream?.active && mediaStream.getTracks().some((t) => t.readyState === "live")) {
3800
+ video.srcObject = mediaStream;
3801
+ video.play().catch(() => {});
3802
+ stopWatch();
3803
+ } else if (video.srcObject instanceof MediaStream) {
3804
+ streamManager.updateMediaStream(camName, video.srcObject);
3805
+ video.play().catch(() => {});
3806
+ stopWatch();
3807
+ }
3808
+ }
3809
+ }, { immediate: true });
3810
+ cleanupFns.push(stopWatch);
3811
+ }
3812
+ function setupCachedStreamWatchers(stream, video, camName) {
3813
+ let wasDisconnected = stream.status.value !== "connected";
3814
+ const stopStatusWatch = watch(() => stream.status.value, (status) => {
3815
+ if (status === "reconnecting" || status === "connecting") wasDisconnected = true;
3816
+ else if (status === "connected" && wasDisconnected) {
3817
+ wasDisconnected = false;
3818
+ setTimeout(() => {
3819
+ if (video.srcObject instanceof MediaStream) {
3820
+ streamManager.updateMediaStream(camName, video.srcObject);
3821
+ video.play().catch(() => {});
3822
+ }
3823
+ }, 100);
3824
+ }
3825
+ }, { immediate: true });
3826
+ cleanupFns.push(stopStatusWatch);
3827
+ const stopPlayingWatch = watch(() => stream.isPlaying.value, (playing) => {
3828
+ if (playing && stream.activeMode.value !== "mse") {
3829
+ if (video.srcObject instanceof MediaStream) {
3830
+ streamManager.updateMediaStream(camName, video.srcObject);
3831
+ video.play().catch(() => {});
3832
+ }
3833
+ }
3834
+ }, { immediate: true });
3835
+ cleanupFns.push(stopPlayingWatch);
3836
+ }
3837
+ async function start() {
3838
+ if (isCameraDisabled.value) return;
3839
+ if (activityModeManager.inStandby.value) activityModeManager.resumeFromStandby();
3840
+ else await currentStream.value?.start();
3841
+ }
3842
+ function stop() {
3843
+ currentStream.value?.stop();
3844
+ }
3845
+ async function restart() {
3846
+ await currentStream.value?.restart();
3847
+ }
3848
+ function resumeFromStandby() {
3849
+ if (isCameraDisabled.value) return;
3850
+ activityModeManager.resumeFromStandby();
3851
+ }
3852
+ async function setMode(mode) {
3853
+ const camName = cameraName.value;
3854
+ if (camName) streamManager.updateMediaStream(camName, null);
3855
+ await currentStream.value?.setMode(mode);
3856
+ }
3857
+ async function setResolution(resolution) {
3858
+ await currentStream.value?.setResolution(resolution);
3859
+ }
3860
+ async function setMicrophone(track) {
3861
+ await currentStream.value?.setMicrophone(track);
3862
+ }
3863
+ function setActivityMode(mode) {
3864
+ activityModeManager.setMode(mode);
3865
+ }
3866
+ function reportActivity(detected) {
3867
+ activityModeManager.reportActivity(detected);
3868
+ }
3869
+ function setMuted(newMuted) {
3870
+ currentStream.value?.setMuted(newMuted);
3871
+ const video = videoElement.value;
3872
+ if (video) video.muted = newMuted;
3873
+ }
3874
+ async function playStream() {
3875
+ await currentStream.value?.play();
3876
+ }
3877
+ function pauseStream() {
3878
+ currentStream.value?.pause();
3879
+ }
3880
+ function togglePip() {
3881
+ if (document.pictureInPictureElement) document.exitPictureInPicture();
3882
+ else if (document.pictureInPictureEnabled && videoElement.value) videoElement.value.requestPictureInPicture();
3883
+ }
3884
+ const renderElement = computed(() => videoElement.value);
3885
+ function captureScreenshot() {
3886
+ const video = videoElement.value;
3887
+ if (!video || video.videoWidth === 0) return null;
3888
+ const tmp = document.createElement("canvas");
3889
+ tmp.width = video.videoWidth;
3890
+ tmp.height = video.videoHeight;
3891
+ tmp.getContext("2d")?.drawImage(video, 0, 0);
3892
+ return tmp.toDataURL("image/png");
3893
+ }
3894
+ watch([
3895
+ containerElement,
3896
+ resolvedCameraDevice,
3897
+ isConnected,
3898
+ autoStartReady
3899
+ ], () => {
3900
+ if (!initialized.value) initialize();
3901
+ else if (autoStartReady.value && shouldAutoStart() && !isPlaying.value && status.value === "idle" && !isCameraDisabled.value) currentStream.value?.start();
3902
+ }, { immediate: true });
3903
+ watch(isCameraDisabled, (disabled, wasDisabled) => {
3904
+ if (!initialized.value) return;
3905
+ if (!wasDisabled && disabled) currentStream.value?.stop();
3906
+ else if (wasDisabled && !disabled && shouldAutoStart()) currentStream.value?.restart();
3907
+ });
3908
+ function cleanup() {
3909
+ if (cleanedUp.value) return;
3910
+ cleanedUp.value = true;
3911
+ if (startDelayTimer) {
3912
+ clearTimeout(startDelayTimer);
3913
+ startDelayTimer = void 0;
3914
+ }
3915
+ currentStream.value?.setMuted(true);
3916
+ const video = videoElement.value;
3917
+ if (video) video.muted = true;
3918
+ for (const stopFn of cleanupFns) stopFn();
3919
+ cleanupFns.length = 0;
3920
+ if (isolated) ownedConnection?.stop();
3921
+ else {
3922
+ const camName = cameraName.value;
3923
+ const video = videoElement.value;
3924
+ if (camName && initialized.value) streamManager.release(camName, video, containerElement);
3925
+ }
3926
+ activityModeManager.dispose();
3927
+ }
3928
+ onBeforeUnmount(cleanup);
3929
+ tryOnScopeDispose(cleanup);
3930
+ return {
3931
+ status,
3932
+ isPlaying,
3933
+ activeMode,
3934
+ activeResolution,
3935
+ hasAudio,
3936
+ hasBackchannel,
3937
+ error,
3938
+ isReconnecting,
3939
+ activityMode: activityModeManager.mode,
3940
+ inStandby: activityModeManager.inStandby,
3941
+ hasActivity: activityModeManager.hasActivity,
3942
+ isBusy,
3943
+ hasSound,
3944
+ hasIntercom,
3945
+ videoElement,
3946
+ containerElement,
3947
+ fullscreenElement,
3948
+ renderElement,
3949
+ stream: currentStream,
3950
+ muted,
3951
+ paused,
3952
+ nativeWidth,
3953
+ nativeHeight,
3954
+ isPip,
3955
+ supportsPip,
3956
+ isFullscreen,
3957
+ start,
3958
+ stop,
3959
+ restart,
3960
+ resumeFromStandby,
3961
+ setMode,
3962
+ setResolution,
3963
+ setMicrophone,
3964
+ setActivityMode,
3965
+ reportActivity,
3966
+ setMuted,
3967
+ play: playStream,
3968
+ pause: pauseStream,
3969
+ togglePip,
3970
+ isCameraDisabled,
3971
+ toggleFullscreen,
3972
+ captureScreenshot
3973
+ };
3974
+ }
3975
+ //#endregion
3976
+ //#region src/composables/useCoreManager.ts
3977
+ function useCoreManager() {
3978
+ const ctx = useCameraUi();
3979
+ const namespaces = NamespaceManager.coreManagerNamespaces();
3980
+ const proxy = (rpc) => rpc.createProxy(namespaces.coreManagerRpc);
3981
+ return {
3982
+ getFFmpegPath: () => rpcCall(ctx.rpc, (rpc) => proxy(rpc).getFFmpegPath()),
3983
+ getServerAddresses: () => rpcCall(ctx.rpc, (rpc) => proxy(rpc).getServerAddresses()),
3984
+ getPluginsByInterface: (interfaceName) => rpcCall(ctx.rpc, (rpc) => proxy(rpc).getPluginsByInterface(interfaceName))
3985
+ };
3986
+ }
3987
+ //#endregion
3988
+ //#region src/composables/useOAuth.ts
3989
+ var POLL_ACTIVE_MS = 1500;
3990
+ var POLL_IDLE_MS = 3e4;
3991
+ var oauthStates = /* @__PURE__ */ new Map();
3992
+ function clearOAuthCache() {
3993
+ oauthStates.clear();
3994
+ }
3995
+ function createOAuthState(pluginName) {
3996
+ const ctx = useCameraUi();
3997
+ const state = ref({ status: "disconnected" });
3998
+ const metadata = ref(null);
3999
+ let timer = null;
4000
+ const call = (fn) => rpcCall(ctx.rpc, async (rpc) => {
4001
+ const id = await resolvePluginId(ctx.rpc, pluginName);
4002
+ if (!id) throw new Error(`Plugin "${pluginName}" not found`);
4003
+ return fn(rpc.createProxy(NamespaceManager.pluginNamespaces(id).pluginChildRpc));
4004
+ });
4005
+ async function refresh() {
4006
+ state.value = await call((proxy) => proxy.getOAuthState());
4007
+ if (!metadata.value) metadata.value = await call((proxy) => proxy.getOAuthMetadata());
4008
+ }
4009
+ function startPolling(intervalMs) {
4010
+ if (timer) clearInterval(timer);
4011
+ timer = setInterval(() => {
4012
+ refresh().catch(() => void 0);
4013
+ }, intervalMs);
4014
+ }
4015
+ watch(() => state.value.status, (status) => {
4016
+ startPolling(status === "awaiting_user" || status === "polling" ? POLL_ACTIVE_MS : POLL_IDLE_MS);
4017
+ }, { immediate: true });
4018
+ refresh().catch(() => void 0);
4019
+ return {
4020
+ state,
4021
+ metadata,
4022
+ call,
4023
+ refresh
4024
+ };
4025
+ }
4026
+ function useOAuth(pluginName) {
4027
+ if (!oauthStates.has(pluginName)) oauthStates.set(pluginName, createOAuthState(pluginName));
4028
+ const { state, metadata, call, refresh } = oauthStates.get(pluginName);
4029
+ async function startDeviceFlow(scope) {
4030
+ state.value = await call((proxy) => proxy.startDeviceFlow(scope));
4031
+ }
4032
+ async function startAuthCodeFlow(scope) {
4033
+ state.value = await call((proxy) => proxy.startAuthCodeFlow(scope));
4034
+ }
4035
+ async function cancel() {
4036
+ if (state.value.status === "awaiting_user" || state.value.status === "polling") await call((proxy) => proxy.cancelDeviceFlow?.() ?? proxy.cancelAuthCodeFlow?.() ?? Promise.resolve());
4037
+ await refresh();
4038
+ }
4039
+ async function disconnect() {
4040
+ await call((proxy) => proxy.disconnect());
4041
+ await refresh();
4042
+ }
4043
+ return {
4044
+ state,
4045
+ metadata,
4046
+ startDeviceFlow,
4047
+ startAuthCodeFlow,
4048
+ cancel,
4049
+ disconnect,
4050
+ refresh
4051
+ };
4052
+ }
4053
+ //#endregion
4054
+ //#region src/composables/useTerminal.ts
4055
+ function useTerminal() {
4056
+ const cameraUi = useCameraUi();
4057
+ const { rpc, isConnected: isClientConnected } = cameraUi;
4058
+ const namespaces = NamespaceManager.terminalManagerNamespaces();
4059
+ const sessionId = shallowRef();
4060
+ const isConnected = ref(false);
4061
+ const isConnecting = ref(false);
4062
+ const error = ref();
4063
+ const dimensions = ref({
4064
+ cols: 80,
4065
+ rows: 24
4066
+ });
4067
+ let streamAbortController;
4068
+ let currentOptions;
4069
+ let streamGen = 0;
4070
+ const proxy = (r) => r.createProxy(namespaces.terminalManagerRpc);
4071
+ async function connect(options) {
4072
+ if (isConnecting.value || isConnected.value) return;
4073
+ isConnecting.value = true;
4074
+ error.value = void 0;
4075
+ currentOptions = options;
4076
+ try {
4077
+ const session = await rpcCall(rpc, (r) => proxy(r).createSession({
4078
+ cols: options?.cols ?? 80,
4079
+ rows: options?.rows ?? 24,
4080
+ cwd: options?.cwd,
4081
+ shell: options?.shell,
4082
+ env: options?.env
4083
+ }));
4084
+ sessionId.value = session.sessionId;
4085
+ dimensions.value = session.dimensions;
4086
+ isConnected.value = true;
4087
+ streamAbortController = new AbortController();
4088
+ const gen = ++streamGen;
4089
+ consumeOutputStream(session.sessionId, options, gen);
4090
+ } catch (err) {
4091
+ error.value = err instanceof Error ? err : new Error(String(err));
4092
+ options?.onError?.(error.value);
4093
+ } finally {
4094
+ isConnecting.value = false;
4095
+ }
4096
+ }
4097
+ async function consumeOutputStream(sid, options, gen) {
4098
+ try {
4099
+ const current = rpc.value;
4100
+ if (!current) return;
4101
+ for await (const chunk of proxy(current).generateOutput(sid)) {
4102
+ if (gen !== streamGen || streamAbortController?.signal.aborted) break;
4103
+ options?.onData?.(chunk);
4104
+ }
4105
+ } catch (err) {
4106
+ if (gen === streamGen && !streamAbortController?.signal.aborted) {
4107
+ error.value = err instanceof Error ? err : new Error(String(err));
4108
+ options?.onError?.(error.value);
4109
+ }
4110
+ } finally {
4111
+ if (gen === streamGen && !streamAbortController?.signal.aborted) {
4112
+ isConnected.value = false;
4113
+ options?.onClose?.();
4114
+ }
4115
+ }
4116
+ }
4117
+ async function write(data) {
4118
+ if (!sessionId.value || !isConnected.value) return;
4119
+ try {
4120
+ await rpcCall(rpc, (r) => proxy(r).writeInput(sessionId.value, data));
4121
+ } catch (err) {
4122
+ error.value = err instanceof Error ? err : new Error(String(err));
4123
+ currentOptions?.onError?.(error.value);
4124
+ }
4125
+ }
4126
+ async function resize(dims) {
4127
+ if (!sessionId.value || !isConnected.value) return;
4128
+ try {
4129
+ await rpcCall(rpc, (r) => proxy(r).resize(sessionId.value, dims));
4130
+ dimensions.value = dims;
4131
+ } catch (err) {
4132
+ error.value = err instanceof Error ? err : new Error(String(err));
4133
+ currentOptions?.onError?.(error.value);
4134
+ }
4135
+ }
4136
+ async function close() {
4137
+ streamAbortController?.abort();
4138
+ streamGen++;
4139
+ if (sessionId.value) try {
4140
+ await rpcCall(rpc, (r) => proxy(r).closeSession(sessionId.value), {
4141
+ awaitConnect: false,
4142
+ maxRetries: 0
4143
+ });
4144
+ } catch {}
4145
+ sessionId.value = void 0;
4146
+ isConnected.value = false;
4147
+ currentOptions = void 0;
4148
+ }
4149
+ function handleReconnected() {
4150
+ if (!sessionId.value && !isConnected.value) return;
4151
+ const savedOptions = currentOptions;
4152
+ streamAbortController?.abort();
4153
+ streamGen++;
4154
+ sessionId.value = void 0;
4155
+ isConnected.value = false;
4156
+ currentOptions = void 0;
4157
+ savedOptions?.onClose?.();
4158
+ if (savedOptions) connect(savedOptions);
4159
+ }
4160
+ cameraUi.on("reconnected", handleReconnected);
4161
+ tryOnScopeDispose(() => {
4162
+ cameraUi.off("reconnected", handleReconnected);
4163
+ close();
4164
+ });
4165
+ return {
4166
+ sessionId: readonly(sessionId),
4167
+ isConnected: readonly(isConnected),
4168
+ isConnecting: readonly(isConnecting),
4169
+ isClientConnected: readonly(isClientConnected),
4170
+ error: readonly(error),
4171
+ dimensions: readonly(dimensions),
4172
+ connect,
4173
+ write,
4174
+ resize,
4175
+ close
4176
+ };
4177
+ }
4178
+ //#endregion
4179
+ export { CAMERA_UI_INJECTION_KEY, NamespaceManager, STREAM_CONFIG, acquireSensorManager, clearCameraCache, clearOAuthCache, clearPluginCache, clearSensorCache, clearSnapshotCache, clearStorageCache, createActivityMode, createCameraUiPlugin, createReactiveCameraDevice, createSensorManager, getSnapshotUrl, isReactiveAudioSensor, isReactiveBatteryInfo, isReactiveClassifierSensor, isReactiveContactSensor, isReactiveDoorbellTrigger, isReactiveFaceSensor, isReactiveGarageControl, isReactiveHumidityInfo, isReactiveLeakSensor, isReactiveLicensePlateSensor, isReactiveLightControl, isReactiveLockControl, isReactiveMotionSensor, isReactiveObjectSensor, isReactiveOccupancySensor, isReactivePTZControl, isReactiveSecuritySystem, isReactiveSirenControl, isReactiveSmokeSensor, isReactiveSwitchControl, isReactiveTemperatureInfo, refreshClientSubscriptions, releaseSensorManager, resetClientState, rpcCall, 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 };