@camera.ui/browser 0.0.145 → 0.0.147

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.
package/dist/index.d.ts CHANGED
@@ -163,6 +163,8 @@ export declare interface CameraStream {
163
163
  readonly isPlaying: Ref<boolean>;
164
164
  readonly activeMode: Ref<'webrtc' | 'webrtc/tcp' | 'mse'>;
165
165
  readonly activeResolution: Ref<StreamingRole>;
166
+ readonly degradedFrom: Ref<StreamingRole | null>;
167
+ readonly unsupported: Ref<boolean>;
166
168
  readonly hasAudio: Ref<boolean>;
167
169
  readonly hasBackchannel: Ref<boolean>;
168
170
  readonly error: Ref<Error | undefined>;
@@ -837,6 +839,8 @@ export declare interface ReactiveStream {
837
839
  readonly requestedMode: Ref<VideoStreamingMode>;
838
840
  readonly activeResolution: Ref<StreamingRole>;
839
841
  readonly requestedResolution: Ref<StreamingRole>;
842
+ readonly degradedFrom: Ref<StreamingRole | null>;
843
+ readonly degradeReason: Ref<StreamDegradeReason | null>;
840
844
  readonly source: Ref<CameraSource | undefined>;
841
845
  readonly hasVideo: Ref<boolean>;
842
846
  readonly hasAudio: Ref<boolean>;
@@ -1023,6 +1027,8 @@ export declare const STREAM_CONFIG: {
1023
1027
  };
1024
1028
  };
1025
1029
 
1030
+ declare type StreamDegradeReason = 'codec';
1031
+
1026
1032
  export declare interface StreamOptions {
1027
1033
  camera: MaybeRefOrGetter<ReactiveCameraDevice | undefined>;
1028
1034
  videoElement: MaybeRefOrGetter<HTMLVideoElement | undefined | null>;
@@ -1034,7 +1040,7 @@ export declare interface StreamOptions {
1034
1040
  webrtcConnectTimeout?: number;
1035
1041
  }
1036
1042
 
1037
- export declare type StreamStatus = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'error' | 'closed';
1043
+ export declare type StreamStatus = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'error' | 'unsupported' | 'closed';
1038
1044
 
1039
1045
  /* Excluded from this release type: SwitchControlProperties */
1040
1046
 
package/dist/index.js CHANGED
@@ -2322,6 +2322,7 @@ var STREAM_CONFIG = {
2322
2322
  };
2323
2323
  //#endregion
2324
2324
  //#region src/streaming/utils.ts
2325
+ var H265_TEST_CODEC = "hvc1.1.6.L153.B0";
2325
2326
  function safariVersion() {
2326
2327
  return navigator.userAgent.match(/version\/(\d+)/i);
2327
2328
  }
@@ -2354,14 +2355,27 @@ function isWebRTCCompatibleVideo(codec) {
2354
2355
  if (codec === "H265") return isH265Supported();
2355
2356
  return false;
2356
2357
  }
2357
- function isH265Supported() {
2358
- try {
2359
- const videoCodecs = RTCRtpSender?.getCapabilities("video")?.codecs;
2360
- if (!videoCodecs) return false;
2361
- return videoCodecs.some((c) => c.mimeType.toLowerCase().includes("h265") || c.mimeType.toLowerCase().includes("hevc"));
2358
+ var mseH265;
2359
+ var webrtcH265;
2360
+ function canPlayH265(transport) {
2361
+ if (transport === "auto") return canPlayH265("webrtc") || canPlayH265("mse");
2362
+ if (transport === "mse") {
2363
+ if (mseH265 === void 0) mseH265 = !!getMediaSourceConstructor()?.isTypeSupported(`video/mp4; codecs="${H265_TEST_CODEC}"`);
2364
+ return mseH265;
2365
+ }
2366
+ if (webrtcH265 === void 0) try {
2367
+ webrtcH265 = !!(RTCRtpReceiver?.getCapabilities("video")?.codecs)?.some((c) => c.mimeType.toLowerCase().includes("h265") || c.mimeType.toLowerCase().includes("hevc"));
2362
2368
  } catch {
2363
- return false;
2369
+ webrtcH265 = false;
2364
2370
  }
2371
+ return webrtcH265;
2372
+ }
2373
+ function reportH265DecodeFailure(transport) {
2374
+ if (transport === "mse") mseH265 = false;
2375
+ else webrtcH265 = false;
2376
+ }
2377
+ function isH265Supported() {
2378
+ return canPlayH265("webrtc");
2365
2379
  }
2366
2380
  function checkWebRTCCompatibility(probe) {
2367
2381
  const audioStreams = probe.audio.filter((s) => s.direction === "sendonly");
@@ -2755,6 +2769,8 @@ var StreamConnection = class {
2755
2769
  requestedMode;
2756
2770
  activeResolution;
2757
2771
  requestedResolution;
2772
+ degradedFrom;
2773
+ degradeReason;
2758
2774
  source;
2759
2775
  hasVideo;
2760
2776
  hasAudio;
@@ -2766,14 +2782,20 @@ var StreamConnection = class {
2766
2782
  paused;
2767
2783
  nativeWidth;
2768
2784
  nativeHeight;
2785
+ camera;
2786
+ videoElement;
2787
+ containerElement;
2788
+ target;
2789
+ isReady;
2790
+ effectiveMode;
2791
+ triedSourceIds = /* @__PURE__ */ new Set();
2769
2792
  options;
2793
+ wsTransport;
2794
+ scope = effectScope(true);
2770
2795
  connectionGeneration = 0;
2771
2796
  abortController = new AbortController();
2772
- scope = effectScope(true);
2773
- offTabVisible;
2774
- offTabPaused;
2797
+ probedSourceId;
2775
2798
  wasPausedByVisibility = false;
2776
- wsTransport;
2777
2799
  wsHandle;
2778
2800
  webrtcHandler;
2779
2801
  mseHandler;
@@ -2783,15 +2805,11 @@ var StreamConnection = class {
2783
2805
  lastMediaStream = null;
2784
2806
  stopWatchers = [];
2785
2807
  mseMonitorInterval;
2808
+ offTabVisible;
2809
+ offTabPaused;
2786
2810
  onVideoPauseBound;
2787
2811
  onVideoPlayBound;
2788
2812
  onVideoResizeBound;
2789
- camera;
2790
- videoElement;
2791
- containerElement;
2792
- target;
2793
- isReady;
2794
- effectiveMode;
2795
2813
  startWsConnectTimeout;
2796
2814
  stopWsConnectTimeout;
2797
2815
  startConnectTimeout;
@@ -2810,6 +2828,8 @@ var StreamConnection = class {
2810
2828
  this.status = ref("idle");
2811
2829
  this.activeMode = ref("webrtc");
2812
2830
  this.activeResolution = ref("low-resolution");
2831
+ this.degradedFrom = ref(null);
2832
+ this.degradeReason = ref(null);
2813
2833
  this.source = shallowRef();
2814
2834
  this.hasVideo = ref(false);
2815
2835
  this.hasAudio = ref(false);
@@ -2846,11 +2866,18 @@ var StreamConnection = class {
2846
2866
  this.activeMode.value = "mse";
2847
2867
  if (this.mseHandler?.isReady) this.status.value = "connected";
2848
2868
  else this.startMSE();
2849
- } else this.restart();
2869
+ } else {
2870
+ this.markSourceUndecodable();
2871
+ this.restart();
2872
+ }
2873
+ else if (this.status.value === "connected" && !this.isPlaying.value && !this.abortController.signal.aborted && this.sourceLikelyH265()) this.handleNoFramesWhileConnected();
2850
2874
  }, STREAM_CONFIG.WEBRTC.CONNECT_TIMEOUT, { immediate: false });
2851
2875
  const mseConnectTimeout = useTimeoutFn(() => {
2852
2876
  if (this.abortController.signal.aborted || this.status.value === "connected" || this.status.value === "closed") return;
2853
- if (this.mseHandler && !this.mseHandler.isReady) this.restart();
2877
+ if (this.mseHandler && !this.mseHandler.isReady) {
2878
+ this.markSourceUndecodable();
2879
+ this.restart();
2880
+ }
2854
2881
  }, STREAM_CONFIG.WEBRTC.CONNECT_TIMEOUT, { immediate: false });
2855
2882
  const reconnectTimeout = useTimeoutFn(() => {
2856
2883
  if (!this.abortController.signal.aborted) this.restart();
@@ -2901,22 +2928,43 @@ var StreamConnection = class {
2901
2928
  this.cleanup();
2902
2929
  this.abortController = new AbortController();
2903
2930
  const gen = ++this.connectionGeneration;
2931
+ this.triedSourceIds.clear();
2904
2932
  this.status.value = "connecting";
2905
2933
  this.error.value = void 0;
2906
2934
  this.isPlaying.value = false;
2907
2935
  try {
2908
- if (!this.initializeSource()) {
2936
+ const init = this.initializeSource();
2937
+ if (init === "unsupported") {
2938
+ log.debug("start() — no source with a playable codec");
2939
+ this.setUnsupported();
2940
+ return;
2941
+ }
2942
+ if (!init) {
2909
2943
  log.debug("start() — no streaming source available");
2910
2944
  this.error.value = /* @__PURE__ */ new Error("No streaming source available");
2911
2945
  this.status.value = "error";
2912
2946
  return;
2913
2947
  }
2914
- await this.probeStream();
2915
- if (gen !== this.connectionGeneration) {
2916
- log.debug("start() generation stale after probe, aborting");
2917
- return;
2948
+ for (let attempt = 0; attempt < 3; attempt++) {
2949
+ const probedSourceId = this.source.value?._id;
2950
+ const probe = await this.probeStream();
2951
+ if (gen !== this.connectionGeneration) {
2952
+ log.debug("start() — generation stale after probe, aborting");
2953
+ return;
2954
+ }
2955
+ if (probe && probedSourceId && this.probeSaysUnplayable(probe)) this.triedSourceIds.add(probedSourceId);
2956
+ const reinit = this.initializeSource();
2957
+ if (reinit === "unsupported") {
2958
+ this.setUnsupported();
2959
+ return;
2960
+ }
2961
+ if (!reinit) {
2962
+ this.error.value = /* @__PURE__ */ new Error("No streaming source available");
2963
+ this.status.value = "error";
2964
+ return;
2965
+ }
2966
+ if (!probe || this.source.value?._id === probedSourceId) break;
2918
2967
  }
2919
- this.initializeSource();
2920
2968
  log.debug("start() — connecting WebSocket");
2921
2969
  this.connectWebSocket();
2922
2970
  } catch (err) {
@@ -2940,15 +2988,27 @@ var StreamConnection = class {
2940
2988
  if (this.requestedMode.value === mode) return;
2941
2989
  this.requestedMode.value = mode;
2942
2990
  this.activeMode.value = mode === "auto" ? "webrtc" : mode;
2991
+ if (this.status.value === "unsupported") {
2992
+ this.stop();
2993
+ await this.start();
2994
+ return;
2995
+ }
2943
2996
  if (this.status.value === "connected") await this.restart();
2944
2997
  }
2945
2998
  async setResolution(resolution) {
2946
2999
  if (this.requestedResolution.value === resolution) return;
2947
3000
  this.requestedResolution.value = resolution;
3001
+ if (this.status.value === "unsupported") {
3002
+ this.stop();
3003
+ await this.start();
3004
+ return;
3005
+ }
2948
3006
  const result = this.getSourceForResolution(resolution);
2949
- if (result && result.source._id !== this.source.value?._id) {
3007
+ if (result && result !== "unsupported" && result.source._id !== this.source.value?._id) {
2950
3008
  this.source.value = result.source;
2951
3009
  this.activeResolution.value = result.effectiveResolution;
3010
+ this.degradedFrom.value = result.degradedFrom;
3011
+ this.degradeReason.value = result.degradedFrom ? "codec" : null;
2952
3012
  if (this.status.value === "connected") await this.restart();
2953
3013
  }
2954
3014
  }
@@ -3023,7 +3083,12 @@ var StreamConnection = class {
3023
3083
  return;
3024
3084
  }
3025
3085
  try {
3026
- if (!this.initializeSource()) {
3086
+ const init = this.initializeSource();
3087
+ if (init === "unsupported") {
3088
+ this.setUnsupported();
3089
+ return;
3090
+ }
3091
+ if (!init) {
3027
3092
  this.error.value = /* @__PURE__ */ new Error("No streaming source available");
3028
3093
  this.status.value = "error";
3029
3094
  return;
@@ -3057,6 +3122,12 @@ var StreamConnection = class {
3057
3122
  this.restart();
3058
3123
  });
3059
3124
  this.stopWatchers.push(stopAuthWatch);
3125
+ const stopSourcesWatch = watch(() => this.camera.value?.sources.value, () => {
3126
+ if (this.status.value !== "unsupported") return;
3127
+ this.stop();
3128
+ this.start();
3129
+ });
3130
+ this.stopWatchers.push(stopSourcesWatch);
3060
3131
  const stopModeWatch = watch(() => toValue(this.options.mode), (newMode) => {
3061
3132
  if (newMode !== void 0 && newMode !== this.requestedMode.value) this.setMode(newMode);
3062
3133
  });
@@ -3107,6 +3178,9 @@ var StreamConnection = class {
3107
3178
  if (newCamera !== oldCamera) {
3108
3179
  if (this.status.value === "connected") {
3109
3180
  if (newCamera && newVideo && newClient) this.restart();
3181
+ } else if (this.status.value === "unsupported" && newCamera && newVideo && newClient) {
3182
+ this.stop();
3183
+ this.start();
3110
3184
  }
3111
3185
  } else if (newVideo !== oldVideo && newVideo) {
3112
3186
  if (newVideo.srcObject instanceof MediaStream) {
@@ -3144,6 +3218,14 @@ var StreamConnection = class {
3144
3218
  });
3145
3219
  this.stopWatchers.push(stopCameraWatch);
3146
3220
  }
3221
+ isSourcePlayable(source) {
3222
+ if (this.triedSourceIds.has(source._id)) return false;
3223
+ if (source.videoCodec !== "H265") return true;
3224
+ const mode = this.requestedMode.value;
3225
+ if (mode === "auto") return canPlayH265("auto");
3226
+ if (mode === "mse") return canPlayH265("mse");
3227
+ return canPlayH265("webrtc");
3228
+ }
3147
3229
  getSourceForResolution(resolution) {
3148
3230
  const cam = this.camera.value;
3149
3231
  if (!cam) return void 0;
@@ -3152,43 +3234,89 @@ var StreamConnection = class {
3152
3234
  "mid-resolution",
3153
3235
  "low-resolution"
3154
3236
  ];
3155
- const exactSource = cam.sources.value.find((s) => s.role === resolution);
3156
- if (exactSource) return {
3157
- source: exactSource,
3158
- effectiveResolution: resolution
3159
- };
3160
- const startIndex = resolutionOrder.indexOf(resolution);
3237
+ const startIndex = Math.max(0, resolutionOrder.indexOf(resolution));
3238
+ let skippedUnplayable = false;
3161
3239
  for (let i = startIndex; i < resolutionOrder.length; i++) {
3162
3240
  const src = cam.sources.value.find((s) => s.role === resolutionOrder[i]);
3163
- if (src) return {
3241
+ if (!src) continue;
3242
+ if (!this.isSourcePlayable(src)) {
3243
+ skippedUnplayable = true;
3244
+ continue;
3245
+ }
3246
+ return {
3164
3247
  source: src,
3165
- effectiveResolution: resolutionOrder[i]
3248
+ effectiveResolution: resolutionOrder[i],
3249
+ degradedFrom: skippedUnplayable ? resolution : null
3166
3250
  };
3167
3251
  }
3168
3252
  const fallback = cam.streamSource.value;
3169
- if (fallback) return {
3253
+ if (fallback && this.isSourcePlayable(fallback)) return {
3170
3254
  source: fallback,
3171
- effectiveResolution: fallback.role ?? "low-resolution"
3255
+ effectiveResolution: fallback.role ?? "low-resolution",
3256
+ degradedFrom: skippedUnplayable ? resolution : null
3172
3257
  };
3258
+ if (skippedUnplayable || fallback) return "unsupported";
3173
3259
  }
3174
3260
  initializeSource() {
3175
3261
  const result = this.getSourceForResolution(this.requestedResolution.value);
3176
3262
  if (!result) return false;
3263
+ if (result === "unsupported") return "unsupported";
3177
3264
  this.source.value = result.source;
3178
3265
  this.activeResolution.value = result.effectiveResolution;
3266
+ this.degradedFrom.value = result.degradedFrom;
3267
+ this.degradeReason.value = result.degradedFrom ? "codec" : null;
3268
+ if (this.probedSourceId && this.probedSourceId !== result.source._id) {
3269
+ this.probeInfo.value = void 0;
3270
+ this.probedSourceId = void 0;
3271
+ this.hasVideo.value = false;
3272
+ this.hasAudio.value = false;
3273
+ this.hasBackchannel.value = false;
3274
+ }
3179
3275
  return true;
3180
3276
  }
3277
+ setUnsupported() {
3278
+ const codec = this.camera.value?.sources.value.find((s) => s.videoCodec)?.videoCodec ?? "H265";
3279
+ this.error.value = /* @__PURE__ */ new Error(`${codec} cannot be played on this device`);
3280
+ this.status.value = "unsupported";
3281
+ }
3282
+ probeSaysUnplayable(probe) {
3283
+ const video = probe.video.filter((v) => v.direction === "sendonly");
3284
+ if (!video.length) return false;
3285
+ if (!video.every((v) => v.codec === "H265")) return false;
3286
+ const mode = this.requestedMode.value;
3287
+ return !canPlayH265(mode === "auto" ? "auto" : mode === "mse" ? "mse" : "webrtc");
3288
+ }
3289
+ sourceLikelyH265() {
3290
+ const src = this.source.value;
3291
+ if (!src) return false;
3292
+ if (src.videoCodec === "H265") return true;
3293
+ if (src.videoCodec !== void 0) return false;
3294
+ const video = this.probeInfo.value?.video.filter((v) => v.direction === "sendonly") ?? [];
3295
+ return video.length > 0 && video.every((v) => v.codec === "H265");
3296
+ }
3297
+ markSourceUndecodable() {
3298
+ const src = this.source.value;
3299
+ if (!src || !this.sourceLikelyH265()) return;
3300
+ this.triedSourceIds.add(src._id);
3301
+ }
3302
+ handleNoFramesWhileConnected() {
3303
+ reportH265DecodeFailure(this.activeMode.value === "mse" ? "mse" : "webrtc");
3304
+ this.markSourceUndecodable();
3305
+ this.restart();
3306
+ }
3181
3307
  async probeStream() {
3182
3308
  const cam = this.camera.value;
3183
- if (!cam || !this.source.value || this.abortController.signal.aborted) return void 0;
3309
+ const source = this.source.value;
3310
+ if (!cam || !source || this.abortController.signal.aborted) return void 0;
3184
3311
  try {
3185
- const probe = await cam.probeStream(this.source.value._id, {
3312
+ const probe = await cam.probeStream(source._id, {
3186
3313
  video: true,
3187
3314
  audio: ["pcma", "opus"],
3188
3315
  microphone: true
3189
3316
  });
3190
3317
  if (probe && !this.abortController.signal.aborted) {
3191
3318
  this.probeInfo.value = probe;
3319
+ this.probedSourceId = source._id;
3192
3320
  this.hasBackchannel.value = probe.audio.some((a) => a.direction === "recvonly");
3193
3321
  this.hasAudio.value = probe.audio.filter((a) => a.direction === "sendonly").length > 0;
3194
3322
  this.hasVideo.value = probe.video.filter((v) => v.direction === "sendonly").length > 0;
@@ -3294,7 +3422,6 @@ var StreamConnection = class {
3294
3422
  if (this.abortController.signal.aborted) return;
3295
3423
  const video = this.videoElement.value;
3296
3424
  if (!video) return;
3297
- this.stopConnectTimeout();
3298
3425
  this.activeMode.value = this.requestedMode.value === "auto" ? "webrtc" : this.requestedMode.value;
3299
3426
  this.lastMediaStream = stream;
3300
3427
  video.srcObject = stream;
@@ -3393,7 +3520,10 @@ var StreamConnection = class {
3393
3520
  this.startWebRTC("webrtc");
3394
3521
  }
3395
3522
  handleFirstFrame() {
3396
- if (!this.isPlaying.value && !this.abortController.signal.aborted) this.isPlaying.value = true;
3523
+ if (!this.isPlaying.value && !this.abortController.signal.aborted) {
3524
+ this.stopConnectTimeout();
3525
+ this.isPlaying.value = true;
3526
+ }
3397
3527
  }
3398
3528
  monitorMseBuffer() {
3399
3529
  const video = this.videoElement.value;
@@ -3800,11 +3930,13 @@ function useCameraStream(options) {
3800
3930
  const isPlaying = computed(() => currentStream.value?.isPlaying.value ?? false);
3801
3931
  const activeMode = computed(() => currentStream.value?.activeMode.value ?? "webrtc");
3802
3932
  const activeResolution = computed(() => currentStream.value?.activeResolution.value ?? "low-resolution");
3933
+ const degradedFrom = computed(() => currentStream.value?.degradedFrom.value ?? null);
3934
+ const unsupported = computed(() => status.value === "unsupported");
3803
3935
  const hasAudio = computed(() => currentStream.value?.hasAudio.value ?? false);
3804
3936
  const hasBackchannel = computed(() => currentStream.value?.hasBackchannel.value ?? false);
3805
3937
  const error = computed(() => currentStream.value?.error.value);
3806
3938
  const isReconnecting = computed(() => status.value === "reconnecting");
3807
- const isBusy = computed(() => cameraDeviceLoading.value || !isPlaying.value && status.value !== "error");
3939
+ const isBusy = computed(() => cameraDeviceLoading.value || !isPlaying.value && status.value !== "error" && status.value !== "unsupported");
3808
3940
  const hasSound = computed(() => hasAudio.value && status.value === "connected");
3809
3941
  const hasIntercom = computed(() => Boolean(typeof navigator !== "undefined" && navigator.mediaDevices) && hasBackchannel.value);
3810
3942
  const muted = computed(() => currentStream.value?.muted.value ?? true);
@@ -3848,7 +3980,6 @@ function useCameraStream(options) {
3848
3980
  video.disablePictureInPicture = false;
3849
3981
  video.preload = "auto";
3850
3982
  video.poster = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
3851
- video.style.backgroundColor = "#000";
3852
3983
  video.style.position = "absolute";
3853
3984
  video.style.width = "100%";
3854
3985
  video.style.height = "100%";
@@ -4197,6 +4328,8 @@ function useCameraStream(options) {
4197
4328
  isPlaying,
4198
4329
  activeMode,
4199
4330
  activeResolution,
4331
+ degradedFrom,
4332
+ unsupported,
4200
4333
  hasAudio,
4201
4334
  hasBackchannel,
4202
4335
  error,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camera.ui/browser",
3
- "version": "0.0.145",
3
+ "version": "0.0.147",
4
4
  "description": "camera.ui browser client",
5
5
  "author": "seydx (https://github.com/cameraui/clients)",
6
6
  "type": "module",
@@ -31,7 +31,7 @@
31
31
  "peerDependencies": {
32
32
  "@camera.ui/logger": ">=0.0.2",
33
33
  "@camera.ui/rpc": ">=1.0.10",
34
- "@camera.ui/sdk": ">=0.0.31",
34
+ "@camera.ui/sdk": ">=0.0.33",
35
35
  "@camera.ui/transport": ">=0.0.1",
36
36
  "@vueuse/core": ">=14.3.0",
37
37
  "vue": ">=3.5.40"
@@ -39,7 +39,7 @@
39
39
  "devDependencies": {
40
40
  "@camera.ui/logger": "file:../../packages/logger",
41
41
  "@camera.ui/rpc": "^1.0.10",
42
- "@camera.ui/sdk": "~0.0.31",
42
+ "@camera.ui/sdk": "~0.0.33",
43
43
  "@camera.ui/transport": "file:../../packages/transport",
44
44
  "@eneris/push-receiver": "^4.3.1",
45
45
  "@microsoft/api-extractor": "^7.58.12",
@@ -61,7 +61,7 @@
61
61
  "typescript": "5.9.3",
62
62
  "typescript-eslint": "^8.65.0",
63
63
  "unplugin-dts": "^1.0.3",
64
- "updates": "^17.19.1",
64
+ "updates": "^17.19.2",
65
65
  "vite": "^8.1.5",
66
66
  "vitest": "^4.1.10",
67
67
  "vue": "^3.5.40",