@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.
- package/dist/composites/adaptive-downlink.d.ts +99 -7
- package/dist/composites/camera-stream-player.d.ts +11 -11
- package/dist/composites/client-network-sample.d.ts +56 -0
- package/dist/generated/system-hooks.d.ts +2 -0
- package/dist/index.cjs +280 -66
- package/dist/index.js +280 -67
- package/dist/lib/format-last-seen.d.ts +12 -3
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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`. */
|