@camstack/ui-library 1.2.200 → 1.2.202

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.
@@ -3,9 +3,28 @@
3
3
  * Used as `hints.downlinkMbps` so adaptive selection is based on a measured
4
4
  * path, not viewport size or IP class. Failures return null (caller keeps
5
5
  * the default-low / ceiling path and may stay on the lowest rung).
6
+ *
7
+ * D635 (2026-09-25): the probe used to time a 128 KiB blob from the REQUEST,
8
+ * fired in parallel with `createSession` on a hub at ~80 % CPU. It read the
9
+ * LAN as 1.2–1.46 Mbps — the elapsed time was mostly waiting for the first
10
+ * byte, i.e. latency. It now (1) times the body from its FIRST byte, (2) reads
11
+ * a 1 MiB blob so the transfer dominates timer jitter, and (3) runs after the
12
+ * session's first decoded frame, never during session setup.
6
13
  */
7
14
  export declare const ADAPTIVE_PROBE_PATH = "/adaptive-probe";
15
+ /**
16
+ * Whole-probe deadline, from the request. A probe cut here is still a
17
+ * measurement of whatever body arrived (see {@link measureDownlinkMbps}).
18
+ */
8
19
  export declare const ADAPTIVE_PROBE_TIMEOUT_MS = 2500;
20
+ /**
21
+ * Fewest body bytes AFTER the first chunk that make a rate. 64 KiB: at the
22
+ * `high` threshold (8 Mbps) that is 65 ms of transfer — four times a 16 ms
23
+ * WebView timer tick. Less is a guess, and a guess is unknown, not slow.
24
+ */
25
+ export declare const ADAPTIVE_PROBE_MIN_MEASURED_BYTES: number;
26
+ /** How long the probe waits for the session's first decoded frame. */
27
+ export declare const FIRST_FRAME_PROBE_TIMEOUT_MS = 15000;
9
28
  export type AdaptiveTier = 'high' | 'mid' | 'low';
10
29
  /** Mbps needed to attempt each rung (bitrate / 0.6 headroom, rounded). */
11
30
  export declare const DOWNLINK_HIGH_MBPS = 8;
@@ -42,7 +61,7 @@ export declare function shouldUpgradeAfterProbe(mbps: number): boolean;
42
61
  export declare function resolveProbeOrigin(serverUrl: string | undefined, pageOrigin: string | undefined): string | null;
43
62
  /** Which offer composed the hints. A climb and a cold start read identically
44
63
  * in the hub's log; only the client knows which one it just sent. */
45
- export type AdaptiveOfferPhase = 'cold-start' | 'probe-climb' | 'renegotiate';
64
+ export type AdaptiveOfferPhase = 'cold-start' | 'probe-climb' | 'renegotiate' | 'probe-refused';
46
65
  /**
47
66
  * What the CLIENT sent for one adaptive decision. Deliberately not the hub's
48
67
  * verdict — the hub already logs its own pick, and a client line that repeated
@@ -59,6 +78,8 @@ export interface AdaptiveHintsReport {
59
78
  readonly devicePixelRatio?: number;
60
79
  readonly downlinkMbps?: number;
61
80
  readonly prefersTier?: string;
81
+ /** For `probe-refused`: the HTTP status the hub answered the probe with (401/429…). */
82
+ readonly probeStatus?: number;
62
83
  }
63
84
  /**
64
85
  * Identity of a decision, for "log once per CHANGE, not per tick".
@@ -69,12 +90,83 @@ export interface AdaptiveHintsReport {
69
90
  * pick the same rung are the same decision and must collapse to one line.
70
91
  */
71
92
  export declare function adaptiveHintsKey(report: AdaptiveHintsReport): string;
