@camstack/ui-library 1.2.181 → 1.2.183

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`. */
@@ -26976,6 +26978,590 @@ async function measureDownlinkMbps(origin, deps) {
26976
26978
  }
26977
26979
  }
26978
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, window, strong = false }) {
27240
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
27241
+ className: "flex items-baseline justify-between gap-2 leading-[1.25]",
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.jsxs)("span", {
27246
+ className: "flex items-baseline gap-1 text-right",
27247
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27248
+ className: strong ? "text-white font-semibold tabular-nums" : "text-white/90 tabular-nums",
27249
+ children: value
27250
+ }), window !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27251
+ className: "text-sky-300/80 tabular-nums",
27252
+ children: window
27253
+ })]
27254
+ })]
27255
+ });
27256
+ }
27257
+ function SectionHeader({ title, measuredAt, now }) {
27258
+ const stale = measuredAt !== null && now - measuredAt > STALE_AFTER_MS;
27259
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
27260
+ className: "flex items-baseline justify-between gap-3 mt-2 first:mt-0 pb-0.5 border-b border-white/20",
27261
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27262
+ className: "text-white/80 font-semibold tracking-wide uppercase text-[9px]",
27263
+ children: title
27264
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
27265
+ className: stale ? "text-amber-300 text-[9px]" : "text-white/45 text-[9px]",
27266
+ children: [stale ? "STALE · " : "", age(measuredAt, now)]
27267
+ })]
27268
+ });
27269
+ }
27270
+ /** The pill's dot: the verdict, in the one place that is always visible. */
27271
+ var VERDICT_DOT = {
27272
+ unknown: "bg-white/50",
27273
+ decoding: "bg-emerald-400",
27274
+ "decoder-stall": "bg-amber-400",
27275
+ "transport-stall": "bg-rose-400",
27276
+ "frozen-unknown": "bg-white/50"
27277
+ };
27278
+ function StreamDebugOverlay(props) {
27279
+ const { stats, server, serverError, serverWired, now, sessionId } = props;
27280
+ const verdict = freezeVerdict(stats);
27281
+ const win = stats?.window;
27282
+ const [expanded, setExpanded] = (0, react$1.useState)(false);
27283
+ const stale = stats !== null && now - stats.at > STALE_AFTER_MS;
27284
+ if (!expanded) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
27285
+ type: "button",
27286
+ onClick: () => setExpanded(true),
27287
+ className: "absolute bottom-10 left-2 z-20 transform-gpu flex items-center gap-1.5 rounded bg-neutral-950/90 ring-1 ring-white/20 px-1.5 py-0.5 text-[9px] font-mono text-white/85 tabular-nums select-text",
27288
+ "data-testid": "stream-debug-pill",
27289
+ title: verdict.label,
27290
+ children: [
27291
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `h-1.5 w-1.5 rounded-full ${VERDICT_DOT[verdict.kind]}` }),
27292
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: stats?.width !== null && stats?.width !== void 0 ? `${stats.width}×${stats.height}` : UNKNOWN }),
27293
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [n(stats?.fps), "fps"] }),
27294
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [n(win?.kbps), "k"] }),
27295
+ stale && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27296
+ className: "text-amber-300",
27297
+ children: "stale"
27298
+ })
27299
+ ]
27300
+ });
27301
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
27302
+ className: "absolute bottom-10 left-2 z-20 transform-gpu w-[13.5rem] max-h-[70%] overflow-y-auto rounded-md bg-neutral-950/95 ring-1 ring-white/25 shadow-lg px-1.5 py-1 text-[9px] font-mono text-white/90 select-text",
27303
+ "data-testid": "stream-debug-overlay",
27304
+ children: [
27305
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
27306
+ type: "button",
27307
+ onClick: () => setExpanded(false),
27308
+ className: `w-full text-left rounded px-1.5 py-0.5 mb-1 text-[9px] font-semibold ${VERDICT_CLASS[verdict.kind]}`,
27309
+ children: verdict.label
27310
+ }),
27311
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionHeader, {
27312
+ title: "decode",
27313
+ measuredAt: stats?.at ?? null,
27314
+ now
27315
+ }),
27316
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27317
+ label: "framesDecoded",
27318
+ value: n(stats?.framesDecoded),
27319
+ window: win?.framesDecoded === null ? void 0 : `+${n(win?.framesDecoded)}`,
27320
+ strong: true
27321
+ }),
27322
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27323
+ label: "freezeCount",
27324
+ value: n(stats?.freezeCount),
27325
+ strong: true
27326
+ }),
27327
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27328
+ label: "frozen",
27329
+ value: n(stats?.totalFreezeMs, { suffix: "ms" }),
27330
+ window: win?.freezeMs === null ? void 0 : `+${n(win?.freezeMs)}ms`,
27331
+ strong: true
27332
+ }),
27333
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27334
+ label: "pliCount",
27335
+ value: n(stats?.pliCount),
27336
+ window: win?.pliCount === null ? void 0 : `+${n(win?.pliCount)}`,
27337
+ strong: true
27338
+ }),
27339
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27340
+ label: "framesDropped",
27341
+ value: n(stats?.framesDropped)
27342
+ }),
27343
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27344
+ label: "decode",
27345
+ value: n(stats?.avgDecodeMs, {
27346
+ digits: 1,
27347
+ suffix: "ms"
27348
+ }),
27349
+ window: win?.decodeMs === null ? void 0 : `${n(win?.decodeMs, { digits: 1 })}ms`
27350
+ }),
27351
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionHeader, {
27352
+ title: "picture",
27353
+ measuredAt: stats?.at ?? null,
27354
+ now
27355
+ }),
27356
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27357
+ label: "resolution",
27358
+ value: stats?.width !== null && stats?.width !== void 0 && stats.height !== null ? `${stats.width}×${stats.height}` : UNKNOWN
27359
+ }),
27360
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27361
+ label: "fps",
27362
+ value: n(stats?.fps)
27363
+ }),
27364
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27365
+ label: "codec",
27366
+ value: stats?.codec === null || stats?.codec === void 0 ? UNKNOWN : `${stats.codec}${stats.codecProfileLevelId !== null ? ` ${stats.codecProfileLevelId}` : ""}`
27367
+ }),
27368
+ (stats?.resolutionLog.length ?? 0) > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
27369
+ className: "mt-0.5 text-white/60 leading-tight",
27370
+ children: stats?.resolutionLog.map((mark) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
27371
+ className: "flex justify-between",
27372
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
27373
+ mark.width,
27374
+ "×",
27375
+ mark.height
27376
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
27377
+ className: "text-white/40",
27378
+ children: age(mark.at, now)
27379
+ })]
27380
+ }, `${mark.at}-${mark.width}x${mark.height}`))
27381
+ }),
27382
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionHeader, {
27383
+ title: "transport",
27384
+ measuredAt: stats?.at ?? null,
27385
+ now
27386
+ }),
27387
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27388
+ label: "bitrate",
27389
+ value: n(win?.kbps, { suffix: " kbps" })
27390
+ }),
27391
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27392
+ label: "rtt",
27393
+ value: n(stats?.rttMs, {
27394
+ digits: 0,
27395
+ suffix: "ms"
27396
+ })
27397
+ }),
27398
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27399
+ label: "jitter",
27400
+ value: n(stats?.jitterMs, {
27401
+ digits: 1,
27402
+ suffix: "ms"
27403
+ })
27404
+ }),
27405
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27406
+ label: "packetsLost",
27407
+ value: n(stats?.packetsLost),
27408
+ window: win?.packetsLost === null ? void 0 : `+${n(win?.packetsLost)}`
27409
+ }),
27410
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27411
+ label: "loss",
27412
+ value: n(stats?.lossPercent, {
27413
+ digits: 2,
27414
+ suffix: "%"
27415
+ }),
27416
+ window: win?.lossPercent === null ? void 0 : n(win?.lossPercent, {
27417
+ digits: 2,
27418
+ suffix: "%"
27419
+ })
27420
+ }),
27421
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27422
+ label: "nackCount",
27423
+ value: n(stats?.nackCount),
27424
+ window: win?.nackCount === null ? void 0 : `+${n(win?.nackCount)}`
27425
+ }),
27426
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27427
+ label: "candidate pair",
27428
+ value: s(stats?.candidatePairType)
27429
+ }),
27430
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27431
+ label: "protocol",
27432
+ value: s(stats?.candidateProtocol)
27433
+ }),
27434
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27435
+ label: "ice",
27436
+ value: s(stats?.iceConnectionState)
27437
+ }),
27438
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27439
+ label: "dtls",
27440
+ value: s(stats?.dtlsState)
27441
+ }),
27442
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionHeader, {
27443
+ title: "server",
27444
+ measuredAt: server?.observedAt ?? null,
27445
+ now
27446
+ }),
27447
+ !serverWired ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
27448
+ className: "text-white/50 leading-tight py-0.5",
27449
+ children: "not wired on this host"
27450
+ }) : serverError !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
27451
+ className: "text-rose-300 leading-tight py-0.5 break-words",
27452
+ children: serverError
27453
+ }) : server === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
27454
+ className: "text-white/50 leading-tight py-0.5",
27455
+ children: "waiting for the first answer…"
27456
+ }) : !server.alive ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
27457
+ className: "text-amber-300 leading-tight py-0.5",
27458
+ children: "session not found on the server — it ended, or this node never had it"
27459
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
27460
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27461
+ label: "served rung",
27462
+ value: s(server.servedTier),
27463
+ strong: true
27464
+ }),
27465
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27466
+ label: "adaptive",
27467
+ value: b(server.adaptive)
27468
+ }),
27469
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27470
+ label: "source",
27471
+ value: s(server.camStreamId)
27472
+ }),
27473
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27474
+ label: "source kind",
27475
+ value: server.sourceKind
27476
+ }),
27477
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27478
+ label: "ingest node",
27479
+ value: s(server.ingestNodeId)
27480
+ }),
27481
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27482
+ label: "· remote",
27483
+ value: b(server.ingestIsRemote)
27484
+ }),
27485
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27486
+ label: "egress leg",
27487
+ value: server.egressLeg,
27488
+ strong: true
27489
+ }),
27490
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27491
+ label: "codec src→out",
27492
+ value: `${s(server.sourceCodec)}→${s(server.negotiatedCodec)}`
27493
+ }),
27494
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27495
+ label: "broker",
27496
+ value: s(server.brokerStatus)
27497
+ }),
27498
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27499
+ label: "· bitrate",
27500
+ value: n(server.brokerBitrateKbps, { suffix: " kbps" })
27501
+ }),
27502
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27503
+ label: "· input fps",
27504
+ value: n(server.brokerInputFps)
27505
+ }),
27506
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27507
+ label: "rtp sent",
27508
+ value: n(server.rtpPacketsSent)
27509
+ }),
27510
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27511
+ label: "track mode",
27512
+ value: s(server.trackMode)
27513
+ }),
27514
+ server.lastDecision === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27515
+ label: "ladder",
27516
+ value: "never moved"
27517
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
27518
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27519
+ label: "ladder move",
27520
+ value: `${server.lastDecision.cause}${server.lastDecision.tier !== null ? ` → ${server.lastDecision.tier}` : ""}`,
27521
+ strong: true
27522
+ }),
27523
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27524
+ label: "· when",
27525
+ value: age(server.lastDecision.at, now)
27526
+ }),
27527
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27528
+ label: "· lossRatio",
27529
+ value: n(server.lastDecision.lossRatio, { digits: 4 })
27530
+ }),
27531
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27532
+ label: "· clientKbps",
27533
+ value: n(server.lastDecision.clientKbps, { suffix: " kbps" })
27534
+ }),
27535
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27536
+ label: "· stalledFor",
27537
+ value: n(server.lastDecision.stalledForMs, { suffix: "ms" })
27538
+ }),
27539
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27540
+ label: "· serverSending",
27541
+ value: b(server.lastDecision.serverSending)
27542
+ }),
27543
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27544
+ label: "· transcode-down",
27545
+ value: b(server.lastDecision.forcedTranscodeDown)
27546
+ })
27547
+ ] }),
27548
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27549
+ label: "promotions",
27550
+ value: n(server.upgradeCount)
27551
+ }),
27552
+ server.tierFailures.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
27553
+ label: "tier failures",
27554
+ value: server.tierFailures.map((f) => `${f.tier}:${f.failures}`).join(" ")
27555
+ })
27556
+ ] }),
27557
+ sessionId !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
27558
+ className: "mt-1 pt-0.5 border-t border-white/15 text-white/40 break-all leading-tight",
27559
+ children: sessionId
27560
+ })
27561
+ ]
27562
+ });
27563
+ }
27564
+ //#endregion
26979
27565
  //#region src/composites/camera-stream-player.tsx
26980
27566
  /**
26981
27567
  * Silence (or restore) a live WebRTC stream FOR REAL.
@@ -27074,7 +27660,7 @@ function mergePlayerHints(autoHints, override, downlinkMbps, coldStart) {
27074
27660
  return coldStart ? stripDownlinkForColdStart(merged) : merged;
27075
27661
  }
27076
27662
  var FIRST_FRAME_TIMEOUT_MS = 8e3;
27077
- 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 }) {
27663
+ 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 }) {
27078
27664
  const videoRef = (0, react$1.useRef)(null);
27079
27665
  const containerRef = (0, react$1.useRef)(null);
27080
27666
  const pcRef = (0, react$1.useRef)(null);
@@ -27971,55 +28557,77 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
27971
28557
  const webkit = video !== null && typeof video.webkitSetPresentationMode === "function";
27972
28558
  setIsPipSupported(standard || webkit);
27973
28559
  }, []);
27974
- const [statsText, setStatsText] = (0, react$1.useState)("");
27975
- const statsPrev = (0, react$1.useRef)(null);
28560
+ const [debugStats, setDebugStats] = (0, react$1.useState)(null);
28561
+ const [serverDebug, setServerDebug] = (0, react$1.useState)(null);
28562
+ const [serverDebugError, setServerDebugError] = (0, react$1.useState)(null);
28563
+ const [debugNow, setDebugNow] = (0, react$1.useState)(() => Date.now());
28564
+ (0, react$1.useEffect)(() => {
28565
+ if (!showStats) return;
28566
+ const id = setInterval(() => setDebugNow(Date.now()), 500);
28567
+ return () => clearInterval(id);
28568
+ }, [showStats]);
27976
28569
  (0, react$1.useEffect)(() => {
27977
28570
  if (!showStats) {
27978
- setStatsText("");
27979
- statsPrev.current = null;
28571
+ setDebugStats(null);
27980
28572
  return;
27981
28573
  }
28574
+ const prev = { current: null };
28575
+ let cancelled = false;
27982
28576
  const tick = async () => {
27983
28577
  const pc = pcRef.current;
27984
28578
  if (!pc) return;
27985
28579
  try {
27986
28580
  const report = await pc.getStats();
27987
- let bytes = 0;
27988
- let ts = 0;
27989
- let fps = 0;
27990
- let w = 0;
27991
- let h = 0;
27992
- let rtt = 0;
27993
- report.forEach((raw) => {
27994
- const s = raw;
27995
- if (s.type === "inbound-rtp" && (s.kind ?? s.mediaType) === "video") {
27996
- bytes = s.bytesReceived ?? 0;
27997
- ts = s.timestamp;
27998
- fps = s.framesPerSecond ?? 0;
27999
- w = s.frameWidth ?? 0;
28000
- h = s.frameHeight ?? 0;
28001
- } else if (s.type === "candidate-pair" && s.nominated && typeof s.currentRoundTripTime === "number") rtt = s.currentRoundTripTime;
28581
+ if (cancelled) return;
28582
+ const next = deriveWebrtcDebugStats({
28583
+ report,
28584
+ prev: prev.current,
28585
+ now: Date.now(),
28586
+ iceConnectionState: pc.iceConnectionState,
28587
+ dtlsState: pc.connectionState
28002
28588
  });
28003
- let kbps = 0;
28004
- if (statsPrev.current && ts > statsPrev.current.ts) kbps = Math.round((bytes - statsPrev.current.bytes) * 8 / (ts - statsPrev.current.ts));
28005
- statsPrev.current = {
28006
- bytes,
28007
- ts
28008
- };
28009
- const parts = [];
28010
- if (w && h) parts.push(`${w}×${h}`);
28011
- if (fps) parts.push(`${Math.round(fps)}fps`);
28012
- if (kbps > 0) parts.push(`${kbps} kbps`);
28013
- if (rtt) parts.push(`${Math.round(rtt * 1e3)}ms`);
28014
- setStatsText(parts.join(" · "));
28589
+ prev.current = next;
28590
+ setDebugStats(next);
28015
28591
  } catch {}
28016
28592
  };
28017
28593
  tick();
28018
28594
  const id = setInterval(() => {
28019
28595
  tick();
28020
28596
  }, 1e3);
28021
- return () => clearInterval(id);
28597
+ return () => {
28598
+ cancelled = true;
28599
+ clearInterval(id);
28600
+ };
28022
28601
  }, [showStats]);
28602
+ (0, react$1.useEffect)(() => {
28603
+ if (!showStats || !getSessionDebug) {
28604
+ setServerDebug(null);
28605
+ setServerDebugError(null);
28606
+ return;
28607
+ }
28608
+ let cancelled = false;
28609
+ const tick = async () => {
28610
+ const sid = activeSessionIdRef.current;
28611
+ if (sid === null) return;
28612
+ try {
28613
+ const answer = await getSessionDebug(sid);
28614
+ if (cancelled) return;
28615
+ setServerDebug(answer);
28616
+ setServerDebugError(null);
28617
+ } catch (err) {
28618
+ if (cancelled) return;
28619
+ setServerDebugError(err instanceof Error ? err.message : String(err));
28620
+ }
28621
+ };
28622
+ tick();
28623
+ const id = setInterval(() => {
28624
+ tick();
28625
+ }, 1e3);
28626
+ return () => {
28627
+ cancelled = true;
28628
+ clearInterval(id);
28629
+ };
28630
+ }, [showStats, getSessionDebug]);
28023
28631
  const netPrev = (0, react$1.useRef)(null);
28024
28632
  (0, react$1.useEffect)(() => {
28025
28633
  if (!onClientNetworkSample) {
@@ -28206,9 +28814,13 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
28206
28814
  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",
28207
28815
  children: label
28208
28816
  }),
28209
- showStats && statsText && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
28210
- 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",
28211
- children: statsText
28817
+ showStats && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StreamDebugOverlay, {
28818
+ stats: debugStats,
28819
+ server: serverDebug,
28820
+ serverError: serverDebugError,
28821
+ serverWired: getSessionDebug !== void 0,
28822
+ now: debugNow,
28823
+ sessionId: activeSessionIdRef.current
28212
28824
  }),
28213
28825
  overlay,
28214
28826
  showControls && !stillImg && state === "connecting" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -51561,6 +52173,13 @@ function useDeviceWebrtc(trpc, deviceId, pollIntervalMs = 5e3) {
51561
52173
  } catch {
51562
52174
  return { pendingRenegotiation: null };
51563
52175
  }
52176
+ }, [trpc, deviceId]),
52177
+ getSessionDebug: (0, react$1.useCallback)(async (sessionId) => {
52178
+ if (deviceId === null) throw new Error("getSessionDebug: no device bound to this player");
52179
+ return trpc.webrtcSession.getSessionDebug.query({
52180
+ deviceId,
52181
+ sessionId
52182
+ });
51564
52183
  }, [trpc, deviceId])
51565
52184
  };
51566
52185
  }
@@ -52077,6 +52696,7 @@ exports.StepTimings = StepTimings;
52077
52696
  exports.StepTreeMaster = StepTreeMaster;
52078
52697
  exports.Stepper = Stepper;
52079
52698
  exports.StreamBrokerSelector = StreamBrokerSelector;
52699
+ exports.StreamDebugOverlay = StreamDebugOverlay;
52080
52700
  exports.StreamPanel = StreamPanel;
52081
52701
  exports.Switch = Switch;
52082
52702
  exports.SwitchHeroCard = SwitchHeroCard;
@@ -52142,6 +52762,7 @@ exports.cursorFractionFor = cursorFractionFor;
52142
52762
  exports.darkColors = require_theme_index.darkColors$1;
52143
52763
  exports.defaultTheme = require_theme_index.defaultTheme$1;
52144
52764
  exports.deriveDeviceKind = deriveDeviceKind;
52765
+ exports.deriveWebrtcDebugStats = deriveWebrtcDebugStats;
52145
52766
  exports.describeDeviceSelector = describeDeviceSelector;
52146
52767
  exports.deviceMatchesFilter = deviceMatchesFilter;
52147
52768
  exports.deviceOptionLabel = deviceOptionLabel;
@@ -52158,6 +52779,7 @@ exports.findTimezone = _camstack_types.findTimezone;
52158
52779
  exports.formatControlDateTime = formatControlDateTime;
52159
52780
  exports.formatLastSeen = formatLastSeen;
52160
52781
  exports.formatNumeric = formatNumeric;
52782
+ exports.freezeVerdict = freezeVerdict;
52161
52783
  exports.fuzzyMatch = fuzzyMatch;
52162
52784
  exports.getClassColor = getClassColor;
52163
52785
  exports.getPhaseVisual = getPhaseVisual;
@@ -53327,6 +53949,7 @@ exports.useWebrtcSessionAddIceCandidate = useWebrtcSessionAddIceCandidate;
53327
53949
  exports.useWebrtcSessionCloseSession = useWebrtcSessionCloseSession;
53328
53950
  exports.useWebrtcSessionCreateSession = useWebrtcSessionCreateSession;
53329
53951
  exports.useWebrtcSessionGetIceCandidates = useWebrtcSessionGetIceCandidates;
53952
+ exports.useWebrtcSessionGetSessionDebug = useWebrtcSessionGetSessionDebug;
53330
53953
  exports.useWebrtcSessionGetSessionState = useWebrtcSessionGetSessionState;
53331
53954
  exports.useWebrtcSessionHandleAnswer = useWebrtcSessionHandleAnswer;
53332
53955
  exports.useWebrtcSessionHandleOffer = useWebrtcSessionHandleOffer;