@camstack/ui-library 1.2.100 → 1.2.101
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 +51 -0
- package/dist/composites/camera-stream-player.d.ts +17 -1
- package/dist/composites/index.d.ts +1 -0
- package/dist/index.cjs +111 -12
- package/dist/index.js +111 -12
- package/package.json +1 -1
|
@@ -18,6 +18,57 @@ export declare function stripDownlinkForColdStart<T extends {
|
|
|
18
18
|
}>(hints: T): Omit<T, 'downlinkMbps'>;
|
|
19
19
|
/** After a probe, only mid/high justify a make-before-break climb. */
|
|
20
20
|
export declare function shouldUpgradeAfterProbe(mbps: number): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* The origin `/adaptive-probe` must be fetched from — the HUB, not the page.
|
|
23
|
+
*
|
|
24
|
+
* The page origin was the original assumption ("same-origin as the player",
|
|
25
|
+
* `adaptive-probe.routes.ts`) and it is only true for the web viewer and the
|
|
26
|
+
* iframe embed, both served by the hub. The NATIVE viewer loads this same
|
|
27
|
+
* player from an asset bundled with the app (`useNativeEmbedSource` resolves
|
|
28
|
+
* `assets/embed/camstack-embed.html` through `Image.resolveAssetSource`), so
|
|
29
|
+
* inside the WKWebView `window.location.origin` is `file://` — and
|
|
30
|
+
* `file:///adaptive-probe` is not a request any hub can answer. The probe
|
|
31
|
+
* therefore returned null on EVERY native connect, `downlinkMbps` was never
|
|
32
|
+
* sent, `pickAdaptiveTier` scored every session `default-low`, and
|
|
33
|
+
* `shouldUpgradeAfterProbe` was never even reached — so the make-before-break
|
|
34
|
+
* climb that the cold-start default depends on could not run. That is the
|
|
35
|
+
* whole of "the automatic quality always drops to low" on the app.
|
|
36
|
+
*
|
|
37
|
+
* The hub the player is already talking to is the `serverUrl` prop, so it
|
|
38
|
+
* wins; the page origin remains as the fallback for a host that passes a
|
|
39
|
+
* relative/empty `serverUrl`. Null means no probe is possible and the caller
|
|
40
|
+
* must skip it rather than fetch a URL that cannot exist.
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveProbeOrigin(serverUrl: string | undefined, pageOrigin: string | undefined): string | null;
|
|
43
|
+
/** Which offer composed the hints. A climb and a cold start read identically
|
|
44
|
+
* in the hub's log; only the client knows which one it just sent. */
|
|
45
|
+
export type AdaptiveOfferPhase = 'cold-start' | 'probe-climb' | 'renegotiate';
|
|
46
|
+
/**
|
|
47
|
+
* What the CLIENT sent for one adaptive decision. Deliberately not the hub's
|
|
48
|
+
* verdict — the hub already logs its own pick, and a client line that repeated
|
|
49
|
+
* it would prove nothing about the client. This is the other half of that
|
|
50
|
+
* join: the inputs the hub scored, plus whether the downlink probe had an
|
|
51
|
+
* origin it could even reach.
|
|
52
|
+
*/
|
|
53
|
+
export interface AdaptiveHintsReport {
|
|
54
|
+
readonly phase: AdaptiveOfferPhase;
|
|
55
|
+
/** Null when no http(s) origin resolved — the probe did not run at all. */
|
|
56
|
+
readonly probeOrigin: string | null;
|
|
57
|
+
readonly viewportWidth?: number;
|
|
58
|
+
readonly viewportHeight?: number;
|
|
59
|
+
readonly devicePixelRatio?: number;
|
|
60
|
+
readonly downlinkMbps?: number;
|
|
61
|
+
readonly prefersTier?: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Identity of a decision, for "log once per CHANGE, not per tick".
|
|
65
|
+
*
|
|
66
|
+
* `downlinkMbps` is a fresh measurement on every probe and would otherwise
|
|
67
|
+
* make every offer look like a new decision. What the ladder actually consumes
|
|
68
|
+
* is the RUNG it maps to (`suggestedAdaptiveTier`), so two measurements that
|
|
69
|
+
* pick the same rung are the same decision and must collapse to one line.
|
|
70
|
+
*/
|
|
71
|
+
export declare function adaptiveHintsKey(report: AdaptiveHintsReport): string;
|
|
21
72
|
export declare function measureDownlinkMbps(origin: string, deps: {
|
|
22
73
|
readonly fetchImpl: (url: string, init: {
|
|
23
74
|
signal: AbortSignal;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
2
|
import { CamProfile } from '@camstack/types';
|
|
3
3
|
import { ReconnectAction } from './reconnect-schedule';
|
|
4
|
+
import { AdaptiveHintsReport } from './adaptive-downlink';
|
|
4
5
|
type WebkitPresentationMode = 'inline' | 'picture-in-picture' | 'fullscreen';
|
|
5
6
|
declare global {
|
|
6
7
|
interface HTMLVideoElement {
|
|
@@ -227,6 +228,21 @@ export interface CameraStreamPlayerProps {
|
|
|
227
228
|
* Use for network simulation (benchmark) or manual tier forcing.
|
|
228
229
|
*/
|
|
229
230
|
hintsOverride?: Partial<ClientStreamHints>;
|
|
231
|
+
/**
|
|
232
|
+
* Observer of the ADAPTIVE decision this client just made — fired once per
|
|
233
|
+
* decision CHANGE (see `adaptiveHintsKey`), never per tick.
|
|
234
|
+
*
|
|
235
|
+
* Deliberately the client's half only: the viewport it measured, the pixel
|
|
236
|
+
* ratio, the downlink it managed to measure (and the origin it probed, null
|
|
237
|
+
* when none was reachable), and the phase of the offer that carried them.
|
|
238
|
+
* The hub logs its own pick from the same inputs; duplicating that here
|
|
239
|
+
* would be a second opinion on someone else's decision. Hosts join the two
|
|
240
|
+
* halves — the RN viewer forwards this over the embed log bridge so a
|
|
241
|
+
* "quality is stuck on low" report carries what the app actually SENT.
|
|
242
|
+
*
|
|
243
|
+
* Inert unless wired.
|
|
244
|
+
*/
|
|
245
|
+
onAdaptiveHints?: (report: AdaptiveHintsReport) => void;
|
|
230
246
|
/**
|
|
231
247
|
* Host-driven fast reconnect. Bump this number (monotonic) to force a
|
|
232
248
|
* full reconnect WITHOUT remounting the player — used by the embed when
|
|
@@ -258,5 +274,5 @@ export interface CameraStreamPlayerProps {
|
|
|
258
274
|
*/
|
|
259
275
|
onVideoElement?: (el: HTMLVideoElement | null) => void;
|
|
260
276
|
}
|
|
261
|
-
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, reoffer, posterUrl, hintsOverride, reconnectSignal, onControlChannel, onVideoElement, }: CameraStreamPlayerProps): import("react").JSX.Element;
|
|
277
|
+
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, reoffer, posterUrl, hintsOverride, onAdaptiveHints, reconnectSignal, onControlChannel, onVideoElement, }: CameraStreamPlayerProps): import("react").JSX.Element;
|
|
262
278
|
export {};
|
|
@@ -12,6 +12,7 @@ export { AudioWaveform } from './audio-waveform';
|
|
|
12
12
|
export type { BatteryBadgeProps, BatteryStatusLike } from './battery-badge';
|
|
13
13
|
export { BatteryBadge } from './battery-badge';
|
|
14
14
|
export * from './breadcrumb';
|
|
15
|
+
export type { AdaptiveHintsReport, AdaptiveOfferPhase, } from './adaptive-downlink';
|
|
15
16
|
export type { CameraStreamPlayerProps, ClientOfferResult, ClientStreamHints, PlayerConnectionState, PlayerWebrtcTarget, SessionSignalingState, SignalingResult, } from './camera-stream-player';
|
|
16
17
|
export { CameraStreamPlayer } from './camera-stream-player';
|
|
17
18
|
export { AutotrackSection } from './cap-settings/AutotrackSection';
|
package/dist/index.cjs
CHANGED
|
@@ -24373,6 +24373,62 @@ function stripDownlinkForColdStart(hints) {
|
|
|
24373
24373
|
function shouldUpgradeAfterProbe(mbps) {
|
|
24374
24374
|
return suggestedAdaptiveTier(mbps) !== "low";
|
|
24375
24375
|
}
|
|
24376
|
+
/** An http(s) origin, or null for anything that cannot serve the probe. */
|
|
24377
|
+
function httpOrigin(value) {
|
|
24378
|
+
if (value === void 0 || value === "") return null;
|
|
24379
|
+
let parsed;
|
|
24380
|
+
try {
|
|
24381
|
+
parsed = new URL(value);
|
|
24382
|
+
} catch {
|
|
24383
|
+
return null;
|
|
24384
|
+
}
|
|
24385
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
24386
|
+
return parsed.origin;
|
|
24387
|
+
}
|
|
24388
|
+
/**
|
|
24389
|
+
* The origin `/adaptive-probe` must be fetched from — the HUB, not the page.
|
|
24390
|
+
*
|
|
24391
|
+
* The page origin was the original assumption ("same-origin as the player",
|
|
24392
|
+
* `adaptive-probe.routes.ts`) and it is only true for the web viewer and the
|
|
24393
|
+
* iframe embed, both served by the hub. The NATIVE viewer loads this same
|
|
24394
|
+
* player from an asset bundled with the app (`useNativeEmbedSource` resolves
|
|
24395
|
+
* `assets/embed/camstack-embed.html` through `Image.resolveAssetSource`), so
|
|
24396
|
+
* inside the WKWebView `window.location.origin` is `file://` — and
|
|
24397
|
+
* `file:///adaptive-probe` is not a request any hub can answer. The probe
|
|
24398
|
+
* therefore returned null on EVERY native connect, `downlinkMbps` was never
|
|
24399
|
+
* sent, `pickAdaptiveTier` scored every session `default-low`, and
|
|
24400
|
+
* `shouldUpgradeAfterProbe` was never even reached — so the make-before-break
|
|
24401
|
+
* climb that the cold-start default depends on could not run. That is the
|
|
24402
|
+
* whole of "the automatic quality always drops to low" on the app.
|
|
24403
|
+
*
|
|
24404
|
+
* The hub the player is already talking to is the `serverUrl` prop, so it
|
|
24405
|
+
* wins; the page origin remains as the fallback for a host that passes a
|
|
24406
|
+
* relative/empty `serverUrl`. Null means no probe is possible and the caller
|
|
24407
|
+
* must skip it rather than fetch a URL that cannot exist.
|
|
24408
|
+
*/
|
|
24409
|
+
function resolveProbeOrigin(serverUrl, pageOrigin) {
|
|
24410
|
+
return httpOrigin(serverUrl) ?? httpOrigin(pageOrigin);
|
|
24411
|
+
}
|
|
24412
|
+
/**
|
|
24413
|
+
* Identity of a decision, for "log once per CHANGE, not per tick".
|
|
24414
|
+
*
|
|
24415
|
+
* `downlinkMbps` is a fresh measurement on every probe and would otherwise
|
|
24416
|
+
* make every offer look like a new decision. What the ladder actually consumes
|
|
24417
|
+
* is the RUNG it maps to (`suggestedAdaptiveTier`), so two measurements that
|
|
24418
|
+
* pick the same rung are the same decision and must collapse to one line.
|
|
24419
|
+
*/
|
|
24420
|
+
function adaptiveHintsKey(report) {
|
|
24421
|
+
const rung = report.downlinkMbps === void 0 ? "none" : suggestedAdaptiveTier(report.downlinkMbps);
|
|
24422
|
+
return [
|
|
24423
|
+
report.phase,
|
|
24424
|
+
report.viewportWidth ?? "-",
|
|
24425
|
+
report.viewportHeight ?? "-",
|
|
24426
|
+
report.devicePixelRatio ?? "-",
|
|
24427
|
+
rung,
|
|
24428
|
+
report.prefersTier ?? "-",
|
|
24429
|
+
report.probeOrigin ?? "unreachable"
|
|
24430
|
+
].join("|");
|
|
24431
|
+
}
|
|
24376
24432
|
async function measureDownlinkMbps(origin, deps) {
|
|
24377
24433
|
const base = origin.replace(/\/+$/, "");
|
|
24378
24434
|
const ctrl = new AbortController();
|
|
@@ -24455,9 +24511,19 @@ function computeClientHints(container) {
|
|
|
24455
24511
|
}
|
|
24456
24512
|
var downlinkCache = null;
|
|
24457
24513
|
var DOWNLINK_TTL_MS = 3e4;
|
|
24458
|
-
|
|
24459
|
-
|
|
24460
|
-
|
|
24514
|
+
/**
|
|
24515
|
+
* Measure the downlink against the HUB, not the page. `serverUrl` is the hub
|
|
24516
|
+
* this player is already signalling to; the page origin is only a fallback,
|
|
24517
|
+
* and inside the native WKWebView it is `file://` — see `resolveProbeOrigin`
|
|
24518
|
+
* for the full account of what that cost.
|
|
24519
|
+
*/
|
|
24520
|
+
/** The origin the probe WILL use, for the client's own decision log. */
|
|
24521
|
+
function probeOriginFor(serverUrl) {
|
|
24522
|
+
return resolveProbeOrigin(serverUrl, typeof window !== "undefined" ? window.location.origin : void 0);
|
|
24523
|
+
}
|
|
24524
|
+
async function measuredDownlinkMbps(serverUrl) {
|
|
24525
|
+
const origin = probeOriginFor(serverUrl);
|
|
24526
|
+
if (origin === null) return void 0;
|
|
24461
24527
|
if (downlinkCache && downlinkCache.origin === origin && Date.now() - downlinkCache.at < DOWNLINK_TTL_MS) return downlinkCache.mbps;
|
|
24462
24528
|
const mbps = await measureDownlinkMbps(origin, {
|
|
24463
24529
|
fetchImpl: (url, init) => fetch(url, init),
|
|
@@ -24480,7 +24546,7 @@ function mergePlayerHints(autoHints, override, downlinkMbps, coldStart) {
|
|
|
24480
24546
|
return coldStart ? stripDownlinkForColdStart(merged) : merged;
|
|
24481
24547
|
}
|
|
24482
24548
|
var FIRST_FRAME_TIMEOUT_MS = 8e3;
|
|
24483
|
-
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, reoffer, posterUrl, hintsOverride, reconnectSignal, onControlChannel, onVideoElement }) {
|
|
24549
|
+
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, reoffer, posterUrl, hintsOverride, onAdaptiveHints, reconnectSignal, onControlChannel, onVideoElement }) {
|
|
24484
24550
|
const videoRef = (0, react$1.useRef)(null);
|
|
24485
24551
|
const containerRef = (0, react$1.useRef)(null);
|
|
24486
24552
|
const pcRef = (0, react$1.useRef)(null);
|
|
@@ -24504,6 +24570,30 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
24504
24570
|
onVideoElementRef.current?.(videoRef.current);
|
|
24505
24571
|
return () => onVideoElementRef.current?.(null);
|
|
24506
24572
|
}, []);
|
|
24573
|
+
/** Same pattern for the adaptive-decision observer — a ref so wiring it does
|
|
24574
|
+
* not re-create the mount-stable connect callbacks. */
|
|
24575
|
+
const onAdaptiveHintsRef = (0, react$1.useRef)(onAdaptiveHints);
|
|
24576
|
+
onAdaptiveHintsRef.current = onAdaptiveHints;
|
|
24577
|
+
/** Last reported decision key — the "once per CHANGE, not per tick" latch. */
|
|
24578
|
+
const lastAdaptiveKeyRef = (0, react$1.useRef)(null);
|
|
24579
|
+
/**
|
|
24580
|
+
* Report the hints THIS offer carried, when they differ from the last
|
|
24581
|
+
* reported decision. Mount-stable (`[]` deps by contract) so the connect
|
|
24582
|
+
* callbacks can call it without gaining a dependency.
|
|
24583
|
+
*/
|
|
24584
|
+
const reportAdaptiveHints = (0, react$1.useCallback)((phase, hints, probeOrigin) => {
|
|
24585
|
+
const cb = onAdaptiveHintsRef.current;
|
|
24586
|
+
if (!cb) return;
|
|
24587
|
+
const report = {
|
|
24588
|
+
phase,
|
|
24589
|
+
probeOrigin,
|
|
24590
|
+
...hints
|
|
24591
|
+
};
|
|
24592
|
+
const key = adaptiveHintsKey(report);
|
|
24593
|
+
if (key === lastAdaptiveKeyRef.current) return;
|
|
24594
|
+
lastAdaptiveKeyRef.current = key;
|
|
24595
|
+
cb(report);
|
|
24596
|
+
}, []);
|
|
24507
24597
|
/** The live session being polled for `pendingRenegotiation` (client-offer). */
|
|
24508
24598
|
const activeSessionIdRef = (0, react$1.useRef)(null);
|
|
24509
24599
|
/** Timer for the session-state (renegotiation) poll loop. */
|
|
@@ -24636,8 +24726,10 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
24636
24726
|
const abort = new AbortController();
|
|
24637
24727
|
connectAbortRef.current = abort;
|
|
24638
24728
|
try {
|
|
24639
|
-
const probePromise = !withDownlink ? measuredDownlinkMbps() : void 0;
|
|
24640
|
-
const
|
|
24729
|
+
const probePromise = !withDownlink ? measuredDownlinkMbps(serverUrl) : void 0;
|
|
24730
|
+
const mergedHints = mergePlayerHints(computeClientHints(containerRef.current), hintsOverride, withDownlink ? await measuredDownlinkMbps(serverUrl) : void 0, !withDownlink);
|
|
24731
|
+
reportAdaptiveHints(withDownlink ? "probe-climb" : "cold-start", mergedHints, probeOriginFor(serverUrl));
|
|
24732
|
+
const { sessionId, sdpOffer, iceServers } = await createSession(mergedHints);
|
|
24641
24733
|
sessionIdsRef.current.add(sessionId);
|
|
24642
24734
|
if (probePromise !== void 0) probePromise.then((mbps) => {
|
|
24643
24735
|
if (mbps === void 0 || !shouldUpgradeAfterProbe(mbps)) return;
|
|
@@ -24752,7 +24844,8 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
24752
24844
|
closeSession,
|
|
24753
24845
|
updateState,
|
|
24754
24846
|
onError,
|
|
24755
|
-
hintsOverride
|
|
24847
|
+
hintsOverride,
|
|
24848
|
+
reportAdaptiveHints
|
|
24756
24849
|
]);
|
|
24757
24850
|
const connectWhep = (0, react$1.useCallback)(async () => {
|
|
24758
24851
|
if (!mountedRef.current) return;
|
|
@@ -24828,7 +24921,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
24828
24921
|
const ms = () => Math.round(performance.now() - t0);
|
|
24829
24922
|
const timing = {};
|
|
24830
24923
|
try {
|
|
24831
|
-
const probePromise = !withDownlink ? measuredDownlinkMbps() : void 0;
|
|
24924
|
+
const probePromise = !withDownlink ? measuredDownlinkMbps(serverUrl) : void 0;
|
|
24832
24925
|
const iceServers = await getIceServers?.();
|
|
24833
24926
|
timing.iceServersMs = ms();
|
|
24834
24927
|
if (!mountedRef.current || abort.signal.aborted) return;
|
|
@@ -24936,8 +25029,9 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
24936
25029
|
await pc.setLocalDescription(offer);
|
|
24937
25030
|
timing.localSetMs = ms();
|
|
24938
25031
|
if (!mountedRef.current || abort.signal.aborted) return;
|
|
24939
|
-
const downlinkMbps = withDownlink ? await measuredDownlinkMbps() : void 0;
|
|
25032
|
+
const downlinkMbps = withDownlink ? await measuredDownlinkMbps(serverUrl) : void 0;
|
|
24940
25033
|
const clientOfferHints = mergePlayerHints(computeClientHints(containerRef.current), hintsOverride, downlinkMbps, !withDownlink);
|
|
25034
|
+
reportAdaptiveHints(withDownlink ? "probe-climb" : "cold-start", clientOfferHints, probeOriginFor(serverUrl));
|
|
24941
25035
|
const signalingStart = ms();
|
|
24942
25036
|
const { sessionId: sid, sdpAnswer } = await handleOffer(pc.localDescription?.sdp ?? offer.sdp ?? "", void 0, clientOfferHints);
|
|
24943
25037
|
timing.signalingRttMs = ms() - signalingStart;
|
|
@@ -24998,6 +25092,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
24998
25092
|
scheduleReconnect();
|
|
24999
25093
|
}
|
|
25000
25094
|
}, [
|
|
25095
|
+
serverUrl,
|
|
25001
25096
|
handleOffer,
|
|
25002
25097
|
getIceServers,
|
|
25003
25098
|
addIceCandidate,
|
|
@@ -25008,7 +25103,8 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
25008
25103
|
updateState,
|
|
25009
25104
|
onError,
|
|
25010
25105
|
startSessionStatePoll,
|
|
25011
|
-
hintsOverride
|
|
25106
|
+
hintsOverride,
|
|
25107
|
+
reportAdaptiveHints
|
|
25012
25108
|
]);
|
|
25013
25109
|
renegotiateRef.current = (0, react$1.useCallback)(async (sessionId, target) => {
|
|
25014
25110
|
const reofferFn = reoffer;
|
|
@@ -25115,12 +25211,13 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
25115
25211
|
pc.close();
|
|
25116
25212
|
return;
|
|
25117
25213
|
}
|
|
25118
|
-
const downlinkMbps = await measuredDownlinkMbps();
|
|
25214
|
+
const downlinkMbps = await measuredDownlinkMbps(serverUrl);
|
|
25119
25215
|
const reofferHints = {
|
|
25120
25216
|
...computeClientHints(containerRef.current),
|
|
25121
25217
|
...downlinkMbps !== void 0 ? { downlinkMbps } : {},
|
|
25122
25218
|
...hintsOverride
|
|
25123
25219
|
};
|
|
25220
|
+
reportAdaptiveHints("renegotiate", reofferHints, probeOriginFor(serverUrl));
|
|
25124
25221
|
const { sessionId: sid, sdpAnswer } = await reofferFn(sessionId, pc.localDescription?.sdp ?? offer.sdp ?? "", target, reofferHints);
|
|
25125
25222
|
if (!mountedRef.current) {
|
|
25126
25223
|
pc.close();
|
|
@@ -25167,13 +25264,15 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
25167
25264
|
renegotiatingRef.current = false;
|
|
25168
25265
|
}
|
|
25169
25266
|
}, [
|
|
25267
|
+
serverUrl,
|
|
25170
25268
|
reoffer,
|
|
25171
25269
|
getIceServers,
|
|
25172
25270
|
addIceCandidate,
|
|
25173
25271
|
getIceCandidates,
|
|
25174
25272
|
updateState,
|
|
25175
25273
|
startSessionStatePoll,
|
|
25176
|
-
hintsOverride
|
|
25274
|
+
hintsOverride,
|
|
25275
|
+
reportAdaptiveHints
|
|
25177
25276
|
]);
|
|
25178
25277
|
const connect = useClientOffer ? connectClientOffer : useServerOffer ? connectServerOffer : connectWhep;
|
|
25179
25278
|
const connectRef = (0, react$1.useRef)(() => {});
|
package/dist/index.js
CHANGED
|
@@ -24349,6 +24349,62 @@ function stripDownlinkForColdStart(hints) {
|
|
|
24349
24349
|
function shouldUpgradeAfterProbe(mbps) {
|
|
24350
24350
|
return suggestedAdaptiveTier(mbps) !== "low";
|
|
24351
24351
|
}
|
|
24352
|
+
/** An http(s) origin, or null for anything that cannot serve the probe. */
|
|
24353
|
+
function httpOrigin(value) {
|
|
24354
|
+
if (value === void 0 || value === "") return null;
|
|
24355
|
+
let parsed;
|
|
24356
|
+
try {
|
|
24357
|
+
parsed = new URL(value);
|
|
24358
|
+
} catch {
|
|
24359
|
+
return null;
|
|
24360
|
+
}
|
|
24361
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
24362
|
+
return parsed.origin;
|
|
24363
|
+
}
|
|
24364
|
+
/**
|
|
24365
|
+
* The origin `/adaptive-probe` must be fetched from — the HUB, not the page.
|
|
24366
|
+
*
|
|
24367
|
+
* The page origin was the original assumption ("same-origin as the player",
|
|
24368
|
+
* `adaptive-probe.routes.ts`) and it is only true for the web viewer and the
|
|
24369
|
+
* iframe embed, both served by the hub. The NATIVE viewer loads this same
|
|
24370
|
+
* player from an asset bundled with the app (`useNativeEmbedSource` resolves
|
|
24371
|
+
* `assets/embed/camstack-embed.html` through `Image.resolveAssetSource`), so
|
|
24372
|
+
* inside the WKWebView `window.location.origin` is `file://` — and
|
|
24373
|
+
* `file:///adaptive-probe` is not a request any hub can answer. The probe
|
|
24374
|
+
* therefore returned null on EVERY native connect, `downlinkMbps` was never
|
|
24375
|
+
* sent, `pickAdaptiveTier` scored every session `default-low`, and
|
|
24376
|
+
* `shouldUpgradeAfterProbe` was never even reached — so the make-before-break
|
|
24377
|
+
* climb that the cold-start default depends on could not run. That is the
|
|
24378
|
+
* whole of "the automatic quality always drops to low" on the app.
|
|
24379
|
+
*
|
|
24380
|
+
* The hub the player is already talking to is the `serverUrl` prop, so it
|
|
24381
|
+
* wins; the page origin remains as the fallback for a host that passes a
|
|
24382
|
+
* relative/empty `serverUrl`. Null means no probe is possible and the caller
|
|
24383
|
+
* must skip it rather than fetch a URL that cannot exist.
|
|
24384
|
+
*/
|
|
24385
|
+
function resolveProbeOrigin(serverUrl, pageOrigin) {
|
|
24386
|
+
return httpOrigin(serverUrl) ?? httpOrigin(pageOrigin);
|
|
24387
|
+
}
|
|
24388
|
+
/**
|
|
24389
|
+
* Identity of a decision, for "log once per CHANGE, not per tick".
|
|
24390
|
+
*
|
|
24391
|
+
* `downlinkMbps` is a fresh measurement on every probe and would otherwise
|
|
24392
|
+
* make every offer look like a new decision. What the ladder actually consumes
|
|
24393
|
+
* is the RUNG it maps to (`suggestedAdaptiveTier`), so two measurements that
|
|
24394
|
+
* pick the same rung are the same decision and must collapse to one line.
|
|
24395
|
+
*/
|
|
24396
|
+
function adaptiveHintsKey(report) {
|
|
24397
|
+
const rung = report.downlinkMbps === void 0 ? "none" : suggestedAdaptiveTier(report.downlinkMbps);
|
|
24398
|
+
return [
|
|
24399
|
+
report.phase,
|
|
24400
|
+
report.viewportWidth ?? "-",
|
|
24401
|
+
report.viewportHeight ?? "-",
|
|
24402
|
+
report.devicePixelRatio ?? "-",
|
|
24403
|
+
rung,
|
|
24404
|
+
report.prefersTier ?? "-",
|
|
24405
|
+
report.probeOrigin ?? "unreachable"
|
|
24406
|
+
].join("|");
|
|
24407
|
+
}
|
|
24352
24408
|
async function measureDownlinkMbps(origin, deps) {
|
|
24353
24409
|
const base = origin.replace(/\/+$/, "");
|
|
24354
24410
|
const ctrl = new AbortController();
|
|
@@ -24431,9 +24487,19 @@ function computeClientHints(container) {
|
|
|
24431
24487
|
}
|
|
24432
24488
|
var downlinkCache = null;
|
|
24433
24489
|
var DOWNLINK_TTL_MS = 3e4;
|
|
24434
|
-
|
|
24435
|
-
|
|
24436
|
-
|
|
24490
|
+
/**
|
|
24491
|
+
* Measure the downlink against the HUB, not the page. `serverUrl` is the hub
|
|
24492
|
+
* this player is already signalling to; the page origin is only a fallback,
|
|
24493
|
+
* and inside the native WKWebView it is `file://` — see `resolveProbeOrigin`
|
|
24494
|
+
* for the full account of what that cost.
|
|
24495
|
+
*/
|
|
24496
|
+
/** The origin the probe WILL use, for the client's own decision log. */
|
|
24497
|
+
function probeOriginFor(serverUrl) {
|
|
24498
|
+
return resolveProbeOrigin(serverUrl, typeof window !== "undefined" ? window.location.origin : void 0);
|
|
24499
|
+
}
|
|
24500
|
+
async function measuredDownlinkMbps(serverUrl) {
|
|
24501
|
+
const origin = probeOriginFor(serverUrl);
|
|
24502
|
+
if (origin === null) return void 0;
|
|
24437
24503
|
if (downlinkCache && downlinkCache.origin === origin && Date.now() - downlinkCache.at < DOWNLINK_TTL_MS) return downlinkCache.mbps;
|
|
24438
24504
|
const mbps = await measureDownlinkMbps(origin, {
|
|
24439
24505
|
fetchImpl: (url, init) => fetch(url, init),
|
|
@@ -24456,7 +24522,7 @@ function mergePlayerHints(autoHints, override, downlinkMbps, coldStart) {
|
|
|
24456
24522
|
return coldStart ? stripDownlinkForColdStart(merged) : merged;
|
|
24457
24523
|
}
|
|
24458
24524
|
var FIRST_FRAME_TIMEOUT_MS = 8e3;
|
|
24459
|
-
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, reoffer, posterUrl, hintsOverride, reconnectSignal, onControlChannel, onVideoElement }) {
|
|
24525
|
+
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, reoffer, posterUrl, hintsOverride, onAdaptiveHints, reconnectSignal, onControlChannel, onVideoElement }) {
|
|
24460
24526
|
const videoRef = useRef(null);
|
|
24461
24527
|
const containerRef = useRef(null);
|
|
24462
24528
|
const pcRef = useRef(null);
|
|
@@ -24480,6 +24546,30 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
24480
24546
|
onVideoElementRef.current?.(videoRef.current);
|
|
24481
24547
|
return () => onVideoElementRef.current?.(null);
|
|
24482
24548
|
}, []);
|
|
24549
|
+
/** Same pattern for the adaptive-decision observer — a ref so wiring it does
|
|
24550
|
+
* not re-create the mount-stable connect callbacks. */
|
|
24551
|
+
const onAdaptiveHintsRef = useRef(onAdaptiveHints);
|
|
24552
|
+
onAdaptiveHintsRef.current = onAdaptiveHints;
|
|
24553
|
+
/** Last reported decision key — the "once per CHANGE, not per tick" latch. */
|
|
24554
|
+
const lastAdaptiveKeyRef = useRef(null);
|
|
24555
|
+
/**
|
|
24556
|
+
* Report the hints THIS offer carried, when they differ from the last
|
|
24557
|
+
* reported decision. Mount-stable (`[]` deps by contract) so the connect
|
|
24558
|
+
* callbacks can call it without gaining a dependency.
|
|
24559
|
+
*/
|
|
24560
|
+
const reportAdaptiveHints = useCallback((phase, hints, probeOrigin) => {
|
|
24561
|
+
const cb = onAdaptiveHintsRef.current;
|
|
24562
|
+
if (!cb) return;
|
|
24563
|
+
const report = {
|
|
24564
|
+
phase,
|
|
24565
|
+
probeOrigin,
|
|
24566
|
+
...hints
|
|
24567
|
+
};
|
|
24568
|
+
const key = adaptiveHintsKey(report);
|
|
24569
|
+
if (key === lastAdaptiveKeyRef.current) return;
|
|
24570
|
+
lastAdaptiveKeyRef.current = key;
|
|
24571
|
+
cb(report);
|
|
24572
|
+
}, []);
|
|
24483
24573
|
/** The live session being polled for `pendingRenegotiation` (client-offer). */
|
|
24484
24574
|
const activeSessionIdRef = useRef(null);
|
|
24485
24575
|
/** Timer for the session-state (renegotiation) poll loop. */
|
|
@@ -24612,8 +24702,10 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
24612
24702
|
const abort = new AbortController();
|
|
24613
24703
|
connectAbortRef.current = abort;
|
|
24614
24704
|
try {
|
|
24615
|
-
const probePromise = !withDownlink ? measuredDownlinkMbps() : void 0;
|
|
24616
|
-
const
|
|
24705
|
+
const probePromise = !withDownlink ? measuredDownlinkMbps(serverUrl) : void 0;
|
|
24706
|
+
const mergedHints = mergePlayerHints(computeClientHints(containerRef.current), hintsOverride, withDownlink ? await measuredDownlinkMbps(serverUrl) : void 0, !withDownlink);
|
|
24707
|
+
reportAdaptiveHints(withDownlink ? "probe-climb" : "cold-start", mergedHints, probeOriginFor(serverUrl));
|
|
24708
|
+
const { sessionId, sdpOffer, iceServers } = await createSession(mergedHints);
|
|
24617
24709
|
sessionIdsRef.current.add(sessionId);
|
|
24618
24710
|
if (probePromise !== void 0) probePromise.then((mbps) => {
|
|
24619
24711
|
if (mbps === void 0 || !shouldUpgradeAfterProbe(mbps)) return;
|
|
@@ -24728,7 +24820,8 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
24728
24820
|
closeSession,
|
|
24729
24821
|
updateState,
|
|
24730
24822
|
onError,
|
|
24731
|
-
hintsOverride
|
|
24823
|
+
hintsOverride,
|
|
24824
|
+
reportAdaptiveHints
|
|
24732
24825
|
]);
|
|
24733
24826
|
const connectWhep = useCallback(async () => {
|
|
24734
24827
|
if (!mountedRef.current) return;
|
|
@@ -24804,7 +24897,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
24804
24897
|
const ms = () => Math.round(performance.now() - t0);
|
|
24805
24898
|
const timing = {};
|
|
24806
24899
|
try {
|
|
24807
|
-
const probePromise = !withDownlink ? measuredDownlinkMbps() : void 0;
|
|
24900
|
+
const probePromise = !withDownlink ? measuredDownlinkMbps(serverUrl) : void 0;
|
|
24808
24901
|
const iceServers = await getIceServers?.();
|
|
24809
24902
|
timing.iceServersMs = ms();
|
|
24810
24903
|
if (!mountedRef.current || abort.signal.aborted) return;
|
|
@@ -24912,8 +25005,9 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
24912
25005
|
await pc.setLocalDescription(offer);
|
|
24913
25006
|
timing.localSetMs = ms();
|
|
24914
25007
|
if (!mountedRef.current || abort.signal.aborted) return;
|
|
24915
|
-
const downlinkMbps = withDownlink ? await measuredDownlinkMbps() : void 0;
|
|
25008
|
+
const downlinkMbps = withDownlink ? await measuredDownlinkMbps(serverUrl) : void 0;
|
|
24916
25009
|
const clientOfferHints = mergePlayerHints(computeClientHints(containerRef.current), hintsOverride, downlinkMbps, !withDownlink);
|
|
25010
|
+
reportAdaptiveHints(withDownlink ? "probe-climb" : "cold-start", clientOfferHints, probeOriginFor(serverUrl));
|
|
24917
25011
|
const signalingStart = ms();
|
|
24918
25012
|
const { sessionId: sid, sdpAnswer } = await handleOffer(pc.localDescription?.sdp ?? offer.sdp ?? "", void 0, clientOfferHints);
|
|
24919
25013
|
timing.signalingRttMs = ms() - signalingStart;
|
|
@@ -24974,6 +25068,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
24974
25068
|
scheduleReconnect();
|
|
24975
25069
|
}
|
|
24976
25070
|
}, [
|
|
25071
|
+
serverUrl,
|
|
24977
25072
|
handleOffer,
|
|
24978
25073
|
getIceServers,
|
|
24979
25074
|
addIceCandidate,
|
|
@@ -24984,7 +25079,8 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
24984
25079
|
updateState,
|
|
24985
25080
|
onError,
|
|
24986
25081
|
startSessionStatePoll,
|
|
24987
|
-
hintsOverride
|
|
25082
|
+
hintsOverride,
|
|
25083
|
+
reportAdaptiveHints
|
|
24988
25084
|
]);
|
|
24989
25085
|
renegotiateRef.current = useCallback(async (sessionId, target) => {
|
|
24990
25086
|
const reofferFn = reoffer;
|
|
@@ -25091,12 +25187,13 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
25091
25187
|
pc.close();
|
|
25092
25188
|
return;
|
|
25093
25189
|
}
|
|
25094
|
-
const downlinkMbps = await measuredDownlinkMbps();
|
|
25190
|
+
const downlinkMbps = await measuredDownlinkMbps(serverUrl);
|
|
25095
25191
|
const reofferHints = {
|
|
25096
25192
|
...computeClientHints(containerRef.current),
|
|
25097
25193
|
...downlinkMbps !== void 0 ? { downlinkMbps } : {},
|
|
25098
25194
|
...hintsOverride
|
|
25099
25195
|
};
|
|
25196
|
+
reportAdaptiveHints("renegotiate", reofferHints, probeOriginFor(serverUrl));
|
|
25100
25197
|
const { sessionId: sid, sdpAnswer } = await reofferFn(sessionId, pc.localDescription?.sdp ?? offer.sdp ?? "", target, reofferHints);
|
|
25101
25198
|
if (!mountedRef.current) {
|
|
25102
25199
|
pc.close();
|
|
@@ -25143,13 +25240,15 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
25143
25240
|
renegotiatingRef.current = false;
|
|
25144
25241
|
}
|
|
25145
25242
|
}, [
|
|
25243
|
+
serverUrl,
|
|
25146
25244
|
reoffer,
|
|
25147
25245
|
getIceServers,
|
|
25148
25246
|
addIceCandidate,
|
|
25149
25247
|
getIceCandidates,
|
|
25150
25248
|
updateState,
|
|
25151
25249
|
startSessionStatePoll,
|
|
25152
|
-
hintsOverride
|
|
25250
|
+
hintsOverride,
|
|
25251
|
+
reportAdaptiveHints
|
|
25153
25252
|
]);
|
|
25154
25253
|
const connect = useClientOffer ? connectClientOffer : useServerOffer ? connectServerOffer : connectWhep;
|
|
25155
25254
|
const connectRef = useRef(() => {});
|