@camstack/ui-library 1.2.180 → 1.2.182

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
@@ -7213,6 +7213,8 @@ var useWebrtcSessionCloseSession = trpc.webrtcSession.closeSession.useMutation;
7213
7213
  var useWebrtcSessionHasAdaptiveBitrate = trpc.webrtcSession.hasAdaptiveBitrate.useQuery;
7214
7214
  /** Generated alias around `trpc.webrtcSession.getSessionState.useQuery`. */
7215
7215
  var useWebrtcSessionGetSessionState = trpc.webrtcSession.getSessionState.useQuery;
7216
+ /** Generated alias around `trpc.webrtcSession.getSessionDebug.useQuery`. */
7217
+ var useWebrtcSessionGetSessionDebug = trpc.webrtcSession.getSessionDebug.useQuery;
7216
7218
  /** Generated alias around `trpc.zoneAnalytics.getCurrentSnapshot.useQuery`. */
7217
7219
  var useZoneAnalyticsGetCurrentSnapshot = trpc.zoneAnalytics.getCurrentSnapshot.useQuery;
7218
7220
  /** Generated alias around `trpc.zoneAnalytics.getCurrentSnapshotBatch.useQuery`. */
@@ -16642,7 +16644,8 @@ function readProfileOffer(value) {
16642
16644
  profile,
16643
16645
  offered,
16644
16646
  missingSources,
16645
- canvas: typeof value["canvas"] === "string" ? value["canvas"] : ""
16647
+ canvas: typeof value["canvas"] === "string" ? value["canvas"] : "",
16648
+ published: typeof value["published"] === "boolean" ? value["published"] : offered
16646
16649
  };
16647
16650
  }
16648
16651
  /**
@@ -16887,7 +16890,7 @@ function CameraGridLayoutEditor({ deviceId }) {
16887
16890
  setLoaded("unreadable");
16888
16891
  });
16889
16892
  }, [dev, reload]);
16890
- const save = (0, react$1.useCallback)(async () => {
16893
+ const save = (0, react$1.useCallback)(async (extra) => {
16891
16894
  const layout = dev?.cameraGridLayout;
16892
16895
  if (draft === null || !layout) return;
16893
16896
  setSaving(true);
@@ -16896,7 +16899,8 @@ function CameraGridLayoutEditor({ deviceId }) {
16896
16899
  const outcome = parseGridView(await layout.saveLayout({
16897
16900
  name: draft.name,
16898
16901
  fps: draft.fps,
16899
- cells: draft.cells.map((cell) => ({ ...cell }))
16902
+ cells: draft.cells.map((cell) => ({ ...cell })),
16903
+ ...extra?.publishedProfiles !== void 0 ? { publishedProfiles: [...extra.publishedProfiles] } : {}
16900
16904
  }));
16901
16905
  if (outcome !== null && outcome !== "unreadable") {
16902
16906
  setLoaded(outcome);
@@ -17084,7 +17088,12 @@ function CameraGridLayoutEditor({ deviceId }) {
17084
17088
  }),
17085
17089
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GridProfileReport, {
17086
17090
  offers: loaded.profiles,
17087
- cameras
17091
+ cameras,
17092
+ onToggle: (profile) => {
17093
+ if (draft === null) return;
17094
+ const current = loaded.profiles.filter((row) => row.published).map((row) => row.profile);
17095
+ save({ publishedProfiles: current.includes(profile) ? current.filter((entry) => entry !== profile) : [...current, profile] });
17096
+ }
17088
17097
  }),
17089
17098
  draft.cells[selected] !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CellInspector, {
17090
17099
  cell: draft.cells[selected],
@@ -17114,7 +17123,7 @@ function CameraGridLayoutEditor({ deviceId }) {
17114
17123
  * Read-only on purpose: what is on offer is decided by the source cameras'
17115
17124
  * assignments, and a control here would be a switch that writes nothing (D62).
17116
17125
  */
17117
- function GridProfileReport({ offers, cameras }) {
17126
+ function GridProfileReport({ offers, cameras, onToggle }) {
17118
17127
  const nameOf = (deviceId) => cameras.find((camera) => camera.id === deviceId)?.name ?? `#${String(deviceId)}`;
17119
17128
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17120
17129
  className: "flex flex-col gap-1 rounded border border-border p-2 text-[10.5px]",
@@ -17123,20 +17132,33 @@ function GridProfileReport({ offers, cameras }) {
17123
17132
  children: "Published profiles"
17124
17133
  }), offers.map((offer) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
17125
17134
  className: "flex items-baseline gap-2",
17126
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
17127
- className: "w-10 uppercase",
17128
- children: offer.profile
17129
- }), offer.offered ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
17130
- className: "text-foreground-subtle",
17131
- children: [
17132
- offer.canvas,
17133
- " — composed from every source’s ",
17134
- offer.profile
17135
- ]
17136
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
17137
- className: "text-danger",
17138
- children: offer.missingSources.length === 0 ? "not offered — this grid has no cells yet" : `not offered — no ${offer.profile} on ${offer.missingSources.map(nameOf).join(", ")}`
17139
- })]
17135
+ children: [
17136
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
17137
+ type: "checkbox",
17138
+ checked: offer.published,
17139
+ disabled: !offer.offered,
17140
+ "aria-label": `publish ${offer.profile}`,
17141
+ onChange: () => {
17142
+ onToggle(offer.profile);
17143
+ }
17144
+ }),
17145
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
17146
+ className: "w-10 uppercase",
17147
+ children: offer.profile
17148
+ }),
17149
+ offer.offered ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
17150
+ className: "text-foreground-subtle",
17151
+ children: [
17152
+ offer.canvas,
17153
+ " —",
17154
+ " ",
17155
+ offer.published ? `composed from every source’s ${offer.profile}` : "serviceable, not published"
17156
+ ]
17157
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
17158
+ className: "text-danger",
17159
+ children: offer.missingSources.length === 0 ? "not offered — this grid has no cells yet" : `not offered — no ${offer.profile} on ${offer.missingSources.map(nameOf).join(", ")}`
17160
+ })
17161
+ ]
17140
17162
  }, offer.profile))]
