Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: VLprojects/webrtc-issue-detector
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.9.0
Choose a base ref
...
head repository: VLprojects/webrtc-issue-detector
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref

Commits on Mar 15, 2024

  1. Copy the full SHA
    6279163 View commit details
  2. chore: rollback unnecessary changes (#18)

    * chore: rollback unnecessary changes
    
    * fix: export base detector in index file
    evgmel authored Mar 15, 2024
    Copy the full SHA
    2513968 View commit details
  3. Copy the full SHA
    8b1dd7d View commit details
  4. Copy the full SHA
    603ffe9 View commit details
  5. Copy the full SHA
    2d1791e View commit details

Commits on May 16, 2024

  1. feat: added connectionId to NetworkScores

    * feat: added ability to specify id for pc
    
    * fixes after review: used candidate pair id
    
    * renamed parameter back
    
    * renamed pc -> connectionId
    IlgamGabdullin authored May 16, 2024
    Copy the full SHA
    95d17d4 View commit details
  2. Copy the full SHA
    859163d View commit details

Commits on Jul 15, 2024

  1. feat: Add configurable params for detectors that could support them (#24

    )
    
    * make InboundNetworkIssueDetector consistent with other detectors
    
    * make params configurable for network media sync and outbound network
    
    * restore export to not cause breakages
    
    * retrigger checks
    
    * add blank lines in between class variables
    MFarejowicz authored Jul 15, 2024
    Copy the full SHA
    c3e2481 View commit details
  2. Copy the full SHA
    fdf90c2 View commit details

Commits on Aug 13, 2024

  1. feat: LD-6968 add detector of frozen video tracks (#25)

    * feat: LD-6968 add detector of dead video tracks
    
    * fix: LD-6968 use trackId as iceCandidate
    
    * chore(release): 1.12.0-LD-6968-dead-video-track-detect.1 [skip ci]
    
    * refactor: LD-6968 do changes after code review
    
    * style: LD-6968 use `trackIdentifier` instead of `iceCandidate`
    
    * style: LD-6968 do changes after review
    
    * style: LD-6968 update reason
    
    * chore(release): 1.12.0-LD-6968-dead-video-track-detect.2 [skip ci]
    
    ---------
    
    Co-authored-by: vlprojects-bot <info@vlprojects.pro>
    vlad-livedigital and vlprojects-bot authored Aug 13, 2024
    Copy the full SHA
    36ce6a3 View commit details
  2. Copy the full SHA
    1bcc718 View commit details

Commits on Dec 3, 2024

  1. feat: on stats parsed callback (#28)

    * feat: on stats parsed callback
    
    * chore(release): 1.13.0-tt-212-on-parsed-stats.1 [skip ci]
    
    * fix: change handleNewPeerConnection args
    
    * chore(release): 1.13.0-tt-212-on-parsed-stats.2 [skip ci]
    
    ---------
    
    Co-authored-by: vlprojects-bot <info@vlprojects.pro>
    panov-va and vlprojects-bot authored Dec 3, 2024
    Copy the full SHA
    4646ede View commit details
  2. Copy the full SHA
    ccd5837 View commit details

Commits on Dec 25, 2024

  1. Copy the full SHA
    1d9a872 View commit details
  2. Copy the full SHA
    40c47fd View commit details

Commits on Jan 23, 2025

  1. Copy the full SHA
    a9985a1 View commit details
  2. Copy the full SHA
    4c7a59f View commit details
  3. Copy the full SHA
    d6b7dcf View commit details
  4. Copy the full SHA
    028c944 View commit details

Commits on Feb 3, 2025

  1. Copy the full SHA
    2b3b502 View commit details
  2. Copy the full SHA
    9cf4ce7 View commit details

Commits on Feb 4, 2025

  1. Copy the full SHA
    25c4d15 View commit details
  2. Copy the full SHA
    2bc109d View commit details

Commits on Feb 7, 2025

  1. Copy the full SHA
    e37c2ff View commit details
  2. Copy the full SHA
    a6c653d View commit details
4 changes: 3 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -15,6 +15,8 @@
"no-underscore-dangle": "off",
"max-len": ["error", { "code": 120 }],
"import/extensions": "off",
"import/no-cycle": "off"
"import/no-cycle": "off",
"no-continue": "off",
"import/prefer-default-export": "off"
}
}
30 changes: 30 additions & 0 deletions .github/workflows/pr-code-checker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: PR Code Checker

on:
pull_request:
branches: [ master ]

jobs:
lint_and_check_build:
runs-on: ubuntu-latest
steps:
- name: Check Out Repo
uses: actions/checkout@v4

- name: Setup nodejs
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'

- name: Install yarn
run: npm install -g yarn

- name: Install dependencies
run: yarn

- name: Run linter
run: yarn lint

- name: Run builder
run: yarn build
17 changes: 17 additions & 0 deletions .github/workflows/pr-linter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: "Lint PR"

on:
pull_request_target:
types:
- opened
- edited
- synchronize
branches: [ master ]

jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.VL_BOT_TOKEN }}
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ jobs:
persist-credentials: false

