@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/index.cjs CHANGED
@@ -7046,6 +7046,8 @@ var useDeviceManagerGetBindings = trpc.deviceManager.getBindings.useQuery;
7046
7046
  var useDeviceManagerGetBindingsBatch = trpc.deviceManager.getBindingsBatch.useQuery;
7047
7047
  /** Generated alias around `trpc.deviceManager.getAllBindings.useQuery`. */
7048
7048
  var useDeviceManagerGetAllBindings = trpc.deviceManager.getAllBindings.useQuery;
7049
+ /** Generated alias around `trpc.deviceManager.listDeviceIdsByCaps.useQuery`. */
7050
+ var useDeviceManagerListDeviceIdsByCaps = trpc.deviceManager.listDeviceIdsByCaps.useQuery;
7049
7051
  /** Generated alias around `trpc.deviceManager.setWrapperActive.useMutation`. */
7050
7052
  var useDeviceManagerSetWrapperActive = trpc.deviceManager.setWrapperActive.useMutation;
7051
7053
  /** Generated alias around `trpc.deviceManager.listWrappersForCap.useQuery`. */
@@ -29544,9 +29546,26 @@ function nextReconnectAction(attempt, random = Math.random) {
29544
29546
  * Used as `hints.downlinkMbps` so adaptive selection is based on a measured
29545
29547
  * path, not viewport size or IP class. Failures return null (caller keeps
29546
29548
  * the default-low / ceiling path and may stay on the lowest rung).
29549
+ *
29550
+ * D635 (2026-09-25): the probe used to time a 128 KiB blob from the REQUEST,
29551
+ * fired in parallel with `createSession` on a hub at ~80 % CPU. It read the
29552
+ * LAN as 1.2–1.46 Mbps — the elapsed time was mostly waiting for the first
29553
+ * byte, i.e. latency. It now (1) times the body from its FIRST byte, (2) reads
29554
+ * a 1 MiB blob so the transfer dominates timer jitter, and (3) runs after the
29555
+ * session's first decoded frame, never during session setup.
29547
29556
  */
29548
29557
  var ADAPTIVE_PROBE_PATH = "/adaptive-probe";
29558
+ /**
29559
+ * Whole-probe deadline, from the request. A probe cut here is still a
29560
+ * measurement of whatever body arrived (see {@link measureDownlinkMbps}).
29561
+ */
29549
29562
  var ADAPTIVE_PROBE_TIMEOUT_MS = 2500;
29563
+ /** How long the probe waits for the session's first decoded frame. */
29564
+ var FIRST_FRAME_PROBE_TIMEOUT_MS = 15e3;
29565
+ /** How often the first-frame wait looks at the video element. */
29566
+ var FIRST_FRAME_POLL_MS = 250;
29567
+ /** `HTMLMediaElement.HAVE_CURRENT_DATA`: a frame is decoded and presentable. */
29568
+ var HAVE_CURRENT_DATA = 2;
29550
29569
  /** Map a measured downlink onto the adaptive ladder. Mid is a real rung. */
29551
29570
  function suggestedAdaptiveTier(mbps) {
29552
29571
  if (mbps >= 8) return "high";
@@ -29620,27 +29639,153 @@ function adaptiveHintsKey(report) {
29620
29639
  report.devicePixelRatio ?? "-",
29621
29640
  rung,
29622
29641
  report.prefersTier ?? "-",
29623
- report.probeOrigin ?? "unreachable"
29642
+ report.probeOrigin ?? "unreachable",
29643
+ report.probeStatus ?? "-"
29624
29644
  ].join("|");
29625
29645
  }
29646
+ function mbpsOf(bytes, elapsedMs) {
29647
+ if (bytes < 65536 || !(elapsedMs > 0)) return null;
29648
+ return bytes * 8 / elapsedMs / 1e3;
29649
+ }
29650
+ /**
29651
+ * Stream the body and time it from the FIRST chunk to the last one. The first
29652
+ * chunk's own bytes are excluded — they arrived at the instant the clock
29653
+ * started. A read that fails (the deadline abort) ends the measurement with
29654
+ * what DID arrive: a slow link cut at 2.5 s is still a measured slow link.
29655
+ */
29656
+ async function timeStreamedBody(reader, now) {
29657
+ let firstAt = null;
29658
+ let lastAt = 0;
29659
+ let bytes = 0;
29660
+ try {
29661
+ for (;;) {
29662
+ const chunk = await reader.read();
29663
+ if (chunk.done) break;
29664
+ const size = chunk.value?.byteLength ?? 0;
29665
+ const at = now();
29666
+ if (firstAt === null) firstAt = at;
29667
+ else bytes += size;
29668
+ lastAt = at;
29669
+ }
29670
+ } catch {
29671
+ reader.cancel().catch(() => void 0);
29672
+ }
29673
+ if (firstAt === null) return null;
29674
+ return mbpsOf(bytes, lastAt - firstAt);
29675
+ }
29626
29676
  async function measureDownlinkMbps(origin, deps) {
29627
29677
  const base = origin.replace(/\/+$/, "");
29628
29678
  const ctrl = new AbortController();
29629
29679
  const timer = setTimeout(() => ctrl.abort(), ADAPTIVE_PROBE_TIMEOUT_MS);
29630
- const started = deps.now();
29631
29680
  try {
29632
29681
  const res = await deps.fetchImpl(`${base}${ADAPTIVE_PROBE_PATH}`, { signal: ctrl.signal });
29633
- if (!res.ok) return null;
29634
- const buf = await res.arrayBuffer();
29635
- const elapsedMs = Math.max(1, deps.now() - started);
29636
- if (buf.byteLength < 1024) return null;
29637
- return buf.byteLength * 8 / elapsedMs / 1e3;
29682
+ if (!res.ok) {
29683
+ if (res.status !== void 0) deps.onRefused?.(res.status);
29684
+ return null;
29685
+ }
29686
+ const reader = res.body?.getReader() ?? null;
29687
+ if (reader !== null) return await timeStreamedBody(reader, deps.now);
29688
+ const headersAt = deps.now();
29689
+ return mbpsOf((await res.arrayBuffer()).byteLength, deps.now() - headersAt);
29638
29690
  } catch {
29639
29691
  return null;
29640
29692
  } finally {
29641
29693
  clearTimeout(timer);
29642
29694
  }
29643
29695
  }
29696
+ /**
29697
+ * Resolve once the session presents its first decoded frame — the moment
29698
+ * session setup (createSession, ICE, DTLS, the first IDR) is over and a probe
29699
+ * no longer competes with it for the hub or the link.
29700
+ */
29701
+ async function waitForFirstFrame(deps, timeoutMs = FIRST_FRAME_PROBE_TIMEOUT_MS) {
29702
+ const deadline = deps.now() + timeoutMs;
29703
+ for (;;) {
29704
+ if (deps.signal.aborted) return "aborted";
29705
+ if ((deps.readyState() ?? 0) >= HAVE_CURRENT_DATA) return "frame";
29706
+ if (deps.now() >= deadline) return "timeout";
29707
+ await deps.sleep(FIRST_FRAME_POLL_MS);
29708
+ }
29709
+ }
29710
+ async function probeAfterFirstFrame(deps) {
29711
+ if (await deps.waitFrame() !== "frame") return void 0;
29712
+ return deps.measure();
29713
+ }
29714
+ /** App-wide probe cache + single-flight, one per origin. */
29715
+ function createDownlinkProbe(deps) {
29716
+ const readings = /* @__PURE__ */ new Map();
29717
+ const inFlight = /* @__PURE__ */ new Map();
29718
+ const refusals = /* @__PURE__ */ new Set();
29719
+ const cached = (origin) => {
29720
+ const r = readings.get(origin);
29721
+ return r !== void 0 && deps.now() - r.at <= deps.ttlMs ? r.mbps : void 0;
29722
+ };
29723
+ return {
29724
+ cached,
29725
+ firstRefusal: (origin, status) => {
29726
+ const key = `${origin}|${String(status)}`;
29727
+ if (refusals.has(key)) return false;
29728
+ refusals.add(key);
29729
+ return true;
29730
+ },
29731
+ latest: (origin) => {
29732
+ const r = readings.get(origin);
29733
+ return r === void 0 ? null : {
29734
+ mbps: r.mbps,
29735
+ ageMs: Math.max(0, deps.now() - r.at)
29736
+ };
29737
+ },
29738
+ measured: (origin, measure) => {
29739
+ const hit = cached(origin);
29740
+ if (hit !== void 0) return Promise.resolve(hit);
29741
+ const running = inFlight.get(origin);
29742
+ if (running !== void 0) return running;
29743
+ const probe = measure().then((mbps) => {
29744
+ if (mbps === null || !Number.isFinite(mbps)) return void 0;
29745
+ readings.set(origin, {
29746
+ mbps,
29747
+ at: deps.now()
29748
+ });
29749
+ return mbps;
29750
+ }).catch(() => void 0).finally(() => {
29751
+ inFlight.delete(origin);
29752
+ });
29753
+ inFlight.set(origin, probe);
29754
+ return probe;
29755
+ }
29756
+ };
29757
+ }
29758
+ //#endregion
29759
+ //#region src/composites/client-network-sample.ts
29760
+ /** Schema bound of `estimatedBandwidthKbps` / `packetLossPercent`. */
29761
+ var MAX_LOSS_PERCENT = 100;
29762
+ function nextClientSample(prev, cur, ctx) {
29763
+ if (ctx.renegotiating) return {
29764
+ baseline: null,
29765
+ sample: null
29766
+ };
29767
+ if (prev === null || prev.pc !== cur.pc || cur.ts <= prev.ts) return {
29768
+ baseline: cur,
29769
+ sample: null
29770
+ };
29771
+ const dLost = Math.max(0, cur.lost - prev.lost);
29772
+ const denom = dLost + Math.max(0, cur.recv - prev.recv);
29773
+ const packetLossPercent = denom > 0 ? Math.min(MAX_LOSS_PERCENT, dLost / denom * 100) : 0;
29774
+ const estimatedBandwidthKbps = Math.max(0, Math.round((cur.bytes - prev.bytes) * 8 / (cur.ts - prev.ts)));
29775
+ return {
29776
+ baseline: cur,
29777
+ sample: {
29778
+ rttMs: Math.round(cur.rttS * 1e3),
29779
+ jitterMs: Math.round(cur.jitterS * 1e3),
29780
+ estimatedBandwidthKbps,
29781
+ packetLossPercent,
29782
+ ...ctx.probe !== null ? {
29783
+ downlinkMbps: ctx.probe.mbps,
29784
+ downlinkAgeMs: ctx.probe.ageMs
29785
+ } : {}
29786
+ }
29787
+ };
29788
+ }
29644
29789
  //#endregion
29645
29790
  //#region src/composites/stream-debug/webrtc-debug-stats.ts
29646
29791
  /** Beyond this the log is a scroll, not a signal. */
@@ -30184,11 +30329,11 @@ function StreamDebugOverlay(props) {
30184
30329
  }),
