@camera.ui/browser 0.0.119 → 0.0.120

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 +186 -135
  2. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -3350,15 +3350,17 @@ var StreamManager = class {
3350
3350
  entry.refCount--;
3351
3351
  if (consumerContainerRef) {
3352
3352
  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(() => {});
3353
+ const video = entry.sharedVideoElement;
3354
+ const releasingContainer = consumerContainerRef.value;
3355
+ const videoParent = video?.parentElement ?? null;
3356
+ if (entry.refCount > 0 && video && (videoParent === releasingContainer || videoParent === null || !videoParent.isConnected)) {
3357
+ const target = this.pickVisibleConsumer(entry, releasingContainer);
3358
+ if (target) {
3359
+ entry.containerElementRef = target.ref;
3360
+ if (video.parentElement !== target.el) {
3361
+ target.el.appendChild(video);
3362
+ video.play().catch(() => {});
3360
3363
  }
3361
- break;
3362
3364
  }
3363
3365
  }
3364
3366
  }
@@ -3389,6 +3391,22 @@ var StreamManager = class {
3389
3391
  resolution: entry.stream.activeResolution.value
3390
3392
  }));
3391
3393
  }
3394
+ pickVisibleConsumer(entry, excludeContainer) {
3395
+ let fallback;
3396
+ for (const ref of entry.consumerContainerRefs) {
3397
+ const el = ref.value;
3398
+ if (!el || el === excludeContainer || !el.isConnected) continue;
3399
+ if (el.checkVisibility?.() ?? el.offsetParent !== null) return {
3400
+ ref,
3401
+ el
3402
+ };
3403
+ fallback ??= {
3404
+ ref,
3405
+ el
3406
+ };
3407
+ }
3408
+ return fallback;
3409
+ }
3392
3410
  cancelRelease(cameraName) {
3393
3411
  const timer = this.releaseTimers.get(cameraName);
3394
3412
  if (timer) {
@@ -3530,15 +3548,15 @@ function acquireAutoStaggerDelay() {
3530
3548
  autoStaggerLastTouch = now;
3531
3549
  return delay;
3532
3550
  }
3551
+ function isElementVisible(el) {
3552
+ if (!el || !el.isConnected) return false;
3553
+ return el.checkVisibility?.() ?? el.offsetParent !== null;
3554
+ }
3533
3555
  function useCameraStream(options) {
3534
3556
  const { activityConfig, autoStart: autoStartOption = true, isolated = false } = options;
3535
3557
  const shouldAutoStart = () => toValue(autoStartOption);
3536
3558
  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);
3559
+ const cleanupFns = [];
3542
3560
  const { isConnected } = useCameraUi();
3543
3561
  const cameraGetter = computed(() => toValue(options.camera));
3544
3562
  const isCameraString = computed(() => typeof cameraGetter.value === "string");
@@ -3551,21 +3569,54 @@ function useCameraStream(options) {
3551
3569
  if (isCameraString.value) return cameraDeviceFromLookup.value;
3552
3570
  return cameraGetter.value;
3553
3571
  });
3572
+ let startDelayTimer;
3573
+ let ownedConnection;
3554
3574
  const containerElement = shallowRef();
3555
3575
  const fullscreenElement = shallowRef();
3556
3576
  const videoElement = shallowRef();
3557
3577
  const streamVideoElementRef = shallowRef();
3558
3578
  const currentStream = shallowRef();
3579
+ const cameraDeviceRef = shallowRef();
3559
3580
  const isUsingCachedStream = ref(false);
3560
3581
  const initialized = ref(false);
3561
3582
  const cleanedUp = ref(false);
3562
- const cameraDeviceRef = shallowRef();
3583
+ const nativeWidth = ref(0);
3584
+ const nativeHeight = ref(0);
3585
+ const isPip = ref(false);
3586
+ const autoStartReady = ref(startDelay <= 0);
3587
+ if (startDelay > 0) startDelayTimer = setTimeout(() => {
3588
+ autoStartReady.value = true;
3589
+ }, startDelay);
3563
3590
  watch(resolvedCameraDevice, (device) => {
3564
3591
  cameraDeviceRef.value = device;
3565
3592
  }, { immediate: true });
3566
3593
  const isCameraDisabled = computed(() => cameraDeviceRef.value?.disabled.value === true);
3567
- const cleanupFns = [];
3568
- let ownedConnection;
3594
+ const status = computed(() => currentStream.value?.status.value ?? "idle");
3595
+ const isPlaying = computed(() => currentStream.value?.isPlaying.value ?? false);
3596
+ const activeMode = computed(() => currentStream.value?.activeMode.value ?? "webrtc");
3597
+ const activeResolution = computed(() => currentStream.value?.activeResolution.value ?? "low-resolution");
3598
+ const hasAudio = computed(() => currentStream.value?.hasAudio.value ?? false);
3599
+ const hasBackchannel = computed(() => currentStream.value?.hasBackchannel.value ?? false);
3600
+ const error = computed(() => currentStream.value?.error.value);
3601
+ const isReconnecting = computed(() => status.value === "reconnecting");
3602
+ const isBusy = computed(() => cameraDeviceLoading.value || !isPlaying.value && status.value !== "error");
3603
+ const hasSound = computed(() => hasAudio.value && status.value === "connected");
3604
+ const hasIntercom = computed(() => Boolean(typeof navigator !== "undefined" && navigator.mediaDevices) && hasBackchannel.value);
3605
+ const muted = computed(() => currentStream.value?.muted.value ?? true);
3606
+ const paused = computed(() => currentStream.value?.paused.value ?? false);
3607
+ const supportsPip = computed(() => typeof document !== "undefined" && document.pictureInPictureEnabled && !!videoElement.value);
3608
+ const renderElement = computed(() => videoElement.value);
3609
+ const fullscreenTarget = computed(() => fullscreenElement.value ?? containerElement.value);
3610
+ const activityModeManager = createActivityMode({
3611
+ initialMode: toValue(options.activityMode) ?? "always-on",
3612
+ config: activityConfig,
3613
+ onStreamStart: () => {
3614
+ if (!isCameraDisabled.value) currentStream.value?.start();
3615
+ },
3616
+ onStreamStop: () => currentStream.value?.stop(),
3617
+ isStreamPlaying: () => isPlaying.value
3618
+ });
3619
+ const { isFullscreen, toggle: toggleFullscreen } = useCuiFullscreen(fullscreenTarget);
3569
3620
  function createOwnedStream() {
3570
3621
  ownedConnection = createStreamConnection({
3571
3622
  camera: cameraDeviceRef,
@@ -3596,117 +3647,24 @@ function useCameraStream(options) {
3596
3647
  if (video.parentElement && video.parentElement !== container) video.parentElement.removeChild(video);
3597
3648
  if (video.parentElement !== container) container.appendChild(video);
3598
3649
  }
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);
3650
+ function reclaimSharedVideo() {
3651
+ if (isolated) return;
3652
+ const container = containerElement.value;
3653
+ const video = videoElement.value;
3654
+ if (!container || !video || video.parentElement === container) return;
3655
+ if (!isElementVisible(container) || isElementVisible(video.parentElement)) return;
3656
+ insertVideoIntoContainer(video, container);
3657
+ video.play().catch(() => {});
3658
+ const camName = cameraName.value;
3659
+ const entry = camName ? streamManager.get(camName) : void 0;
3660
+ if (entry) entry.containerElementRef = containerElement;
3677
3661
  }
3678
- const isPip = ref(false);
3679
- const supportsPip = computed(() => typeof document !== "undefined" && document.pictureInPictureEnabled && !!videoElement.value);
3680
3662
  function onEnterPip() {
3681
3663
  isPip.value = true;
3682
3664
  }
3683
3665
  function onLeavePip() {
3684
3666
  isPip.value = false;
3685
3667
  }
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
3668
  function initializeIsolated() {
3711
3669
  if (initialized.value) return;
3712
3670
  const container = containerElement.value;
@@ -3879,7 +3837,6 @@ function useCameraStream(options) {
3879
3837
  if (document.pictureInPictureElement) document.exitPictureInPicture();
3880
3838
  else if (document.pictureInPictureEnabled && videoElement.value) videoElement.value.requestPictureInPicture();
3881
3839
  }
3882
- const renderElement = computed(() => videoElement.value);
3883
3840
  function captureScreenshot() {
3884
3841
  const video = videoElement.value;
3885
3842
  if (!video || video.videoWidth === 0) return null;
@@ -3889,20 +3846,6 @@ function useCameraStream(options) {
3889
3846
  tmp.getContext("2d")?.drawImage(video, 0, 0);
3890
3847
  return tmp.toDataURL("image/png");
3891
3848
  }
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
3849
  function cleanup() {
3907
3850
  if (cleanedUp.value) return;
3908
3851
  cleanedUp.value = true;
@@ -3923,6 +3866,114 @@ function useCameraStream(options) {
3923
3866
  }
3924
3867
  activityModeManager.dispose();
3925
3868
  }
3869
+ function applyCanvasStyles(canvas) {
3870
+ const style = toValue(options.canvasStyle);
3871
+ const cls = toValue(options.canvasClass);
3872
+ if (style) if (typeof style === "string") canvas.style.cssText += ";" + style;
3873
+ else if (Array.isArray(style)) {
3874
+ for (const s of style) if (typeof s === "string") canvas.style.cssText += ";" + s;
3875
+ else if (s) Object.assign(canvas.style, s);
3876
+ } else Object.assign(canvas.style, style);
3877
+ if (cls) if (typeof cls === "string") canvas.className = cls;
3878
+ else if (Array.isArray(cls)) canvas.className = cls.filter(Boolean).join(" ");
3879
+ else for (const [name, active] of Object.entries(cls)) canvas.classList.toggle(name, !!active);
3880
+ }
3881
+ if (!isolated && typeof IntersectionObserver !== "undefined") {
3882
+ let visibilityObserver;
3883
+ const stopVisibilityWatch = watch(containerElement, (container) => {
3884
+ visibilityObserver?.disconnect();
3885
+ visibilityObserver = void 0;
3886
+ if (!container) return;
3887
+ visibilityObserver = new IntersectionObserver((entries) => {
3888
+ if (entries.some((e) => e.isIntersecting)) reclaimSharedVideo();
3889
+ });
3890
+ visibilityObserver.observe(container);
3891
+ }, { immediate: true });
3892
+ cleanupFns.push(() => {
3893
+ stopVisibilityWatch();
3894
+ visibilityObserver?.disconnect();
3895
+ });
3896
+ }
3897
+ if (options.canvasStyle !== void 0 || options.canvasClass !== void 0) {
3898
+ let stopCanvasListener;
3899
+ const stopContainerWatch = watch(containerElement, (container) => {
3900
+ stopCanvasListener?.();
3901
+ stopCanvasListener = void 0;
3902
+ if (!container) return;
3903
+ Array.from(container.querySelectorAll("canvas")).forEach((node) => {
3904
+ applyCanvasStyles(node);
3905
+ });
3906
+ const observer = new MutationObserver((records) => {
3907
+ for (const r of records) Array.from(r.addedNodes).forEach((added) => {
3908
+ if (added instanceof HTMLCanvasElement) applyCanvasStyles(added);
3909
+ else if (added instanceof HTMLElement) Array.from(added.querySelectorAll("canvas")).forEach((c) => applyCanvasStyles(c));
3910
+ });
3911
+ });
3912
+ observer.observe(container, {
3913
+ childList: true,
3914
+ subtree: true
3915
+ });
3916
+ stopCanvasListener = () => observer.disconnect();
3917
+ }, { immediate: true });
3918
+ cleanupFns.push(() => {
3919
+ stopContainerWatch();
3920
+ stopCanvasListener?.();
3921
+ });
3922
+ }
3923
+ if (options.videoStyle !== void 0 || options.videoClass !== void 0) {
3924
+ const stopVideoStyleWatch = watch([
3925
+ videoElement,
3926
+ () => toValue(options.videoStyle),
3927
+ () => toValue(options.videoClass)
3928
+ ], ([video, style, cls]) => {
3929
+ if (!video) return;
3930
+ if (style) if (typeof style === "string") video.style.cssText += ";" + style;
3931
+ else if (Array.isArray(style)) {
3932
+ for (const s of style) if (typeof s === "string") video.style.cssText += ";" + s;
3933
+ else if (s) Object.assign(video.style, s);
3934
+ } else Object.assign(video.style, style);
3935
+ if (cls) if (typeof cls === "string") video.className = cls;
3936
+ else if (Array.isArray(cls)) video.className = cls.filter(Boolean).join(" ");
3937
+ else for (const [name, active] of Object.entries(cls)) video.classList.toggle(name, active);
3938
+ }, { immediate: true });
3939
+ cleanupFns.push(stopVideoStyleWatch);
3940
+ }
3941
+ const stopDimensionSync = watch([() => currentStream.value?.nativeWidth.value, () => currentStream.value?.nativeHeight.value], ([w, h]) => {
3942
+ if (w && w > 0) nativeWidth.value = w;
3943
+ if (h && h > 0) nativeHeight.value = h;
3944
+ });
3945
+ cleanupFns.push(stopDimensionSync);
3946
+ const stopPipWatch = watch(videoElement, (video, oldVideo) => {
3947
+ if (oldVideo) {
3948
+ oldVideo.removeEventListener("enterpictureinpicture", onEnterPip);
3949
+ oldVideo.removeEventListener("leavepictureinpicture", onLeavePip);
3950
+ }
3951
+ if (video) {
3952
+ video.addEventListener("enterpictureinpicture", onEnterPip);
3953
+ video.addEventListener("leavepictureinpicture", onLeavePip);
3954
+ }
3955
+ }, { immediate: true });
3956
+ cleanupFns.push(stopPipWatch);
3957
+ watch([containerElement, videoElement], ([container, video]) => {
3958
+ if (container && video) insertVideoIntoContainer(video, container);
3959
+ }, { immediate: true });
3960
+ watch(() => toValue(options.activityMode), (newMode) => {
3961
+ if (newMode && newMode !== activityModeManager.mode.value) activityModeManager.setMode(newMode);
3962
+ });
3963
+ watch([
3964
+ containerElement,
3965
+ resolvedCameraDevice,
3966
+ isConnected,
3967
+ autoStartReady
3968
+ ], () => {
3969
+ if (!initialized.value) initialize();
3970
+ else if (autoStartReady.value && shouldAutoStart() && !isPlaying.value && status.value === "idle" && !isCameraDisabled.value) currentStream.value?.start();
3971
+ }, { immediate: true });
3972
+ watch(isCameraDisabled, (disabled, wasDisabled) => {
3973
+ if (!initialized.value) return;
3974
+ if (!wasDisabled && disabled) currentStream.value?.stop();
3975
+ else if (wasDisabled && !disabled && shouldAutoStart()) currentStream.value?.restart();
3976
+ });
3926
3977
  onBeforeUnmount(cleanup);
3927
3978
  tryOnScopeDispose(cleanup);
3928
3979
  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.120",
4
4
  "description": "camera.ui browser client",
5
5
  "author": "seydx (https://github.com/cameraui/clients)",
6
6
  "type": "module",
@@ -30,14 +30,14 @@
30
30
  },
31
31
  "peerDependencies": {
32
32
  "@camera.ui/rpc": ">=1.0.4",
33
- "@camera.ui/sdk": ">=0.0.8",
33
+ "@camera.ui/sdk": ">=0.0.9",
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
39
  "@camera.ui/rpc": "^1.0.4",
40
- "@camera.ui/sdk": "~0.0.8",
40
+ "@camera.ui/sdk": "~0.0.9",
41
41
  "@camera.ui/transport": "file:../../packages/transport",
42
42
  "@eneris/push-receiver": "^4.3.1",
43
43
  "@microsoft/api-extractor": "^7.58.9",
@@ -54,12 +54,12 @@
54
54
  "eslint": "9.39.2",
55
55
  "eslint-plugin-vue": "^10.9.2",
56
56
  "globals": "^17.7.0",
57
- "prettier": "^3.8.5",
57
+ "prettier": "^3.9.1",
58
58
  "rimraf": "^6.1.3",
59
59
  "typescript": "5.9.3",
60
60
  "typescript-eslint": "^8.62.0",
61
61
  "unplugin-dts": "^1.0.3",
62
- "updates": "^17.18.0",
62
+ "updates": "^17.18.1",
63
63
  "vite": "^8.1.0",
64
64
  "vitest": "^4.1.9",
65
65
  "vue": "^3.5.39",