Skip to content

Commit

Permalink
Develop refactor stats adapter (#43)
Browse files Browse the repository at this point in the history
* save

* save
  • Loading branch information
balazskreith authored Feb 18, 2025
1 parent 2fd72f0 commit 1d1b41c
Show file tree
Hide file tree
Showing 16 changed files with 635 additions and 232 deletions.
27 changes: 22 additions & 5 deletions src/ClientMonitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,18 @@ export declare interface ClientMonitor {
export class ClientMonitor extends EventEmitter {
public static readonly samplingSchemaVersion = schemaVersion;

public readonly statsAdapters = new StatsAdapters();
// public readonly statsAdapters = new StatsAdapters();
public readonly mappedPeerConnections = new Map<string, PeerConnectionMonitor>();
public readonly detectors: Detectors;


public scoreCalculator: ScoreCalculator;
public bufferingSampleData: boolean;
public closed = false;
public lastSampledAt = 0;
public lastCollectingStatsAt = 0;


public cpuPerformanceAlertOn = false;

public sendingAudioBitrate = -1;
Expand All @@ -62,6 +64,10 @@ export class ClientMonitor extends EventEmitter {
public avgRttInSec = -1;
public score = 5.0;

private _browser?: {
name: 'chrome' | 'firefox' | 'safari' | 'edge' | 'opera' | 'unknown';
version: string;
}
private readonly _sources: Sources;
private _timer?: ReturnType<typeof setInterval>;
private _samplingTick = 0;
Expand Down Expand Up @@ -160,6 +166,20 @@ export class ClientMonitor extends EventEmitter {
public set callId(callId: string | undefined) { this.config.callId = callId; }
public get appData() { return this.config.appData; }
public set appData(appData: Record<string, unknown> | undefined) { this.config.appData = appData; }
public set browser(browser: { name: 'chrome' | 'firefox' | 'safari' | 'edge' | 'opera' | 'unknown', version: string } | undefined) {
if (this.closed || !browser) return;
if (this._browser) throw new Error('Browser info is already set, cannot change it');

this._browser = browser;

for (const peerConnection of this.peerConnections) {
this._sources.addStatsAdapters(peerConnection);
}
}

public get browser() {
return this._browser;
}

public close(): void {
if (this.closed) {
Expand Down Expand Up @@ -188,10 +208,7 @@ export class ClientMonitor extends EventEmitter {

await Promise.allSettled(this.peerConnections.map(async (peerConnection) => {
try {
const collectedStats = await peerConnection.getStats();
const adaptedStats = this.statsAdapters.adapt(collectedStats);

peerConnection.accept(adaptedStats);
const collectedStats = await peerConnection.collect();

result.push([peerConnection.peerConnectionId, collectedStats as RTCStats[]]);
} catch (err) {
Expand Down
32 changes: 32 additions & 0 deletions src/adapters/Firefox94StatsAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { RtcStats } from "../schema/W3cStatsIdentifiers";

export class Firefox94StatsAdapter {
public readonly name = 'firefox94StatsAdapter';

public adapt(rtcStats: RtcStats[]): RtcStats[] {
for (const rtcStatValue of rtcStats) {
if (!rtcStatValue) continue;
const rawType = rtcStatValue.type;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rawValue = rtcStatValue as any;
if (!rawType || typeof rawType !== "string") continue;
if (
rawType === "inbound-rtp" ||
rawType === "outbound-rtp" ||
rawType === "remote-inbound-rtp" ||
rawType === "remote-outbound-rtp"
) {
if (rawValue.mediaType && !rawValue.kind) {
rawValue.kind = rawValue.mediaType;
delete rawValue.mediaType;
}
}
// firefox put the track identifier inside brackets ({})
if (rawValue.trackIdentifier) {
rawValue.trackIdentifier = rawValue.trackIdentifier.replace("{", "").replace("}", "");
}
}

return rtcStats;
}
}
79 changes: 79 additions & 0 deletions src/adapters/FirefoxTransportStatsAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { IceCandidatePairStats, IceTransportStats } from "../schema/ClientSample";
import { RtcStats } from "../schema/W3cStatsIdentifiers";
import { StatsAdapter } from "./StatsAdapter";
import * as W3C from "../schema/W3cStatsIdentifiers";

type SelectedIceCandidatePairStats = (RtcStats & IceCandidatePairStats & { selected?: boolean });

export class FirefoxTransportStatsAdapter implements StatsAdapter {
public readonly name = 'FirefoxTransportStatsAdapter';

private _selectedCandidatePair?: SelectedIceCandidatePairStats;

public stats: IceTransportStats & RtcStats = {
type: 'transport',
timestamp: 0,
id: '',
packetsSent: 0,
packetsReceived: 0,
bytesSent: 0,
bytesReceived: 0,
// iceRole: 'unknown';
// iceLocalUsernameFragment?: string;
// dtlsState?: string;
// iceState?: string;
selectedCandidatePairId: undefined,
// localCertificateId?: string;
// remoteCertificateId?: string;
// tlsVersion?: string;
// dtlsCipher?: string;
// dtlsRole?: string;
// srtpCipher?: string;
selectedCandidatePairChanges: -1,
};


public constructor() {
// empty
}

adapt(stats: RtcStats[]): RtcStats[] {
let selectedCandidatePair: SelectedIceCandidatePairStats | undefined;

for (const stat of stats) {
if (stat.type !== W3C.StatsType.candidatePair) continue;

const pair = stat as SelectedIceCandidatePairStats;

if (!pair.selected) continue;

selectedCandidatePair = pair;
break;
}

if (!selectedCandidatePair) return stats;

if (this._selectedCandidatePair?.id !== selectedCandidatePair.id) {
this.stats.selectedCandidatePairChanges = (this.stats.selectedCandidatePairChanges ?? 0) + 1;
this.stats.selectedCandidatePairId = selectedCandidatePair.id;
this.stats.id = selectedCandidatePair.transportId ?? 'transport_0';
} else {
const deltaPacketsReceived = (selectedCandidatePair.packetsReceived ?? 0) - (this._selectedCandidatePair.packetsReceived ?? 0);
const deltaPacketsSent = (selectedCandidatePair.packetsSent ?? 0) - (this._selectedCandidatePair.packetsSent ?? 0);
const deltaBytesReceived = (selectedCandidatePair.bytesReceived ?? 0) - (this._selectedCandidatePair.bytesReceived ?? 0);
const deltaBytesSent = (selectedCandidatePair.bytesSent ?? 0) - (this._selectedCandidatePair.bytesSent ?? 0);

if (0 < deltaPacketsReceived) this.stats.bytesReceived = (this.stats.bytesReceived ?? 0) + deltaBytesReceived;
if (0 < deltaPacketsSent) this.stats.bytesSent = (this.stats.bytesSent ?? 0) + deltaBytesSent;
if (0 < deltaPacketsReceived) this.stats.packetsReceived = (this.stats.packetsReceived ?? 0) + deltaPacketsReceived;
if (0 < deltaPacketsSent) this.stats.packetsSent = (this.stats.packetsSent ?? 0) + deltaPacketsSent;
}

this._selectedCandidatePair = selectedCandidatePair;
this.stats.timestamp = selectedCandidatePair.timestamp;

stats.push(this.stats);

return stats;
}
}
8 changes: 8 additions & 0 deletions src/adapters/StatsAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as W3CStats from '../schema/W3cStatsIdentifiers';


export interface StatsAdapter {
name: string;
adapt(stats: W3CStats.RtcStats[]): W3CStats.RtcStats[];
postAdapt?(stats: W3CStats.RtcStats[]): W3CStats.RtcStats[];
}
39 changes: 29 additions & 10 deletions src/adapters/StatsAdapters.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,53 @@
import * as W3CStats from '../schema/W3cStatsIdentifiers';
import { createLogger } from '../utils/logger';
import { StatsAdapter } from './StatsAdapter';

const logger = createLogger('StatsAdapter');

export type StatsAdapter = (stats: W3CStats.RtcStats[]) => W3CStats.RtcStats[];

export class StatsAdapters {
private readonly _adapters: StatsAdapter[] = [];
public readonly adapters = new Map<string, StatsAdapter>();

public add(adapter: StatsAdapter) {
this._adapters.push(adapter);
if (this.adapters.has(adapter.name)) {
return logger.warn('Adapter with name already exists', adapter.name);
}

this.adapters.set(adapter.name, adapter);
}

public remove(adapter: StatsAdapter) {
const index = this._adapters.indexOf(adapter);
if (index < 0) return;
this._adapters.splice(index, 1);
public remove(adapter: StatsAdapter | string) {
if (typeof adapter === 'string') {
return this.adapters.delete(adapter);
}
return this.adapters.delete(adapter.name);
}

public adapt(input: W3CStats.RtcStats[]): W3CStats.RtcStats[] {
let result: W3CStats.RtcStats[] = input;

for (const adapter of this._adapters) {
for (const adapter of this.adapters.values()) {
try {
result = adapter(result);
result = adapter.adapt(result);
} catch (err) {
logger.warn('Error adapting stats', err);
}
}

return result;
}

public postAdapt(input: W3CStats.RtcStats[]): W3CStats.RtcStats[] {
let result: W3CStats.RtcStats[] = input;

for (const adapter of this.adapters.values()) {
if (!adapter.postAdapt) continue;
try {
result = adapter.postAdapt(result);
} catch (err) {
logger.warn('Error post adapting stats', err);
}
}

return result;
}
}
28 changes: 0 additions & 28 deletions src/adapters/firefox94StatsAdapter.ts

This file was deleted.

18 changes: 0 additions & 18 deletions src/adapters/mediasoupAdapter.ts

This file was deleted.

Loading

0 comments on commit 1d1b41c

Please sign in to comment.