30185
30330
  server.lastDecision === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
30186
30331
  label: "ladder",
30187
- value: "never moved"
30332
+ value: server.climbHold ? "held" : "never moved"
30188
30333
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
30189
30334
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
30190
30335
  label: "ladder move",
30191
- value: `${server.lastDecision.cause}${server.lastDecision.tier !== null ? ` → ${server.lastDecision.tier}` : ""}`,
30336
+ value: `${server.lastDecision.causeDetail ?? server.lastDecision.cause}${server.lastDecision.tier !== null ? ` → ${server.lastDecision.tier}` : ""}`,
30192
30337
  strong: true
30193
30338
  }),
30194
30339
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
@@ -30216,6 +30361,10 @@ function StreamDebugOverlay(props) {
30216
30361
  value: b(server.lastDecision.forcedTranscodeDown)
30217
30362
  })
30218
30363
  ] }),
30364
+ server.climbHold ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
30365
+ label: "climb",
30366
+ value: server.climbHold.summary
30367
+ }) : null,
30219
30368
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
30220
30369
  label: "promotions",
30221
30370
  value: n(server.upgradeCount)
@@ -30294,8 +30443,15 @@ function computeClientHints(container) {
30294
30443
  }
30295
30444
  return hints;
30296
30445
  }
30297
- var downlinkCache = null;
30298
- var DOWNLINK_TTL_MS = 3e4;
30446
+ /**
30447
+ * ONE probe store for the whole app: every player of every camera shares the
30448
+ * cache and the probe in flight (review I6 — a grid decoding together fired
30449
+ * N concurrent 1 MiB probes, each reading the link ÷ N).
30450
+ */
30451
+ var downlinkProbe = createDownlinkProbe({
30452
+ now: () => Date.now(),
30453
+ ttlMs: 3e4
30454
+ });
30299
30455
  /**
30300
30456
  * Measure the downlink against the HUB, not the page. `serverUrl` is the hub
30301
30457
  * this player is already signalling to; the page origin is only a fallback,
@@ -30306,21 +30462,44 @@ var DOWNLINK_TTL_MS = 3e4;
30306
30462
  function probeOriginFor(serverUrl) {
30307
30463
  return resolveProbeOrigin(serverUrl, typeof window !== "undefined" ? hubOrigin() : void 0);
30308
30464
  }
30309
- async function measuredDownlinkMbps(serverUrl) {
30465
+ function measuredDownlinkMbps(serverUrl, authToken, onRefused) {
30310
30466
  const origin = probeOriginFor(serverUrl);
30311
- if (origin === null) return void 0;
30312
- if (downlinkCache && downlinkCache.origin === origin && Date.now() - downlinkCache.at < DOWNLINK_TTL_MS) return downlinkCache.mbps;
30313
- const mbps = await measureDownlinkMbps(origin, {
30314
- fetchImpl: (url, init) => fetch(url, init),
30467
+ if (origin === null) return Promise.resolve(void 0);
30468
+ return downlinkProbe.measured(origin, () => measureDownlinkMbps(origin, {
30469
+ onRefused: (status) => {
30470
+ if (downlinkProbe.firstRefusal(origin, status)) onRefused(origin, status);
30471
+ },
30472
+ fetchImpl: (url, init) => fetch(url, {
30473
+ ...init,
30474
+ ...authToken !== null ? { headers: { authorization: `Bearer ${authToken}` } } : {}
30475
+ }),
30315
30476
  now: () => Date.now()
30477
+ }));
30478
+ }
30479
+ /** The cached probe only — never measures (review I5: a renegotiation must not
30480
+ * push 1 MiB into the link that just stalled, nor wait 2.5 s for it). */
30481
+ function cachedDownlinkMbps(serverUrl) {
30482
+ const origin = probeOriginFor(serverUrl);
30483
+ return origin === null ? void 0 : downlinkProbe.cached(origin);
30484
+ }
30485
+ /** The newest probe whatever it read — carried in the telemetry (review I4). */
30486
+ function latestProbeReading(serverUrl) {
30487
+ const origin = probeOriginFor(serverUrl);
30488
+ return origin === null ? null : downlinkProbe.latest(origin);
30489
+ }
30490
+ function probeAfterThisStreamsFirstFrame(args) {
30491
+ return probeAfterFirstFrame({
30492
+ waitFrame: () => waitForFirstFrame({
30493
+ readyState: () => {
30494
+ const v = args.video();
30495
+ return v !== null && v.srcObject === args.stream ? v.readyState : null;
30496
+ },
30497
+ signal: args.signal,
30498
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
30499
+ now: () => Date.now()
30500
+ }),
30501
+ measure: () => measuredDownlinkMbps(args.serverUrl, args.authToken, args.onRefused)
30316
30502
  });
30317
- if (mbps === null || !Number.isFinite(mbps)) return void 0;
30318
- downlinkCache = {
30319
- origin,
30320
- mbps,
30321
- at: Date.now()
30322
- };
30323
- return mbps;
30324
30503
  }
