@camstack/ui-library 1.1.13 → 1.1.15

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.
@@ -13,9 +13,10 @@ interface TimelineControlsProps {
13
13
  }
14
14
  /**
15
15
  * Date selector + zoom-in / zoom-out buttons for the recording timeline.
16
- * Zoom cycles through ZOOM_LEVELS (widest → narrowest), centred on the
17
- * current view midpoint. Horizontal pan is wired via wheel events in
18
- * RecordingTimeline directly.
16
+ * Zoom cycles through ZOOM_LEVELS (widest → narrowest), anchored on the view's
17
+ * RIGHT edge so on today's timeline "now" stays pinned to the right instead
18
+ * of drifting to a midday range. Horizontal pan (wheel) then moves the window
19
+ * back/forward in time. Pan is wired via wheel events in RecordingTimeline.
19
20
  */
20
21
  export declare function TimelineControls({ day, onDayChange, view, dayBounds, onViewChange, }: TimelineControlsProps): import("react").JSX.Element;
21
22
  export {};
@@ -58,10 +58,17 @@ export declare function dayWindow(date: Date, nowMs: number): DayWindow;
58
58
  /** Available zoom levels in ms, from widest (24h) to narrowest (15 min). */
59
59
  export declare const ZOOM_LEVELS: readonly number[];
60
60
  /**
61
- * Produce a `levelMs`-wide DayWindow centred on `centerMs`, clamped within
62
- * `dayBounds`. If `levelMs >= dayBounds span`, returns `dayBounds` unchanged.
61
+ * Produce a `levelMs`-wide window whose RIGHT edge stays anchored at
62
+ * `rightEdgeMs` (clamped within `dayBounds`), rather than centring on the view
63
+ * midpoint. On today's timeline the view's right edge is "now", so zooming this
64
+ * way keeps the most recent footage pinned to the right instead of drifting to
65
+ * a midday range that no longer contains now. Panning then moves back/forward.
66
+ *
67
+ * Falls back to `dayBounds` when `levelMs` meets or exceeds the day span (can't
68
+ * show a wider window than the day holds — e.g. early morning with little
69
+ * elapsed time).
63
70
  */
64
- export declare function zoomWindow(_current: DayWindow, dayBounds: DayWindow, levelMs: number, centerMs: number): DayWindow;
71
+ export declare function zoomWindowAnchoredRight(dayBounds: DayWindow, levelMs: number, rightEdgeMs: number): DayWindow;
65
72
  /**
66
73
  * Shift `win` by `deltaMs`, preserving width, clamped within `dayBounds`.
67
74
  */
package/dist/index.cjs CHANGED
@@ -32695,23 +32695,21 @@ var ZOOM_LEVELS = [
32695
32695
  15 * 6e4
32696
32696
  ];
32697
32697
  /**
32698
- * Produce a `levelMs`-wide DayWindow centred on `centerMs`, clamped within
32699
- * `dayBounds`. If `levelMs >= dayBounds span`, returns `dayBounds` unchanged.
32698
+ * Produce a `levelMs`-wide window whose RIGHT edge stays anchored at
32699
+ * `rightEdgeMs` (clamped within `dayBounds`), rather than centring on the view
32700
+ * midpoint. On today's timeline the view's right edge is "now", so zooming this
32701
+ * way keeps the most recent footage pinned to the right instead of drifting to
32702
+ * a midday range that no longer contains now. Panning then moves back/forward.
32703
+ *
32704
+ * Falls back to `dayBounds` when `levelMs` meets or exceeds the day span (can't
32705
+ * show a wider window than the day holds — e.g. early morning with little
32706
+ * elapsed time).
32700
32707
  */