72
- export declare function measureDownlinkMbps(origin: string, deps: {
93
+ /** The slice of a body reader the probe uses (a `ReadableStreamDefaultReader` fits). */
94
+ export interface ProbeBodyReader {
95
+ read(): Promise<{
96
+ readonly done: boolean;
97
+ readonly value?: Uint8Array;
98
+ }>;
99
+ cancel(): Promise<void>;
100
+ }
101
+ /** The slice of a fetch `Response` the probe uses. */
102
+ export interface ProbeResponse {
103
+ readonly ok: boolean;
104
+ readonly status?: number;
105
+ readonly body?: {
106
+ getReader(): ProbeBodyReader;
107
+ } | null;
108
+ arrayBuffer(): Promise<ArrayBuffer>;
109
+ }
110
+ export interface DownlinkProbeDeps {
73
111
  readonly fetchImpl: (url: string, init: {
74
112
  signal: AbortSignal;
75
- }) => Promise<{
76
- ok: boolean;
77
- arrayBuffer: () => Promise<ArrayBuffer>;
78
- }>;
113
+ }) => Promise<ProbeResponse>;
114
+ readonly now: () => number;
115
+ /**
116
+ * The hub answered but refused (401 without a session, 429 rate-limited).
117
+ * The reading is still unknown — but a missing token must not be silent,
118
+ * or it is indistinguishable from a failed probe (review N3, D391).
119
+ */
120
+ readonly onRefused?: (status: number) => void;
121
+ }
122
+ export declare function measureDownlinkMbps(origin: string, deps: DownlinkProbeDeps): Promise<number | null>;
123
+ export type FirstFrameOutcome = 'frame' | 'timeout' | 'aborted';
124
+ export interface FirstFrameDeps {
125
+ /** The live video element's `readyState`, or null when there is none yet. */
126
+ readonly readyState: () => number | null;
127
+ readonly signal: AbortSignal;
128
+ readonly sleep: (ms: number) => Promise<void>;
79
129
  readonly now: () => number;
80
- }): Promise<number | null>;
130
+ }
131
+ /**
132
+ * Resolve once the session presents its first decoded frame — the moment
133
+ * session setup (createSession, ICE, DTLS, the first IDR) is over and a probe
134
+ * no longer competes with it for the hub or the link.
135
+ */
136
+ export declare function waitForFirstFrame(deps: FirstFrameDeps, timeoutMs?: number): Promise<FirstFrameOutcome>;
137
+ /**
138
+ * The cold-start probe: measure only once the first frame is up. No frame ⇒
139
+ * no measurement (undefined) — the session is failing for a reason a probe
140
+ * cannot fix, and a climb would only add a second connection to it.
141
+ */
142
+ export interface ProbeAfterFirstFrameDeps {
143
+ readonly waitFrame: () => Promise<FirstFrameOutcome>;
144
+ readonly measure: () => Promise<number | undefined>;
145
+ }
146
+ export declare function probeAfterFirstFrame(deps: ProbeAfterFirstFrameDeps): Promise<number | undefined>;
147
+ /** The latest probe for an origin, whatever it read or how old it is. */
148
+ export interface LatestProbe {
149
+ readonly mbps: number;
150
+ readonly ageMs: number;
151
+ }
152
+ export interface DownlinkProbeStoreDeps {
153
+ readonly now: () => number;
154
+ /** How long a reading answers `measured`/`cached` without re-probing. */
155
+ readonly ttlMs: number;
156
+ }
157
+ export interface DownlinkProbeStore {
158
+ /**
159
+ * The cached reading while fresh, else ONE probe for the origin — concurrent
160
+ * callers share the probe in flight (review I6: a grid decoding together
161
+ * fired N 1 MiB probes, each reading the link ÷ N). Undefined = unknown.
162
+ */
163
+ measured(origin: string, measure: () => Promise<number | null>): Promise<number | undefined>;
164
+ /** The cached reading while fresh — never probes (review I5: renegotiation). */
165
+ cached(origin: string): number | undefined;
166
+ /** The newest reading whatever its age — what the telemetry reports (I4). */
167
+ latest(origin: string): LatestProbe | null;
168
+ /** True the FIRST time `origin` refuses with `status` in this app — say it once. */
169
+ firstRefusal(origin: string, status: number): boolean;
170
+ }
171
+ /** App-wide probe cache + single-flight, one per origin. */
172
+ export declare function createDownlinkProbe(deps: DownlinkProbeStoreDeps): DownlinkProbeStore;
@@ -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 { ClientNetworkSample } from './client-network-sample';
5
6
  import { StreamDebugSample } from './stream-debug/webrtc-debug-stats.js';
6
7
  type WebkitPresentationMode = 'inline' | 'picture-in-picture' | 'fullscreen';