30325
30504
  function mergePlayerHints(autoHints, override, downlinkMbps, coldStart) {
30326
30505
  const merged = {
@@ -30331,7 +30510,7 @@ function mergePlayerHints(autoHints, override, downlinkMbps, coldStart) {
30331
30510
  return coldStart ? stripDownlinkForColdStart(merged) : merged;
30332
30511
  }
30333
30512
  var FIRST_FRAME_TIMEOUT_MS = 8e3;
30334
- 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 }) {
30513
+ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, muted: initialMuted = true, showControls = true, showStats = false, 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 }) {
30335
30514
  const videoRef = (0, react$1.useRef)(null);
30336
30515
  const containerRef = (0, react$1.useRef)(null);
30337
30516
  const pcRef = (0, react$1.useRef)(null);
@@ -30379,6 +30558,19 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
30379
30558
  lastAdaptiveKeyRef.current = key;
30380
30559
  cb(report);
30381
30560
  }, []);
30561
+ /**
30562
+ * A refused probe (401 without a session, 429) goes out on the SAME channel
30563
+ * as the adaptive decisions — hosts already forward it over their log bridge
30564
+ * — once per origin app-wide (`firstRefusal`), so a missing `probeAuthToken`
30565
+ * is visible instead of reading like a failed probe (review N3).
30566
+ */
30567
+ const reportProbeRefused = (0, react$1.useCallback)((origin, status) => {
30568
+ onAdaptiveHintsRef.current?.({
30569
+ phase: "probe-refused",
30570
+ probeOrigin: origin,
30571
+ probeStatus: status
30572
+ });
30573
+ }, []);
30382
30574
  /** The live session being polled for `pendingRenegotiation` (client-offer). */
