Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3.7.0 #28

Merged
merged 12 commits into from
Apr 22, 2024
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
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
Loading