@camstack/ui-library 1.2.185 → 1.2.187

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.
@@ -2,6 +2,7 @@ import { ReactNode } from 'react';
2
2
  import { CamProfile, WebrtcSessionDebug } from '@camstack/types';
3
3
  import { ReconnectAction } from './reconnect-schedule';
4
4
  import { AdaptiveHintsReport } from './adaptive-downlink';
5
+ import { StreamDebugSample } from './stream-debug/webrtc-debug-stats.js';
5
6
  type WebkitPresentationMode = 'inline' | 'picture-in-picture' | 'fullscreen';
6
7
  declare global {
7
8
  interface HTMLVideoElement {
@@ -116,6 +117,20 @@ export interface CameraStreamPlayerProps {
116
117
  /** Show a live client-side WebRTC stats overlay (bitrate/fps/res/RTT),
117
118
  * computed from the PeerConnection's getStats(). Default: false. */
118
119
  showStats?: boolean;
120
+ /**
121
+ * Report the same measurements to a HOST instead of (or as well as) drawing
122
+ * them.
123
+ *
124
+ * The overlay is a DOM layer, and in the native app the player lives inside a
125
+ * WebView whose touches the RN gesture layer owns — so the panel that opens
126
+ * on a tap can never be opened there. The numbers still have to reach the
127
+ * operator, so they travel out to whoever mounted the player and are rendered
128
+ * in its own UI, next to the battery and link details.
129
+ *
130
+ * Wiring this turns the two 1 Hz polls on by itself: a host that asked for
131
+ * samples wants them whether or not the in-picture overlay is drawn.
132
+ */
133
+ onDebugStats?: (sample: StreamDebugSample) => void;
119
134
  /** When provided, polls inbound-video getStats() every 5 s during playback
120
135
  * and reports per-window health metrics (frames received/decoded/dropped,
121
136
  * freezes, jitter, playback-clock advance). The embed forwards these over the
@@ -287,5 +302,5 @@ export interface CameraStreamPlayerProps {
287
302
  */
288
303
  onVideoElement?: (el: HTMLVideoElement | null) => void;
289
304
  }
290
- export declare function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay, muted: initialMuted, showControls, showStats, onPlaybackStats, onClientNetworkSample, onConnectTiming, className, onStateChange, onError, onReconnectAttempt, overlay, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, getSessionDebug, reoffer, posterUrl, hintsOverride, onAdaptiveHints, reconnectSignal, onControlChannel, onVideoElement, }: CameraStreamPlayerProps): import("react").JSX.Element;
305
+ export declare function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay, muted: initialMuted, showControls, showStats, onDebugStats, onPlaybackStats, onClientNetworkSample, onConnectTiming, className, onStateChange, onError, onReconnectAttempt, overlay, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, getSessionDebug, reoffer, posterUrl, hintsOverride, onAdaptiveHints, reconnectSignal, onControlChannel, onVideoElement, }: CameraStreamPlayerProps): import("react").JSX.Element;
291
306
  export {};
@@ -20,7 +20,7 @@ export { CameraStreamPlayer } from './camera-stream-player';
20
20
  export { StreamDebugOverlay } from './stream-debug/stream-debug-overlay';
21
21
  export type { StreamDebugOverlayProps } from './stream-debug/stream-debug-overlay';
22
22
  export { deriveWebrtcDebugStats, freezeVerdict, } from './stream-debug/webrtc-debug-stats';
23
- export type { DeriveWebrtcDebugStatsInput, FreezeVerdict, FreezeVerdictKind, ResolutionMark, WebrtcDebugStats, WebrtcDebugWindow, } from './stream-debug/webrtc-debug-stats';
23
+ export type { DeriveWebrtcDebugStatsInput, FreezeVerdict, FreezeVerdictKind, ResolutionMark, StreamDebugSample, WebrtcDebugStats, WebrtcDebugWindow, } from './stream-debug/webrtc-debug-stats';
24
24
  export { AutotrackSection } from './cap-settings/AutotrackSection';