7
8
  declare global {
@@ -87,15 +88,7 @@ export interface ClientStreamHints {
87
88
  downlinkMbps?: number;
88
89
  prefersTier?: string;
89
90
  }
90
- /** Client-observed network conditions, sampled from the live PeerConnection.
91
- * Matches the `networkQuality.reportClientStats` mutation input (minus
92
- * `deviceId`, which the caller adds). */
93
- export interface ClientNetworkSample {
94
- readonly rttMs: number;
95
- readonly jitterMs: number;
96
- readonly estimatedBandwidthKbps: number;
97
- readonly packetLossPercent: number;
98
- }
91
+ export type { ClientNetworkSample } from './client-network-sample';
99
92
  export interface CameraStreamPlayerProps {
100
93
  /** Server base URL (e.g. window.location.origin) */
101
94
  serverUrl: string;
@@ -147,6 +140,14 @@ export interface CameraStreamPlayerProps {
147
140
  * tier. Inert (zero behaviour change) unless a callback is passed; wire it
148
141
  * only for adaptive sessions. */
149
142
  onClientNetworkSample?: (sample: ClientNetworkSample) => void;
143
+ /**
144
+ * Bearer for `/adaptive-probe`, which requires a session (D635). A page the
145
+ * hub serves carries the session cookie and needs none; a host whose page
146
+ * cannot (the native app's `file://` embed) passes its token here. Absent
147
+ * and cookieless, the probe reads unknown and the broker's own ladder
148
+ * decides (try-climb on a clean path).
149
+ */
150
+ probeAuthToken?: string | null;
150
151
  /**
151
152
  * Cold-start phase breakdown, fired ONCE per connect when ICE reaches
152
153
  * connected (the same object the `[WebRTC timing]` console line prints:
@@ -302,5 +303,4 @@ export interface CameraStreamPlayerProps {
302
303
  */
303
304
  onVideoElement?: (el: HTMLVideoElement | null) => void;
304
305
  }
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;
306
- export {};
306
+ export declare function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay, muted: initialMuted, showControls, showStats, onDebugStats, onPlaybackStats, onClientNetworkSample, probeAuthToken, 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;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * What the player reports to the broker every ~2 s
3
+ * (`networkQuality.reportClientStats`), as a pure step over two `getStats`
4
+ * readings — so the two ways it used to lie can be pinned (D635 round 3):
5
+ *
6
+ * - Across a PC swap (a tier change's make-before-break) the byte baseline was
7
+ * the OLD connection's, so the new PC's first reading clamped to 0 kbps; and
8
+ * while a renegotiation was in flight the reporter read a closing PC. Both
9
+ * zeros were matched by the broker against real sends (review I1). A new PC
10
+ * now re-baselines, and nothing is reported while renegotiating.
11
+ * - The downlink probe never reached the broker unless it justified a climb.
12
+ * The sample now carries the latest probe whatever it read, with its age
13
+ * (review I4), so "measured slow" is no longer indistinguishable from
14
+ * "failed".
15
+ */
16
+ /** Client-observed network conditions. Matches the `networkQuality.reportClientStats`
17
+ * mutation input (minus `deviceId`, which the caller adds). */
18
+ export interface ClientNetworkSample {
19
+ readonly rttMs: number;
20
+ readonly jitterMs: number;
21
+ readonly estimatedBandwidthKbps: number;
22
+ readonly packetLossPercent: number;
23
+ /** Latest `/adaptive-probe` reading, Mbps, whatever it read; absent = never / failed. */
24
+ readonly downlinkMbps?: number;
25
+ /** Age of that reading now, ms. */
26
+ readonly downlinkAgeMs?: number;
27
+ }
28
+ /** One inbound-video `getStats` reading, and the connection it came from. */
29
+ export interface InboundReading {
30
+ /** The RTCPeerConnection read — identity only. */
31
+ readonly pc: object;
32
+ readonly lost: number;
33
+ readonly recv: number;
34
+ readonly bytes: number;
35
+ /** getStats timestamp, ms. */
36
+ readonly ts: number;
37
+ readonly rttS: number;
38
+ readonly jitterS: number;
39
+ }
40
+ /** The latest probe as the player holds it. */
41
+ export interface ProbeReading {
42
+ readonly mbps: number;
43
+ readonly ageMs: number;
44
+ }
45
+ export interface SampleContext {
46
+ /** A renegotiation (tier change) is in flight: the PC being read is on its way out. */
47
+ readonly renegotiating: boolean;
48
+ readonly probe: ProbeReading | null;
49
+ }
50
+ export interface SampleStep {
51
+ /** The reading to diff the next one against; null = start over. */
52
+ readonly baseline: InboundReading | null;
53
+ /** What to report now; null = report nothing this window. */
54
+ readonly sample: ClientNetworkSample | null;
55
+ }
56
+ export declare function nextClientSample(prev: InboundReading | null, cur: InboundReading, ctx: SampleContext): SampleStep;
@@ -537,6 +537,8 @@ export declare const useDeviceManagerGetBindings: typeof trpc.deviceManager.getB
537
537
  export declare const useDeviceManagerGetBindingsBatch: typeof trpc.deviceManager.getBindingsBatch.useQuery;
538
538
  /** Generated alias around `trpc.deviceManager.getAllBindings.useQuery`. */
539
539
  export declare const useDeviceManagerGetAllBindings: typeof trpc.deviceManager.getAllBindings.useQuery;
540
+ /** Generated alias around `trpc.deviceManager.listDeviceIdsByCaps.useQuery`. */
541
+ export declare const useDeviceManagerListDeviceIdsByCaps: typeof trpc.deviceManager.listDeviceIdsByCaps.useQuery;
540
542
  /** Generated alias around `trpc.deviceManager.setWrapperActive.useMutation`. */
541
543
  export declare const useDeviceManagerSetWrapperActive: typeof trpc.deviceManager.setWrapperActive.useMutation;
542
544
  /** Generated alias around `trpc.deviceManager.listWrappersForCap.useQuery`. */