30383
30575
  const activeSessionIdRef = (0, react$1.useRef)(null);
30384
30576
  /** Timer for the session-state (renegotiation) poll loop. */
@@ -30501,7 +30693,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
30501
30693
  tick();
30502
30694
  }, SESSION_STATE_POLL_MS);
30503
30695
  }, [getSessionState, reoffer]);
30504
- const connectServerOffer = (0, react$1.useCallback)(async (seamless = false, withDownlink = seamless) => {
30696
+ const connectServerOffer = (0, react$1.useCallback)(async (seamless = false, withDownlink = seamless, knownDownlinkMbps) => {
30505
30697
  if (!mountedRef.current || !createSession || !sendAnswer) return;
30506
30698
  const oldPc = seamless ? pcRef.current : null;
30507
30699
  const oldSessions = seamless ? new Set(sessionIdsRef.current) : null;
@@ -30511,18 +30703,10 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
30511
30703
  const abort = new AbortController();
30512
30704
  connectAbortRef.current = abort;
30513
30705
  try {
30514
- const probePromise = !withDownlink ? measuredDownlinkMbps(serverUrl) : void 0;
30515
- const mergedHints = mergePlayerHints(computeClientHints(containerRef.current), hintsOverride, withDownlink ? await measuredDownlinkMbps(serverUrl) : void 0, !withDownlink);
30706
+ const mergedHints = mergePlayerHints(computeClientHints(containerRef.current), hintsOverride, withDownlink ? knownDownlinkMbps ?? await measuredDownlinkMbps(serverUrl, probeAuthToken ?? null, reportProbeRefused) : void 0, !withDownlink);
30516
30707
  reportAdaptiveHints(withDownlink ? "probe-climb" : "cold-start", mergedHints, probeOriginFor(serverUrl));
30517
30708
  const { sessionId, sdpOffer, iceServers } = await createSession(mergedHints);
30518
30709
  sessionIdsRef.current.add(sessionId);
30519
- if (probePromise !== void 0) probePromise.then((mbps) => {
30520
- if (mbps === void 0 || !shouldUpgradeAfterProbe(mbps)) return;
30521
- if (!mountedRef.current || abort.signal.aborted) return;
30522
- if (probeUpgradeRef.current) return;
30523
- probeUpgradeRef.current = true;
30524
- connectServerOffer(true, true);
30525
- });
30526
30710
  if (!mountedRef.current || abort.signal.aborted) {
30527
30711
  closeSession?.(sessionId).catch(() => {});
30528
30712
  sessionIdsRef.current.delete(sessionId);
@@ -30531,6 +30715,20 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
30531
30715
  const pc = new RTCPeerConnection({ iceServers: [...iceServers ?? [], { urls: "stun:stun.l.google.com:19302" }] });
30532
30716
  if (!seamless) pcRef.current = pc;
30533
30717
  const combinedStream = new MediaStream();
30718
+ if (!withDownlink) probeAfterThisStreamsFirstFrame({
30719
+ video: () => videoRef.current,
30720
+ stream: combinedStream,
30721
+ signal: abort.signal,
30722
+ serverUrl,
30723
+ authToken: probeAuthToken ?? null,
30724
+ onRefused: reportProbeRefused
30725
+ }).then((mbps) => {
30726
+ if (mbps === void 0 || !shouldUpgradeAfterProbe(mbps)) return;
30727
+ if (!mountedRef.current || abort.signal.aborted) return;
30728
+ if (probeUpgradeRef.current) return;
30729
+ probeUpgradeRef.current = true;
30730
+ connectServerOffer(true, true, mbps);
30731
+ });
30534
30732
  let swappedIn = false;
30535
30733
  const swapInOnce = () => {
30536
30734
  if (swappedIn || !mountedRef.current) return;
@@ -30630,7 +30828,9 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
30630
30828
  updateState,
30631
30829
  onError,
30632
30830
  hintsOverride,
30633
- reportAdaptiveHints
30831
+ reportAdaptiveHints,
30832
+ probeAuthToken,
30833
+ reportProbeRefused
30634
30834
  ]);
30635
30835
  const connectWhep = (0, react$1.useCallback)(async () => {
30636
30836
  if (!mountedRef.current) return;
@@ -30693,7 +30893,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
30693
30893
  updateState,
30694
30894
  onError
30695
30895
  ]);
30696
- const connectClientOffer = (0, react$1.useCallback)(async (seamless = false, withDownlink = seamless) => {
30896
+ const connectClientOffer = (0, react$1.useCallback)(async (seamless = false, withDownlink = seamless, knownDownlinkMbps) => {
30697
30897
  if (!mountedRef.current || !handleOffer) return;
30698
30898
  const oldPc = seamless ? pcRef.current : null;
30699
30899
  const oldSessions = seamless ? new Set(sessionIdsRef.current) : null;
@@ -30706,13 +30906,26 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
30706
30906
  const ms = () => Math.round(performance.now() - t0);
30707
30907
  const timing = {};
30708
30908
  try {
30709
- const probePromise = !withDownlink ? measuredDownlinkMbps(serverUrl) : void 0;
30710
30909
  const iceServers = await getIceServers?.();
30711
30910
  timing.iceServersMs = ms();
30712
30911
  if (!mountedRef.current || abort.signal.aborted) return;
30713
30912
  const pc = new RTCPeerConnection({ iceServers: [...iceServers ?? [], { urls: "stun:stun.l.google.com:19302" }] });
30714
30913
  if (!seamless) pcRef.current = pc;
30715
30914
  const combinedStream = new MediaStream();
30915
+ if (!withDownlink) probeAfterThisStreamsFirstFrame({
30916
+ video: () => videoRef.current,
30917
+ stream: combinedStream,
30918
+ signal: abort.signal,
30919
+ serverUrl,
30920
+ authToken: probeAuthToken ?? null,
30921
+ onRefused: reportProbeRefused
30922
+ }).then((mbps) => {
30923
+ if (mbps === void 0 || !shouldUpgradeAfterProbe(mbps)) return;
30924
+ if (!mountedRef.current || abort.signal.aborted) return;
30925
+ if (probeUpgradeRef.current) return;
30926
+ probeUpgradeRef.current = true;
30927
+ connectClientOffer(true, true, mbps);
30928
+ });
30716
30929
  let swappedIn = false;
30717
30930
  const swapInOnce = () => {
30718
30931
  if (swappedIn || !mountedRef.current) return;
@@ -30814,7 +31027,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
30814
31027
  await pc.setLocalDescription(offer);
30815
31028
  timing.localSetMs = ms();
30816
31029
  if (!mountedRef.current || abort.signal.aborted) return;
30817
- const downlinkMbps = withDownlink ? await measuredDownlinkMbps(serverUrl) : void 0;
31030
+ const downlinkMbps = withDownlink ? knownDownlinkMbps ?? await measuredDownlinkMbps(serverUrl, probeAuthToken ?? null, reportProbeRefused) : void 0;
30818
31031
  const clientOfferHints = mergePlayerHints(computeClientHints(containerRef.current), hintsOverride, downlinkMbps, !withDownlink);
30819
31032
  reportAdaptiveHints(withDownlink ? "probe-climb" : "cold-start", clientOfferHints, probeOriginFor(serverUrl));
30820
31033
  const signalingStart = ms();
@@ -30822,13 +31035,6 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
30822
31035
  timing.signalingRttMs = ms() - signalingStart;
30823
31036
  sessionId = sid;
30824
31037
  sessionIdsRef.current.add(sid);
30825
- if (probePromise !== void 0) probePromise.then((mbps) => {
30826
- if (mbps === void 0 || !shouldUpgradeAfterProbe(mbps)) return;
30827
- if (!mountedRef.current || abort.signal.aborted) return;
30828
- if (probeUpgradeRef.current) return;
30829
- probeUpgradeRef.current = true;
30830
- connectClientOffer(true, true);
30831
- });
30832
31038
  activeSessionIdRef.current = sid;
30833
31039
  if (!mountedRef.current || abort.signal.aborted) {
30834
31040
  closeSession?.(sid).catch(() => {});
@@ -30889,7 +31095,9 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
30889
31095
  onError,
30890
31096
  startSessionStatePoll,
30891
31097
  hintsOverride,
30892
- reportAdaptiveHints
31098
+ reportAdaptiveHints,
31099
+ probeAuthToken,
31100
+ reportProbeRefused
30893
31101
  ]);
30894
31102
  renegotiateRef.current = (0, react$1.useCallback)(async (sessionId, target) => {
30895
31103
  const reofferFn = reoffer;
@@ -30996,7 +31204,7 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
30996
31204
  pc.close();
30997
31205
  return;
30998
31206
  }
30999
- const downlinkMbps = await measuredDownlinkMbps(serverUrl);
31207
+ const downlinkMbps = cachedDownlinkMbps(serverUrl);
31000
31208
  const reofferHints = {
31001
31209
  ...computeClientHints(containerRef.current),
31002
31210
  ...downlinkMbps !== void 0 ? { downlinkMbps } : {},
@@ -31355,31 +31563,27 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
31355
31563
  jitterS = s.jitter ?? 0;
31356
31564
  } else if (s.type === "candidate-pair" && s.nominated && typeof s.currentRoundTripTime === "number") rttS = s.currentRoundTripTime;
31357
31565
  });
31358
- const prev = netPrev.current;
31359
- netPrev.current = {
31566
+ const step = nextClientSample(netPrev.current, {
31567
+ pc,
31360
31568
  lost,
31361
31569
  recv,
31362
31570
  bytes,
31363
- ts
31364
- };
31365
- if (!prev || ts <= prev.ts) return;
31366
- const dLost = Math.max(0, lost - prev.lost);
31367
- const denom = dLost + Math.max(0, recv - prev.recv);
31368
- const packetLossPercent = denom > 0 ? Math.min(100, dLost / denom * 100) : 0;
31369
- const estimatedBandwidthKbps = Math.max(0, Math.round((bytes - prev.bytes) * 8 / (ts - prev.ts)));
31370
- onClientNetworkSample({
31371
- rttMs: Math.round(rttS * 1e3),
31372
- jitterMs: Math.round(jitterS * 1e3),
31373
- estimatedBandwidthKbps,
31374
- packetLossPercent
31571
+ ts,
31572
+ rttS,
31573
+ jitterS
31574
+ }, {
31575
+ renegotiating: renegotiatingRef.current,
31576
+ probe: latestProbeReading(serverUrl)
31375
31577
  });
31578
+ netPrev.current = step.baseline;
31579
+ if (step.sample !== null) onClientNetworkSample(step.sample);
31376
31580
  } catch {}
31377
31581
  };
31378
31582
  const id = setInterval(() => {
31379
31583
  tick();
31380
31584
  }, 2e3);
31381
31585
  return () => clearInterval(id);
31382
- }, [onClientNetworkSample]);
31586
+ }, [onClientNetworkSample, serverUrl]);
31383
31587
  const pbPrev = (0, react$1.useRef)(null);
31384
31588
  (0, react$1.useEffect)(() => {
31385
31589
  if (!onPlaybackStats) {
@@ -45811,9 +46015,11 @@ function resolveControlAlign(type, placement) {
45811
46015
  * Relative "last seen" formatter for the offline affordance.
45812
46016
  *
45813
46017
  * Renders a short relative-time label from a millisecond epoch:
45814
- * "just now", "5m ago", "3h ago", "2d ago". Returns `null` for
45815
- * missing/non-positive inputs so callers can render nothing rather
45816
- * than a misleading "—" or "NaN ago".
46018
+ * "just now", "5m ago", "3h ago", "2d ago". Returns `null` for missing
46019
+ * inputs, or inputs below {@link MIN_PLAUSIBLE_TIMESTAMP_MS} (a
46020
+ * placeholder or a seeded "known value, unknown change time" stamp), so
46021
+ * callers can render nothing rather than a misleading "—", "NaN ago", or
46022
+ * (for a seeded `1`) "20000 days ago".
45817
46023
  *
45818
46024
  * Future timestamps (clock skew between the device and the browser)
45819
46025
  * collapse to "just now" instead of a negative duration.
@@ -45823,8 +46029,15 @@ var MINUTE_MS = 60 * SECOND_MS;
45823
46029
  var HOUR_MS = 60 * MINUTE_MS;
45824
46030
  var DAY_MS = 24 * HOUR_MS;
45825
46031
  var JUST_NOW_MS = 30 * SECOND_MS;
46032
+ /**
46033
+ * The earliest timestamp a real "last seen" could plausibly carry. Below
46034
+ * this, a value is a placeholder or a seeded "known value, unknown change
46035
+ * time" stamp (e.g. Homematic's `HM_SEEDED_CHANGED_AT = 1`) — never a real
46036
+ * instant. Year 2000 predates every device this renders.
46037
+ */
46038
+ var MIN_PLAUSIBLE_TIMESTAMP_MS = Date.UTC(2e3, 0, 1);
45826
46039
  function formatLastSeen(timestampMs) {
45827
- if (timestampMs === null || timestampMs === void 0 || timestampMs <= 0) return null;
46040
+ if (timestampMs === null || timestampMs === void 0 || timestampMs < MIN_PLAUSIBLE_TIMESTAMP_MS) return null;
45828
46041
  const delta = Date.now() - timestampMs;
45829
46042
  if (delta < JUST_NOW_MS) return "just now";
45830
46043
  if (delta < HOUR_MS) return `${String(Math.floor(delta / MINUTE_MS))}m ago`;
@@ -56450,6 +56663,7 @@ exports.useDeviceManagerGetStreamSources = useDeviceManagerGetStreamSources;
56450
56663
  exports.useDeviceManagerGetWireableFields = useDeviceManagerGetWireableFields;
56451
56664
  exports.useDeviceManagerListAll = useDeviceManagerListAll;
56452
56665
  exports.useDeviceManagerListBindableCapsForDeviceType = useDeviceManagerListBindableCapsForDeviceType;
56666
+ exports.useDeviceManagerListDeviceIdsByCaps = useDeviceManagerListDeviceIdsByCaps;
56453
56667
  exports.useDeviceManagerListLocationDescriptions = useDeviceManagerListLocationDescriptions;
56454
56668
  exports.useDeviceManagerListLocationIcons = useDeviceManagerListLocationIcons;
56455
56669
  exports.useDeviceManagerListLocations = useDeviceManagerListLocations;