25
25
  export { ConsumablesPanel } from './cap-settings/ConsumablesPanel';
26
26
  export type { CapSettingsComponentProps } from './cap-settings/index';
@@ -1,18 +1,4 @@
1
- /**
2
- * Pure derivation of the stream-debug overlay's CLIENT half from one
3
- * `RTCPeerConnection.getStats()` report.
4
- *
5
- * Why it is pure: this is the surface an operator reads while a camera is
6
- * frozen, and the numbers it shows decide whether the stall is in the
7
- * transport or in the decoder. A derivation that can only be exercised by
8
- * mounting a player and waiting for a real freeze is a derivation nobody
9
- * checks. Everything here is a function of (report, previous sample, clock).
10
- *
11
- * D393 — a measurement that FAILED is `null`, never `0`. A stat the browser
12
- * did not report is absent, and "absent" must never render as "zero" in a
13
- * debug tool: `freezeCount: 0` means the decoder measured no freeze,
14
- * `freezeCount: null` means nobody measured. Those are opposite conclusions.
15
- */
1
+ import { WebrtcSessionDebug } from '@camstack/types';
16
2
  /** One resolution the stream actually presented, and when it started. */
17
3
  export interface ResolutionMark {
18
4
  readonly width: number;
@@ -127,3 +113,39 @@ export interface FreezeVerdict {
127
113
  readonly label: string;
128
114
  }
129
115
  export declare function freezeVerdict(stats: WebrtcDebugStats | null): FreezeVerdict;
116
+ /**
117
+ * One reported sample: both halves of the stream-debug answer, plus the session
118
+ * they describe.
119
+ *
120
+ * It exists because the overlay cannot always be the surface. In the native app
121
+ * the player lives inside a WebView whose touches belong to the RN gesture
122
+ * layer, so the in-picture panel can never be opened there — the numbers have
123
+ * to leave the player and be drawn by the host, next to the battery and link
124
+ * details where the operator already looks.
125
+ *
126
+ * Everything in it is nullable for the same reason it is nullable everywhere
127
+ * else in this file: a measurement that did not happen is not a zero.
128
+ */
129
+ export interface StreamDebugSample {
130
+ /** When this sample was taken, by the reporting player's clock. */
131
+ readonly at: number;
132
+ /**
133
+ * The verdict, computed HERE.
134
+ *
135
+ * The host that renders this sample is not always a web page — the native
136
+ * app draws it in React Native — and `@camstack/ui-library` is a DOM
137
+ * component library whose bundle a React Native app must not import (it
138
+ * reaches for browser globals and for `Array.prototype.toSorted`, which
139
+ * Hermes does not have). Sending the verdict rather than the function that
140
+ * derives it keeps that boundary one-way.
141
+ */
142
+ readonly verdict: FreezeVerdictKind;
143
+ /** The session both halves describe; `null` before the player has one. */
144
+ readonly sessionId: string | null;
145
+ readonly stats: WebrtcDebugStats | null;
146
+ readonly server: WebrtcSessionDebug | null;
147
+ /** Why the last server poll produced nothing, verbatim. */
148
+ readonly serverError: string | null;
149
+ /** Whether the player was given a way to ask the server at all. */
150
+ readonly serverWired: boolean;
151
+ }
package/dist/index.cjs CHANGED
@@ -27667,7 +27667,7 @@ function mergePlayerHints(autoHints, override, downlinkMbps, coldStart) {
27667
27667
  return coldStart ? stripDownlinkForColdStart(merged) : merged;
27668
27668
  }
27669
27669
  var FIRST_FRAME_TIMEOUT_MS = 8e3;
27670
- function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, muted: initialMuted = true, showControls = true, showStats = false, onPlaybackStats, onClientNetworkSample, onConnectTiming, className = "", onStateChange, onError, onReconnectAttempt, overlay, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, getSessionDebug, reoffer, posterUrl, hintsOverride, onAdaptiveHints, reconnectSignal, onControlChannel, onVideoElement }) {
27670
+ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, muted: initialMuted = true, showControls = true, showStats = false, onDebugStats, onPlaybackStats, onClientNetworkSample, onConnectTiming, className = "", onStateChange, onError, onReconnectAttempt, overlay, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, getSessionDebug, reoffer, posterUrl, hintsOverride, onAdaptiveHints, reconnectSignal, onControlChannel, onVideoElement }) {
27671
27671
  const videoRef = (0, react$1.useRef)(null);
27672
27672
  const containerRef = (0, react$1.useRef)(null);
27673
27673
  const pcRef = (0, react$1.useRef)(null);
@@ -28568,13 +28568,14 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
28568
28568
  const [serverDebug, setServerDebug] = (0, react$1.useState)(null);
28569
28569
  const [serverDebugError, setServerDebugError] = (0, react$1.useState)(null);
28570
28570
  const [debugNow, setDebugNow] = (0, react$1.useState)(() => Date.now());
28571
+ const debugWanted = showStats || onDebugStats !== void 0;
28571
28572
  (0, react$1.useEffect)(() => {
28572
28573
  if (!showStats) return;
28573
28574
  const id = setInterval(() => setDebugNow(Date.now()), 500);
28574
28575
  return () => clearInterval(id);
28575
28576
  }, [showStats]);
28576
28577
  (0, react$1.useEffect)(() => {
28577
- if (!showStats) {
28578
+ if (!debugWanted) {
28578
28579
  setDebugStats(null);
28579
28580
  return;
28580
28581
  }
@@ -28605,9 +28606,9 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
28605
28606
  cancelled = true;
28606
28607
  clearInterval(id);
28607
28608
  };
28608
- }, [showStats]);
28609
+ }, [debugWanted]);
28609
28610
  (0, react$1.useEffect)(() => {
28610
- if (!showStats || !getSessionDebug) {
28611
+ if (!debugWanted || !getSessionDebug) {
28611
28612
  setServerDebug(null);
28612
28613
  setServerDebugError(null);
28613
28614
  return;
@@ -28637,7 +28638,32 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
28637
28638
  cancelled = true;
28638
28639
  clearInterval(id);
28639
28640
  };
28640
- }, [showStats, getSessionDebug]);
28641
+ }, [debugWanted, getSessionDebug]);
28642
+ const onDebugStatsRef = (0, react$1.useRef)(onDebugStats);
28643
+ onDebugStatsRef.current = onDebugStats;
28644
+ (0, react$1.useEffect)(() => {
28645
+ if (onDebugStats === void 0) return;
28646
+ const emit = () => {
28647
+ onDebugStatsRef.current?.({
28648
+ at: Date.now(),
28649
+ verdict: freezeVerdict(debugStatsRef.current).kind,
28650
+ sessionId: activeSessionIdRef.current,
28651
+ stats: debugStatsRef.current,
28652
+ server: serverDebugRef.current,
28653
+ serverError: serverDebugErrorRef.current,
28654
+ serverWired: getSessionDebug !== void 0
28655
+ });
28656
+ };
28657
+ emit();
28658
+ const id = setInterval(emit, 1e3);
28659
+ return () => clearInterval(id);
28660
+ }, [onDebugStats === void 0, getSessionDebug]);
28661
+ const debugStatsRef = (0, react$1.useRef)(null);
28662
+ debugStatsRef.current = debugStats;
28663
+ const serverDebugRef = (0, react$1.useRef)(null);
28664
+ serverDebugRef.current = serverDebug;
28665
+ const serverDebugErrorRef = (0, react$1.useRef)(null);
28666
+ serverDebugErrorRef.current = serverDebugError;
28641
28667
  const netPrev = (0, react$1.useRef)(null);
