Skip to content

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

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

Merged
merged 8 commits into from
Aug 13, 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
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,
FrozenVideoTrackDetector,
} 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 FrozenVideoTrackDetector(),
],
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.2",
"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,
FrozenVideoTrackDetector,
} 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 FrozenVideoTrackDetector(),
];

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

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

class FrozenVideoTrackDetector extends BaseIssueDetector {
readonly #lastMarkedAt = new Map<string, number>();

readonly #timeoutMs: number;

readonly #framesDroppedThreshold: number;

constructor(params: FrozenVideoTrackDetectorParams = {}) {
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;
}

// We skip it when ratio is too low because it should be handled by FramesDroppedIssueDetector
if (ratioFramesDropped >= this.#framesDroppedThreshold) {
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.FrozenVideoTrack,
trackIdentifier: 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);
return false;
}

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

return true;
}

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

export default FrozenVideoTrackDetector;
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 FrozenVideoTrackDetector } from './FrozenVideoTrackDetector';
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',
FrozenVideoTrack = 'frozen-video-track',
}

export type IssuePayload = {
Expand Down