- name: Use Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'
51 changes: 26 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -51,13 +51,13 @@ By default, WebRTCIssueDetector can be created with minimum of mandatory constru
```typescript
import WebRTCIssueDetector, {
QualityLimitationsIssueDetector,
FramesDroppedIssueDetector,
FramesEncodedSentIssueDetector,
InboundNetworkIssueDetector,
OutboundNetworkIssueDetector,
NetworkMediaSyncIssueDetector,
AvailableOutgoingBitrateIssueDetector,
UnknownVideoDecoderImplementationDetector,
FrozenVideoTrackDetector,
VideoDecoderIssueDetector,
} from 'webrtc-issue-detector';

const widWithDefaultConstructorArgs = new WebRTCIssueDetector();
@@ -67,13 +67,13 @@ const widWithDefaultConstructorArgs = new WebRTCIssueDetector();
const widWithCustomConstructorArgs = new WebRTCIssueDetector({
detectors: [ // you are free to change the detectors list according to your needs
new QualityLimitationsIssueDetector(),
new FramesDroppedIssueDetector(),
new FramesEncodedSentIssueDetector(),
new InboundNetworkIssueDetector(),
new OutboundNetworkIssueDetector(),
new NetworkMediaSyncIssueDetector(),
new AvailableOutgoingBitrateIssueDetector(),
new UnknownVideoDecoderImplementationDetector(),
new FrozenVideoTrackDetector(),
new VideoDecoderIssueDetector(),
],
getStatsInterval: 10_000, // set custom stats parsing interval
onIssues: (payload: IssueDetectorResult) => {
@@ -104,34 +104,18 @@ const exampleIssue = {
}
```

### FramesDroppedIssueDetector
### VideoDecoderIssueDetector
Detects issues with decoder.
```js
const exampleIssue = {
type: 'cpu',
reason: 'decoder-cpu-throttling',
statsSample: {
deltaFramesDropped: 100,
deltaFramesReceived: 1000,
deltaFramesDecoded: 900,
framesDroppedPct: 10,
affectedStreamsPercent: 67,
throtthedStreams: [
{ ssrc: 123, allDecodeTimePerFrame: [1.2, 1.6, 1.9, 2.4, 2.9], volatility: 1.7 },
]
},
ssrc: 1234,
}
```

### FramesEncodedSentIssueDetector
Detects issues with outbound network throughput.
```js
const exampleIssue = {
type: 'network',
reason: 'outbound-network-throughput',
statsSample: {
deltaFramesSent: 900,
deltaFramesEncoded: 1000,
missedFramesPct: 10,
},
ssrc: 1234,
}
```

