@camstack/ui-library 1.1.36 → 1.1.37

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.
@@ -84,6 +84,15 @@ export interface ClientStreamHints {
84
84
  downlinkMbps?: number;
85
85
  prefersTier?: string;
86
86
  }
87
+ /** Client-observed network conditions, sampled from the live PeerConnection.
88
+ * Matches the `networkQuality.reportClientStats` mutation input (minus
89
+ * `deviceId`, which the caller adds). */
90
+ export interface ClientNetworkSample {
91
+ readonly rttMs: number;
92
+ readonly jitterMs: number;
93
+ readonly estimatedBandwidthKbps: number;
94
+ readonly packetLossPercent: number;
95
+ }
87
96
  export interface CameraStreamPlayerProps {
88
97
  /** Server base URL (e.g. window.location.origin) */
89
98
  serverUrl: string;
@@ -111,6 +120,16 @@ export interface CameraStreamPlayerProps {
111
120
  * existing host log bridge (`{type:'log'}`), so they respect the app's
112
121
  * debug-mode flag. Used to diagnose recorded VOD-over-WebRTC stutter. */
113
122
  onPlaybackStats?: (data: Record<string, unknown>) => void;
123
+ /**
124
+ * When provided, samples the inbound-video getStats() ~every 2s and reports
125
+ * the CLIENT's live network conditions (packet-loss %, jitter, downlink
126
+ * bitrate, RTT) so the caller can push them to the server's
127
+ * `networkQuality.reportClientStats`. This is what lets the broker's ADAPTIVE
128
+ * ladder actually downgrade "auto" under a poor link: without a client
129
+ * signal the server sees only coarse RTCP RR and tends to stay on the top
130
+ * tier. Inert (zero behaviour change) unless a callback is passed; wire it
131
+ * only for adaptive sessions. */
132
+ onClientNetworkSample?: (sample: ClientNetworkSample) => void;
114
133
  /** Additional CSS classes for the root container */
115
134
  className?: string;
116
135
  /** Called on connection state change */
@@ -215,5 +234,5 @@ export interface CameraStreamPlayerProps {
215
234
  */
216
235
  onControlChannel?: (channel: RTCDataChannel) => void;
217
236
  }
218
- export declare function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay, muted: initialMuted, showControls, showStats, onPlaybackStats, className, onStateChange, onError, overlay, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, reoffer, posterUrl, hintsOverride, reconnectSignal, onControlChannel, }: CameraStreamPlayerProps): import("react").JSX.Element;
237
+ export declare function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay, muted: initialMuted, showControls, showStats, onPlaybackStats, onClientNetworkSample, className, onStateChange, onError, overlay, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, reoffer, posterUrl, hintsOverride, reconnectSignal, onControlChannel, }: CameraStreamPlayerProps): import("react").JSX.Element;
219
238
  export {};
package/dist/index.cjs CHANGED
@@ -38937,7 +38937,7 @@ function computeClientHints(container) {
38937
38937
  var RECONNECT_BASE_DELAY_MS = 1500;
38938
38938
  var RECONNECT_MAX_DELAY_MS = 15e3;
38939
38939
  var MAX_RECONNECT_ATTEMPTS = 40;
38940
- function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, muted: initialMuted = true, showControls = true, showStats = false, onPlaybackStats, className = "", onStateChange, onError, overlay, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, reoffer, posterUrl, hintsOverride, reconnectSignal, onControlChannel }) {
38940
+ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, muted: initialMuted = true, showControls = true, showStats = false, onPlaybackStats, onClientNetworkSample, className = "", onStateChange, onError, overlay, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, reoffer, posterUrl, hintsOverride, reconnectSignal, onControlChannel }) {
38941
38941
  const videoRef = (0, react$1.useRef)(null);
38942
38942
  const containerRef = (0, react$1.useRef)(null);
38943
38943
  const pcRef = (0, react$1.useRef)(null);
@@ -39705,6 +39705,58 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
39705
39705
  }, 1e3);
39706
39706
  return () => clearInterval(id);
39707
39707
  }, [showStats]);