28642
28668
  (0, react$1.useEffect)(() => {
28643
28669
  if (!onClientNetworkSample) {
package/dist/index.js CHANGED
@@ -27643,7 +27643,7 @@ function mergePlayerHints(autoHints, override, downlinkMbps, coldStart) {
27643
27643
  return coldStart ? stripDownlinkForColdStart(merged) : merged;
27644
27644
  }
27645
27645
  var FIRST_FRAME_TIMEOUT_MS = 8e3;
27646
- function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, muted: initialMuted = true, showControls = true, showStats = false, onPlaybackStats, onClientNetworkSample, onConnectTiming, className = "", onStateChange, onError, onReconnectAttempt, overlay, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, getSessionDebug, reoffer, posterUrl, hintsOverride, onAdaptiveHints, reconnectSignal, onControlChannel, onVideoElement }) {
27646
+ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, muted: initialMuted = true, showControls = true, showStats = false, onDebugStats, onPlaybackStats, onClientNetworkSample, onConnectTiming, className = "", onStateChange, onError, onReconnectAttempt, overlay, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, getSessionDebug, reoffer, posterUrl, hintsOverride, onAdaptiveHints, reconnectSignal, onControlChannel, onVideoElement }) {
27647
27647
  const videoRef = useRef(null);
27648
27648
  const containerRef = useRef(null);
27649
27649
  const pcRef = useRef(null);
@@ -28544,13 +28544,14 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
28544
28544
  const [serverDebug, setServerDebug] = useState(null);
28545
28545
  const [serverDebugError, setServerDebugError] = useState(null);
28546
28546
  const [debugNow, setDebugNow] = useState(() => Date.now());
28547
+ const debugWanted = showStats || onDebugStats !== void 0;
28547
28548
  useEffect(() => {
28548
28549
  if (!showStats) return;
28549
28550
  const id = setInterval(() => setDebugNow(Date.now()), 500);
28550
28551
  return () => clearInterval(id);
28551
28552
  }, [showStats]);
28552
28553
  useEffect(() => {
28553
- if (!showStats) {
28554
+ if (!debugWanted) {
28554
28555
  setDebugStats(null);
28555
28556
  return;
28556
28557
  }
@@ -28581,9 +28582,9 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
28581
28582
  cancelled = true;
28582
28583
  clearInterval(id);
28583
28584
  };
28584
- }, [showStats]);
28585
+ }, [debugWanted]);
28585
28586
  useEffect(() => {
28586
- if (!showStats || !getSessionDebug) {
28587
+ if (!debugWanted || !getSessionDebug) {
28587
28588
  setServerDebug(null);
28588
28589
  setServerDebugError(null);
28589
28590
  return;
@@ -28613,7 +28614,32 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
28613
28614
  cancelled = true;
28614
28615
  clearInterval(id);
28615
28616
  };
28616
- }, [showStats, getSessionDebug]);
28617
+ }, [debugWanted, getSessionDebug]);
28618
+ const onDebugStatsRef = useRef(onDebugStats);
28619
+ onDebugStatsRef.current = onDebugStats;
28620
+ useEffect(() => {
28621
+ if (onDebugStats === void 0) return;
28622
+ const emit = () => {
28623
+ onDebugStatsRef.current?.({
28624
+ at: Date.now(),
28625
+ verdict: freezeVerdict(debugStatsRef.current).kind,
28626
+ sessionId: activeSessionIdRef.current,
28627
+ stats: debugStatsRef.current,
28628
+ server: serverDebugRef.current,
28629
+ serverError: serverDebugErrorRef.current,
28630
+ serverWired: getSessionDebug !== void 0
28631
+ });
28632
+ };
28633
+ emit();
28634
+ const id = setInterval(emit, 1e3);
28635
+ return () => clearInterval(id);
28636
+ }, [onDebugStats === void 0, getSessionDebug]);
28637
+ const debugStatsRef = useRef(null);
28638
+ debugStatsRef.current = debugStats;
28639
+ const serverDebugRef = useRef(null);
28640
+ serverDebugRef.current = serverDebug;
28641
+ const serverDebugErrorRef = useRef(null);
28642
+ serverDebugErrorRef.current = serverDebugError;
28617
28643
  const netPrev = useRef(null);
28618
28644
  useEffect(() => {
28619
28645
  if (!onClientNetworkSample) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/ui-library",
3
- "version": "1.2.185",
3
+ "version": "1.2.187",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",