feat(web): observe selected pair responses (#181)
Add local, generation-guarded selected-pair response evidence without granting it routing or recovery authority.
This commit is contained in:
1 parent
9980376a80
commit
f81577c4c5
10 files changed
+285
-2
No files matched your search
@@ -53,6 +53,21 @@ function candidateEndpoint(
|
||||
return `${host}:${port}`;
|
||||
}
|
||||
|
||||
function candidatePairResponses(metrics: ConnectionMetrics): string {
|
||||
if (metrics.candidatePairResponsesReceived === null) {
|
||||
return "未知";
|
||||
}
|
||||
if (
|
||||
metrics.intervalCandidatePairResponsesReceived === null ||
|
||||
metrics.candidatePairSampleWindowMs === null
|
||||
) {
|
||||
return `${metrics.candidatePairResponsesReceived} 累计 · 区间未知`;
|
||||
}
|
||||
return `${metrics.candidatePairResponsesReceived} 累计 · +${
|
||||
metrics.intervalCandidatePairResponsesReceived
|
||||
} / ${(metrics.candidatePairSampleWindowMs / 1_000).toFixed(1)} s`;
|
||||
}
|
||||
|
||||
function Metric({ label, value, title }: { label: string; value: string; title?: string }) {
|
||||
return (
|
||||
<div className="metric" title={title}>
|
||||
@@ -164,6 +179,16 @@ export function StatsGrid({
|
||||
title={remoteCandidateEndpoint}
|
||||
/>
|
||||
)}
|
||||
<Metric
|
||||
label="候选对 ID"
|
||||
value={metrics.selectedCandidatePairId ?? "未知"}
|
||||
title={metrics.selectedCandidatePairId ?? undefined}
|
||||
/>
|
||||
<Metric
|
||||
label="STUN 响应"
|
||||
value={candidatePairResponses(metrics)}
|
||||
title="当前候选对的累计响应,以及同一候选对相邻有效采样窗的增量;零增量不代表断链"
|
||||
/>
|
||||
<Metric label="视频 Codec" value={metrics.codec ?? "未知"} />
|
||||
{metrics.codecProfile && (
|
||||
<Metric label="视频 Codec profile token" value={metrics.codecProfile} />
|
||||
|
||||
@@ -4,6 +4,8 @@ export const DIAGNOSTIC_SCHEMA_VERSION = 1;
|
||||
|
||||
const DIAGNOSTIC_METRIC_KEYS = [
|
||||
"sampleWindowMs", "captureWidth", "captureHeight",
|
||||
"candidatePairResponsesReceived", "intervalCandidatePairResponsesReceived",
|
||||
"candidatePairSampleWindowMs",
|
||||
"captureFramesPerSecond", "mediaSourceFramesPerSecond", "path",
|
||||
"iceProtocol", "localRelayProtocol", "localCandidateType",
|
||||
"remoteCandidateType", "rttMs", "bitrateKbps", "availableOutgoingKbps",
|
||||
|
||||
@@ -15,6 +15,9 @@ export interface ConnectionMetrics {
|
||||
rtpRid: string | null;
|
||||
trackIdentifier: string | null;
|
||||
selectedCandidatePairId: string | null;
|
||||
candidatePairResponsesReceived: number | null;
|
||||
intervalCandidatePairResponsesReceived: number | null;
|
||||
candidatePairSampleWindowMs: number | null;
|
||||
captureWidth: number | null;
|
||||
captureHeight: number | null;
|
||||
captureFramesPerSecond: number | null;
|
||||
@@ -93,6 +96,9 @@ export const EMPTY_METRICS: ConnectionMetrics = {
|
||||
rtpRid: null,
|
||||
trackIdentifier: null,
|
||||
selectedCandidatePairId: null,
|
||||
candidatePairResponsesReceived: null,
|
||||
intervalCandidatePairResponsesReceived: null,
|
||||
candidatePairSampleWindowMs: null,
|
||||
captureWidth: null,
|
||||
captureHeight: null,
|
||||
captureFramesPerSecond: null,
|
||||
|
||||
@@ -10,7 +10,12 @@ type StatsRecord = Record<string, unknown> & {
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
const MAX_CANDIDATE_PAIR_SAMPLE_WINDOW_MS = 5_000;
|
||||
|
||||
export interface StatsAccumulator {
|
||||
candidatePairId: string | null;
|
||||
candidatePairResponsesReceived: number | null;
|
||||
candidatePairTimestamp: number | null;
|
||||
mediaId: string | null;
|
||||
ssrc: number | null;
|
||||
trackIdentifier: string | null;
|
||||
@@ -76,6 +81,9 @@ export function captureMetrics(
|
||||
|
||||
export function createStatsAccumulator(): StatsAccumulator {
|
||||
return {
|
||||
candidatePairId: null,
|
||||
candidatePairResponsesReceived: null,
|
||||
candidatePairTimestamp: null,
|
||||
mediaId: null,
|
||||
ssrc: null,
|
||||
trackIdentifier: null,
|
||||
@@ -354,6 +362,16 @@ function positiveIntegerValue(
|
||||
return value !== null && Number.isInteger(value) && value > 0 ? value : null;
|
||||
}
|
||||
|
||||
function nonNegativeIntegerValue(
|
||||
record: StatsRecord | null,
|
||||
key: string,
|
||||
): number | null {
|
||||
const value = numberValue(record, key);
|
||||
return value !== null && Number.isSafeInteger(value) && value >= 0
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
function candidateAddressValue(record: StatsRecord | null): string | null {
|
||||
const value = stringValue(record, "address");
|
||||
return value !== null &&
|
||||
@@ -433,6 +451,34 @@ export function collectConnectionMetricsFromReport(
|
||||
? "direct"
|
||||
: "unknown";
|
||||
|
||||
const candidatePairId = pair?.id ?? null;
|
||||
const candidatePairResponsesReceived = nonNegativeIntegerValue(
|
||||
pair,
|
||||
"responsesReceived",
|
||||
);
|
||||
const candidatePairTimestamp = numberValue(pair, "timestamp");
|
||||
const candidatePairSampleWindowMs =
|
||||
candidatePairId !== null &&
|
||||
candidatePairId === previous.candidatePairId &&
|
||||
candidatePairResponsesReceived !== null &&
|
||||
previous.candidatePairResponsesReceived !== null &&
|
||||
candidatePairResponsesReceived >= previous.candidatePairResponsesReceived &&
|
||||
candidatePairTimestamp !== null &&
|
||||
previous.candidatePairTimestamp !== null &&
|
||||
candidatePairTimestamp > previous.candidatePairTimestamp &&
|
||||
candidatePairTimestamp - previous.candidatePairTimestamp <=
|
||||
MAX_CANDIDATE_PAIR_SAMPLE_WINDOW_MS
|
||||
? candidatePairTimestamp - previous.candidatePairTimestamp
|
||||
: null;
|
||||
const intervalCandidatePairResponsesReceived = intervalDelta(
|
||||
candidatePairResponsesReceived,
|
||||
previous.candidatePairResponsesReceived,
|
||||
candidatePairSampleWindowMs !== null,
|
||||
);
|
||||
previous.candidatePairId = candidatePairId;
|
||||
previous.candidatePairResponsesReceived = candidatePairResponsesReceived;
|
||||
previous.candidatePairTimestamp = candidatePairTimestamp;
|
||||
|
||||
const mediaId = media?.id ?? null;
|
||||
const ssrc = numberValue(media, "ssrc");
|
||||
const trackIdentifier = media
|
||||
@@ -763,6 +809,9 @@ export function collectConnectionMetricsFromReport(
|
||||
rtpRid: stringValue(media, "rid"),
|
||||
trackIdentifier,
|
||||
selectedCandidatePairId: pair?.id ?? null,
|
||||
candidatePairResponsesReceived,
|
||||
intervalCandidatePairResponsesReceived,
|
||||
candidatePairSampleWindowMs,
|
||||
path,
|
||||
iceProtocol,
|
||||
localRelayProtocol,
|
||||
|
||||
Reference in new issue
Block a user