@@ -233,6 +217,23 @@ const exampleIssue = {
}
```


### MissingStreamDataDetector
Detects issues with missing data in active inbound streams
```ts
const exampleIssue = {
type: 'stream',
reason: 'missing-video-stream-data' | 'missing-audio-stream-data',
trackIdentifier: 'some-track-id',
statsSample: {
bytesReceivedDelta: 0, // always zero if issue detected
bytesReceived: 2392384,
trackDetached: false,
trackEnded: false,
},
}
```

## Roadmap

- [ ] Adaptive getStats() call interval based on last getStats() execution time
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "webrtc-issue-detector",
"version": "1.9.0",
"version": "1.16.3",
"description": "WebRTC diagnostic tool that detects issues with network or user devices",
"repository": "git@github.com:VLprojects/webrtc-issue-detector.git",
"author": "Roman Kuzakov <roman.kuzakov@gmail.com>",
1 change: 1 addition & 0 deletions src/NetworkScoresCalculator.ts
Original file line number Diff line number Diff line change
@@ -32,6 +32,7 @@ class NetworkScoresCalculator implements INetworkScoresCalculator {
return {
outbound,
inbound,
connectionId,
statsSamples: {
inboundStatsSample,
outboundStatsSample,
40 changes: 25 additions & 15 deletions src/WebRTCIssueDetector.ts
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import {
IssueDetector,
IssuePayload,
Logger,
NetworkScores,
StatsReportItem,
WebRTCIssueDetectorConstructorParams,
WebRTCStatsParsed,
@@ -16,16 +17,17 @@ import PeriodicWebRTCStatsReporter from './parser/PeriodicWebRTCStatsReporter';
import DefaultNetworkScoresCalculator from './NetworkScoresCalculator';
import {
AvailableOutgoingBitrateIssueDetector,
FramesDroppedIssueDetector,
FramesEncodedSentIssueDetector,
InboundNetworkIssueDetector,
NetworkMediaSyncIssueDetector,
OutboundNetworkIssueDetector,
QualityLimitationsIssueDetector,
UnknownVideoDecoderImplementationDetector,
FrozenVideoTrackDetector,
VideoDecoderIssueDetector,
} from './detectors';
import { CompositeRTCStatsParser, RTCStatsParser } from './parser';
import createLogger from './utils/logger';
import MissingStreamDataDetector from './detectors/MissingStreamDataDetector';

class WebRTCIssueDetector {
readonly eventEmitter: WebRTCIssueEmitter;
@@ -58,13 +60,14 @@ class WebRTCIssueDetector {

this.detectors = params.detectors ?? [
new QualityLimitationsIssueDetector(),
new FramesDroppedIssueDetector(),
new FramesEncodedSentIssueDetector(),
new InboundNetworkIssueDetector(),
new OutboundNetworkIssueDetector(),
new NetworkMediaSyncIssueDetector(),
new AvailableOutgoingBitrateIssueDetector(),
new UnknownVideoDecoderImplementationDetector(),
new FrozenVideoTrackDetector(),
new VideoDecoderIssueDetector(),
new MissingStreamDataDetector(),
];

this.networkScoresCalculator = params.networkScoresCalculator ?? new DefaultNetworkScoresCalculator();
@@ -86,19 +89,23 @@ class WebRTCIssueDetector {
}

this.statsReporter.on(PeriodicWebRTCStatsReporter.STATS_REPORT_READY_EVENT, (report: StatsReportItem) => {
this.detectIssues({
data: report.stats,
});

this.calculateNetworkScores(report.stats);
const networkScores = this.calculateNetworkScores(report.stats);
this.detectIssues({ data: report.stats }, networkScores);
});

this.statsReporter.on(PeriodicWebRTCStatsReporter.STATS_REPORTS_PARSED, (data: { timeTaken: number }) => {
this.statsReporter.on(PeriodicWebRTCStatsReporter.STATS_REPORTS_PARSED, (data: {
timeTaken: number,
reportItems: StatsReportItem[],
}) => {
const payload = {
timeTaken: data.timeTaken,
ts: Date.now(),
};

if (params.onStats) {
params.onStats(data.reportItems);
}

this.eventEmitter.emit(EventType.StatsParsingFinished, payload);
});
}
@@ -131,7 +138,7 @@ class WebRTCIssueDetector {
this.statsReporter.stopReporting();
}

public handleNewPeerConnection(pc: RTCPeerConnection): void {
public handleNewPeerConnection(pc: RTCPeerConnection, id?: string): void {
if (!this.#running && this.autoAddPeerConnections) {
this.logger.debug('Skip handling new peer connection. Detector is not running', pc);
return;
@@ -145,23 +152,26 @@ class WebRTCIssueDetector {

this.logger.debug('Handling new peer connection', pc);

this.compositeStatsParser.addPeerConnection({ pc });
this.compositeStatsParser.addPeerConnection({ pc, id });
}

private emitIssues(issues: IssuePayload[]): void {
this.eventEmitter.emit(EventType.Issue, issues);
}

private detectIssues({ data }: DetectIssuesPayload): void {
const issues = this.detectors.reduce<IssuePayload[]>((acc, detector) => [...acc, ...detector.detect(data)], []);
private detectIssues({ data }: DetectIssuesPayload, networkScores: NetworkScores): void {
const issues = this.detectors
.reduce<IssuePayload[]>((acc, detector) => [...acc, ...detector.detect(data, networkScores)], []);

if (issues.length > 0) {
this.emitIssues(issues);
}
}

private calculateNetworkScores(data: WebRTCStatsParsed): void {
private calculateNetworkScores(data: WebRTCStatsParsed): NetworkScores {
const networkScores = this.networkScoresCalculator.calculate(data);
this.eventEmitter.emit(EventType.NetworkScoresUpdated, networkScores);
return networkScores;
}

private wrapRTCPeerConnection(): void {
61 changes: 47 additions & 14 deletions src/detectors/BaseIssueDetector.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { IssueDetector, IssueDetectorResult, WebRTCStatsParsed } from '../types';
import {
IssueDetector,
IssueDetectorResult,
NetworkScores,
WebRTCStatsParsed,
WebRTCStatsParsedWithNetworkScores,
} from '../types';
import { scheduleTask } from '../utils/tasks';
import { CLEANUP_PREV_STATS_TTL_MS } from '../utils/constants';
import { CLEANUP_PREV_STATS_TTL_MS, MAX_PARSED_STATS_STORAGE_SIZE } from '../utils/constants';

export interface PrevStatsCleanupPayload {
connectionId: string;
@@ -9,23 +15,34 @@ export interface PrevStatsCleanupPayload {

export interface BaseIssueDetectorParams {
statsCleanupTtlMs?: number;
maxParsedStatsStorageSize?: number;
}

abstract class BaseIssueDetector implements IssueDetector {
readonly #lastProcessedStats: Map<string, WebRTCStatsParsed | undefined>;
readonly #parsedStatsStorage: Map<string, WebRTCStatsParsedWithNetworkScores[]> = new Map();

readonly #statsCleanupDelayMs: number;

readonly #maxParsedStatsStorageSize: number;

constructor(params: BaseIssueDetectorParams = {}) {
this.#lastProcessedStats = new Map();
this.#statsCleanupDelayMs = params.statsCleanupTtlMs ?? CLEANUP_PREV_STATS_TTL_MS;
this.#maxParsedStatsStorageSize = params.maxParsedStatsStorageSize ?? MAX_PARSED_STATS_STORAGE_SIZE;
}

abstract performDetection(data: WebRTCStatsParsed): IssueDetectorResult;
abstract performDetection(data: WebRTCStatsParsedWithNetworkScores): IssueDetectorResult;

detect(data: WebRTCStatsParsed): IssueDetectorResult {
const result = this.performDetection(data);
detect(data: WebRTCStatsParsed, networkScores?: NetworkScores): IssueDetectorResult {
const parsedStatsWithNetworkScores = {
...data,
networkScores: {
...networkScores,
statsSamples: networkScores?.statsSamples || {},
},
};
const result = this.performDetection(parsedStatsWithNetworkScores);

this.setLastProcessedStats(data.connection.id, parsedStatsWithNetworkScores);
this.performPrevStatsCleanup({
connectionId: data.connection.id,
});
@@ -36,7 +53,7 @@ abstract class BaseIssueDetector implements IssueDetector {
protected performPrevStatsCleanup(payload: PrevStatsCleanupPayload): void {
const { connectionId, cleanupCallback } = payload;

if (!this.#lastProcessedStats.has(connectionId)) {
if (!this.#parsedStatsStorage.has(connectionId)) {
return;
}

@@ -53,16 +70,32 @@ abstract class BaseIssueDetector implements IssueDetector {
});
}

protected setLastProcessedStats(connectionId: string, parsedStats: WebRTCStatsParsed): void {
this.#lastProcessedStats.set(connectionId, parsedStats);
protected setLastProcessedStats(connectionId: string, parsedStats: WebRTCStatsParsedWithNetworkScores): void {
if (!connectionId || parsedStats.connection.id !== connectionId) {
return;
}

const connectionStats = this.#parsedStatsStorage.get(connectionId) ?? [];
connectionStats.push(parsedStats);

if (connectionStats.length > this.#maxParsedStatsStorageSize) {
connectionStats.shift();
}

this.#parsedStatsStorage.set(connectionId, connectionStats);
}

protected getLastProcessedStats(connectionId: string): WebRTCStatsParsedWithNetworkScores | undefined {
const connectionStats = this.#parsedStatsStorage.get(connectionId);
return connectionStats?.[connectionStats.length - 1];
}

protected getLastProcessedStats(connectionId: string): WebRTCStatsParsed | undefined {
return this.#lastProcessedStats.get(connectionId);
protected getAllLastProcessedStats(connectionId: string): WebRTCStatsParsedWithNetworkScores[] {
return this.#parsedStatsStorage.get(connectionId) ?? [];
}

private deleteLastProcessedStats(connectionId: string): void {
this.#lastProcessedStats.delete(connectionId);
protected deleteLastProcessedStats(connectionId: string): void {
this.#parsedStatsStorage.delete(connectionId);
}
}

Loading