32701
- function zoomWindow(_current, dayBounds, levelMs, centerMs) {
32708
+ function zoomWindowAnchoredRight(dayBounds, levelMs, rightEdgeMs) {
32702
32709
  if (levelMs >= dayBounds.toMs - dayBounds.fromMs) return dayBounds;
32703
- let fromMs = centerMs - levelMs / 2;
32704
- let toMs = centerMs + levelMs / 2;
32705
- if (fromMs < dayBounds.fromMs) {
32706
- fromMs = dayBounds.fromMs;
32707
- toMs = fromMs + levelMs;
32708
- }
32709
- if (toMs > dayBounds.toMs) {
32710
- toMs = dayBounds.toMs;
32711
- fromMs = toMs - levelMs;
32712
- }
32710
+ const toMs = Math.min(Math.max(rightEdgeMs, dayBounds.fromMs + levelMs), dayBounds.toMs);
32713
32711
  return {
32714
- fromMs,
32712
+ fromMs: toMs - levelMs,
32715
32713
  toMs
32716
32714
  };
32717
32715
  }
@@ -33026,9 +33024,10 @@ function parseDateInput(value) {
33026
33024
  }
33027
33025
  /**
33028
33026
  * Date selector + zoom-in / zoom-out buttons for the recording timeline.
33029
- * Zoom cycles through ZOOM_LEVELS (widest → narrowest), centred on the
33030
- * current view midpoint. Horizontal pan is wired via wheel events in
33031
- * RecordingTimeline directly.
33027
+ * Zoom cycles through ZOOM_LEVELS (widest → narrowest), anchored on the view's
33028
+ * RIGHT edge so on today's timeline "now" stays pinned to the right instead
33029
+ * of drifting to a midday range. Horizontal pan (wheel) then moves the window
33030
+ * back/forward in time. Pan is wired via wheel events in RecordingTimeline.
33032
33031
  */
33033
33032
  function TimelineControls({ day, onDayChange, view, dayBounds, onViewChange }) {
33034
33033
  const handleDateChange = (e) => {
@@ -33050,16 +33049,16 @@ function TimelineControls({ day, onDayChange, view, dayBounds, onViewChange }) {
33050
33049
  }
33051
33050
  return best;
33052
33051
  })();
33053
- const centerMs = (view.fromMs + view.toMs) / 2;
33052
+ const rightEdgeMs = view.toMs;
33054
33053
  const handleZoomIn = () => {
33055
33054
  const nextLevel = ZOOM_LEVELS[Math.min(currentLevelIdx + 1, ZOOM_LEVELS.length - 1)];
33056
33055
  if (nextLevel === void 0) return;
33057
- onViewChange(zoomWindow(view, dayBounds, nextLevel, centerMs));
33056
+ onViewChange(zoomWindowAnchoredRight(dayBounds, nextLevel, rightEdgeMs));
33058
33057
  };
33059
33058
  const handleZoomOut = () => {
33060
33059
  const nextLevel = ZOOM_LEVELS[Math.max(currentLevelIdx - 1, 0)];
33061
33060
  if (nextLevel === void 0) return;
33062
- onViewChange(zoomWindow(view, dayBounds, nextLevel, centerMs));
33061
+ onViewChange(zoomWindowAnchoredRight(dayBounds, nextLevel, rightEdgeMs));
33063
33062
  };
33064
33063
  const canZoomIn = currentLevelIdx < ZOOM_LEVELS.length - 1;
33065
33064
  const canZoomOut = currentLevelIdx > 0;
@@ -37361,8 +37360,12 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
37361
37360
  updateState("connecting");
37362
37361
  const abort = new AbortController();
37363
37362
  connectAbortRef.current = abort;
37363
+ const t0 = performance.now();
37364
+ const ms = () => Math.round(performance.now() - t0);
37365
+ const timing = {};
37364
37366
  try {
37365
37367
  const iceServers = await getIceServers?.();
37368
+ timing.iceServersMs = ms();
37366
37369
  if (!mountedRef.current || abort.signal.aborted) return;
37367
37370
  const pc = new RTCPeerConnection({ iceServers: [...iceServers ?? [], { urls: "stun:stun.l.google.com:19302" }] });
37368
37371
  if (!seamless) pcRef.current = pc;
@@ -37385,6 +37388,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
37385
37388
  }
37386
37389
  };
37387
37390
  pc.ontrack = (event) => {
37391
+ if (timing.firstTrackMs === void 0) timing.firstTrackMs = ms();
37388
37392
  console.debug("[WebRTC] ontrack (client-offer)", event.track.kind, event.track.readyState);
37389
37393
  combinedStream.addTrack(event.track);
37390
37394
  if (seamless) {
@@ -37396,11 +37400,20 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
37396
37400
  videoRef.current.play().catch(() => {});
37397
37401
  }
37398
37402
  };
