@@ -2,134 +2,108 @@ import {
22 IssueDetectorResult ,
33 IssueReason ,
44 IssueType ,
5- ParsedInboundVideoStreamStats ,
5+ MosQuality ,
66 WebRTCStatsParsed ,
7+ WebRTCStatsParsedWithNetworkScores ,
78} from '../types' ;
9+ import { isSvcSpatialLayerChanged } from '../utils/video' ;
810import BaseIssueDetector from './BaseIssueDetector' ;
911
1012interface FrozenVideoTrackDetectorParams {
11- timeoutMs ?: number ;
12- framesDroppedThreshold ?: number ;
13+ avgFreezeDurationThresholdMs ?: number ;
14+ frozenDurationThresholdPct ?: number ;
1315}
1416
15- class FrozenVideoTrackDetector extends BaseIssueDetector {
16- readonly #lastMarkedAt = new Map < string , number > ( ) ;
17+ interface FrozenStream {
18+ ssrc : number ;
19+ avgFreezeDurationMs : number ;
20+ frozenDurationPct : number ;
21+ }
1722
18- readonly #timeoutMs: number ;
23+ class FrozenVideoTrackDetector extends BaseIssueDetector {
24+ #avgFreezeDurationThresholdMs: number ;
1925
20- readonly #framesDroppedThreshold : number ;
26+ #frozenDurationThresholdPct : number ;
2127
2228 constructor ( params : FrozenVideoTrackDetectorParams = { } ) {
2329 super ( ) ;
24- this . #timeoutMs = params . timeoutMs ?? 10_000 ;
25- this . #framesDroppedThreshold = params . framesDroppedThreshold ?? 0.5 ;
30+ this . #avgFreezeDurationThresholdMs = params . avgFreezeDurationThresholdMs ?? 1_000 ;
31+ this . #frozenDurationThresholdPct = params . frozenDurationThresholdPct ?? 30 ;
2632 }
2733
28- performDetection ( data : WebRTCStatsParsed ) : IssueDetectorResult {
29- const { connection : { id : connectionId } } = data ;
30- const issues = this . processData ( data ) ;
31- this . setLastProcessedStats ( connectionId , data ) ;
32- return issues ;
34+ performDetection ( data : WebRTCStatsParsedWithNetworkScores ) : IssueDetectorResult {
35+ const inboundScore = data . networkScores . inbound ;
36+ if ( inboundScore !== undefined && inboundScore <= MosQuality . BAD ) {
37+ // do not execute detection on stats based on poor network quality
38+ // to avoid false positives
39+ return [ ] ;
40+ }
41+
42+ return this . processData ( data ) ;
3343 }
3444
3545 private processData ( data : WebRTCStatsParsed ) : IssueDetectorResult {
36- const { connection : { id : connectionId } } = data ;
37- const previousStats = this . getLastProcessedStats ( connectionId ) ;
3846 const issues : IssueDetectorResult = [ ] ;
39-
40- if ( ! previousStats ) {
41- return issues ;
47+ const allLastProcessedStats = this . getAllLastProcessedStats ( data . connection . id ) ;
48+ if ( allLastProcessedStats . length === 0 ) {
49+ return [ ] ;
4250 }
4351
44- const { video : { inbound : newInbound } } = data ;
45- const { video : { inbound : prevInbound } } = previousStats ;
46-
47- const mapByTrackId = ( items : ParsedInboundVideoStreamStats [ ] ) => new Map < string , ParsedInboundVideoStreamStats > (
48- items . map ( ( item ) => [ item . track . trackIdentifier , item ] as const ) ,
49- ) ;
50-
51- const newInboundByTrackId = mapByTrackId ( newInbound ) ;
52- const prevInboundByTrackId = mapByTrackId ( prevInbound ) ;
53- const unvisitedTrackIds = new Set ( this . #lastMarkedAt. keys ( ) ) ;
54-
55- Array . from ( newInboundByTrackId . entries ( ) ) . forEach ( ( [ trackId , newInboundItem ] ) => {
56- unvisitedTrackIds . delete ( trackId ) ;
57-
58- const prevInboundItem = prevInboundByTrackId . get ( trackId ) ;
59- if ( ! prevInboundItem ) {
60- return ;
61- }
62-
63- const deltaFramesReceived = newInboundItem . framesReceived - prevInboundItem . framesReceived ;
64- const deltaFramesDropped = newInboundItem . framesDropped - prevInboundItem . framesDropped ;
65- const deltaFramesDecoded = newInboundItem . framesDecoded - prevInboundItem . framesDecoded ;
66- const ratioFramesDropped = deltaFramesDropped / deltaFramesReceived ;
67-
68- if ( deltaFramesReceived === 0 ) {
69- return ;
70- }
71-
72- // We skip it when ratio is too low because it should be handled by VideoDecoderIssueDetector
73- if ( ratioFramesDropped >= this . #framesDroppedThreshold) {
74- return ;
75- }
76-
77- // It seems that track is alive and we can remove mark if it was marked
78- if ( deltaFramesDecoded > 0 ) {
79- this . removeMarkIssue ( trackId ) ;
80- return ;
81- }
82-
83- const hasIssue = this . markIssue ( trackId ) ;
84-
85- if ( ! hasIssue ) {
86- return ;
87- }
88-
89- const statsSample = {
90- framesReceived : newInboundItem . framesReceived ,
91- framesDropped : newInboundItem . framesDropped ,
92- framesDecoded : newInboundItem . framesDecoded ,
93- deltaFramesReceived,
94- deltaFramesDropped,
95- deltaFramesDecoded,
96- } ;
97-
52+ const frozenStreams = data . video . inbound
53+ . map ( ( videoStream ) : FrozenStream | undefined => {
54+ const prevStat = allLastProcessedStats [ allLastProcessedStats . length - 1 ]
55+ . video . inbound . find ( ( stream ) => stream . ssrc === videoStream . ssrc ) ;
56+
57+ if ( ! prevStat ) {
58+ return undefined ;
59+ }
60+
61+ const isSpatialLayerChanged = isSvcSpatialLayerChanged ( videoStream . ssrc , [
62+ allLastProcessedStats [ allLastProcessedStats . length - 1 ] ,
63+ data ,
64+ ] ) ;
65+
66+ if ( isSpatialLayerChanged ) {
67+ return undefined ;
68+ }
69+
70+ const deltaFreezeCount = videoStream . freezeCount - ( prevStat . freezeCount ?? 0 ) ;
71+ const deltaFreezesTimeMs = ( videoStream . totalFreezesDuration - ( prevStat . totalFreezesDuration ?? 0 ) ) * 1000 ;
72+ const avgFreezeDurationMs = deltaFreezeCount > 0 ? deltaFreezesTimeMs / deltaFreezeCount : 0 ;
73+
74+ const statsTimeDiff = videoStream . timestamp - prevStat . timestamp ;
75+ const frozenDurationPct = ( deltaFreezesTimeMs / statsTimeDiff ) * 100 ;
76+ if ( frozenDurationPct > this . #frozenDurationThresholdPct) {
77+ return {
78+ ssrc : videoStream . ssrc ,
79+ avgFreezeDurationMs,
80+ frozenDurationPct,
81+ } ;
82+ }
83+
84+ if ( avgFreezeDurationMs > this . #avgFreezeDurationThresholdMs) {
85+ return {
86+ ssrc : videoStream . ssrc ,
87+ avgFreezeDurationMs,
88+ frozenDurationPct,
89+ } ;
90+ }
91+
92+ return undefined ;
93+ } )
94+ . filter ( ( stream ) => stream !== undefined ) as FrozenStream [ ] ;
95+
96+ if ( frozenStreams . length > 0 ) {
9897 issues . push ( {
99- statsSample,
10098 type : IssueType . Stream ,
10199 reason : IssueReason . FrozenVideoTrack ,
102- trackIdentifier : trackId ,
100+ statsSample : {
101+ ssrcs : frozenStreams . map ( ( stream ) => stream . ssrc ) ,
102+ } ,
103103 } ) ;
104- } ) ;
105-
106- // just clear unvisited tracks from memory
107- unvisitedTrackIds . forEach ( ( trackId ) => {
108- this . removeMarkIssue ( trackId ) ;
109- } ) ;
110-
111- return issues ;
112- }
113-
114- private markIssue ( trackId : string ) : boolean {
115- const now = Date . now ( ) ;
116-
117- const lastMarkedAt = this . #lastMarkedAt. get ( trackId ) ;
118-
119- if ( ! lastMarkedAt ) {
120- this . #lastMarkedAt. set ( trackId , now ) ;
121- return false ;
122- }
123-
124- if ( now - lastMarkedAt < this . #timeoutMs) {
125- return false ;
126104 }
127105
128- return true ;
129- }
130-
131- private removeMarkIssue ( trackId : string ) : void {
132- this . #lastMarkedAt. delete ( trackId ) ;
106+ return issues ;
133107 }
134108}
135109
0 commit comments