Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ bandwidthRtc.sendDtmf("5", undefined, 200, 100); // 200ms tone, 100ms gap

- Description: returns a `CallStatsSnapshot` parsed from native WebRTC stats. Pass the previous snapshot to get `inboundBitrate`/`outboundBitrate` (bits/s).
- Fields: `packetsReceived`, `packetsLost`, `bytesReceived`, `jitter`, `audioLevel`, `packetsSent`, `bytesSent`, `roundTripTime`, `codec`, `inboundBitrate`, `outboundBitrate`, `timestamp` (seconds)
- Per peer connection: `publishPeer` and `subscribePeer`, each with audio RTP `packetsSent`, `bytesSent`, `packetsReceived`, `bytesReceived`. The publish peer normally only sends and the subscribe peer normally only receives.
- RTCP data from the remote receiver: `remoteFractionLost`, `remoteJitter`, `rtcpRoundTripTime`. These are 0 until the first receiver report arrives.
- With several audio streams, packet and byte counts are summed; jitter, loss, round trip time and audio level show the highest stream
- Debug log: while connected, a snapshot is logged every 5 minutes, and a final one when the call ends (disconnect, fatal error, or the signaling socket closing for good). While in a call (a subscribe stream is available), a snapshot is also logged every 10 seconds under "In-call stats", with a final one when the call ends. The two traces run independently and may log at the same time
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { default } from "./bandwidthRtc";
export { BandwidthRtc as BandwidthRtcV1 } from "./v1/bandwidthRtc";
export * from "./types";
export type { CallStatsSnapshot } from "./v1/types";
export type { CallStatsSnapshot, PeerCallStats } from "./v1/types";
68 changes: 68 additions & 0 deletions src/v1/bandwidthRtc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,74 @@ describe("bandwidthRtcV1 getCallStats", () => {
expect(snap.packetsReceived).toBe(0);
expect(snap.bytesSent).toBe(4000);
});

describe("per-peer counters", () => {
const zeros = { packetsSent: 0, bytesSent: 0, packetsReceived: 0, bytesReceived: 0 };

test("each peer takes its counters only from its own report", async () => {
const sub = report([
{ id: "in", type: "inbound-rtp", kind: "audio", packetsReceived: 10, bytesReceived: 2000 },
{ id: "out", type: "outbound-rtp", kind: "audio", packetsSent: 3, bytesSent: 300 },
]);
const pub = report([
{ id: "out", type: "outbound-rtp", kind: "audio", packetsSent: 20, bytesSent: 4000 },
{ id: "in", type: "inbound-rtp", kind: "audio", packetsReceived: 7, bytesReceived: 700 },
]);
const snap = await setup(jest.fn().mockResolvedValue(sub), jest.fn().mockResolvedValue(pub)).getCallStats();
expect(snap.publishPeer).toEqual({ packetsSent: 20, bytesSent: 4000, packetsReceived: 7, bytesReceived: 700 });
expect(snap.subscribePeer).toEqual({ packetsSent: 3, bytesSent: 300, packetsReceived: 10, bytesReceived: 2000 });
});

test("flat fields ignore the unexpected direction of each peer", async () => {
const sub = report([
{ id: "in", type: "inbound-rtp", kind: "audio", packetsReceived: 10, bytesReceived: 2000 },
{ id: "out", type: "outbound-rtp", kind: "audio", packetsSent: 3, bytesSent: 300 },
]);
const pub = report([
{ id: "out", type: "outbound-rtp", kind: "audio", packetsSent: 20, bytesSent: 4000 },
{ id: "in", type: "inbound-rtp", kind: "audio", packetsReceived: 7, bytesReceived: 700 },
]);
const snap = await setup(jest.fn().mockResolvedValue(sub), jest.fn().mockResolvedValue(pub)).getCallStats();
expect(snap).toMatchObject({ packetsReceived: 10, bytesReceived: 2000, packetsSent: 20, bytesSent: 4000 });
});

test("sums counters across audio streams of a peer", async () => {
const pub = report([
{ id: "out1", type: "outbound-rtp", kind: "audio", packetsSent: 20, bytesSent: 4000 },
{ id: "out2", type: "outbound-rtp", kind: "audio", packetsSent: 7, bytesSent: 900 },
]);
const sub = report([
{ id: "in1", type: "inbound-rtp", kind: "audio", packetsReceived: 10, bytesReceived: 2000 },
{ id: "in2", type: "inbound-rtp", kind: "audio", packetsReceived: 5, bytesReceived: 1000 },
]);
const snap = await setup(jest.fn().mockResolvedValue(sub), jest.fn().mockResolvedValue(pub)).getCallStats();
expect(snap.publishPeer).toEqual({ ...zeros, packetsSent: 27, bytesSent: 4900 });
expect(snap.subscribePeer).toEqual({ ...zeros, packetsReceived: 15, bytesReceived: 3000 });
});

test("ignores non-audio stats", async () => {
const sub = report([
{ id: "vin", type: "inbound-rtp", kind: "video", packetsReceived: 10, bytesReceived: 2000 },
{ id: "vout", type: "outbound-rtp", kind: "video", packetsSent: 3, bytesSent: 300 },
]);
const pub = report([
{ id: "vout", type: "outbound-rtp", kind: "video", packetsSent: 20, bytesSent: 4000 },
{ id: "vin", type: "inbound-rtp", kind: "video", packetsReceived: 7, bytesReceived: 700 },
]);
const snap = await setup(jest.fn().mockResolvedValue(sub), jest.fn().mockResolvedValue(pub)).getCallStats();
expect(snap.publishPeer).toEqual(zeros);
expect(snap.subscribePeer).toEqual(zeros);
});

test("a missing peer connection gives zeros for that peer", async () => {
const snap = await setup(jest.fn().mockResolvedValue(subReport), undefined).getCallStats();
expect(snap.publishPeer).toEqual(zeros);
expect(snap.subscribePeer).toEqual({ ...zeros, packetsReceived: 10, bytesReceived: 2000 });
const none = await setup(undefined, undefined).getCallStats();
expect(none.publishPeer).toEqual(zeros);
expect(none.subscribePeer).toEqual(zeros);
});
});
});

