Skip to content

Commit

Permalink
3.7.0 (#28)
Browse files Browse the repository at this point in the history
* 3.7.0

* add `setUserId`
* typofix for stats
* add `VideoFreezesDetector`
* add `layers` property to outbound track stats
* add `usermediaerror` event
* add `usingTURN` property to peer conections
* add `using-turn` event to monitor
* add `sendingFractionLost` to monitor
* add `receivingFractionLost` to monitor
  • Loading branch information
balazskreith authored Apr 22, 2024
1 parent 4b87725 commit 2637b38
Show file tree
Hide file tree
Showing 12 changed files with 321 additions and 14 deletions.
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Table of Contents:
- [MediaStreamTrack Entry](#mediastreamtrack-entry)
- [InboundRTP Entry](#inboundrtp-entry)
- [OutboundRTP Entry](#outboundrtp-entry)
- [Detectors and Issues](#issues-and-detectors)
- [Detectors and Issues](#detectors-and-issues)
- [Congestion Detector](#congestion-detector)
- [Audio Desync Detector](#audio-desync-detector)
- [CPU Performance Detector](#cpu-performance-detector)
Expand All @@ -30,7 +30,7 @@ Table of Contents:
- [Getting Involved](#getting-involved)
- [License](#license)

## Qucik Start
## Quick Start

Install it from [npm](https://www.npmjs.com/package/@observertc/client-monitor-js) package repository.

Expand Down Expand Up @@ -534,6 +534,33 @@ detector.on('statechanged', onStateChanged);
```
### Video Freeze Detector
```javascript
const detector = monitor.createVideoFreezesDetector({
createIssueOnDetection: {
severity: 'major',
attachments: {
// various custom data
},
}
});
detector.on('freezedVideoStarted', event => {
console.log('Freezed video started');
console.log('TrackId', event.trackId);
console.log('PeerConnectionId', event.peerConnectionId);
console.log('SSRC:', event.ssrc);
});
detector.on('freezedVideoEnded', event => {
console.log('Freezed video ended');
console.log('TrackId', event.trackId);
console.log('Freeze duration in Seconds', event.durationInS);
console.log('PeerConnectionId', event.peerConnectionId);
console.log('SSRC:', event.ssrc);
});
```
## Configurations
```javascript
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@observertc/client-monitor-js",
"version": "3.6.0",
"version": "3.7.0",
"description": "ObserveRTC Client Integration Javascript Library",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand Down
95 changes: 93 additions & 2 deletions src/ClientMonitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import { PeerConnectionEntry, TrackStats } from './entries/StatsEntryInterfaces'
import { AudioDesyncDetector, AudioDesyncDetectorConfig } from './detectors/AudioDesyncDetector';
import { CongestionDetector, CongestionDetectorEvents } from './detectors/CongestionDetector';
import { CpuPerformanceDetector, CpuPerformanceDetectorConfig } from './detectors/CpuPerformanceDetector';
import {
VideoFreezesDetector,
VideoFreezesDetectorConfig,
FreezedVideoStartedEvent,
FreezedVideoEndedEvent,
} from './detectors/VideoFreezesDetector';

const logger = createLogger('ClientMonitor');

Expand Down Expand Up @@ -62,8 +68,14 @@ export interface ClientMonitorEvents {
outgoingBitrateAfterCongestion: number | undefined;
outgoingBitrateBeforeCongestion: number | undefined;
},
'usermediaerror': string,
'cpulimitation': AlertState,
'audio-desync': AlertState,
'freezed-video': {
trackId: string,
peerConnectionId: string | undefined,
},
'using-turn': boolean,
'issue': ClientIssue,
}

Expand Down Expand Up @@ -145,19 +157,28 @@ export class ClientMonitor extends TypedEventEmitter<ClientMonitorEvents> {

public async collect(): Promise<CollectedStats> {
if (this._closed) throw new Error('ClientMonitor is closed');

const wasUsingTURN = this.peerConnections.some(pc => pc.usingTURN);
const collectedStats = await this.collectors.collect();
this.storage.update(collectedStats);
const timestamp = Date.now();

this.emit('stats-collected', {
collectedStats,
elapsedSinceLastCollectedInMs: timestamp - this._lastCollectedAt,
});

this._lastCollectedAt = timestamp;

if (this._config.samplingTick && this._config.samplingTick <= ++this._actualCollectingTick ) {
this._actualCollectingTick = 0;
this.sample();
}

const isUsingTURN = this.peerConnections.some(pc => pc.usingTURN);

if (wasUsingTURN !== isUsingTURN) {
this.emit('using-turn', isUsingTURN);
}
return collectedStats;
}

Expand Down Expand Up @@ -207,6 +228,10 @@ export class ClientMonitor extends TypedEventEmitter<ClientMonitorEvents> {
this._sampler.setMarker(value);
}

public setUserId(userId?: string) {
this._sampler.setUserId(userId);
}

public setMediaDevices(...devices: MediaDevice[]): void {
if (!devices) return;
this.meta.mediaDevices = devices;
Expand All @@ -222,7 +247,12 @@ export class ClientMonitor extends TypedEventEmitter<ClientMonitorEvents> {
}

public addUserMediaError(err: unknown): void {
this._sampler.addUserMediaError(`${err}`);
const message = `${err}`;

if(0 < (this._config.samplingTick ?? 0))
this._sampler.addUserMediaError(message);

this.emit('usermediaerror', message);
}

public setMediaConstraints(constrains: MediaStreamConstraints | MediaTrackConstraints): void {
Expand Down Expand Up @@ -393,6 +423,59 @@ export class ClientMonitor extends TypedEventEmitter<ClientMonitorEvents> {
return detector;
}

public createVideoFreezesDetector(config?: VideoFreezesDetectorConfig & {
createIssueOnDetection?: {
attachments?: Record<string, unknown>,
severity: 'critical' | 'major' | 'minor',
},
}): VideoFreezesDetector {
const existingDetector = this._detectors.get(VideoFreezesDetector.name);

if (existingDetector) return existingDetector as VideoFreezesDetector;

const detector = new VideoFreezesDetector({
});
const onUpdate = () => detector.update(this.storage.inboundRtps());
const {
createIssueOnDetection,
} = config ?? {};

const onFreezeStarted = (event: FreezedVideoStartedEvent) => {
this.emit('freezed-video', {
peerConnectionId: event.peerConnectionId,
trackId: event.trackId,
});
};
const onFreezeEnded = (event: FreezedVideoEndedEvent) => {
if (createIssueOnDetection) {
this.addIssue({
severity: createIssueOnDetection.severity,
description: 'Video Freeze detected',
timestamp: Date.now(),
peerConnectionId: event.peerConnectionId,
mediaTrackId: event.trackId,
attachments: {
durationInS: event.durationInS,
...(createIssueOnDetection.attachments ?? {})
},
});
}
}

detector.once('close', () => {
this.off('stats-collected', onUpdate);
detector.off('freezedVideoStarted', onFreezeStarted);
detector.off('freezedVideoEnded', onFreezeEnded);
this._detectors.delete(VideoFreezesDetector.name);
});
detector.on('freezedVideoStarted', onFreezeStarted);
detector.on('freezedVideoEnded', onFreezeEnded);

this._detectors.set(VideoFreezesDetector.name, detector);

return detector;
}

public createCpuPerformanceIssueDetector(config?: CpuPerformanceDetectorConfig & {
createIssueOnDetection?: {
attachments?: Record<string, unknown>,
Expand Down Expand Up @@ -656,6 +739,14 @@ export class ClientMonitor extends TypedEventEmitter<ClientMonitorEvents> {
return this.storage.highestSeenAvailableIncomingBitrate;
}

public get sendingFractionLost() {
return this.storage.sendingFractionLost;
}

public get receivingFractionLost() {
return this.storage.receivingFractionLost;
}

private _setupTimer(): void {
this._timer && clearInterval(this._timer);
this._timer = undefined;
Expand Down
9 changes: 7 additions & 2 deletions src/Sampler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export class Sampler {
private _localSDP?: string[];
private _marker?: string;
private _sampleSeq = 0;
private _userId?: string;
private readonly _timezoneOffset: number = new Date().getTimezoneOffset();

public constructor(
Expand Down Expand Up @@ -104,6 +105,10 @@ export class Sampler {
this._marker = value;
}

public setUserId(userId?: string) {
this._userId = userId;
}

public clear() {
this._engine = undefined;
this._platform = undefined;
Expand All @@ -124,8 +129,8 @@ export class Sampler {
callId: 'NULL',
clientId: 'NULL',
roomId: 'NULL',
userId: 'NULL',


userId: this._userId,
marker: this._marker,
sampleSeq: this._sampleSeq,
timeZoneOffsetInHours: this._timezoneOffset,
Expand Down
6 changes: 6 additions & 0 deletions src/detectors/CongestionDetector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import EventEmitter from "events";
import { IceCandidatePairEntry, PeerConnectionEntry } from "../entries/StatsEntryInterfaces";

type PeerConnectionState = {
peerConnectionId: string;
congested: boolean;
outgoingBitrateBeforeCongestion?: number;
outgoingBitrateAfterCongestion?: number;
Expand Down Expand Up @@ -32,6 +33,10 @@ export class CongestionDetector extends EventEmitter {

}

public get states(): ReadonlyMap<string, PeerConnectionState> {
return this._states;
}

public update(peerConnections: IterableIterator<PeerConnectionEntry>) {
const visitedPeerConnectionIds = new Set<string>();
let gotCongested = false;
Expand All @@ -44,6 +49,7 @@ export class CongestionDetector extends EventEmitter {

if (!state) {
state = {
peerConnectionId,
congested: false,
// outgoingBitrateBeforeCongestion: 0,
// outgoingBitrateAfterCongestion: 0,
Expand Down
118 changes: 118 additions & 0 deletions src/detectors/VideoFreezesDetector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import EventEmitter from "events";
import { InboundRtpEntry } from "../entries/StatsEntryInterfaces";

export type VideoFreezesDetectorConfig = {
// empty
}

export type FreezedVideoStartedEvent = {
peerConnectionId: string | undefined,
trackId: string,
ssrc: number,
}

export type FreezedVideoEndedEvent = {
peerConnectionId: string,
trackId: string,
durationInS: number,
}

export type VideoFreezesDetectorEvents = {
freezedVideoStarted: [FreezedVideoStartedEvent],
freezedVideoEnded: [FreezedVideoEndedEvent],
close: [],
}

type InboundRtpStatsTrace = {
ssrc: number,
lastFreezeCount: number,
freezedStartedDuration?: number,
freezed: boolean,
visited: boolean,
}

export declare interface VideoFreezesDetector {
on<K extends keyof VideoFreezesDetectorEvents>(event: K, listener: (...events: VideoFreezesDetectorEvents[K]) => void): this;
off<K extends keyof VideoFreezesDetectorEvents>(event: K, listener: (...events: VideoFreezesDetectorEvents[K]) => void): this;
once<K extends keyof VideoFreezesDetectorEvents>(event: K, listener: (...events: VideoFreezesDetectorEvents[K]) => void): this;
emit<K extends keyof VideoFreezesDetectorEvents>(event: K, ...events: VideoFreezesDetectorEvents[K]): boolean;
}

export class VideoFreezesDetector extends EventEmitter {
private _closed = false;
private readonly _traces = new Map<number, InboundRtpStatsTrace>();

public constructor(
public readonly config: VideoFreezesDetectorConfig,
) {
super();
this.setMaxListeners(Infinity);

}

public close() {
if (this._closed) return;
this._closed = true;

this._traces.clear();
this.emit('close');
}

public update(inboundRtps: IterableIterator<InboundRtpEntry>) {
for (const inboundRtp of inboundRtps) {
const stats = inboundRtp.stats;
const trackId = inboundRtp.getTrackId();
const ssrc = stats.ssrc;
if (stats.kind !== 'video' || trackId === undefined) {
continue;
}

let trace = this._traces.get(ssrc);
if (!trace) {
trace = {
ssrc,
lastFreezeCount: 0,
freezed: false,
freezedStartedDuration: 0,
visited: false,
};
this._traces.set(ssrc, trace);
}

const wasFreezed = trace.freezed;

trace.visited = true;
trace.freezed = 0 < Math.max(0, (stats.freezeCount ?? 0) - trace.lastFreezeCount);
trace.lastFreezeCount = stats.freezeCount ?? 0;

if (!wasFreezed && trace.freezed) {
trace.freezedStartedDuration = stats.totalFreezesDuration ?? 0;
this.emit('freezedVideoStarted', {
peerConnectionId: inboundRtp.getPeerConnection()?.peerConnectionId,
trackId,
ssrc,
})
} else if (wasFreezed && !trace.freezed) {
const durationInS = Math.max(0, (stats.totalFreezesDuration ?? 0) - (trace.freezedStartedDuration ?? 0));

trace.freezedStartedDuration = undefined;

0 < durationInS && this.emit('freezedVideoEnded', {
peerConnectionId: inboundRtp.getPeerConnection()?.peerConnectionId,
trackId,
durationInS,
})
}
}

for (const trace of this._traces.values()) {
if (trace.visited) {
trace.visited = false;

continue;
}

this._traces.delete(trace.ssrc);
}
}
}
Loading

0 comments on commit 2637b38

Please sign in to comment.