37403
+ let timingLogged = false;
37399
37404
  pc.oniceconnectionstatechange = () => {
37400
37405
  if (!mountedRef.current) return;
37401
37406
  const iceState = pc.iceConnectionState;
37402
37407
  console.debug("[WebRTC] ICE state (client-offer):", iceState);
37403
37408
  if (iceState === "connected" || iceState === "completed") {
37409
+ if (!timingLogged) {
37410
+ timingLogged = true;
37411
+ timing.iceConnectedMs = ms();
37412
+ console.info("[WebRTC timing] client-offer connected", {
37413
+ streamKey,
37414
+ ...timing
37415
+ });
37416
+ }
37404
37417
  reconnectAttemptsRef.current = 0;
37405
37418
  if (seamless) swapInOnce();
37406
37419
  updateState("playing");
@@ -37428,9 +37441,13 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
37428
37441
  onControlChannelRef.current(control);
37429
37442
  }
37430
37443
  const offer = await pc.createOffer();
37444
+ timing.offerCreatedMs = ms();
37431
37445
  await pc.setLocalDescription(offer);
37446
+ timing.localSetMs = ms();
37432
37447
  if (!mountedRef.current || abort.signal.aborted) return;
37448
+ const signalingStart = ms();
37433
37449
  const { sessionId: sid, sdpAnswer } = await handleOffer(pc.localDescription?.sdp ?? offer.sdp ?? "");
37450
+ timing.signalingRttMs = ms() - signalingStart;
37434
37451
  sessionId = sid;
37435
37452
  sessionIdsRef.current.add(sid);
37436
37453
  activeSessionIdRef.current = sid;
@@ -37445,6 +37462,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
37445
37462
  type: "answer",
37446
37463
  sdp: sdpAnswer
37447
37464
  });
37465
+ timing.answerSetMs = ms();
37448
37466
  console.debug("[WebRTC] Client-offer answer set (trickle)", { sessionId: sid.slice(0, 8) });
