Skip to content

Commit ccbd5f2

Browse files
feat: LD-6968 add detector of dead video tracks
1 parent fdf90c2 commit ccbd5f2

File tree

5 files changed

+112
-0
lines changed

5 files changed

+112
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import WebRTCIssueDetector, {
5858
NetworkMediaSyncIssueDetector,
5959
AvailableOutgoingBitrateIssueDetector,
6060
UnknownVideoDecoderImplementationDetector,
61+
DeadVideoTrackDetector,
6162
} from 'webrtc-issue-detector';
6263

6364
const widWithDefaultConstructorArgs = new WebRTCIssueDetector();
@@ -74,6 +75,7 @@ const widWithCustomConstructorArgs = new WebRTCIssueDetector({
7475
new NetworkMediaSyncIssueDetector(),
7576
new AvailableOutgoingBitrateIssueDetector(),
7677
new UnknownVideoDecoderImplementationDetector(),
78+
new DeadVideoTrackDetector(),
7779
],
7880
getStatsInterval: 10_000, // set custom stats parsing interval
7981
onIssues: (payload: IssueDetectorResult) => {

src/WebRTCIssueDetector.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
OutboundNetworkIssueDetector,
2424
QualityLimitationsIssueDetector,
2525
UnknownVideoDecoderImplementationDetector,
26+
DeadVideoTrackDetector,
2627
} from './detectors';
2728
import { CompositeRTCStatsParser, RTCStatsParser } from './parser';
2829
import createLogger from './utils/logger';
@@ -65,6 +66,7 @@ class WebRTCIssueDetector {
6566
new NetworkMediaSyncIssueDetector(),
6667
new AvailableOutgoingBitrateIssueDetector(),
6768
new UnknownVideoDecoderImplementationDetector(),
69+
new DeadVideoTrackDetector(),
6870
];
6971

7072
this.networkScoresCalculator = params.networkScoresCalculator ?? new DefaultNetworkScoresCalculator();
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import {
2+
IssueDetectorResult,
3+
IssueReason,
4+
IssueType,
5+
ParsedInboundVideoStreamStats,
6+
WebRTCStatsParsed,
7+
} from '../types';
8+
import BaseIssueDetector from './BaseIssueDetector';
9+
10+
const sumStats = (
11+
accessor: (stat: ParsedInboundVideoStreamStats) => number,
12+
stats: ParsedInboundVideoStreamStats[],
13+
) => stats.reduce((sum, stat) => sum + accessor(stat), 0);
14+
15+
const sumPacketsReceived = sumStats.bind(null, (stat) => stat.packetsReceived);
16+
const sumDecodedFrames = sumStats.bind(null, (stat) => stat.framesDecoded);
17+
18+
const hasNewInboundTraffic = (data: WebRTCStatsParsed, prevData: WebRTCStatsParsed): boolean => {
19+
const { video: { inbound: newInbound } } = data;
20+
const { video: { inbound: prevInbound } } = prevData;
21+
22+
return sumPacketsReceived(newInbound) > sumPacketsReceived(prevInbound);
23+
};
24+
25+
const hasNewDecodedFrames = (data: WebRTCStatsParsed, prevData: WebRTCStatsParsed): boolean => {
26+
const { video: { inbound: newInbound } } = data;
27+
const { video: { inbound: prevInbound } } = prevData;
28+
29+
return sumDecodedFrames(newInbound) > sumDecodedFrames(prevInbound);
30+
};
31+
32+
class DeadVideoTrackDetector extends BaseIssueDetector {
33+
#lastMarkedAt: number | undefined;
34+
35+
#timeoutMs: number;
36+
37+
constructor(params: { timeoutMs?: number } = {}) {
38+
super();
39+
this.#timeoutMs = params.timeoutMs ?? 10_000;
40+
}
41+
42+
performDetection(data: WebRTCStatsParsed): IssueDetectorResult {
43+
const { connection: { id: connectionId } } = data;
44+
const issues = this.processData(data);
45+
this.setLastProcessedStats(connectionId, data);
46+
return issues;
47+
}
48+
49+
private processData(data: WebRTCStatsParsed): IssueDetectorResult {
50+
const { connection: { id: connectionId } } = data;
51+
const previousStats = this.getLastProcessedStats(connectionId);
52+
const issues: IssueDetectorResult = [];
53+
54+
if (!previousStats) {
55+
return issues;
56+
}
57+
58+
if (hasNewInboundTraffic(data, previousStats)) {
59+
if (hasNewDecodedFrames(data, previousStats)) {
60+
this.removeMarkIssue();
61+
} else {
62+
const hasIssue = this.markIssue();
63+
64+
if (hasIssue) {
65+
const statsSample = {
66+
packetsReceived: sumPacketsReceived(data.video.inbound),
67+
framesDecoded: sumDecodedFrames(data.video.inbound),
68+
deltaFramesDecoded: sumDecodedFrames(data.video.inbound) - sumDecodedFrames(previousStats.video.inbound),
69+
deltaPacketsReceived:
70+
sumPacketsReceived(data.video.inbound) - sumPacketsReceived(previousStats.video.inbound),
71+
};
72+
73+
issues.push({
74+
statsSample,
75+
type: IssueType.Stream,
76+
reason: IssueReason.DeadVideoTrack,
77+
iceCandidate: data.connection.local.id,
78+
});
79+
}
80+
}
81+
}
82+
83+
return issues;
84+
}
85+
86+
private markIssue(): boolean {
87+
const now = Date.now();
88+
89+
if (!this.#lastMarkedAt) {
90+
this.#lastMarkedAt = now;
91+
return false;
92+
}
93+
94+
if (now - this.#lastMarkedAt < this.#timeoutMs) {
95+
return false;
96+
}
97+
98+
return true;
99+
}
100+
101+
private removeMarkIssue(): void {
102+
this.#lastMarkedAt = undefined;
103+
}
104+
}
105+
106+
export default DeadVideoTrackDetector;

src/detectors/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export { default as NetworkMediaSyncIssueDetector } from './NetworkMediaSyncIssu
77
export { default as OutboundNetworkIssueDetector } from './OutboundNetworkIssueDetector';
88
export { default as QualityLimitationsIssueDetector } from './QualityLimitationsIssueDetector';
99
export { default as UnknownVideoDecoderImplementationDetector } from './UnknownVideoDecoderImplementationDetector';
10+
export { default as DeadVideoTrackDetector } from './DeadVideoTrackDetector';

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export enum IssueReason {
8181
UnknownVideoDecoderIssue = 'unknown-video-decoder',
8282
LowInboundMOS = 'low-inbound-mean-opinion-score',
8383
LowOutboundMOS = 'low-outbound-mean-opinion-score',
84+
DeadVideoTrack = 'dead-video-track',
8485
}
8586

8687
export type IssuePayload = {

0 commit comments

Comments
 (0)