@camera.ui/browser 0.0.119 → 0.0.121

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 +216 -177
  2. package/package.json +14 -13
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { Logger } from "@camera.ui/logger";
1
2
  import { computed, inject, markRaw, onBeforeUnmount, reactive, readonly, ref, shallowRef, toValue, watch } from "vue";
2
3
  import { tryOnScopeDispose, useTimeoutFn, whenever } from "@vueuse/core";
3
4
  import { SensorType } from "@camera.ui/sdk";
@@ -1823,6 +1824,7 @@ function refreshClientSubscriptions() {
1823
1824
  //#endregion
1824
1825
  //#region src/plugin.ts
1825
1826
  var CAMERA_UI_INJECTION_KEY = Symbol("camera-ui");
1827
+ var log$2 = new Logger("plugin");
1826
1828
  function isContext(input) {
1827
1829
  return "rpc" in input && "isConnected" in input;
1828
1830
  }
@@ -1857,7 +1859,7 @@ function makeContextFromTransport(input) {
1857
1859
  for (const cb of set) try {
1858
1860
  cb();
1859
1861
  } catch (err) {
1860
- console.warn("[camera.ui] context listener threw:", err);
1862
+ log$2.warn("context listener threw:", err);
1861
1863
  }
1862
1864
  }
1863
1865
  natsTransport.subscribeClient((next) => {
@@ -2034,14 +2036,7 @@ function createActivityMode(options) {
2034
2036
  //#endregion
2035
2037
  //#region src/composables/useTabVisibility.ts
2036
2038
  var DEFAULT_TAB_PAUSE_MS = 3e4;
2037
- function log$1(...args) {
2038
- const d = /* @__PURE__ */ new Date();
2039
- const hh = String(d.getHours()).padStart(2, "0");
2040
- const mm = String(d.getMinutes()).padStart(2, "0");
2041
- const ss = String(d.getSeconds()).padStart(2, "0");
2042
- const ms = String(d.getMilliseconds()).padStart(3, "0");
2043
- console.log(`[TabVisibility ${hh}:${mm}:${ss}.${ms}]`, ...args);
2044
- }
2039
+ var log$1 = new Logger("TabVisibility");
2045
2040
  var _isVisible = ref(typeof document === "undefined" ? true : document.visibilityState === "visible");
2046
2041
  var _hiddenAt = null;
2047
2042
  var _hiddenListeners = /* @__PURE__ */ new Set();
@@ -2049,19 +2044,19 @@ var _pausedListeners = /* @__PURE__ */ new Set();
2049
2044
  var _visibleListeners = /* @__PURE__ */ new Set();
2050
2045
  var _initialized = false;
2051
2046
  function _firePausedEntry(entry) {
2052
- log$1(`paused listener fired (delay ${entry.delayMs}ms reached)`);
2047
+ log$1.debug(`paused listener fired (delay ${entry.delayMs}ms reached)`);
2053
2048
  try {
2054
2049
  entry.cb();
2055
2050
  } catch (err) {
2056
- console.error("[TabVisibility] onTabPaused listener threw:", err);
2051
+ log$1.error("onTabPaused listener threw:", err);
2057
2052
  }
2058
2053
  }
2059
2054
  function _schedulePausedEntry(entry) {
2060
- log$1(`paused listener scheduled in ${entry.delayMs}ms`);
2055
+ log$1.debug(`paused listener scheduled in ${entry.delayMs}ms`);
2061
2056
  entry.timer = setTimeout(() => {
2062
2057
  entry.timer = null;
2063
2058
  if (_isVisible.value) {
2064
- log$1("paused listener skipped — tab visible before delay");
2059
+ log$1.debug("paused listener skipped — tab visible before delay");
2065
2060
  return;
2066
2061
  }
2067
2062
  _firePausedEntry(entry);
@@ -2081,11 +2076,11 @@ function _init() {
2081
2076
  if (!visible && _isVisible.value) {
2082
2077
  _isVisible.value = false;
2083
2078
  _hiddenAt = Date.now();
2084
- log$1(`tab → hidden (hidden=${_hiddenListeners.size} paused=${_pausedListeners.size})`);
2079
+ log$1.debug(`tab → hidden (hidden=${_hiddenListeners.size} paused=${_pausedListeners.size})`);
2085
2080
  for (const entry of _hiddenListeners) try {
2086
2081
  entry.cb();
2087
2082
  } catch (err) {
2088
- console.error("[TabVisibility] onTabHidden listener threw:", err);
2083
+ log$1.error("onTabHidden listener threw:", err);
2089
2084
  }
2090
2085
  for (const entry of _pausedListeners) _schedulePausedEntry(entry);
2091
2086
  } else if (visible && !_isVisible.value) {
@@ -2093,11 +2088,11 @@ function _init() {
2093
2088
  const hiddenMs = _hiddenAt != null ? Date.now() - _hiddenAt : 0;
2094
2089
  _hiddenAt = null;
2095
2090
  _isVisible.value = true;
2096
- log$1(`tab → visible (hiddenMs=${hiddenMs}, visible-listeners=${_visibleListeners.size})`);
2091
+ log$1.debug(`tab → visible (hiddenMs=${hiddenMs}, visible-listeners=${_visibleListeners.size})`);
2097
2092
  for (const { cb } of _visibleListeners) try {
2098
2093
  cb({ hiddenMs });
2099
2094
  } catch (err) {
2100
- console.error("[TabVisibility] onTabVisible listener threw:", err);
2095
+ log$1.error("onTabVisible listener threw:", err);
2101
2096
  }
2102
2097
  }
2103
2098
  });
@@ -2576,14 +2571,7 @@ async function processWebRTCMessage(handler, msg) {
2576
2571
  }
2577
2572
  //#endregion
2578
2573
  //#region src/streaming/streamConnection.ts
2579
- function log(...args) {
2580
- const d = /* @__PURE__ */ new Date();
2581
- const hh = String(d.getHours()).padStart(2, "0");
2582
- const mm = String(d.getMinutes()).padStart(2, "0");
2583
- const ss = String(d.getSeconds()).padStart(2, "0");
2584
- const ms = String(d.getMilliseconds()).padStart(3, "0");
2585
- console.log(`[StreamConnection ${hh}:${mm}:${ss}.${ms}]`, ...args);
2586
- }
2574
+ var log = new Logger("StreamConnection");
2587
2575
  var StreamConnection = class {
2588
2576
  status;
2589
2577
  activeMode;
@@ -2691,19 +2679,19 @@ var StreamConnection = class {
2691
2679
  this.stopReconnectTimeout = stopReconnectTimeout;
2692
2680
  this.setupWatchers();
2693
2681
  this.offTabPaused = onTabPaused(() => {
2694
- log(`onTabPaused fired — status=${this.status.value}, isReady=${this.isReady.value}, target=${!!this.target.value}`);
2682
+ log.debug(`onTabPaused fired — status=${this.status.value}, isReady=${this.isReady.value}, target=${!!this.target.value}`);
2695
2683
  if (this.status.value === "idle" || this.status.value === "closed") {
2696
- log(`onTabPaused — already in ${this.status.value}, skipping stop()`);
2684
+ log.debug(`onTabPaused — already in ${this.status.value}, skipping stop()`);
2697
2685
  return;
2698
2686
  }
2699
2687
  this.wasPausedByVisibility = true;
2700
2688
  this.stop();
2701
- log("onTabPaused — stop() done, wasPausedByVisibility=true");
2689
+ log.debug("onTabPaused — stop() done, wasPausedByVisibility=true");
2702
2690
  });
2703
2691
  this.offTabVisible = onTabVisible(() => {
2704
- log(`onTabVisible fired — wasPausedByVisibility=${this.wasPausedByVisibility}, status=${this.status.value}, isReady=${this.isReady.value}, target=${!!this.target.value}`);
2692
+ log.debug(`onTabVisible fired — wasPausedByVisibility=${this.wasPausedByVisibility}, status=${this.status.value}, isReady=${this.isReady.value}, target=${!!this.target.value}`);
2705
2693
  if (!this.wasPausedByVisibility) {
2706
- log("onTabVisible — not paused by visibility, no-op");
2694
+ log.debug("onTabVisible — not paused by visibility, no-op");
2707
2695
  return;
2708
2696
  }
2709
2697
  this.wasPausedByVisibility = false;
@@ -2714,13 +2702,13 @@ var StreamConnection = class {
2714
2702
  }, { immediate: true });
2715
2703
  }
2716
2704
  async start() {
2717
- log(`start() called — status=${this.status.value}, isReady=${this.isReady.value}`);
2705
+ log.debug(`start() called — status=${this.status.value}, isReady=${this.isReady.value}`);
2718
2706
  if (this.status.value !== "idle" && this.status.value !== "closed") {
2719
- log(`start() — skipped, status is ${this.status.value}`);
2707
+ log.debug(`start() — skipped, status is ${this.status.value}`);
2720
2708
  return;
2721
2709
  }
2722
2710
  if (!this.isReady.value) {
2723
- log("start() — skipped, isReady=false");
2711
+ log.debug("start() — skipped, isReady=false");
2724
2712
  return;
2725
2713
  }
2726
2714
  this.cleanup();
@@ -2731,32 +2719,32 @@ var StreamConnection = class {
2731
2719
  this.isPlaying.value = false;
2732
2720
  try {
2733
2721
  if (!this.initializeSource()) {
2734
- log("start() — no streaming source available");
2722
+ log.debug("start() — no streaming source available");
2735
2723
  this.error.value = /* @__PURE__ */ new Error("No streaming source available");
2736
2724
  this.status.value = "error";
2737
2725
  return;
2738
2726
  }
2739
2727
  await this.probeStream();
2740
2728
  if (gen !== this.connectionGeneration) {
2741
- log("start() — generation stale after probe, aborting");
2729
+ log.debug("start() — generation stale after probe, aborting");
2742
2730
  return;
2743
2731
  }
2744
2732
  this.initializeSource();
2745
- log("start() — connecting WebSocket");
2733
+ log.debug("start() — connecting WebSocket");
2746
2734
  this.connectWebSocket();
2747
2735
  } catch (err) {
2748
2736
  if (gen !== this.connectionGeneration) return;
2749
- log("start() — error:", err);
2737
+ log.debug("start() — error:", err);
2750
2738
  this.error.value = err instanceof Error ? err : new Error(String(err));
2751
2739
  this.status.value = "error";
2752
2740
  }
2753
2741
  }
2754
2742
  stop() {
2755
2743
  if (this.status.value === "closed") {
2756
- log("stop() — already closed, no-op");
2744
+ log.debug("stop() — already closed, no-op");
2757
2745
  return;
2758
2746
  }
2759
- log(`stop() — was ${this.status.value}, transitioning to closed`);
2747
+ log.debug(`stop() — was ${this.status.value}, transitioning to closed`);
2760
2748
  ++this.connectionGeneration;
2761
2749
  this.status.value = "closed";
2762
2750
  this.cleanup();
@@ -2855,14 +2843,14 @@ var StreamConnection = class {
2855
2843
  }
2856
2844
  startWhenReady() {
2857
2845
  if (this.isReady.value) {
2858
- log("startWhenReady — already ready, calling start() now");
2846
+ log.debug("startWhenReady — already ready, calling start() now");
2859
2847
  this.start();
2860
2848
  return;
2861
2849
  }
2862
- log(`startWhenReady — not ready (camera=${!!this.camera.value}, video=${!!this.videoElement.value}, target=${!!this.target.value}), watching isReady`);
2850
+ log.debug(`startWhenReady — not ready (camera=${!!this.camera.value}, video=${!!this.videoElement.value}, target=${!!this.target.value}), watching isReady`);
2863
2851
  const stop = watch(this.isReady, (ready) => {
2864
2852
  if (!ready) return;
2865
- log("startWhenReady — isReady → true, calling start()");
2853
+ log.debug("startWhenReady — isReady → true, calling start()");
2866
2854
  stop();
2867
2855
  this.start();
2868
2856
  });
@@ -3350,15 +3338,17 @@ var StreamManager = class {
3350
3338
  entry.refCount--;
3351
3339
  if (consumerContainerRef) {
3352
3340
  entry.consumerContainerRefs.delete(consumerContainerRef);
3353
- if (entry.refCount > 0 && consumerContainerRef.value === entry.containerElementRef.value) for (const ref of entry.consumerContainerRefs) {
3354
- const candidate = ref.value;
3355
- if (candidate && candidate !== consumerContainerRef.value) {
3356
- entry.containerElementRef = ref;
3357
- if (entry.sharedVideoElement && entry.sharedVideoElement.parentElement !== candidate) {
3358
- candidate.appendChild(entry.sharedVideoElement);
3359
- entry.sharedVideoElement.play().catch(() => {});
3341
+ const video = entry.sharedVideoElement;
3342
+ const releasingContainer = consumerContainerRef.value;
3343
+ const videoParent = video?.parentElement ?? null;
3344
+ if (entry.refCount > 0 && video && (videoParent === releasingContainer || videoParent === null || !videoParent.isConnected)) {
3345
+ const target = this.pickVisibleConsumer(entry, releasingContainer);
3346
+ if (target) {
3347
+ entry.containerElementRef = target.ref;
3348
+ if (video.parentElement !== target.el) {
3349
+ target.el.appendChild(video);
3350
+ video.play().catch(() => {});
3360
3351
  }
3361
- break;
3362
3352
  }
3363
3353
  }
3364
3354
  }
@@ -3389,6 +3379,22 @@ var StreamManager = class {
3389
3379
  resolution: entry.stream.activeResolution.value
3390
3380
  }));
3391
3381
  }
3382
+ pickVisibleConsumer(entry, excludeContainer) {
3383
+ let fallback;
3384
+ for (const ref of entry.consumerContainerRefs) {
3385
+ const el = ref.value;
3386
+ if (!el || el === excludeContainer || !el.isConnected) continue;
3387
+ if (el.checkVisibility?.() ?? el.offsetParent !== null) return {
3388
+ ref,
3389
+ el
3390
+ };
3391
+ fallback ??= {
3392
+ ref,
3393
+ el
3394
+ };
3395
+ }
3396
+ return fallback;
3397
+ }
3392
3398
  cancelRelease(cameraName) {
3393
3399
  const timer = this.releaseTimers.get(cameraName);
3394
3400
  if (timer) {
@@ -3530,15 +3536,15 @@ function acquireAutoStaggerDelay() {
3530
3536
  autoStaggerLastTouch = now;
3531
3537
  return delay;
3532
3538
  }
3539
+ function isElementVisible(el) {
3540
+ if (!el || !el.isConnected) return false;
3541
+ return el.checkVisibility?.() ?? el.offsetParent !== null;
3542
+ }
3533
3543
  function useCameraStream(options) {
3534
3544
  const { activityConfig, autoStart: autoStartOption = true, isolated = false } = options;
3535
3545
  const shouldAutoStart = () => toValue(autoStartOption);
3536
3546
  const startDelay = options.startDelay ?? acquireAutoStaggerDelay();
3537
- const autoStartReady = ref(startDelay <= 0);
3538
- let startDelayTimer;
3539
- if (startDelay > 0) startDelayTimer = setTimeout(() => {
3540
- autoStartReady.value = true;
3541
- }, startDelay);
3547
+ const cleanupFns = [];
3542
3548
  const { isConnected } = useCameraUi();
3543
3549
  const cameraGetter = computed(() => toValue(options.camera));
3544
3550
  const isCameraString = computed(() => typeof cameraGetter.value === "string");
@@ -3551,21 +3557,54 @@ function useCameraStream(options) {
3551
3557
  if (isCameraString.value) return cameraDeviceFromLookup.value;
3552
3558
  return cameraGetter.value;
3553
3559
  });
3560
+ let startDelayTimer;
3561
+ let ownedConnection;
3554
3562
  const containerElement = shallowRef();
3555
3563
  const fullscreenElement = shallowRef();
3556
3564
  const videoElement = shallowRef();
3557
3565
  const streamVideoElementRef = shallowRef();
3558
3566
  const currentStream = shallowRef();
3567
+ const cameraDeviceRef = shallowRef();
3559
3568
  const isUsingCachedStream = ref(false);
3560
3569
  const initialized = ref(false);
3561
3570
  const cleanedUp = ref(false);
3562
- const cameraDeviceRef = shallowRef();
3571
+ const nativeWidth = ref(0);
3572
+ const nativeHeight = ref(0);
3573
+ const isPip = ref(false);
3574
+ const autoStartReady = ref(startDelay <= 0);
3575
+ if (startDelay > 0) startDelayTimer = setTimeout(() => {
3576
+ autoStartReady.value = true;
3577
+ }, startDelay);
3563
3578
  watch(resolvedCameraDevice, (device) => {
3564
3579
  cameraDeviceRef.value = device;
3565
3580
  }, { immediate: true });
3566
3581
  const isCameraDisabled = computed(() => cameraDeviceRef.value?.disabled.value === true);
3567
- const cleanupFns = [];
3568
- let ownedConnection;
3582
+ const status = computed(() => currentStream.value?.status.value ?? "idle");
3583
+ const isPlaying = computed(() => currentStream.value?.isPlaying.value ?? false);
3584
+ const activeMode = computed(() => currentStream.value?.activeMode.value ?? "webrtc");
3585
+ const activeResolution = computed(() => currentStream.value?.activeResolution.value ?? "low-resolution");
3586
+ const hasAudio = computed(() => currentStream.value?.hasAudio.value ?? false);
3587
+ const hasBackchannel = computed(() => currentStream.value?.hasBackchannel.value ?? false);
3588
+ const error = computed(() => currentStream.value?.error.value);
3589
+ const isReconnecting = computed(() => status.value === "reconnecting");
3590
+ const isBusy = computed(() => cameraDeviceLoading.value || !isPlaying.value && status.value !== "error");
3591
+ const hasSound = computed(() => hasAudio.value && status.value === "connected");
3592
+ const hasIntercom = computed(() => Boolean(typeof navigator !== "undefined" && navigator.mediaDevices) && hasBackchannel.value);
3593
+ const muted = computed(() => currentStream.value?.muted.value ?? true);
3594
+ const paused = computed(() => currentStream.value?.paused.value ?? false);
3595
+ const supportsPip = computed(() => typeof document !== "undefined" && document.pictureInPictureEnabled && !!videoElement.value);
3596
+ const renderElement = computed(() => videoElement.value);
3597
+ const fullscreenTarget = computed(() => fullscreenElement.value ?? containerElement.value);
3598
+ const activityModeManager = createActivityMode({
3599
+ initialMode: toValue(options.activityMode) ?? "always-on",
3600
+ config: activityConfig,
3601
+ onStreamStart: () => {
3602
+ if (!isCameraDisabled.value) currentStream.value?.start();
3603
+ },
3604
+ onStreamStop: () => currentStream.value?.stop(),
3605
+ isStreamPlaying: () => isPlaying.value
3606
+ });
3607
+ const { isFullscreen, toggle: toggleFullscreen } = useCuiFullscreen(fullscreenTarget);
3569
3608
  function createOwnedStream() {
3570
3609
  ownedConnection = createStreamConnection({
3571
3610
  camera: cameraDeviceRef,
@@ -3596,117 +3635,24 @@ function useCameraStream(options) {
3596
3635
  if (video.parentElement && video.parentElement !== container) video.parentElement.removeChild(video);
3597
3636
  if (video.parentElement !== container) container.appendChild(video);
3598
3637
  }
3599
- watch([containerElement, videoElement], ([container, video]) => {
3600
- if (container && video) insertVideoIntoContainer(video, container);
3601
- }, { immediate: true });
3602
- const status = computed(() => currentStream.value?.status.value ?? "idle");
3603
- const isPlaying = computed(() => currentStream.value?.isPlaying.value ?? false);
3604
- const activeMode = computed(() => currentStream.value?.activeMode.value ?? "webrtc");
3605
- const activeResolution = computed(() => currentStream.value?.activeResolution.value ?? "low-resolution");
3606
- const hasAudio = computed(() => currentStream.value?.hasAudio.value ?? false);
3607
- const hasBackchannel = computed(() => currentStream.value?.hasBackchannel.value ?? false);
3608
- const error = computed(() => currentStream.value?.error.value);
3609
- const isReconnecting = computed(() => status.value === "reconnecting");
3610
- const isBusy = computed(() => cameraDeviceLoading.value || !isPlaying.value && status.value !== "error");
3611
- const hasSound = computed(() => hasAudio.value && status.value === "connected");
3612
- const hasIntercom = computed(() => Boolean(typeof navigator !== "undefined" && navigator.mediaDevices) && hasBackchannel.value);
3613
- const muted = computed(() => currentStream.value?.muted.value ?? true);
3614
- const paused = computed(() => currentStream.value?.paused.value ?? false);
3615
- const nativeWidth = ref(0);
3616
- const nativeHeight = ref(0);
3617
- const stopDimensionSync = watch([() => currentStream.value?.nativeWidth.value, () => currentStream.value?.nativeHeight.value], ([w, h]) => {
3618
- if (w && w > 0) nativeWidth.value = w;
3619
- if (h && h > 0) nativeHeight.value = h;
3620
- });
3621
- cleanupFns.push(stopDimensionSync);
3622
- if (options.canvasStyle !== void 0 || options.canvasClass !== void 0) {
3623
- let stopCanvasListener;
3624
- function applyCanvasStyles(canvas) {
3625
- const style = toValue(options.canvasStyle);
3626
- const cls = toValue(options.canvasClass);
3627
- if (style) if (typeof style === "string") canvas.style.cssText += ";" + style;
3628
- else if (Array.isArray(style)) {
3629
- for (const s of style) if (typeof s === "string") canvas.style.cssText += ";" + s;
3630
- else if (s) Object.assign(canvas.style, s);
3631
- } else Object.assign(canvas.style, style);
3632
- if (cls) if (typeof cls === "string") canvas.className = cls;
3633
- else if (Array.isArray(cls)) canvas.className = cls.filter(Boolean).join(" ");
3634
- else for (const [name, active] of Object.entries(cls)) canvas.classList.toggle(name, !!active);
3635
- }
3636
- const stopContainerWatch = watch(containerElement, (container) => {
3637
- stopCanvasListener?.();
3638
- stopCanvasListener = void 0;
3639
- if (!container) return;
3640
- Array.from(container.querySelectorAll("canvas")).forEach((node) => {
3641
- applyCanvasStyles(node);
3642
- });
3643
- const observer = new MutationObserver((records) => {
3644
- for (const r of records) Array.from(r.addedNodes).forEach((added) => {
3645
- if (added instanceof HTMLCanvasElement) applyCanvasStyles(added);
3646
- else if (added instanceof HTMLElement) Array.from(added.querySelectorAll("canvas")).forEach((c) => applyCanvasStyles(c));
3647
- });
3648
- });
3649
- observer.observe(container, {
3650
- childList: true,
3651
- subtree: true
3652
- });
3653
- stopCanvasListener = () => observer.disconnect();
3654
- }, { immediate: true });
3655
- cleanupFns.push(() => {
3656
- stopContainerWatch();
3657
- stopCanvasListener?.();
3658
- });
3659
- }
3660
- if (options.videoStyle !== void 0 || options.videoClass !== void 0) {
3661
- const stopVideoStyleWatch = watch([
3662
- videoElement,
3663
- () => toValue(options.videoStyle),
3664
- () => toValue(options.videoClass)
3665
- ], ([video, style, cls]) => {
3666
- if (!video) return;
3667
- if (style) if (typeof style === "string") video.style.cssText += ";" + style;
3668
- else if (Array.isArray(style)) {
3669
- for (const s of style) if (typeof s === "string") video.style.cssText += ";" + s;
3670
- else if (s) Object.assign(video.style, s);
3671
- } else Object.assign(video.style, style);
3672
- if (cls) if (typeof cls === "string") video.className = cls;
3673
- else if (Array.isArray(cls)) video.className = cls.filter(Boolean).join(" ");
3674
- else for (const [name, active] of Object.entries(cls)) video.classList.toggle(name, active);
3675
- }, { immediate: true });
3676
- cleanupFns.push(stopVideoStyleWatch);
3638
+ function reclaimSharedVideo() {
3639
+ if (isolated) return;
3640
+ const container = containerElement.value;
3641
+ const video = videoElement.value;
3642
+ if (!container || !video || video.parentElement === container) return;
3643
+ if (!isElementVisible(container) || isElementVisible(video.parentElement)) return;
3644
+ insertVideoIntoContainer(video, container);
3645
+ video.play().catch(() => {});
3646
+ const camName = cameraName.value;
3647
+ const entry = camName ? streamManager.get(camName) : void 0;
3648
+ if (entry) entry.containerElementRef = containerElement;
3677
3649
  }
3678
- const isPip = ref(false);
3679
- const supportsPip = computed(() => typeof document !== "undefined" && document.pictureInPictureEnabled && !!videoElement.value);
3680
3650
  function onEnterPip() {
3681
3651
  isPip.value = true;
3682
3652
  }
3683
3653
  function onLeavePip() {
3684
3654
  isPip.value = false;
3685
3655
  }
3686
- const stopPipWatch = watch(videoElement, (video, oldVideo) => {
3687
- if (oldVideo) {
3688
- oldVideo.removeEventListener("enterpictureinpicture", onEnterPip);
3689
- oldVideo.removeEventListener("leavepictureinpicture", onLeavePip);
3690
- }
3691
- if (video) {
3692
- video.addEventListener("enterpictureinpicture", onEnterPip);
3693
- video.addEventListener("leavepictureinpicture", onLeavePip);
3694
- }
3695
- }, { immediate: true });
3696
- cleanupFns.push(stopPipWatch);
3697
- const { isFullscreen, toggle: toggleFullscreen } = useCuiFullscreen(computed(() => fullscreenElement.value ?? containerElement.value));
3698
- const activityModeManager = createActivityMode({
3699
- initialMode: toValue(options.activityMode) ?? "always-on",
3700
- config: activityConfig,
3701
- onStreamStart: () => {
3702
- if (!isCameraDisabled.value) currentStream.value?.start();
3703
- },
3704
- onStreamStop: () => currentStream.value?.stop(),
3705
- isStreamPlaying: () => isPlaying.value
3706
- });
3707
- watch(() => toValue(options.activityMode), (newMode) => {
3708
- if (newMode && newMode !== activityModeManager.mode.value) activityModeManager.setMode(newMode);
3709
- });
3710
3656
  function initializeIsolated() {
3711
3657
  if (initialized.value) return;
3712
3658
  const container = containerElement.value;
@@ -3879,7 +3825,6 @@ function useCameraStream(options) {
3879
3825
  if (document.pictureInPictureElement) document.exitPictureInPicture();
3880
3826
  else if (document.pictureInPictureEnabled && videoElement.value) videoElement.value.requestPictureInPicture();
3881
3827
  }
3882
- const renderElement = computed(() => videoElement.value);
3883
3828
  function captureScreenshot() {
3884
3829
  const video = videoElement.value;
3885
3830
  if (!video || video.videoWidth === 0) return null;
@@ -3889,20 +3834,6 @@ function useCameraStream(options) {
3889
3834
  tmp.getContext("2d")?.drawImage(video, 0, 0);
3890
3835
  return tmp.toDataURL("image/png");
3891
3836
  }
3892
- watch([
3893
- containerElement,
3894
- resolvedCameraDevice,
3895
- isConnected,
3896
- autoStartReady
3897
- ], () => {
3898
- if (!initialized.value) initialize();
3899
- else if (autoStartReady.value && shouldAutoStart() && !isPlaying.value && status.value === "idle" && !isCameraDisabled.value) currentStream.value?.start();
3900
- }, { immediate: true });
3901
- watch(isCameraDisabled, (disabled, wasDisabled) => {
3902
- if (!initialized.value) return;
3903
- if (!wasDisabled && disabled) currentStream.value?.stop();
3904
- else if (wasDisabled && !disabled && shouldAutoStart()) currentStream.value?.restart();
3905
- });
3906
3837
  function cleanup() {
3907
3838
  if (cleanedUp.value) return;
3908
3839
  cleanedUp.value = true;
@@ -3923,6 +3854,114 @@ function useCameraStream(options) {
3923
3854
  }
3924
3855
  activityModeManager.dispose();
3925
3856
  }
3857
+ function applyCanvasStyles(canvas) {
3858
+ const style = toValue(options.canvasStyle);
3859
+ const cls = toValue(options.canvasClass);
3860
+ if (style) if (typeof style === "string") canvas.style.cssText += ";" + style;
3861
+ else if (Array.isArray(style)) {
3862
+ for (const s of style) if (typeof s === "string") canvas.style.cssText += ";" + s;
3863
+ else if (s) Object.assign(canvas.style, s);
3864
+ } else Object.assign(canvas.style, style);
3865
+ if (cls) if (typeof cls === "string") canvas.className = cls;
3866
+ else if (Array.isArray(cls)) canvas.className = cls.filter(Boolean).join(" ");
3867
+ else for (const [name, active] of Object.entries(cls)) canvas.classList.toggle(name, !!active);
3868
+ }
3869
+ if (!isolated && typeof IntersectionObserver !== "undefined") {
3870
+ let visibilityObserver;
3871
+ const stopVisibilityWatch = watch(containerElement, (container) => {
3872
+ visibilityObserver?.disconnect();
3873
+ visibilityObserver = void 0;
3874
+ if (!container) return;
3875
+ visibilityObserver = new IntersectionObserver((entries) => {
3876
+ if (entries.some((e) => e.isIntersecting)) reclaimSharedVideo();
3877
+ });
3878
+ visibilityObserver.observe(container);
3879
+ }, { immediate: true });
3880
+ cleanupFns.push(() => {
3881
+ stopVisibilityWatch();
3882
+ visibilityObserver?.disconnect();
3883
+ });
3884
+ }
3885
+ if (options.canvasStyle !== void 0 || options.canvasClass !== void 0) {
3886
+ let stopCanvasListener;
3887
+ const stopContainerWatch = watch(containerElement, (container) => {
3888
+ stopCanvasListener?.();
3889
+ stopCanvasListener = void 0;
3890
+ if (!container) return;
3891
+ Array.from(container.querySelectorAll("canvas")).forEach((node) => {
3892
+ applyCanvasStyles(node);
3893
+ });
3894
+ const observer = new MutationObserver((records) => {
3895
+ for (const r of records) Array.from(r.addedNodes).forEach((added) => {
3896
+ if (added instanceof HTMLCanvasElement) applyCanvasStyles(added);
3897
+ else if (added instanceof HTMLElement) Array.from(added.querySelectorAll("canvas")).forEach((c) => applyCanvasStyles(c));
3898
+ });
3899
+ });
3900
+ observer.observe(container, {
3901
+ childList: true,
3902
+ subtree: true
3903
+ });
3904
+ stopCanvasListener = () => observer.disconnect();
3905
+ }, { immediate: true });
3906
+ cleanupFns.push(() => {
3907
+ stopContainerWatch();
3908
+ stopCanvasListener?.();
3909
+ });
3910
+ }
3911
+ if (options.videoStyle !== void 0 || options.videoClass !== void 0) {
3912
+ const stopVideoStyleWatch = watch([
3913
+ videoElement,
3914
+ () => toValue(options.videoStyle),
3915
+ () => toValue(options.videoClass)
3916
+ ], ([video, style, cls]) => {
3917
+ if (!video) return;
3918
+ if (style) if (typeof style === "string") video.style.cssText += ";" + style;
3919
+ else if (Array.isArray(style)) {
3920
+ for (const s of style) if (typeof s === "string") video.style.cssText += ";" + s;
3921
+ else if (s) Object.assign(video.style, s);
3922
+ } else Object.assign(video.style, style);
3923
+ if (cls) if (typeof cls === "string") video.className = cls;
3924
+ else if (Array.isArray(cls)) video.className = cls.filter(Boolean).join(" ");
3925
+ else for (const [name, active] of Object.entries(cls)) video.classList.toggle(name, active);
3926
+ }, { immediate: true });
3927
+ cleanupFns.push(stopVideoStyleWatch);
3928
+ }
3929
+ const stopDimensionSync = watch([() => currentStream.value?.nativeWidth.value, () => currentStream.value?.nativeHeight.value], ([w, h]) => {
3930
+ if (w && w > 0) nativeWidth.value = w;
3931
+ if (h && h > 0) nativeHeight.value = h;
3932
+ });
3933
+ cleanupFns.push(stopDimensionSync);
3934
+ const stopPipWatch = watch(videoElement, (video, oldVideo) => {
3935
+ if (oldVideo) {
3936
+ oldVideo.removeEventListener("enterpictureinpicture", onEnterPip);
3937
+ oldVideo.removeEventListener("leavepictureinpicture", onLeavePip);
3938
+ }
3939
+ if (video) {
3940
+ video.addEventListener("enterpictureinpicture", onEnterPip);
3941
+ video.addEventListener("leavepictureinpicture", onLeavePip);
3942
+ }
3943
+ }, { immediate: true });
3944
+ cleanupFns.push(stopPipWatch);
3945
+ watch([containerElement, videoElement], ([container, video]) => {
3946
+ if (container && video) insertVideoIntoContainer(video, container);
3947
+ }, { immediate: true });
3948
+ watch(() => toValue(options.activityMode), (newMode) => {
3949
+ if (newMode && newMode !== activityModeManager.mode.value) activityModeManager.setMode(newMode);
3950
+ });
3951
+ watch([
3952
+ containerElement,
3953
+ resolvedCameraDevice,
3954
+ isConnected,
3955
+ autoStartReady
3956
+ ], () => {
3957
+ if (!initialized.value) initialize();
3958
+ else if (autoStartReady.value && shouldAutoStart() && !isPlaying.value && status.value === "idle" && !isCameraDisabled.value) currentStream.value?.start();
3959
+ }, { immediate: true });
3960
+ watch(isCameraDisabled, (disabled, wasDisabled) => {
3961
+ if (!initialized.value) return;
3962
+ if (!wasDisabled && disabled) currentStream.value?.stop();
3963
+ else if (wasDisabled && !disabled && shouldAutoStart()) currentStream.value?.restart();
3964
+ });
3926
3965
  onBeforeUnmount(cleanup);
3927
3966
  tryOnScopeDispose(cleanup);
3928
3967
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camera.ui/browser",
3
- "version": "0.0.119",
3
+ "version": "0.0.121",
4
4
  "description": "camera.ui browser client",
5
5
  "author": "seydx (https://github.com/cameraui/clients)",
6
6
  "type": "module",
@@ -21,49 +21,50 @@
21
21
  "dev": "vite build --watch",
22
22
  "format": "prettier --write 'src/' --ignore-unknown --no-error-on-unmatched-pattern",
23
23
  "install-updates": "npm i --save",
24
- "lint": "eslint .",
25
- "lint:fix": "eslint --fix .",
26
- "prepublishOnly": "npm i --package-lock-only && npm run lint:fix && npm run format && npm run build",
24
+ "lint": "eslint --fix .",
25
+ "prepublishOnly": "npm i --package-lock-only && npm run lint && npm run format && npm run build",
27
26
  "test": "vitest run --passWithNoTests",
28
27
  "test:watch": "vitest",
29
28
  "update": "updates --update ./"
30
29
  },
31
30
  "peerDependencies": {
31
+ "@camera.ui/logger": ">=0.0.2",
32
32
  "@camera.ui/rpc": ">=1.0.4",
33
- "@camera.ui/sdk": ">=0.0.8",
33
+ "@camera.ui/sdk": ">=0.0.11",
34
34
  "@camera.ui/transport": ">=0.0.1",
35
35
  "@vueuse/core": ">=14.3.0",
36
36
  "vue": ">=3.5.39"
37
37
  },
38
38
  "devDependencies": {
39
+ "@camera.ui/logger": "file:../../packages/logger",
39
40
  "@camera.ui/rpc": "^1.0.4",
40
- "@camera.ui/sdk": "~0.0.8",
41
+ "@camera.ui/sdk": "~0.0.11",
41
42
  "@camera.ui/transport": "file:../../packages/transport",
42
43
  "@eneris/push-receiver": "^4.3.1",
43
44
  "@microsoft/api-extractor": "^7.58.9",
44
45
  "@stylistic/eslint-plugin": "^5.10.0",
45
46
  "@types/webrtc": "^0.0.47",
46
- "@typescript-eslint/parser": "^8.62.0",
47
+ "@typescript-eslint/parser": "^8.62.1",
47
48
  "@vitejs/plugin-vue": "^6.0.7",
48
49
  "@vue/eslint-config-prettier": "^10.2.0",
49
50
  "@vue/eslint-config-typescript": "^14.9.0",
50
- "@vue/language-core": "^3.3.5",
51
+ "@vue/language-core": "^3.3.6",
51
52
  "@vue/tsconfig": "^0.9.1",
52
53
  "@vueuse/core": "^14.3.0",
53
54
  "@webgpu/types": "^0.1.71",
54
55
  "eslint": "9.39.2",
55
56
  "eslint-plugin-vue": "^10.9.2",
56
57
  "globals": "^17.7.0",
57
- "prettier": "^3.8.5",
58
+ "prettier": "^3.9.4",
58
59
  "rimraf": "^6.1.3",
59
60
  "typescript": "5.9.3",
60
- "typescript-eslint": "^8.62.0",
61
+ "typescript-eslint": "^8.62.1",
61
62
  "unplugin-dts": "^1.0.3",
62
- "updates": "^17.18.0",
63
- "vite": "^8.1.0",
63
+ "updates": "^17.18.1",
64
+ "vite": "^8.1.2",
64
65
  "vitest": "^4.1.9",
65
66
  "vue": "^3.5.39",
66
- "vue-tsc": "^3.3.5"
67
+ "vue-tsc": "^3.3.6"
67
68
  },
68
69
  "overrides": {
69
70
  "@vue/language-core": "$@vue/language-core"