37449
37467
  if (getIceCandidates) {
37450
37468
  const seen = /* @__PURE__ */ new Set();
package/dist/index.js CHANGED
@@ -32671,23 +32671,21 @@ var ZOOM_LEVELS = [
32671
32671
  15 * 6e4
32672
32672
  ];
32673
32673
  /**
32674
- * Produce a `levelMs`-wide DayWindow centred on `centerMs`, clamped within
32675
- * `dayBounds`. If `levelMs >= dayBounds span`, returns `dayBounds` unchanged.
32674
+ * Produce a `levelMs`-wide window whose RIGHT edge stays anchored at
32675
+ * `rightEdgeMs` (clamped within `dayBounds`), rather than centring on the view
32676
+ * midpoint. On today's timeline the view's right edge is "now", so zooming this
32677
+ * way keeps the most recent footage pinned to the right instead of drifting to
32678
+ * a midday range that no longer contains now. Panning then moves back/forward.
32679
+ *
32680
+ * Falls back to `dayBounds` when `levelMs` meets or exceeds the day span (can't
32681
+ * show a wider window than the day holds — e.g. early morning with little
32682
+ * elapsed time).
32676
32683
  */
32677
- function zoomWindow(_current, dayBounds, levelMs, centerMs) {
32684
+ function zoomWindowAnchoredRight(dayBounds, levelMs, rightEdgeMs) {
32678
32685
  if (levelMs >= dayBounds.toMs - dayBounds.fromMs) return dayBounds;
32679
- let fromMs = centerMs - levelMs / 2;
32680
- let toMs = centerMs + levelMs / 2;
32681
- if (fromMs < dayBounds.fromMs) {
32682
- fromMs = dayBounds.fromMs;
32683
- toMs = fromMs + levelMs;
32684
- }
32685
- if (toMs > dayBounds.toMs) {
32686
- toMs = dayBounds.toMs;
32687
- fromMs = toMs - levelMs;
32688
- }
32686
+ const toMs = Math.min(Math.max(rightEdgeMs, dayBounds.fromMs + levelMs), dayBounds.toMs);
32689
32687
  return {
32690
- fromMs,
32688
+ fromMs: toMs - levelMs,
32691
32689
  toMs
32692
32690
  };
32693
32691
  }
@@ -33002,9 +33000,10 @@ function parseDateInput(value) {
33002
33000
  }
33003
33001
  /**
33004
33002
  * Date selector + zoom-in / zoom-out buttons for the recording timeline.
33005
- * Zoom cycles through ZOOM_LEVELS (widest → narrowest), centred on the
33006
- * current view midpoint. Horizontal pan is wired via wheel events in
33007
- * RecordingTimeline directly.
33003
+ * Zoom cycles through ZOOM_LEVELS (widest → narrowest), anchored on the view's
33004
+ * RIGHT edge so on today's timeline "now" stays pinned to the right instead
33005
+ * of drifting to a midday range. Horizontal pan (wheel) then moves the window
33006
+ * back/forward in time. Pan is wired via wheel events in RecordingTimeline.
33008
33007
  */
33009
33008
  function TimelineControls({ day, onDayChange, view, dayBounds, onViewChange }) {
33010
33009
  const handleDateChange = (e) => {
@@ -33026,16 +33025,16 @@ function TimelineControls({ day, onDayChange, view, dayBounds, onViewChange }) {
33026
33025
  }
33027
33026
  return best;
33028
33027
  })();
33029
- const centerMs = (view.fromMs + view.toMs) / 2;
33028
+ const rightEdgeMs = view.toMs;
33030
33029
  const handleZoomIn = () => {
33031
33030
  const nextLevel = ZOOM_LEVELS[Math.min(currentLevelIdx + 1, ZOOM_LEVELS.length - 1)];
33032
33031
  if (nextLevel === void 0) return;
33033
- onViewChange(zoomWindow(view, dayBounds, nextLevel, centerMs));
33032
+ onViewChange(zoomWindowAnchoredRight(dayBounds, nextLevel, rightEdgeMs));
33034
33033
  };
33035
33034
  const handleZoomOut = () => {
33036
33035
  const nextLevel = ZOOM_LEVELS[Math.max(currentLevelIdx - 1, 0)];
33037
33036
  if (nextLevel === void 0) return;
33038
- onViewChange(zoomWindow(view, dayBounds, nextLevel, centerMs));
33037
+ onViewChange(zoomWindowAnchoredRight(dayBounds, nextLevel, rightEdgeMs));
33039
33038
  };
33040
33039
  const canZoomIn = currentLevelIdx < ZOOM_LEVELS.length - 1;
33041
33040
  const canZoomOut = currentLevelIdx > 0;
@@ -37337,8 +37336,12 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
37337
37336
  updateState("connecting");
37338
37337
  const abort = new AbortController();
37339
37338
  connectAbortRef.current = abort;
37339
+ const t0 = performance.now();
37340
+ const ms = () => Math.round(performance.now() - t0);
37341
+ const timing = {};
37340
37342
  try {
37341
37343
  const iceServers = await getIceServers?.();
37344
+ timing.iceServersMs = ms();
37342
37345
  if (!mountedRef.current || abort.signal.aborted) return;
37343
37346
  const pc = new RTCPeerConnection({ iceServers: [...iceServers ?? [], { urls: "stun:stun.l.google.com:19302" }] });
37344
37347
  if (!seamless) pcRef.current = pc;
@@ -37361,6 +37364,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
37361
37364
  }
37362
37365
  };
37363
37366
  pc.ontrack = (event) => {
37367
+ if (timing.firstTrackMs === void 0) timing.firstTrackMs = ms();
37364
37368
  console.debug("[WebRTC] ontrack (client-offer)", event.track.kind, event.track.readyState);
37365
37369
  combinedStream.addTrack(event.track);
37366
37370
  if (seamless) {
@@ -37372,11 +37376,20 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
37372
37376
  videoRef.current.play().catch(() => {});
37373
37377
  }
37374
37378
  };
37379
+ let timingLogged = false;
37375
37380
  pc.oniceconnectionstatechange = () => {
37376
37381
  if (!mountedRef.current) return;
37377
37382
  const iceState = pc.iceConnectionState;
37378
37383
  console.debug("[WebRTC] ICE state (client-offer):", iceState);
37379
37384
  if (iceState === "connected" || iceState === "completed") {
37385
+ if (!timingLogged) {
37386
+ timingLogged = true;
37387
+ timing.iceConnectedMs = ms();
37388
+ console.info("[WebRTC timing] client-offer connected", {
37389
+ streamKey,
37390
+ ...timing
37391
+ });
37392
+ }
37380
37393
  reconnectAttemptsRef.current = 0;
37381
37394
  if (seamless) swapInOnce();
37382
37395
  updateState("playing");
@@ -37404,9 +37417,13 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
37404
37417
  onControlChannelRef.current(control);
37405
37418
  }
37406
37419
  const offer = await pc.createOffer();
37420
+ timing.offerCreatedMs = ms();
37407
37421
  await pc.setLocalDescription(offer);
37422
+ timing.localSetMs = ms();
37408
37423
  if (!mountedRef.current || abort.signal.aborted) return;
37424
+ const signalingStart = ms();
37409
37425
  const { sessionId: sid, sdpAnswer } = await handleOffer(pc.localDescription?.sdp ?? offer.sdp ?? "");
37426
+ timing.signalingRttMs = ms() - signalingStart;
37410
37427
  sessionId = sid;
37411
37428
  sessionIdsRef.current.add(sid);
37412
37429
  activeSessionIdRef.current = sid;
@@ -37421,6 +37438,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
37421
37438
  type: "answer",
37422
37439
  sdp: sdpAnswer
37423
37440
  });
37441
+ timing.answerSetMs = ms();
37424
37442
  console.debug("[WebRTC] Client-offer answer set (trickle)", { sessionId: sid.slice(0, 8) });
37425
37443
  if (getIceCandidates) {
37426
37444
  const seen = /* @__PURE__ */ new Set();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/ui-library",
3
- "version": "1.1.13",
3
+ "version": "1.1.15",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",