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

feat: LD-6968 add detector of dead video tracks #25

Merged
merged 8 commits into from
Aug 13, 2024
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import WebRTCIssueDetector, {
NetworkMediaSyncIssueDetector,
AvailableOutgoingBitrateIssueDetector,
UnknownVideoDecoderImplementationDetector,
DeadVideoTrackDetector,
} from 'webrtc-issue-detector';

const widWithDefaultConstructorArgs = new WebRTCIssueDetector();
Expand All @@ -74,6 +75,7 @@ const widWithCustomConstructorArgs = new WebRTCIssueDetector({
new NetworkMediaSyncIssueDetector(),
new AvailableOutgoingBitrateIssueDetector(),
new UnknownVideoDecoderImplementationDetector(),
new DeadVideoTrackDetector(),
],
getStatsInterval: 10_000, // set custom stats parsing interval
onIssues: (payload: IssueDetectorResult) => {
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": "webrtc-issue-detector",
"version": "1.11.0",
"version": "1.12.0-LD-6968-dead-video-track-detect.1",
"description": "WebRTC diagnostic tool that detects issues with network or user devices",
"repository": "[email protected]:VLprojects/webrtc-issue-detector.git",
"author": "Roman Kuzakov <[email protected]>",
Expand Down
2 changes: 2 additions & 0 deletions src/WebRTCIssueDetector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
OutboundNetworkIssueDetector,
QualityLimitationsIssueDetector,
UnknownVideoDecoderImplementationDetector,
DeadVideoTrackDetector,
} from './detectors';
import { CompositeRTCStatsParser, RTCStatsParser } from './parser';
import createLogger from './utils/logger';
Expand Down Expand Up @@ -65,6 +66,7 @@ class WebRTCIssueDetector {
new NetworkMediaSyncIssueDetector(),
new AvailableOutgoingBitrateIssueDetector(),
new UnknownVideoDecoderImplementationDetector(),
new DeadVideoTrackDetector(),
];

this.networkScoresCalculator = params.networkScoresCalculator ?? new DefaultNetworkScoresCalculator();
Expand Down
135 changes: 135 additions & 0 deletions src/detectors/DeadVideoTrackDetector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {
IssueDetectorResult,
IssueReason,
IssueType,
ParsedInboundVideoStreamStats,
WebRTCStatsParsed,
} from '../types';
import BaseIssueDetector from './BaseIssueDetector';

interface DeadVideoTrackDetectorParams {
timeoutMs?: number;
framesDroppedThreshold?: number;
}

class DeadVideoTrackDetector extends BaseIssueDetector {
vlad-livedigital marked this conversation as resolved.
Show resolved Hide resolved
readonly #lastMarkedAt = new Map<string, number>();

readonly #timeoutMs: number;

readonly #framesDroppedThreshold: number;

constructor(params: DeadVideoTrackDetectorParams = {}) {
super();
this.#timeoutMs = params.timeoutMs ?? 10_000;
this.#framesDroppedThreshold = params.framesDroppedThreshold ?? 0.5;
}

performDetection(data: WebRTCStatsParsed): IssueDetectorResult {
const { connection: { id: connectionId } } = data;
const issues = this.processData(data);
this.setLastProcessedStats(connectionId, data);
return issues;
}

private processData(data: WebRTCStatsParsed): IssueDetectorResult {
const { connection: { id: connectionId } } = data;
const previousStats = this.getLastProcessedStats(connectionId);
const issues: IssueDetectorResult = [];

if (!previousStats) {
return issues;
}

const { video: { inbound: newInbound } } = data;
const { video: { inbound: prevInbound } } = previousStats;

const mapByTrackId = (items: ParsedInboundVideoStreamStats[]) => new Map<string, ParsedInboundVideoStreamStats>(
items.map((item) => [item.track.trackIdentifier, item] as const),
);

const newInboundByTrackId = mapByTrackId(newInbound);
const prevInboundByTrackId = mapByTrackId(prevInbound);
const unvisitedTrackIds = new Set(this.#lastMarkedAt.keys());

Array.from(newInboundByTrackId.entries()).forEach(([trackId, newInboundItem]) => {
unvisitedTrackIds.delete(trackId);

const prevInboundItem = prevInboundByTrackId.get(trackId);
if (!prevInboundItem) {
return;
}

const deltaFramesReceived = newInboundItem.framesReceived - prevInboundItem.framesReceived;
const deltaFramesDropped = newInboundItem.framesDropped - prevInboundItem.framesDropped;
const deltaFramesDecoded = newInboundItem.framesDecoded - prevInboundItem.framesDecoded;
const ratioFramesDropped = deltaFramesDropped / deltaFramesReceived;

if (deltaFramesReceived === 0) {
return;
}

if (ratioFramesDropped >= this.#framesDroppedThreshold) {
vlad-livedigital marked this conversation as resolved.
Show resolved Hide resolved
return;
}

// It seems that track is alive and we can remove mark if it was marked
if (deltaFramesDecoded > 0) {
this.removeMarkIssue(trackId);
return;
}

const hasIssue = this.markIssue(trackId);

if (!hasIssue) {
return;
}

const statsSample = {
framesReceived: newInboundItem.framesReceived,
framesDropped: newInboundItem.framesDropped,
framesDecoded: newInboundItem.framesDecoded,
deltaFramesReceived,
deltaFramesDropped,
deltaFramesDecoded,
};

issues.push({
statsSample,
type: IssueType.Stream,
reason: IssueReason.DeadVideoTrack,
iceCandidate: trackId,
});
});

// just clear unvisited tracks from memory
unvisitedTrackIds.forEach((trackId) => {
this.removeMarkIssue(trackId);
});

return issues;
}

private markIssue(trackId: string): boolean {
const now = Date.now();

const lastMarkedAt = this.#lastMarkedAt.get(trackId);

if (!lastMarkedAt) {
this.#lastMarkedAt.set(trackId, now);
vlad-livedigital marked this conversation as resolved.
Show resolved Hide resolved
return false;
}

if (now - lastMarkedAt < this.#timeoutMs) {
return false;
}

return true;
}

private removeMarkIssue(trackId: string): void {
this.#lastMarkedAt.delete(trackId);
}
}

export default DeadVideoTrackDetector;
1 change: 1 addition & 0 deletions src/detectors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export { default as NetworkMediaSyncIssueDetector } from './NetworkMediaSyncIssu
export { default as OutboundNetworkIssueDetector } from './OutboundNetworkIssueDetector';
export { default as QualityLimitationsIssueDetector } from './QualityLimitationsIssueDetector';
export { default as UnknownVideoDecoderImplementationDetector } from './UnknownVideoDecoderImplementationDetector';
export { default as DeadVideoTrackDetector } from './DeadVideoTrackDetector';
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export enum IssueReason {
UnknownVideoDecoderIssue = 'unknown-video-decoder',
LowInboundMOS = 'low-inbound-mean-opinion-score',
LowOutboundMOS = 'low-outbound-mean-opinion-score',
DeadVideoTrack = 'dead-video-track',
}

export type IssuePayload = {
Expand Down