17141
17163
  });
17142
17164
  }
@@ -26956,6 +26978,580 @@ async function measureDownlinkMbps(origin, deps) {
26956
26978
  }
26957
26979
  }
26958
26980
  //#endregion
26981
+ //#region src/composites/stream-debug/webrtc-debug-stats.ts
26982
+ /** Beyond this the log is a scroll, not a signal. */
26983
+ var RESOLUTION_LOG_MAX = 8;
26984
+ function num(value) {
26985
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
26986
+ }
26987
+ function str(value) {
26988
+ return typeof value === "string" && value.length > 0 ? value : null;
26989
+ }
26990
+ /** Δ of two cumulative counters. Null unless BOTH ends were measured; a
26991
+ * counter that went backwards (a reset) yields null rather than a negative. */
26992
+ function delta(now, before) {
26993
+ if (now === null || before === null) return null;
26994
+ const d = now - before;
26995
+ return d < 0 ? null : d;
26996
+ }
26997
+ function lossPercentOf(lost, received) {
26998
+ if (lost === null || received === null) return null;
26999
+ const total = lost + received;
27000
+ if (total <= 0) return null;
27001
+ return lost / total * 100;
27002
+ }
27003
+ function profileLevelIdOf(fmtp) {
27004
+ if (fmtp === null) return null;
27005
+ return /profile-level-id=([0-9a-fA-F]+)/.exec(fmtp)?.[1] ?? null;
27006
+ }
27007
+ function collect(report) {
27008
+ const out = {
27009
+ inbound: null,
27010
+ codecs: /* @__PURE__ */ new Map(),
27011
+ candidates: /* @__PURE__ */ new Map(),
27012
+ pair: null,
27013
+ selectedPairId: null
27014
+ };
27015
+ const pairs = [];
27016
+ report.forEach((raw) => {
27017
+ const s = raw;
27018
+ const id = str(s.id);
27019
+ switch (s.type) {
27020
+ case "inbound-rtp":
27021
+ if ((s.kind ?? s.mediaType) === "video") out.inbound = s;
27022
+ break;
27023
+ case "codec":
27024
+ if (id !== null) out.codecs.set(id, s);
27025
+ break;
27026
+ case "local-candidate":
27027
+ case "remote-candidate":
27028
+ if (id !== null) out.candidates.set(id, s);
27029
+ break;
27030
+ case "candidate-pair":
27031
+ pairs.push(s);
27032
+ break;
27033
+ case "transport":
27034
+ out.selectedPairId = str(s.selectedCandidatePairId);
27035
+ break;
27036
+ default: break;
27037
+ }
27038
+ });
27039
+ const byId = out.selectedPairId;
27040
+ out.pair = pairs.find((p) => str(p.id) === byId) ?? pairs.find((p) => p.selected === true) ?? pairs.find((p) => p.nominated === true && p.state === "succeeded") ?? pairs.find((p) => p.nominated === true) ?? pairs.find((p) => p.state === "succeeded") ?? null;
27041
+ return out;
27042
+ }
27043
+ function readCounters(inbound) {
27044
+ if (inbound === null) return {
27045
+ timestamp: null,
27046
+ bytesReceived: null,
27047
+ packetsReceived: null,
27048
+ packetsLost: null,
27049
+ framesDecoded: null,
27050
+ framesDropped: null,
27051
+ freezeCount: null,
27052
+ totalFreezesDurationS: null,
27053
+ totalDecodeTimeS: null,
27054
+ nackCount: null,
27055
+ pliCount: null
27056
+ };
27057
+ return {
27058
+ timestamp: num(inbound.timestamp),
27059
+ bytesReceived: num(inbound.bytesReceived),
27060
+ packetsReceived: num(inbound.packetsReceived),
27061
+ packetsLost: num(inbound.packetsLost),
27062
+ framesDecoded: num(inbound.framesDecoded),
27063
+ framesDropped: num(inbound.framesDropped),
27064
+ freezeCount: num(inbound.freezeCount),
27065
+ totalFreezesDurationS: num(inbound.totalFreezesDuration),
27066
+ totalDecodeTimeS: num(inbound.totalDecodeTime),
27067
+ nackCount: num(inbound.nackCount),
27068
+ pliCount: num(inbound.pliCount)
27069
+ };
27070
+ }
27071
+ var EMPTY_WINDOW = {
27072
+ spanMs: null,
27073
+ kbps: null,
27074
+ packetsReceived: null,
27075
+ packetsLost: null,
27076
+ lossPercent: null,
27077
+ framesDecoded: null,
27078
+ framesDropped: null,
27079
+ freezeCount: null,
27080
+ freezeMs: null,
27081
+ nackCount: null,
27082
+ pliCount: null,
27083
+ decodeMs: null
27084
+ };
27085
+ function windowOf(now, before) {
27086
+ if (before === null) return EMPTY_WINDOW;
27087
+ const spanMs = delta(now.timestamp, before.timestamp);
27088
+ if (spanMs === null || spanMs <= 0) return EMPTY_WINDOW;
27089
+ const bytes = delta(now.bytesReceived, before.bytesReceived);
27090
+ const lost = delta(now.packetsLost, before.packetsLost);
27091
+ const received = delta(now.packetsReceived, before.packetsReceived);
27092
+ const decoded = delta(now.framesDecoded, before.framesDecoded);
27093
+ const decodeS = delta(now.totalDecodeTimeS, before.totalDecodeTimeS);
27094
+ const freezeS = delta(now.totalFreezesDurationS, before.totalFreezesDurationS);
27095
+ return {
27096
+ spanMs,
27097
+ kbps: bytes === null ? null : Math.round(bytes * 8 / spanMs),
27098
+ packetsReceived: received,
27099
+ packetsLost: lost,
27100
+ lossPercent: lossPercentOf(lost, received),
27101
+ framesDecoded: decoded,
27102
+ framesDropped: delta(now.framesDropped, before.framesDropped),
27103
+ freezeCount: delta(now.freezeCount, before.freezeCount),
27104
+ freezeMs: freezeS === null ? null : Math.round(freezeS * 1e3),
27105
+ nackCount: delta(now.nackCount, before.nackCount),
27106
+ pliCount: delta(now.pliCount, before.pliCount),
27107
+ decodeMs: decodeS === null || decoded === null || decoded <= 0 ? null : decodeS / decoded * 1e3
27108
+ };
27109
+ }
27110
+ function resolutionLogOf(prev, width, height, at) {
27111
+ if (width === null || height === null) return prev;
27112
+ const last = prev.at(-1);
27113
+ if (last !== void 0 && last.width === width && last.height === height) return prev;
27114
+ const next = [...prev, {
27115
+ width,
27116
+ height,
27117
+ at
27118
+ }];
27119
+ return next.length > RESOLUTION_LOG_MAX ? next.slice(next.length - RESOLUTION_LOG_MAX) : next;
27120
+ }
27121
+ /**
27122
+ * Derive one debug sample. Immutable: `prev` is read, never mutated, and the
27123
+ * returned object is the input for the next call.
27124
+ */
27125
+ function deriveWebrtcDebugStats(input) {
27126
+ const { inbound, codecs, candidates, pair } = collect(input.report);
27127
+ const raw = readCounters(inbound);
27128
+ const codecStat = inbound !== null ? codecs.get(str(inbound.codecId) ?? "") : void 0;
27129
+ const mime = str(codecStat?.mimeType);
27130
+ const fmtp = str(codecStat?.sdpFmtpLine);
27131
+ const localCand = pair !== null ? candidates.get(str(pair.localCandidateId) ?? "") : void 0;
27132
+ const remoteCand = pair !== null ? candidates.get(str(pair.remoteCandidateId) ?? "") : void 0;
27133
+ const localType = str(localCand?.candidateType);
27134
+ const remoteType = str(remoteCand?.candidateType);
27135
+ const width = num(inbound?.frameWidth);
27136
+ const height = num(inbound?.frameHeight);
27137
+ const totalFreezeS = raw.totalFreezesDurationS;
27138
+ const decodeS = raw.totalDecodeTimeS;
27139
+ const decodedTotal = raw.framesDecoded;
27140
+ return {
27141
+ at: input.now,
27142
+ hasInboundVideo: inbound !== null,
27143
+ width,
27144
+ height,
27145
+ fps: num(inbound?.framesPerSecond),
27146
+ codec: mime === null ? null : mime.split("/")[1] ?? mime,
27147
+ codecProfileLevelId: profileLevelIdOf(fmtp),
27148
+ jitterMs: jitterMsOf(inbound),
27149
+ packetsReceived: raw.packetsReceived,
27150
+ packetsLost: raw.packetsLost,
27151
+ lossPercent: lossPercentOf(raw.packetsLost, raw.packetsReceived),
27152
+ nackCount: raw.nackCount,
27153
+ pliCount: raw.pliCount,
27154
+ framesDecoded: decodedTotal,
27155
+ framesDropped: raw.framesDropped,
27156
+ freezeCount: raw.freezeCount,
27157
+ totalFreezeMs: totalFreezeS === null ? null : Math.round(totalFreezeS * 1e3),
27158
+ avgDecodeMs: decodeS === null || decodedTotal === null || decodedTotal <= 0 ? null : decodeS / decodedTotal * 1e3,
27159
+ rttMs: rttMsOf(pair),
27160
+ availableIncomingKbps: availableKbpsOf(pair),
27161
+ localCandidateType: localType,
27162
+ remoteCandidateType: remoteType,
27163
+ candidatePairType: localType === null && remoteType === null ? null : `${localType ?? "?"}/${remoteType ?? "?"}`,
27164
+ candidateProtocol: str(localCand?.protocol),
27165
+ iceConnectionState: input.iceConnectionState,
27166
+ dtlsState: input.dtlsState,
27167
+ window: windowOf(raw, input.prev?.raw ?? null),
27168
+ resolutionLog: resolutionLogOf(input.prev?.resolutionLog ?? [], width, height, input.now),
27169
+ raw
27170
+ };
27171
+ }
27172
+ function jitterMsOf(inbound) {
27173
+ const jitter = num(inbound?.jitter);
27174
+ return jitter === null ? null : jitter * 1e3;
27175
+ }
27176
+ function rttMsOf(pair) {
27177
+ const rtt = num(pair?.currentRoundTripTime);
27178
+ return rtt === null ? null : rtt * 1e3;
27179
+ }
27180
+ function availableKbpsOf(pair) {
27181
+ const bps = num(pair?.availableIncomingBitrate);
27182
+ return bps === null ? null : Math.round(bps / 1e3);
27183
+ }
27184
+ var VERDICTS = {
27185
+ unknown: "no window yet",
27186
+ decoding: "decoding",
27187
+ "decoder-stall": "DECODER STALL — bytes arriving, no frames out",
27188
+ "transport-stall": "TRANSPORT STALL — nothing arriving",
27189
+ "frozen-unknown": "frames stopped — byte rate unknown"
27190
+ };
27191
+ function freezeVerdict(stats) {
27192
+ const kind = freezeVerdictKind(stats);
27193
+ return {
27194
+ kind,
27195
+ label: VERDICTS[kind]
27196
+ };
27197
+ }
27198
+ function freezeVerdictKind(stats) {
27199
+ if (stats === null || !stats.hasInboundVideo) return "unknown";
27200
+ const decoded = stats.window.framesDecoded;
27201
+ if (decoded === null) return "unknown";
27202
+ if (decoded > 0) return "decoding";
27203
+ const kbps = stats.window.kbps;
27204
+ if (kbps === null) return "frozen-unknown";
27205
+ return kbps > 0 ? "decoder-stall" : "transport-stall";
27206
+ }
27207
+ //#endregion
27208
+ //#region src/composites/stream-debug/stream-debug-overlay.tsx
27209
+ /** `—` is the ONLY rendering of unknown. Never an empty cell, never 0. */
27210
+ var UNKNOWN = "—";
27211
+ function n(value, opts) {
27212
+ if (value === null || value === void 0 || !Number.isFinite(value)) return UNKNOWN;
27213
+ const digits = opts?.digits ?? 0;
27214
+ return `${value.toFixed(digits)}${opts?.suffix ?? ""}`;
27215
+ }
27216
+ function s(value) {
27217
+ return value === null || value === void 0 || value === "" ? UNKNOWN : value;
27218
+ }
27219
+ function b(value) {
27220
+ return value === null || value === void 0 ? UNKNOWN : value ? "yes" : "no";
27221
+ }
27222
+ /** Age of a measurement, in the form an operator reads at a glance. */
27223
+ function age(at, now) {
27224
+ if (at === null) return UNKNOWN;
27225
+ const ms = Math.max(0, now - at);
27226
+ if (ms < 1e3) return `${ms}ms ago`;
27227
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s ago`;
27228
+ return `${Math.floor(ms / 6e4)}m${Math.floor(ms % 6e4 / 1e3)}s ago`;
27229
+ }
27230
+ /** A measurement older than this is NOT live, and is said to be. */
27231
+ var STALE_AFTER_MS = 3e3;
27232
+ var VERDICT_CLASS = {
27233
+ unknown: "bg-white/15 text-white/70",
27234
+ decoding: "bg-emerald-500/25 text-emerald-200",
27235
+ "decoder-stall": "bg-amber-500/30 text-amber-100",
27236
+ "transport-stall": "bg-rose-500/30 text-rose-100",
27237
+ "frozen-unknown": "bg-white/15 text-white/70"
27238
+ };
27239
+ function Row({ label, value, strong = false }) {
27240
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
27241
+ className: "flex items-baseline justify-between gap-3 leading-tight",
27242
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27243
+ className: "text-white/55 shrink-0",
27244
+ children: label
27245
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27246
+ className: strong ? "text-white font-semibold tabular-nums" : "text-white/90 tabular-nums",
27247
+ children: value
27248
+ })]
27249
+ });
27250
+ }
27251
+ function SectionHeader({ title, measuredAt, now }) {
27252
+ const stale = measuredAt !== null && now - measuredAt > STALE_AFTER_MS;
27253
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
27254
+ className: "flex items-baseline justify-between gap-3 mt-2 first:mt-0 pb-0.5 border-b border-white/20",
27255
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27256
+ className: "text-white/80 font-semibold tracking-wide uppercase text-[9px]",
27257
+ children: title
27258
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
27259
+ className: stale ? "text-amber-300 text-[9px]" : "text-white/45 text-[9px]",
27260
+ children: [stale ? "STALE · " : "", age(measuredAt, now)]
27261
+ })]
27262
+ });
27263
+ }
27264
+ function StreamDebugOverlay(props) {
27265
+ const { stats, server, serverError, serverWired, now, sessionId } = props;
27266
+ const verdict = freezeVerdict(stats);
27267
+ const win = stats?.window;
27268
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
27269
+ className: "absolute top-2 right-2 z-20 transform-gpu w-[15.5rem] max-h-[calc(100%-1rem)] overflow-y-auto rounded-md bg-neutral-950/95 ring-1 ring-white/25 shadow-lg px-2 py-1.5 text-[10px] font-mono text-white/90 select-text",
27270
+ "data-testid": "stream-debug-overlay",
27271
+ children: [
27272
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
27273
+ className: `rounded px-1.5 py-1 mb-1 text-[10px] font-semibold ${VERDICT_CLASS[verdict.kind]}`,
27274
+ children: verdict.label
27275
+ }),
27276
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionHeader, {
27277
+ title: "decode",
27278
+ measuredAt: stats?.at ?? null,
27279
+ now
27280
+ }),
27281
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27282
+ label: "framesDecoded",
27283
+ value: n(stats?.framesDecoded),
27284
+ strong: true
27285
+ }),
27286
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27287
+ label: "· this window",
27288
+ value: n(win?.framesDecoded),
27289
+ strong: true
27290
+ }),
27291
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27292
+ label: "freezeCount",
27293
+ value: n(stats?.freezeCount),
27294
+ strong: true
27295
+ }),
27296
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27297
+ label: "· total frozen",
27298
+ value: n(stats?.totalFreezeMs, { suffix: "ms" }),
27299
+ strong: true
27300
+ }),
27301
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27302
+ label: "· this window",
27303
+ value: n(win?.freezeMs, { suffix: "ms" })
27304
+ }),
27305
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27306
+ label: "pliCount",
27307
+ value: n(stats?.pliCount),
27308
+ strong: true
27309
+ }),
27310
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27311
+ label: "· this window",
27312
+ value: n(win?.pliCount)
27313
+ }),
27314
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27315
+ label: "framesDropped",
27316
+ value: n(stats?.framesDropped)
27317
+ }),
27318
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27319
+ label: "decode time",
27320
+ value: n(stats?.avgDecodeMs, {
27321
+ digits: 1,
27322
+ suffix: "ms"
27323
+ })
27324
+ }),
27325
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27326
+ label: "· this window",
27327
+ value: n(win?.decodeMs, {
27328
+ digits: 1,
27329
+ suffix: "ms"
27330
+ })
27331
+ }),
27332
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionHeader, {
27333
+ title: "picture",
27334
+ measuredAt: stats?.at ?? null,
27335
+ now
27336
+ }),
27337
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27338
+ label: "resolution",
27339
+ value: stats?.width !== null && stats?.width !== void 0 && stats.height !== null ? `${stats.width}×${stats.height}` : UNKNOWN
27340
+ }),
27341
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27342
+ label: "fps",
27343
+ value: n(stats?.fps)
27344
+ }),
27345
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27346
+ label: "codec",
27347
+ value: stats?.codec === null || stats?.codec === void 0 ? UNKNOWN : `${stats.codec}${stats.codecProfileLevelId !== null ? ` ${stats.codecProfileLevelId}` : ""}`
27348
+ }),
27349
+ (stats?.resolutionLog.length ?? 0) > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
27350
+ className: "mt-0.5 text-white/60 leading-tight",
27351
+ children: stats?.resolutionLog.map((mark) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
27352
+ className: "flex justify-between",
27353
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
27354
+ mark.width,
27355
+ "×",
27356
+ mark.height
27357
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27358
+ className: "text-white/40",
27359
+ children: age(mark.at, now)
27360
+ })]
27361
+ }, `${mark.at}-${mark.width}x${mark.height}`))
27362
+ }),
27363
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionHeader, {
27364
+ title: "transport",
27365
+ measuredAt: stats?.at ?? null,
27366
+ now
27367
+ }),
27368
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27369
+ label: "bitrate",
27370
+ value: n(win?.kbps, { suffix: " kbps" })
27371
+ }),
27372
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27373
+ label: "rtt",
27374
+ value: n(stats?.rttMs, {
27375
+ digits: 0,
27376
+ suffix: "ms"
27377
+ })
27378
+ }),
27379
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27380
+ label: "jitter",
27381
+ value: n(stats?.jitterMs, {
27382
+ digits: 1,
27383
+ suffix: "ms"
27384
+ })
27385
+ }),
27386
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27387
+ label: "packetsLost",
27388
+ value: n(stats?.packetsLost)
27389
+ }),
27390
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27391
+ label: "· this window",
27392
+ value: n(win?.packetsLost)
27393
+ }),
27394
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27395
+ label: "loss",
27396
+ value: n(stats?.lossPercent, {
27397
+ digits: 2,
27398
+ suffix: "%"
27399
+ })
27400
+ }),
27401
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27402
+ label: "· this window",
27403
+ value: n(win?.lossPercent, {
27404
+ digits: 2,
27405
+ suffix: "%"
27406
+ })
27407
+ }),
27408
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27409
+ label: "nackCount",
27410
+ value: n(stats?.nackCount)
27411
+ }),
27412
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27413
+ label: "· this window",
27414
+ value: n(win?.nackCount)
27415
+ }),
27416
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27417
+ label: "candidate pair",
27418
+ value: s(stats?.candidatePairType)
27419
+ }),
27420
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27421
+ label: "protocol",
27422
+ value: s(stats?.candidateProtocol)
27423
+ }),
27424
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27425
+ label: "ice",
27426
+ value: s(stats?.iceConnectionState)
27427
+ }),
27428
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27429
+ label: "dtls",
27430
+ value: s(stats?.dtlsState)
27431
+ }),
27432
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionHeader, {
27433
+ title: "server",
27434
+ measuredAt: server?.observedAt ?? null,
27435
+ now
27436
+ }),
27437
+ !serverWired ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
27438
+ className: "text-white/50 leading-tight py-0.5",
27439
+ children: "not wired on this host"
27440
+ }) : serverError !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
27441
+ className: "text-rose-300 leading-tight py-0.5 break-words",
27442
+ children: serverError
27443
+ }) : server === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
27444
+ className: "text-white/50 leading-tight py-0.5",
27445
+ children: "waiting for the first answer…"
27446
+ }) : !server.alive ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
27447
+ className: "text-amber-300 leading-tight py-0.5",
27448
+ children: "session not found on the server — it ended, or this node never had it"
27449
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
27450
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27451
+ label: "served rung",
27452
+ value: s(server.servedTier),
27453
+ strong: true
27454
+ }),
27455
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27456
+ label: "adaptive",
27457
+ value: b(server.adaptive)
27458
+ }),
27459
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27460
+ label: "source",
27461
+ value: s(server.camStreamId)
27462
+ }),
27463
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27464
+ label: "source kind",
27465
+ value: server.sourceKind
27466
+ }),
27467
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27468
+ label: "ingest node",
27469
+ value: s(server.ingestNodeId)
27470
+ }),
27471
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27472
+ label: "· remote",
27473
+ value: b(server.ingestIsRemote)
27474
+ }),
27475
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27476
+ label: "egress leg",
27477
+ value: server.egressLeg,
27478
+ strong: true
27479
+ }),
27480
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27481
+ label: "codec src→out",
27482
+ value: `${s(server.sourceCodec)}→${s(server.negotiatedCodec)}`
27483
+ }),
27484
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27485
+ label: "broker",
27486
+ value: s(server.brokerStatus)
27487
+ }),
27488
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27489
+ label: "· bitrate",
27490
+ value: n(server.brokerBitrateKbps, { suffix: " kbps" })
27491
+ }),
27492
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27493
+ label: "· input fps",
27494
+ value: n(server.brokerInputFps)
27495
+ }),
27496
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27497
+ label: "rtp sent",
27498
+ value: n(server.rtpPacketsSent)
27499
+ }),
27500
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27501
+ label: "track mode",
27502
+ value: s(server.trackMode)
27503
+ }),
27504
+ server.lastDecision === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27505
+ label: "ladder",
27506
+ value: "never moved"
27507
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
27508
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27509
+ label: "ladder move",
27510
+ value: `${server.lastDecision.cause}${server.lastDecision.tier !== null ? ` → ${server.lastDecision.tier}` : ""}`,
27511
+ strong: true
27512
+ }),
27513
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27514
+ label: "· when",
27515
+ value: age(server.lastDecision.at, now)
27516
+ }),
27517
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27518
+ label: "· lossRatio",
27519
+ value: n(server.lastDecision.lossRatio, { digits: 4 })
27520
+ }),
27521
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27522
+ label: "· clientKbps",
27523
+ value: n(server.lastDecision.clientKbps, { suffix: " kbps" })
27524
+ }),
27525
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27526
+ label: "· stalledFor",
27527
+ value: n(server.lastDecision.stalledForMs, { suffix: "ms" })
27528
+ }),
27529
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27530
+ label: "· serverSending",
27531
+ value: b(server.lastDecision.serverSending)
27532
+ }),
27533
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27534
+ label: "· transcode-down",
27535
+ value: b(server.lastDecision.forcedTranscodeDown)
27536
+ })
27537
+ ] }),
27538
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27539
+ label: "promotions",
27540
+ value: n(server.upgradeCount)
27541
+ }),
27542
+ server.tierFailures.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27543
+ label: "tier failures",
27544
+ value: server.tierFailures.map((f) => `${f.tier}:${f.failures}`).join(" ")
27545
+ })
27546
+ ] }),
27547
+ sessionId !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
27548
+ className: "mt-1 pt-0.5 border-t border-white/15 text-white/40 break-all leading-tight",
27549
+ children: sessionId
27550
+ })
27551
+ ]
27552
+ });
27553
+ }
27554
+ //#endregion
26959
27555
  //#region src/composites/camera-stream-player.tsx
26960
27556
  /**
26961
27557
  * Silence (or restore) a live WebRTC stream FOR REAL.
@@ -27054,7 +27650,7 @@ function mergePlayerHints(autoHints, override, downlinkMbps, coldStart) {
27054
27650
  return coldStart ? stripDownlinkForColdStart(merged) : merged;
27055
27651
  }
27056
27652
  var FIRST_FRAME_TIMEOUT_MS = 8e3;
27057
- 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 }) {
27653
+ 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, getSessionDebug, reoffer, posterUrl, hintsOverride, onAdaptiveHints, reconnectSignal, onControlChannel, onVideoElement }) {
27058
27654
  const videoRef = (0, react$1.useRef)(null);
27059
27655
  const containerRef = (0, react$1.useRef)(null);
27060
27656
  const pcRef = (0, react$1.useRef)(null);
@@ -27951,55 +28547,77 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
27951
28547
  const webkit = video !== null && typeof video.webkitSetPresentationMode === "function";
27952
28548
  setIsPipSupported(standard || webkit);
27953
28549
  }, []);
27954
- const [statsText, setStatsText] = (0, react$1.useState)("");
27955
- const statsPrev = (0, react$1.useRef)(null);
28550
+ const [debugStats, setDebugStats] = (0, react$1.useState)(null);
28551
+ const [serverDebug, setServerDebug] = (0, react$1.useState)(null);
28552
+ const [serverDebugError, setServerDebugError] = (0, react$1.useState)(null);
28553
+ const [debugNow, setDebugNow] = (0, react$1.useState)(() => Date.now());
28554
+ (0, react$1.useEffect)(() => {
28555
+ if (!showStats) return;
28556
+ const id = setInterval(() => setDebugNow(Date.now()), 500);
28557
+ return () => clearInterval(id);
28558
+ }, [showStats]);
27956
28559
  (0, react$1.useEffect)(() => {
27957
28560
  if (!showStats) {
27958
- setStatsText("");
27959
- statsPrev.current = null;
28561
+ setDebugStats(null);
27960
28562
  return;
27961
28563
  }
28564
+ const prev = { current: null };
28565
+ let cancelled = false;
27962
28566
  const tick = async () => {
27963
28567
  const pc = pcRef.current;
27964
28568
  if (!pc) return;
27965
28569
  try {
27966
28570
  const report = await pc.getStats();
27967
- let bytes = 0;
27968
- let ts = 0;
27969
- let fps = 0;
27970
- let w = 0;
27971
- let h = 0;
27972
- let rtt = 0;
27973
- report.forEach((raw) => {
27974
- const s = raw;
27975
- if (s.type === "inbound-rtp" && (s.kind ?? s.mediaType) === "video") {
27976
- bytes = s.bytesReceived ?? 0;
27977
- ts = s.timestamp;
27978
- fps = s.framesPerSecond ?? 0;
27979
- w = s.frameWidth ?? 0;
27980
- h = s.frameHeight ?? 0;
27981
- } else if (s.type === "candidate-pair" && s.nominated && typeof s.currentRoundTripTime === "number") rtt = s.currentRoundTripTime;
28571
+ if (cancelled) return;
28572
+ const next = deriveWebrtcDebugStats({
28573
+ report,
28574
+ prev: prev.current,
28575
+ now: Date.now(),
28576
+ iceConnectionState: pc.iceConnectionState,
28577
+ dtlsState: pc.connectionState
27982
28578
  });
27983
- let kbps = 0;
27984
- if (statsPrev.current && ts > statsPrev.current.ts) kbps = Math.round((bytes - statsPrev.current.bytes) * 8 / (ts - statsPrev.current.ts));
27985
- statsPrev.current = {
27986
- bytes,
27987
- ts
27988
- };
27989
- const parts = [];
27990
- if (w && h) parts.push(`${w}×${h}`);
27991
- if (fps) parts.push(`${Math.round(fps)}fps`);
27992
- if (kbps > 0) parts.push(`${kbps} kbps`);
27993
- if (rtt) parts.push(`${Math.round(rtt * 1e3)}ms`);
27994
- setStatsText(parts.join(" · "));
28579
+ prev.current = next;
28580
+ setDebugStats(next);
27995
28581
  } catch {}
27996
28582
  };
27997
28583
  tick();
27998
28584
  const id = setInterval(() => {
27999
28585
  tick();
28000
28586
  }, 1e3);
28001
- return () => clearInterval(id);
28587
+ return () => {
28588
+ cancelled = true;
28589
+ clearInterval(id);
28590
+ };
28002
28591
  }, [showStats]);
28592
+ (0, react$1.useEffect)(() => {
28593
+ if (!showStats || !getSessionDebug) {
28594
+ setServerDebug(null);
28595
+ setServerDebugError(null);
28596
+ return;
28597
+ }
28598
+ let cancelled = false;
28599
+ const tick = async () => {
28600
+ const sid = activeSessionIdRef.current;
28601
+ if (sid === null) return;
28602
+ try {
28603
+ const answer = await getSessionDebug(sid);
28604
+ if (cancelled) return;
28605
+ setServerDebug(answer);
28606
+ setServerDebugError(null);
28607
+ } catch (err) {
28608
+ if (cancelled) return;
28609
+ setServerDebugError(err instanceof Error ? err.message : String(err));
28610
+ }
28611
+ };
28612
+ tick();
28613
+ const id = setInterval(() => {
28614
+ tick();
28615
+ }, 1e3);
28616
+ return () => {
28617
+ cancelled = true;
28618
+ clearInterval(id);
28619
+ };
28620
+ }, [showStats, getSessionDebug]);
28003
28621
  const netPrev = (0, react$1.useRef)(null);
28004
28622
  (0, react$1.useEffect)(() => {
28005
28623
  if (!onClientNetworkSample) {
@@ -28186,9 +28804,13 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
28186
28804
  className: "absolute top-2 left-2 z-10 transform-gpu px-2 py-0.5 rounded bg-black/60 text-[10px] text-white font-medium",
28187
28805
  children: label
28188
28806
  }),
28189
- showStats && statsText && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
28190
- className: "absolute top-2 right-2 z-10 transform-gpu px-2 py-0.5 rounded bg-black/60 text-[10px] text-white font-mono",
28191
- children: statsText
28807
+ showStats && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StreamDebugOverlay, {
28808
+ stats: debugStats,
28809
+ server: serverDebug,
28810
+ serverError: serverDebugError,
28811
+ serverWired: getSessionDebug !== void 0,
28812
+ now: debugNow,
28813
+ sessionId: activeSessionIdRef.current
28192
28814
  }),
28193
28815
  overlay,
28194
28816
  showControls && !stillImg && state === "connecting" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -51541,6 +52163,13 @@ function useDeviceWebrtc(trpc, deviceId, pollIntervalMs = 5e3) {
51541
52163
  } catch {
51542
52164
  return { pendingRenegotiation: null };
51543
52165
  }
52166
+ }, [trpc, deviceId]),
52167
+ getSessionDebug: (0, react$1.useCallback)(async (sessionId) => {
52168
+ if (deviceId === null) throw new Error("getSessionDebug: no device bound to this player");
52169
+ return trpc.webrtcSession.getSessionDebug.query({
52170
+ deviceId,
52171
+ sessionId
52172
+ });
51544
52173
  }, [trpc, deviceId])
51545
52174
  };
51546
52175
  }
@@ -52057,6 +52686,7 @@ exports.StepTimings = StepTimings;
52057
52686
  exports.StepTreeMaster = StepTreeMaster;
52058
52687
  exports.Stepper = Stepper;
52059
52688
  exports.StreamBrokerSelector = StreamBrokerSelector;
52689
+ exports.StreamDebugOverlay = StreamDebugOverlay;
52060
52690
  exports.StreamPanel = StreamPanel;
52061
52691
  exports.Switch = Switch;
52062
52692
  exports.SwitchHeroCard = SwitchHeroCard;
@@ -52122,6 +52752,7 @@ exports.cursorFractionFor = cursorFractionFor;
52122
52752
  exports.darkColors = require_theme_index.darkColors$1;
52123
52753
  exports.defaultTheme = require_theme_index.defaultTheme$1;
52124
52754
  exports.deriveDeviceKind = deriveDeviceKind;
52755
+ exports.deriveWebrtcDebugStats = deriveWebrtcDebugStats;
52125
52756
  exports.describeDeviceSelector = describeDeviceSelector;
52126
52757
  exports.deviceMatchesFilter = deviceMatchesFilter;
52127
52758
  exports.deviceOptionLabel = deviceOptionLabel;
@@ -52138,6 +52769,7 @@ exports.findTimezone = _camstack_types.findTimezone;
52138
52769
  exports.formatControlDateTime = formatControlDateTime;
52139
52770
  exports.formatLastSeen = formatLastSeen;
52140
52771
  exports.formatNumeric = formatNumeric;
52772
+ exports.freezeVerdict = freezeVerdict;
52141
52773
  exports.fuzzyMatch = fuzzyMatch;
52142
52774
  exports.getClassColor = getClassColor;
52143
52775
  exports.getPhaseVisual = getPhaseVisual;
@@ -53307,6 +53939,7 @@ exports.useWebrtcSessionAddIceCandidate = useWebrtcSessionAddIceCandidate;
53307
53939
  exports.useWebrtcSessionCloseSession = useWebrtcSessionCloseSession;
53308
53940
  exports.useWebrtcSessionCreateSession = useWebrtcSessionCreateSession;
53309
53941
  exports.useWebrtcSessionGetIceCandidates = useWebrtcSessionGetIceCandidates;
53942
+ exports.useWebrtcSessionGetSessionDebug = useWebrtcSessionGetSessionDebug;
53310
53943
  exports.useWebrtcSessionGetSessionState = useWebrtcSessionGetSessionState;
53311
53944
  exports.useWebrtcSessionHandleAnswer = useWebrtcSessionHandleAnswer;
53312
53945
  exports.useWebrtcSessionHandleOffer = useWebrtcSessionHandleOffer;