diff --git a/src/detectors/FrozenVideoTrackDetector.ts b/src/detectors/FrozenVideoTrackDetector.ts index 2d23ea6..de33322 100644 --- a/src/detectors/FrozenVideoTrackDetector.ts +++ b/src/detectors/FrozenVideoTrackDetector.ts @@ -1,3 +1,4 @@ +import { isDtxLikeBehavior } from '../helpers/streams'; import { IssueDetectorResult, IssueReason, @@ -71,6 +72,12 @@ class FrozenVideoTrackDetector extends BaseIssueDetector { return undefined; } + const isDtx = isDtxLikeBehavior(videoStream.ssrc, allLastProcessedStats); + if (isDtx) { + // DTX-like behavior detected, ignoring freezes check + return undefined; + } + const deltaFreezeCount = videoStream.freezeCount - (prevStat.freezeCount ?? 0); const deltaFreezesTimeMs = (videoStream.totalFreezesDuration - (prevStat.totalFreezesDuration ?? 0)) * 1000; const avgFreezeDurationMs = deltaFreezeCount > 0 ? deltaFreezesTimeMs / deltaFreezeCount : 0; diff --git a/src/detectors/VideoDecoderIssueDetector.ts b/src/detectors/VideoDecoderIssueDetector.ts index c14ec79..6aa87cb 100644 --- a/src/detectors/VideoDecoderIssueDetector.ts +++ b/src/detectors/VideoDecoderIssueDetector.ts @@ -1,4 +1,5 @@ import { calculateVolatility } from '../helpers/calc'; +import { isDtxLikeBehavior } from '../helpers/streams'; import { IssueDetectorResult, IssueReason, @@ -84,6 +85,12 @@ class VideoDecoderIssueDetector extends BaseIssueDetector { return undefined; } + const isDtx = isDtxLikeBehavior(incomeVideoStream.ssrc, allProcessedStats); + if (isDtx) { + // DTX-like behavior detected, ignoring FPS volatility check + return undefined; + } + const volatility = calculateVolatility(allFps); if (volatility > this.#volatilityThreshold) { diff --git a/src/helpers/calc.ts b/src/helpers/calc.ts index 29db3ed..7f05eb5 100644 --- a/src/helpers/calc.ts +++ b/src/helpers/calc.ts @@ -1,5 +1,14 @@ export const calculateMean = (values: number[]) => values.reduce((acc, val) => acc + val, 0) / values.length; +export const calculateVariance = (mean: number, values: number[]) => values + .reduce((sum, val) => sum + (val - mean) ** 2, 0) / values.length; + +export const calculateStandardDeviation = (values: number[]) => { + const mean = calculateMean(values); + const variance = calculateVariance(mean, values); + return Math.sqrt(variance); +}; + export const calculateVolatility = (values: number[]) => { if (values.length === 0) { throw new Error('Cannot calculate volatility for empty array'); diff --git a/src/helpers/streams.ts b/src/helpers/streams.ts new file mode 100644 index 0000000..522a5d0 --- /dev/null +++ b/src/helpers/streams.ts @@ -0,0 +1,42 @@ +import { WebRTCStatsParsedWithNetworkScores } from '../types'; +import { calculateStandardDeviation } from './calc'; + +export const isDtxLikeBehavior = ( + ssrc: number, + allProcessedStats: WebRTCStatsParsedWithNetworkScores[], + stdDevThreshold = 30, +): boolean => { + const frameIntervals: number[] = []; + for (let i = 1; i < allProcessedStats.length - 1; i += 1) { + const videoStreamStats = allProcessedStats[i]?.video?.inbound.find( + (stream) => stream.ssrc === ssrc, + ); + + if (!videoStreamStats) { + continue; + } + + const previousVideoStreamStats = allProcessedStats[i - 1]?.video?.inbound?.find( + (stream) => stream.ssrc === ssrc, + ); + + if (!videoStreamStats || !previousVideoStreamStats) { + continue; + } + + const deltaTime = videoStreamStats.timestamp - previousVideoStreamStats.timestamp; + const deltaFrames = videoStreamStats.framesDecoded - previousVideoStreamStats.framesDecoded; + + if (deltaFrames > 0) { + const frameInterval = deltaTime / deltaFrames; // Average time per frame + frameIntervals.push(frameInterval); + } + } + + if (frameIntervals.length <= 1) { + return false; + } + + const stdDev = calculateStandardDeviation(frameIntervals); + return stdDev > stdDevThreshold; +};