@camera.ui/browser 0.0.124 → 0.0.125

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 (2) hide show
  1. package/dist/index.js +240 -95
  2. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Logger } from "@camera.ui/logger";
2
- import { computed, inject, markRaw, onBeforeUnmount, reactive, readonly, ref, shallowRef, toValue, watch } from "vue";
2
+ import { computed, effectScope, inject, markRaw, onBeforeUnmount, reactive, readonly, ref, shallowRef, toValue, watch } from "vue";
3
3
  import { tryOnScopeDispose, useTimeoutFn, whenever } from "@vueuse/core";
4
4
  import { SensorType } from "@camera.ui/sdk";
5
5
  //#region src/utils/createDebouncedCache.ts
@@ -302,6 +302,8 @@ function useRpcSubscription(subscribeFn, options = {}) {
302
302
  }
303
303
  } finally {
304
304
  pending = false;
305
+ const current = ctx.rpc.value;
306
+ if (!disposed && current && current !== rpc) bind(current);
305
307
  }
306
308
  }
307
309
  function unbind() {
@@ -337,13 +339,15 @@ function defaultRetryDelayMs(attempt) {
337
339
  }
338
340
  function defaultShouldRetry(err) {
339
341
  if (!(err instanceof Error)) return false;
342
+ const code = err.code;
343
+ if (code === "TIMEOUT" || code === "CONNECTION_CLOSED") return true;
340
344
  const msg = err.message.toLowerCase();
341
345
  if (msg.includes("not connected")) return true;
342
346
  if (msg.includes("connection closed")) return true;
343
347
  if (msg.includes("no responders")) return true;
344
348
  if (msg.includes("connection refused")) return true;
345
349
  if (msg.includes("socket")) return true;
346
- if (msg.includes("timeout")) return true;
350
+ if (msg.includes("timed out") || msg.includes("timeout")) return true;
347
351
  return false;
348
352
  }
349
353
  async function waitForRpc(rpcRef, timeoutMs, signal) {
@@ -417,6 +421,13 @@ function setSnapshot(cameraId, data) {
417
421
  function getSnapshot(cameraId) {
418
422
  return snapshotCache.get(cameraId);
419
423
  }
424
+ function subscribeSnapshot(cameraId, cb) {
425
+ if (!subscribers.has(cameraId)) subscribers.set(cameraId, /* @__PURE__ */ new Set());
426
+ subscribers.get(cameraId).add(cb);
427
+ return () => {
428
+ subscribers.get(cameraId)?.delete(cb);
429
+ };
430
+ }
420
431
  function getSnapshotUrl(cameraId) {
421
432
  const buffer = snapshotCache.get(cameraId);
422
433
  if (!buffer) return void 0;
@@ -464,13 +475,17 @@ function useSnapshot(cameraIdOrName) {
464
475
  }
465
476
  _isLoading.value = true;
466
477
  try {
467
- const device = await deviceManager.getCamera(id);
468
- if (device) {
469
- const result = await device.fetchSnapshot();
470
- if (result) {
471
- setSnapshot(id, result);
472
- snapshot.value = result;
478
+ const device = await acquireCameraDevice(deviceManager, id);
479
+ try {
480
+ if (device) {
481
+ const result = await device.fetchSnapshot();
482
+ if (result) {
483
+ setSnapshot(id, result);
484
+ snapshot.value = result;
485
+ }
473
486
  }
487
+ } finally {
488
+ if (device) releaseCameraDevice(id);
474
489
  }
475
490
  } catch {} finally {
476
491
  _isLoading.value = false;
@@ -487,13 +502,17 @@ function useSnapshot(cameraIdOrName) {
487
502
  if (isCameraDisabled(cameraIdName)) return;
488
503
  _isLoading.value = true;
489
504
  try {
490
- const device = await deviceManager.getCamera(id);
491
- if (device) {
492
- const result = await device.fetchSnapshot(void 0, true);
493
- if (result) {
494
- setSnapshot(id, result);
495
- snapshot.value = result;
505
+ const device = await acquireCameraDevice(deviceManager, id);
506
+ try {
507
+ if (device) {
508
+ const result = await device.fetchSnapshot(void 0, true);
509
+ if (result) {
510
+ setSnapshot(id, result);
511
+ snapshot.value = result;
512
+ }
496
513
  }
514
+ } finally {
515
+ if (device) releaseCameraDevice(id);
497
516
  }
498
517
  } catch {} finally {
499
518
  _isLoading.value = false;
@@ -538,9 +557,12 @@ async function createReactiveCameraDevice(rpcOrContext, initialCamera) {
538
557
  const camera = shallowRef(initialCamera);
539
558
  const connected = ref(false);
540
559
  const frameWorkerConnected = ref(false);
541
- const snapshot = computed(() => getSnapshot(initialCamera._id));
560
+ const snapshot = shallowRef(getSnapshot(initialCamera._id));
542
561
  const snapshotLoading = ref(false);
543
562
  const sensorStates = ref({});
563
+ const unsubscribeSnapshot = subscribeSnapshot(initialCamera._id, () => {
564
+ snapshot.value = getSnapshot(initialCamera._id);
565
+ });
544
566
  const name = computed(() => camera.value.name);
545
567
  const room = computed(() => camera.value.room);
546
568
  const nativeId = computed(() => camera.value.nativeId);
@@ -643,6 +665,7 @@ async function createReactiveCameraDevice(rpcOrContext, initialCamera) {
643
665
  closeSubscription = void 0;
644
666
  closeSensorSubscription?.();
645
667
  closeSensorSubscription = void 0;
668
+ unsubscribeSnapshot();
646
669
  }
647
670
  async function fetchSnapshotFn(sourceId, forceNew) {
648
671
  const useForSnapshotSource = sources.value.find((s) => s.useForSnapshot);
@@ -715,6 +738,27 @@ function clearCameraCache() {
715
738
  cameraCache.clear();
716
739
  pendingLoads$1.clear();
717
740
  }
741
+ async function acquireCameraDevice(deviceManager, id) {
742
+ if (cameraCache.has(id)) return cameraCache.acquire(id, () => {
743
+ throw new Error("Should not create - already cached");
744
+ });
745
+ const pending = pendingLoads$1.get(id);
746
+ if (pending) {
747
+ const device = await pending;
748
+ return device ? cameraCache.acquire(id, () => device) : void 0;
749
+ }
750
+ const loadPromise = deviceManager.getCamera(id);
751
+ pendingLoads$1.set(id, loadPromise);
752
+ try {
753
+ const device = await loadPromise;
754
+ return device ? cameraCache.acquire(id, () => device) : void 0;
755
+ } finally {
756
+ pendingLoads$1.delete(id);
757
+ }
758
+ }
759
+ function releaseCameraDevice(id) {
760
+ cameraCache.release(id);
761
+ }
718
762
  function reconnectAllCameraDevices() {
719
763
  cameraCache.forEachValue((device) => {
720
764
  device.reconnect().catch(() => {});
@@ -749,7 +793,7 @@ function useCameraById(cameraIdOrName) {
749
793
  _isLoading.value = true;
750
794
  try {
751
795
  const device = await pending;
752
- if (device && cameraCache.has(id)) {
796
+ if (device && cameraCache.has(id) && toValue(cameraIdOrName) === id) {
753
797
  const cached = cameraCache.acquire(id, () => device);
754
798
  currentCameraId = id;
755
799
  camera.value = cached;
@@ -770,6 +814,10 @@ function useCameraById(cameraIdOrName) {
770
814
  const device = await loadPromise;
771
815
  if (device) {
772
816
  cameraCache.acquire(id, () => device);
817
+ if (toValue(cameraIdOrName) !== id) {
818
+ cameraCache.release(id);
819
+ return;
820
+ }
773
821
  currentCameraId = id;
774
822
  camera.value = device;
775
823
  }
@@ -862,7 +910,7 @@ function usePlugin(pluginName) {
862
910
  if (pending) {
863
911
  _isLoading.value = true;
864
912
  try {
865
- if (await pending) {
913
+ if (await pending && toValue(pluginName) === name) {
866
914
  const cachedAfterPending = acquirePlugin(name);
867
915
  if (cachedAfterPending) {
868
916
  currentPluginName = name;
@@ -895,6 +943,10 @@ function usePlugin(pluginName) {
895
943
  const result = await loadPromise;
896
944
  if (result) {
897
945
  pluginCache.acquire(name, () => result);
946
+ if (toValue(pluginName) !== name) {
947
+ pluginCache.release(name);
948
+ return;
949
+ }
898
950
  currentPluginName = name;
899
951
  plugin.value = result.proxy;
900
952
  contract.value = result.contract;
@@ -1121,19 +1173,28 @@ function createSensorManager(rpcOrContext, cameraId, sensorSubjectNamespace, sen
1121
1173
  for (const msg of queued) handleGlobalSensorEvent(msg);
1122
1174
  initialized.value = true;
1123
1175
  }
1176
+ function launchInit() {
1177
+ const p = doInit().finally(() => {
1178
+ if (initPromise === p) initPromise = void 0;
1179
+ });
1180
+ initPromise = p;
1181
+ return p;
1182
+ }
1124
1183
  async function ensureInitialized() {
1125
1184
  if (initialized.value) return;
1126
- if (!initPromise) initPromise = doInit().finally(() => {
1127
- initPromise = void 0;
1128
- });
1185
+ if (!initPromise) launchInit();
1129
1186
  return initPromise;
1130
1187
  }
1131
1188
  async function reconnect() {
1132
1189
  initialized.value = false;
1133
- if (!initPromise) initPromise = doInit().finally(() => {
1134
- initPromise = void 0;
1135
- });
1136
- return initPromise;
1190
+ if (initPromise) {
1191
+ const p = initPromise.catch(() => {}).then(() => doInit()).finally(() => {
1192
+ if (initPromise === p) initPromise = void 0;
1193
+ });
1194
+ initPromise = p;
1195
+ return p;
1196
+ }
1197
+ return launchInit();
1137
1198
  }
1138
1199
  function close() {
1139
1200
  globalUnsubscribe?.();
@@ -1830,7 +1891,7 @@ function isContext(input) {
1830
1891
  }
1831
1892
  function makeContextFromTransport(input) {
1832
1893
  const { natsTransport, target, wsTransport } = input;
1833
- let hasBeenConnected = false;
1894
+ let hasBeenConnected = natsTransport.getClient() !== null;
1834
1895
  const rpc = shallowRef(natsTransport.getClient() ?? void 0);
1835
1896
  const isConnected = ref(natsTransport.getClient()?.isConnected ?? false);
1836
1897
  const error = ref(void 0);
@@ -2263,6 +2324,7 @@ function createMSEHandler(options) {
2263
2324
  const { videoElement, onReady, onFirstData, onError, signal } = options;
2264
2325
  let mediaSource = null;
2265
2326
  let sourceBuffer = null;
2327
+ let objectUrl = null;
2266
2328
  let isReady = false;
2267
2329
  let hasFirstData = false;
2268
2330
  let pendingBuffer = new Uint8Array(STREAM_CONFIG.MSE.BUFFER_SIZE);
@@ -2287,7 +2349,8 @@ function createMSEHandler(options) {
2287
2349
  videoElement.disableRemotePlayback = true;
2288
2350
  videoElement.srcObject = mediaSource;
2289
2351
  } else {
2290
- videoElement.src = URL.createObjectURL(mediaSource);
2352
+ objectUrl = URL.createObjectURL(mediaSource);
2353
+ videoElement.src = objectUrl;
2291
2354
  videoElement.srcObject = null;
2292
2355
  }
2293
2356
  return filterSupportedCodecs(supportedCodecs, MediaSourceConstructor.isTypeSupported.bind(MediaSourceConstructor));
@@ -2327,7 +2390,9 @@ function createMSEHandler(options) {
2327
2390
  const data = pendingBuffer.slice(0, pendingLength);
2328
2391
  sourceBuffer.appendBuffer(data);
2329
2392
  pendingLength = 0;
2330
- } catch {}
2393
+ } catch (err) {
2394
+ if (!recoverFromQuota(err)) onError(err instanceof Error ? err : new Error(String(err)));
2395
+ }
2331
2396
  if (!sourceBuffer.updating && sourceBuffer.buffered?.length) {
2332
2397
  const end = sourceBuffer.buffered.end(sourceBuffer.buffered.length - 1);
2333
2398
  const start = end - STREAM_CONFIG.MSE.BUFFER_WINDOW;
@@ -2359,11 +2424,38 @@ function createMSEHandler(options) {
2359
2424
  }
2360
2425
  if (sourceBuffer.updating || pendingLength > 0) {
2361
2426
  const bytes = new Uint8Array(data);
2427
+ if (pendingLength + bytes.byteLength > pendingBuffer.byteLength) {
2428
+ pendingLength = 0;
2429
+ onError(/* @__PURE__ */ new Error("MSE pending buffer overflow"));
2430
+ return;
2431
+ }
2362
2432
  pendingBuffer.set(bytes, pendingLength);
2363
2433
  pendingLength += bytes.byteLength;
2364
2434
  } else try {
2365
2435
  sourceBuffer.appendBuffer(data);
2366
- } catch {}
2436
+ } catch (err) {
2437
+ if (recoverFromQuota(err)) {
2438
+ const bytes = new Uint8Array(data);
2439
+ if (bytes.byteLength <= pendingBuffer.byteLength) {
2440
+ pendingBuffer.set(bytes, 0);
2441
+ pendingLength = bytes.byteLength;
2442
+ }
2443
+ }
2444
+ }
2445
+ }
2446
+ function recoverFromQuota(err) {
2447
+ if (err?.name !== "QuotaExceededError") return false;
2448
+ if (!sourceBuffer || sourceBuffer.updating || !sourceBuffer.buffered?.length) return false;
2449
+ try {
2450
+ const start0 = sourceBuffer.buffered.start(0);
2451
+ const end = sourceBuffer.buffered.end(sourceBuffer.buffered.length - 1);
2452
+ const evictTo = Math.max(start0 + 1, end - STREAM_CONFIG.MSE.BUFFER_WINDOW);
2453
+ if (evictTo <= start0) return false;
2454
+ sourceBuffer.remove(start0, evictTo);
2455
+ return true;
2456
+ } catch {
2457
+ return false;
2458
+ }
2367
2459
  }
2368
2460
  function close() {
2369
2461
  if (sourceBuffer) {
@@ -2377,6 +2469,10 @@ function createMSEHandler(options) {
2377
2469
  mediaSource.endOfStream();
2378
2470
  } catch {}
2379
2471
  mediaSource = null;
2472
+ if (objectUrl) {
2473
+ URL.revokeObjectURL(objectUrl);
2474
+ objectUrl = null;
2475
+ }
2380
2476
  isReady = false;
2381
2477
  hasFirstData = false;
2382
2478
  pendingLength = 0;
@@ -2592,6 +2688,7 @@ var StreamConnection = class {
2592
2688
  options;
2593
2689
  connectionGeneration = 0;
2594
2690
  abortController = new AbortController();
2691
+ scope = effectScope(true);
2595
2692
  offTabVisible;
2596
2693
  offTabPaused;
2597
2694
  wasPausedByVisibility = false;
@@ -2618,6 +2715,8 @@ var StreamConnection = class {
2618
2715
  stopWsConnectTimeout;
2619
2716
  startConnectTimeout;
2620
2717
  stopConnectTimeout;
2718
+ startMseConnectTimeout;
2719
+ stopMseConnectTimeout;
2621
2720
  startReconnectTimeout;
2622
2721
  stopReconnectTimeout;
2623
2722
  constructor(options) {
@@ -2651,55 +2750,62 @@ var StreamConnection = class {
2651
2750
  if (this.requestedMode.value === "auto") return this.activeMode.value;
2652
2751
  return this.requestedMode.value;
2653
2752
  });
2654
- const { onTabPaused, onTabVisible } = useTabVisibility();
2655
- const { start: startWsConnectTimeout, stop: stopWsConnectTimeout } = useTimeoutFn(() => {
2656
- if (this.wsHandle?.readyState === WebSocket.CONNECTING) {
2657
- this.disconnectWebSocket();
2658
- if (!this.abortController.signal.aborted && this.status.value !== "closed") this.restart();
2659
- }
2660
- }, STREAM_CONFIG.WEBRTC.WS_CONNECT_TIMEOUT, { immediate: false });
2661
- this.startWsConnectTimeout = startWsConnectTimeout;
2662
- this.stopWsConnectTimeout = stopWsConnectTimeout;
2663
- const { start: startConnectTimeout, stop: stopConnectTimeout } = useTimeoutFn(() => {
2664
- if (this.webrtcHandler && !this.webrtcHandler.isConnected) if (this.requestedMode.value === "auto") if (this.mseHandler?.isReady) {
2665
- this.activeMode.value = "mse";
2666
- this.status.value = "connected";
2667
- } else {
2668
- this.activeMode.value = "mse";
2669
- this.startMSE();
2670
- }
2671
- else this.restart();
2672
- }, STREAM_CONFIG.WEBRTC.CONNECT_TIMEOUT, { immediate: false });
2673
- this.startConnectTimeout = startConnectTimeout;
2674
- this.stopConnectTimeout = stopConnectTimeout;
2675
- const { start: startReconnectTimeout, stop: stopReconnectTimeout } = useTimeoutFn(() => {
2676
- if (!this.abortController.signal.aborted) this.restart();
2677
- }, STREAM_CONFIG.WEBRTC.RECONNECT_DELAY, { immediate: false });
2678
- this.startReconnectTimeout = startReconnectTimeout;
2679
- this.stopReconnectTimeout = stopReconnectTimeout;
2680
- this.setupWatchers();
2681
- this.offTabPaused = onTabPaused(() => {
2682
- log.debug(`onTabPaused fired — status=${this.status.value}, isReady=${this.isReady.value}, target=${!!this.target.value}`);
2683
- if (this.status.value === "idle" || this.status.value === "closed") {
2684
- log.debug(`onTabPaused already in ${this.status.value}, skipping stop()`);
2685
- return;
2686
- }
2687
- this.wasPausedByVisibility = true;
2688
- this.stop();
2689
- log.debug("onTabPaused stop() done, wasPausedByVisibility=true");
2690
- });
2691
- this.offTabVisible = onTabVisible(() => {
2692
- log.debug(`onTabVisible fired — wasPausedByVisibility=${this.wasPausedByVisibility}, status=${this.status.value}, isReady=${this.isReady.value}, target=${!!this.target.value}`);
2693
- if (!this.wasPausedByVisibility) {
2694
- log.debug("onTabVisible — not paused by visibility, no-op");
2695
- return;
2696
- }
2697
- this.wasPausedByVisibility = false;
2698
- this.startWhenReady();
2753
+ this.scope.run(() => {
2754
+ const { onTabPaused, onTabVisible } = useTabVisibility();
2755
+ const wsConnectTimeout = useTimeoutFn(() => {
2756
+ if (this.wsHandle?.readyState === WebSocket.CONNECTING) {
2757
+ this.disconnectWebSocket();
2758
+ if (!this.abortController.signal.aborted && this.status.value !== "closed") this.restart();
2759
+ }
2760
+ }, STREAM_CONFIG.WEBRTC.WS_CONNECT_TIMEOUT, { immediate: false });
2761
+ const connectTimeout = useTimeoutFn(() => {
2762
+ if (this.webrtcHandler && !this.webrtcHandler.isConnected) if (this.requestedMode.value === "auto") {
2763
+ this.webrtcHandler.close();
2764
+ this.webrtcHandler = void 0;
2765
+ this.activeMode.value = "mse";
2766
+ if (this.mseHandler?.isReady) this.status.value = "connected";
2767
+ else this.startMSE();
2768
+ } else this.restart();
2769
+ }, STREAM_CONFIG.WEBRTC.CONNECT_TIMEOUT, { immediate: false });
2770
+ const mseConnectTimeout = useTimeoutFn(() => {
2771
+ if (this.abortController.signal.aborted || this.status.value === "connected" || this.status.value === "closed") return;
2772
+ if (this.mseHandler && !this.mseHandler.isReady) this.restart();
2773
+ }, STREAM_CONFIG.WEBRTC.CONNECT_TIMEOUT, { immediate: false });
2774
+ const reconnectTimeout = useTimeoutFn(() => {
2775
+ if (!this.abortController.signal.aborted) this.restart();
2776
+ }, STREAM_CONFIG.WEBRTC.RECONNECT_DELAY, { immediate: false });
2777
+ this.startWsConnectTimeout = wsConnectTimeout.start;
2778
+ this.stopWsConnectTimeout = wsConnectTimeout.stop;
2779
+ this.startConnectTimeout = connectTimeout.start;
2780
+ this.stopConnectTimeout = connectTimeout.stop;
2781
+ this.startMseConnectTimeout = mseConnectTimeout.start;
2782
+ this.stopMseConnectTimeout = mseConnectTimeout.stop;
2783
+ this.startReconnectTimeout = reconnectTimeout.start;
2784
+ this.stopReconnectTimeout = reconnectTimeout.stop;
2785
+ this.setupWatchers();
2786
+ this.offTabPaused = onTabPaused(() => {
2787
+ log.debug(`onTabPaused fired — status=${this.status.value}, isReady=${this.isReady.value}, target=${!!this.target.value}`);
2788
+ if (this.status.value === "idle" || this.status.value === "closed") {
2789
+ log.debug(`onTabPaused — already in ${this.status.value}, skipping stop()`);
2790
+ return;
2791
+ }
2792
+ this.wasPausedByVisibility = true;
2793
+ this.stop();
2794
+ log.debug("onTabPaused — stop() done, wasPausedByVisibility=true");
2795
+ });
2796
+ this.offTabVisible = onTabVisible(() => {
2797
+ log.debug(`onTabVisible fired — wasPausedByVisibility=${this.wasPausedByVisibility}, status=${this.status.value}, isReady=${this.isReady.value}, target=${!!this.target.value}`);
2798
+ if (!this.wasPausedByVisibility) {
2799
+ log.debug("onTabVisible — not paused by visibility, no-op");
2800
+ return;
2801
+ }
2802
+ this.wasPausedByVisibility = false;
2803
+ this.startWhenReady();
2804
+ });
2805
+ if (autoStart) whenever(this.isReady, () => {
2806
+ if (this.status.value === "idle" || this.status.value === "closed") this.start();
2807
+ }, { immediate: true });
2699
2808
  });
2700
- if (autoStart) whenever(this.isReady, () => {
2701
- if (this.status.value === "idle" || this.status.value === "closed") this.start();
2702
- }, { immediate: true });
2703
2809
  }
2704
2810
  async start() {
2705
2811
  log.debug(`start() called — status=${this.status.value}, isReady=${this.isReady.value}`);
@@ -2810,7 +2916,14 @@ var StreamConnection = class {
2810
2916
  this.offTabVisible = void 0;
2811
2917
  this.offTabPaused?.();
2812
2918
  this.offTabPaused = void 0;
2919
+ const video = this.videoElement.value;
2920
+ if (video) {
2921
+ if (this.onVideoPauseBound) video.removeEventListener("pause", this.onVideoPauseBound);
2922
+ if (this.onVideoPlayBound) video.removeEventListener("play", this.onVideoPlayBound);
2923
+ if (this.onVideoResizeBound) video.removeEventListener("resize", this.onVideoResizeBound);
2924
+ }
2813
2925
  this.stop();
2926
+ this.scope.stop();
2814
2927
  }
2815
2928
  async restart() {
2816
2929
  this.status.value = "reconnecting";
@@ -3046,7 +3159,12 @@ var StreamConnection = class {
3046
3159
  handleWsMessage(ev) {
3047
3160
  if (this.abortController.signal.aborted) return;
3048
3161
  if (typeof ev.data === "string") {
3049
- const msg = JSON.parse(ev.data);
3162
+ let msg;
3163
+ try {
3164
+ msg = JSON.parse(ev.data);
3165
+ } catch {
3166
+ return;
3167
+ }
3050
3168
  this.handleMessage(msg);
3051
3169
  } else if (this.mseHandler) {
3052
3170
  this.mseHandler.appendBuffer(ev.data);
@@ -3106,12 +3224,15 @@ var StreamConnection = class {
3106
3224
  if (this.requestedMode.value === "auto" && this.mseHandler) {
3107
3225
  this.mseHandler.close();
3108
3226
  this.mseHandler = void 0;
3227
+ this.stopMseConnectTimeout();
3109
3228
  }
3110
3229
  }
3111
3230
  handleWebRTCDisconnected() {
3112
3231
  if (this.status.value !== "closed" && !this.abortController.signal.aborted) {
3113
3232
  this.status.value = "reconnecting";
3114
3233
  if (this.requestedMode.value === "auto" && this.mseHandler?.isReady) {
3234
+ this.webrtcHandler?.close();
3235
+ this.webrtcHandler = void 0;
3115
3236
  this.activeMode.value = "mse";
3116
3237
  this.status.value = "connected";
3117
3238
  } else this.restart();
@@ -3121,6 +3242,8 @@ var StreamConnection = class {
3121
3242
  if (this.abortController.signal.aborted) return;
3122
3243
  this.stopConnectTimeout();
3123
3244
  if (this.requestedMode.value === "auto") {
3245
+ this.webrtcHandler?.close();
3246
+ this.webrtcHandler = void 0;
3124
3247
  this.activeMode.value = "mse";
3125
3248
  if (!this.mseHandler?.isReady) this.startMSE();
3126
3249
  else this.status.value = "connected";
@@ -3130,6 +3253,7 @@ var StreamConnection = class {
3130
3253
  }
3131
3254
  }
3132
3255
  startMSE() {
3256
+ if (this.mseHandler) return;
3133
3257
  const video = this.videoElement.value;
3134
3258
  if (this.abortController.signal.aborted || !video) return;
3135
3259
  this.mseHandler = createMSEHandler({
@@ -3141,17 +3265,24 @@ var StreamConnection = class {
3141
3265
  if (this.requestedMode.value !== "auto") {
3142
3266
  this.error.value = err;
3143
3267
  this.status.value = "error";
3268
+ } else if (this.activeMode.value === "mse" && this.status.value === "connected") {
3269
+ log.debug("MSE error while active in auto mode — restarting:", err);
3270
+ this.restart();
3144
3271
  }
3145
3272
  }
3146
3273
  });
3147
3274
  const codecs = this.mseHandler.setup();
3148
- if (codecs) this.sendWsMessage({
3149
- type: "mse",
3150
- value: codecs
3151
- });
3275
+ if (codecs) {
3276
+ this.sendWsMessage({
3277
+ type: "mse",
3278
+ value: codecs
3279
+ });
3280
+ this.startMseConnectTimeout();
3281
+ }
3152
3282
  }
3153
3283
  handleMSEReady() {
3154
3284
  if (this.abortController.signal.aborted) return;
3285
+ this.stopMseConnectTimeout();
3155
3286
  if (this.requestedMode.value === "auto") {
3156
3287
  if (!this.webrtcHandler?.isConnected) {
3157
3288
  this.activeMode.value = "mse";
@@ -3221,14 +3352,9 @@ var StreamConnection = class {
3221
3352
  this.abortController.abort();
3222
3353
  this.stopConnectTimeout();
3223
3354
  this.stopWsConnectTimeout();
3355
+ this.stopMseConnectTimeout();
3224
3356
  this.stopReconnectTimeout();
3225
3357
  this.stopMseMonitor();
3226
- const videoEl = this.videoElement.value;
3227
- if (videoEl) {
3228
- if (this.onVideoPauseBound) videoEl.removeEventListener("pause", this.onVideoPauseBound);
3229
- if (this.onVideoPlayBound) videoEl.removeEventListener("play", this.onVideoPlayBound);
3230
- if (this.onVideoResizeBound) videoEl.removeEventListener("resize", this.onVideoResizeBound);
3231
- }
3232
3358
  this.lastMediaStream = null;
3233
3359
  this.webrtcHandler?.close();
3234
3360
  this.webrtcHandler = void 0;
@@ -3293,6 +3419,9 @@ var StreamManager = class {
3293
3419
  has(cameraName) {
3294
3420
  return this.streams.has(cameraName);
3295
3421
  }
3422
+ getRefCount(cameraName) {
3423
+ return this.streams.get(cameraName)?.refCount ?? 0;
3424
+ }
3296
3425
  get(cameraName) {
3297
3426
  return this.streams.get(cameraName);
3298
3427
  }
@@ -3367,7 +3496,7 @@ var StreamManager = class {
3367
3496
  for (const timer of this.releaseTimers.values()) clearTimeout(timer);
3368
3497
  this.releaseTimers.clear();
3369
3498
  for (const [cameraName, entry] of this.streams) {
3370
- entry.stream.stop();
3499
+ destroyStream(entry.stream);
3371
3500
  this.streams.delete(cameraName);
3372
3501
  }
3373
3502
  }
@@ -3406,12 +3535,17 @@ var StreamManager = class {
3406
3535
  const entry = this.streams.get(cameraName);
3407
3536
  if (!entry) return;
3408
3537
  if (entry.refCount <= 0) {
3409
- entry.stream.stop();
3538
+ destroyStream(entry.stream);
3410
3539
  this.streams.delete(cameraName);
3411
3540
  }
3412
3541
  this.releaseTimers.delete(cameraName);
3413
3542
  }
3414
3543
  };
3544
+ function destroyStream(stream) {
3545
+ const s = stream;
3546
+ if (typeof s.destroy === "function") s.destroy();
3547
+ else s.stop();
3548
+ }
3415
3549
  var streamManager = new StreamManager();
3416
3550
  //#endregion
3417
3551
  //#region src/composables/useFullscreen.ts
@@ -3552,13 +3686,15 @@ function useCameraStream(options) {
3552
3686
  const cam = cameraGetter.value;
3553
3687
  return typeof cam === "string" ? cam : cam.name.value;
3554
3688
  });
3555
- const { camera: cameraDeviceFromLookup, isLoading: cameraDeviceLoading } = useCameraById(cameraName);
3689
+ const { camera: cameraDeviceFromLookup, isLoading: lookupLoading } = useCameraById(computed(() => isCameraString.value ? cameraName.value : ""));
3690
+ const cameraDeviceLoading = computed(() => isCameraString.value && lookupLoading.value);
3556
3691
  const resolvedCameraDevice = computed(() => {
3557
3692
  if (isCameraString.value) return cameraDeviceFromLookup.value;
3558
3693
  return cameraGetter.value;
3559
3694
  });
3560
3695
  let startDelayTimer;
3561
3696
  let ownedConnection;
3697
+ let registeredCamName;
3562
3698
  const containerElement = shallowRef();
3563
3699
  const fullscreenElement = shallowRef();
3564
3700
  const videoElement = shallowRef();
@@ -3601,7 +3737,13 @@ function useCameraStream(options) {
3601
3737
  onStreamStart: () => {
3602
3738
  if (!isCameraDisabled.value) currentStream.value?.start();
3603
3739
  },
3604
- onStreamStop: () => currentStream.value?.stop(),
3740
+ onStreamStop: () => {
3741
+ if (isUsingCachedStream.value) {
3742
+ const camName = registeredCamName ?? cameraName.value;
3743
+ if (camName && streamManager.getRefCount(camName) > 1) return;
3744
+ }
3745
+ currentStream.value?.stop();
3746
+ },
3605
3747
  isStreamPlaying: () => isPlaying.value
3606
3748
  });
3607
3749
  const { isFullscreen, toggle: toggleFullscreen } = useCuiFullscreen(fullscreenTarget);
@@ -3679,6 +3821,7 @@ function useCameraStream(options) {
3679
3821
  const cached = streamManager.acquire(camName, containerElement);
3680
3822
  if (cached) {
3681
3823
  initialized.value = true;
3824
+ registeredCamName = camName;
3682
3825
  isUsingCachedStream.value = true;
3683
3826
  currentStream.value = cached.stream;
3684
3827
  if (camDevice && cached.cameraDeviceRef) cached.cameraDeviceRef.value = camDevice;
@@ -3693,10 +3836,11 @@ function useCameraStream(options) {
3693
3836
  videoElement.value = video;
3694
3837
  cached.videoElementRef.value = video;
3695
3838
  setupCachedStreamWatchers(cached.stream, video, camName);
3696
- if (shouldAutoStart() && autoStartReady.value && !isCameraDisabled.value) if (cached.stream.activeMode.value !== "mse") attachCachedStream(cached, video, camName);
3839
+ if (shouldAutoStart() && autoStartReady.value && activityModeManager.mode.value === "always-on" && !isCameraDisabled.value) if (cached.stream.activeMode.value !== "mse") attachCachedStream(cached, video, camName);
3697
3840
  else video.play().catch(() => {});
3698
3841
  } else if (camDevice) {
3699
3842
  initialized.value = true;
3843
+ registeredCamName = camName;
3700
3844
  isUsingCachedStream.value = false;
3701
3845
  currentStream.value = ownedStream;
3702
3846
  const video = createVideoElement();
@@ -3846,11 +3990,12 @@ function useCameraStream(options) {
3846
3990
  if (video) video.muted = true;
3847
3991
  for (const stopFn of cleanupFns) stopFn();
3848
3992
  cleanupFns.length = 0;
3849
- if (isolated) ownedConnection?.stop();
3993
+ if (isolated) ownedConnection?.destroy();
3850
3994
  else {
3851
- const camName = cameraName.value;
3995
+ const camName = registeredCamName;
3852
3996
  const video = videoElement.value;
3853
3997
  if (camName && initialized.value) streamManager.release(camName, video, containerElement);
3998
+ if (!initialized.value || currentStream.value !== ownedStream) ownedConnection?.destroy();
3854
3999
  }
3855
4000
  activityModeManager.dispose();
3856
4001
  }
@@ -3955,7 +4100,7 @@ function useCameraStream(options) {
3955
4100
  autoStartReady
3956
4101
  ], () => {
3957
4102
  if (!initialized.value) initialize();
3958
- else if (autoStartReady.value && shouldAutoStart() && !isPlaying.value && status.value === "idle" && !isCameraDisabled.value) currentStream.value?.start();
4103
+ else if (autoStartReady.value && shouldAutoStart() && activityModeManager.mode.value === "always-on" && !isPlaying.value && status.value === "idle" && !isCameraDisabled.value) currentStream.value?.start();
3959
4104
  }, { immediate: true });
3960
4105
  watch(isCameraDisabled, (disabled, wasDisabled) => {
3961
4106
  if (!initialized.value) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camera.ui/browser",
3
- "version": "0.0.124",
3
+ "version": "0.0.125",
4
4
  "description": "camera.ui browser client",
5
5
  "author": "seydx (https://github.com/cameraui/clients)",
6
6
  "type": "module",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "peerDependencies": {
31
31
  "@camera.ui/logger": ">=0.0.2",
32
- "@camera.ui/rpc": ">=1.0.6",
32
+ "@camera.ui/rpc": ">=1.0.7",
33
33
  "@camera.ui/sdk": ">=0.0.11",
34
34
  "@camera.ui/transport": ">=0.0.1",
35
35
  "@vueuse/core": ">=14.3.0",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "devDependencies": {
39
39
  "@camera.ui/logger": "file:../../packages/logger",
40
- "@camera.ui/rpc": "^1.0.6",
40
+ "@camera.ui/rpc": "^1.0.7",
41
41
  "@camera.ui/sdk": "~0.0.11",
42
42
  "@camera.ui/transport": "file:../../packages/transport",
43
43
  "@eneris/push-receiver": "^4.3.1",
@@ -60,8 +60,8 @@
60
60
  "typescript": "5.9.3",
61
61
  "typescript-eslint": "^8.62.1",
62
62
  "unplugin-dts": "^1.0.3",
63
- "updates": "^17.18.1",
64
- "vite": "^8.1.2",
63
+ "updates": "^17.18.2",
64
+ "vite": "^8.1.3",
65
65
  "vitest": "^4.1.9",
66
66
  "vue": "^3.5.39",
67
67
  "vue-tsc": "^3.3.6"