39708
+ const netPrev = (0, react$1.useRef)(null);
39709
+ (0, react$1.useEffect)(() => {
39710
+ if (!onClientNetworkSample) {
39711
+ netPrev.current = null;
39712
+ return;
39713
+ }
39714
+ const tick = async () => {
39715
+ const pc = pcRef.current;
39716
+ if (!pc) return;
39717
+ try {
39718
+ const report = await pc.getStats();
39719
+ let lost = 0;
39720
+ let recv = 0;
39721
+ let bytes = 0;
39722
+ let ts = 0;
39723
+ let jitterS = 0;
39724
+ let rttS = 0;
39725
+ report.forEach((raw) => {
39726
+ const s = raw;
39727
+ if (s.type === "inbound-rtp" && (s.kind ?? s.mediaType) === "video") {
39728
+ lost = s.packetsLost ?? 0;
39729
+ recv = s.packetsReceived ?? 0;
39730
+ bytes = s.bytesReceived ?? 0;
39731
+ ts = s.timestamp;
39732
+ jitterS = s.jitter ?? 0;
39733
+ } else if (s.type === "candidate-pair" && s.nominated && typeof s.currentRoundTripTime === "number") rttS = s.currentRoundTripTime;
39734
+ });
39735
+ const prev = netPrev.current;
39736
+ netPrev.current = {
39737
+ lost,
39738
+ recv,
39739
+ bytes,
39740
+ ts
39741
+ };
39742
+ if (!prev || ts <= prev.ts) return;
39743
+ const dLost = Math.max(0, lost - prev.lost);
39744
+ const denom = dLost + Math.max(0, recv - prev.recv);
39745
+ const packetLossPercent = denom > 0 ? Math.min(100, dLost / denom * 100) : 0;
39746
+ const estimatedBandwidthKbps = Math.max(0, Math.round((bytes - prev.bytes) * 8 / (ts - prev.ts)));
39747
+ onClientNetworkSample({
39748
+ rttMs: Math.round(rttS * 1e3),
39749
+ jitterMs: Math.round(jitterS * 1e3),
39750
+ estimatedBandwidthKbps,
39751
+ packetLossPercent
39752
+ });
39753
+ } catch {}
39754
+ };
39755
+ const id = setInterval(() => {
39756
+ tick();
39757
+ }, 2e3);
39758
+ return () => clearInterval(id);
39759
+ }, [onClientNetworkSample]);
39708
39760
  const pbPrev = (0, react$1.useRef)(null);
39709
39761
  (0, react$1.useEffect)(() => {
39710
39762
  if (!onPlaybackStats) {
package/dist/index.js CHANGED
@@ -38913,7 +38913,7 @@ function computeClientHints(container) {
38913
38913
  var RECONNECT_BASE_DELAY_MS = 1500;
38914
38914
  var RECONNECT_MAX_DELAY_MS = 15e3;
38915
38915
  var MAX_RECONNECT_ATTEMPTS = 40;
38916
- function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, muted: initialMuted = true, showControls = true, showStats = false, onPlaybackStats, className = "", onStateChange, onError, overlay, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, reoffer, posterUrl, hintsOverride, reconnectSignal, onControlChannel }) {
38916
+ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, muted: initialMuted = true, showControls = true, showStats = false, onPlaybackStats, onClientNetworkSample, className = "", onStateChange, onError, overlay, createSession, sendAnswer, handleOffer, getIceServers, addIceCandidate, getIceCandidates, closeSession, getSessionState, reoffer, posterUrl, hintsOverride, reconnectSignal, onControlChannel }) {
38917
38917
  const videoRef = useRef(null);
38918
38918
  const containerRef = useRef(null);
38919
38919
  const pcRef = useRef(null);
@@ -39681,6 +39681,58 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
39681
39681
  }, 1e3);
39682
39682
  return () => clearInterval(id);
39683
39683
  }, [showStats]);
39684
+ const netPrev = useRef(null);
39685
+ useEffect(() => {
39686
+ if (!onClientNetworkSample) {
39687
+ netPrev.current = null;
39688
+ return;
39689
+ }
39690
+ const tick = async () => {
39691
+ const pc = pcRef.current;
39692
+ if (!pc) return;
39693
+ try {
39694
+ const report = await pc.getStats();
39695
+ let lost = 0;
39696
+ let recv = 0;
39697
+ let bytes = 0;
39698
+ let ts = 0;
39699
+ let jitterS = 0;
39700
+ let rttS = 0;
39701
+ report.forEach((raw) => {
39702
+ const s = raw;
39703
+ if (s.type === "inbound-rtp" && (s.kind ?? s.mediaType) === "video") {
39704
+ lost = s.packetsLost ?? 0;
39705
+ recv = s.packetsReceived ?? 0;
39706
+ bytes = s.bytesReceived ?? 0;
39707
+ ts = s.timestamp;
39708
+ jitterS = s.jitter ?? 0;
39709
+ } else if (s.type === "candidate-pair" && s.nominated && typeof s.currentRoundTripTime === "number") rttS = s.currentRoundTripTime;
39710
+ });
39711
+ const prev = netPrev.current;
39712
+ netPrev.current = {
39713
+ lost,
39714
+ recv,
39715
+ bytes,
39716
+ ts
39717
+ };
39718
+ if (!prev || ts <= prev.ts) return;
39719
+ const dLost = Math.max(0, lost - prev.lost);
39720
+ const denom = dLost + Math.max(0, recv - prev.recv);
39721
+ const packetLossPercent = denom > 0 ? Math.min(100, dLost / denom * 100) : 0;
39722
+ const estimatedBandwidthKbps = Math.max(0, Math.round((bytes - prev.bytes) * 8 / (ts - prev.ts)));
39723
+ onClientNetworkSample({
39724
+ rttMs: Math.round(rttS * 1e3),
39725
+ jitterMs: Math.round(jitterS * 1e3),
39726
+ estimatedBandwidthKbps,
39727
+ packetLossPercent
39728
+ });
39729
+ } catch {}
39730
+ };
39731
+ const id = setInterval(() => {
39732
+ tick();
39733
+ }, 2e3);
39734
+ return () => clearInterval(id);
39735
+ }, [onClientNetworkSample]);
39684
39736
  const pbPrev = useRef(null);
39685
39737
  useEffect(() => {
39686
39738
  if (!onPlaybackStats) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/ui-library",
3
- "version": "1.1.36",
3
+ "version": "1.1.37",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",