describe("bandwidthRtcV1 call stats trace", () => {
Expand Down
12 changes: 12 additions & 0 deletions src/v1/bandwidthRtc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,8 @@ export class BandwidthRtc {
remoteFractionLost: 0,
remoteJitter: 0,
rtcpRoundTripTime: 0,
publishPeer: { packetsSent: 0, bytesSent: 0, packetsReceived: 0, bytesReceived: 0 },
subscribePeer: { packetsSent: 0, bytesSent: 0, packetsReceived: 0, bytesReceived: 0 },
};
// Read the peer connections now; they are replaced on reconnect
const subPc = this.subscribingPeerConnection;
Expand All @@ -511,9 +513,14 @@ export class BandwidthRtc {
snapshot.packetsReceived += stat.packetsReceived ?? 0;
snapshot.packetsLost += stat.packetsLost ?? 0;
snapshot.bytesReceived += stat.bytesReceived ?? 0;
snapshot.subscribePeer.packetsReceived += stat.packetsReceived ?? 0;
snapshot.subscribePeer.bytesReceived += stat.bytesReceived ?? 0;
snapshot.jitter = Math.max(snapshot.jitter, stat.jitter ?? 0);
snapshot.audioLevel = Math.max(snapshot.audioLevel, stat.audioLevel ?? 0);
codecId = codecId ?? stat.codecId;
} else if (stat.type === "outbound-rtp" && stat.kind === "audio") {
snapshot.subscribePeer.packetsSent += stat.packetsSent ?? 0;
snapshot.subscribePeer.bytesSent += stat.bytesSent ?? 0;
} else if (stat.type === "transport") {
selectedPairId = stat.selectedCandidatePairId;
} else if (stat.type === "candidate-pair" && (stat.selected || (stat.nominated && stat.state === "succeeded"))) {
Expand All @@ -535,6 +542,11 @@ export class BandwidthRtc {
if (stat.type === "outbound-rtp") {
snapshot.packetsSent += stat.packetsSent ?? 0;
snapshot.bytesSent += stat.bytesSent ?? 0;
snapshot.publishPeer.packetsSent += stat.packetsSent ?? 0;
snapshot.publishPeer.bytesSent += stat.bytesSent ?? 0;
} else if (stat.type === "inbound-rtp") {
snapshot.publishPeer.packetsReceived += stat.packetsReceived ?? 0;
snapshot.publishPeer.bytesReceived += stat.bytesReceived ?? 0;
} else if (stat.type === "remote-inbound-rtp") {
snapshot.remoteFractionLost = Math.max(snapshot.remoteFractionLost, stat.fractionLost ?? 0);
snapshot.remoteJitter = Math.max(snapshot.remoteJitter, stat.jitter ?? 0);
Expand Down
12 changes: 12 additions & 0 deletions src/v1/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ export interface ReadyMetadata {
region: string; // TODO enum
}

/** Audio RTP counters of one peer connection, summed over its audio streams */
export interface PeerCallStats {
packetsSent: number;
bytesSent: number;
packetsReceived: number;
bytesReceived: number;
}

/**
* Call quality snapshot parsed from native WebRTC stats. Times are in seconds, bitrates in bits/s.
* A field the browser has not reported yet is 0, so 0 does not always mean a perfect value.
Expand All @@ -130,4 +138,8 @@ export interface CallStatsSnapshot {
remoteJitter: number;
/** RTCP remote-inbound-rtp roundTripTime */
rtcpRoundTripTime: number;
/** Counters of the peer connection that sends local media. Normally only the sent fields are non-zero. */
publishPeer: PeerCallStats;
/** Counters of the peer connection that receives remote media. Normally only the received fields are non-zero. */
subscribePeer: PeerCallStats;
}
Loading