1
1
import {
2
2
IssueDetectorResult ,
3
+ IssueReason ,
4
+ IssueType ,
3
5
WebRTCStatsParsed ,
4
6
} from '../types' ;
5
7
import BaseIssueDetector , { BaseIssueDetectorParams } from './BaseIssueDetector' ;
6
8
7
9
interface VideoDecoderIssueDetectorParams extends BaseIssueDetectorParams {
8
- decodeTimePerFrameIncreaseSpeedThreshold ?: number ;
9
- minDecodeTimePerFrameIncreaseCases ?: number ;
10
+ volatilityThreshold ?: number ;
11
+ affectedStreamsPercentThreshold ?: number ;
10
12
}
11
13
12
14
class VideoDecoderIssueDetector extends BaseIssueDetector {
13
- #decodeTimePerFrameIncreaseSpeedThreshold : number ;
15
+ #volatilityThreshold : number ;
14
16
15
- #minDecodeTimePerFrameIncreaseCases : number ;
17
+ #affectedStreamsPercentThreshold : number ;
16
18
17
19
constructor ( params : VideoDecoderIssueDetectorParams = { } ) {
18
20
super ( params ) ;
19
- this . #decodeTimePerFrameIncreaseSpeedThreshold = params . decodeTimePerFrameIncreaseSpeedThreshold ?? 1.05 ;
20
- this . #minDecodeTimePerFrameIncreaseCases = params . minDecodeTimePerFrameIncreaseCases ?? 3 ;
21
+ this . #volatilityThreshold = params . volatilityThreshold ?? 1.5 ;
22
+ this . #affectedStreamsPercentThreshold = params . affectedStreamsPercentThreshold ?? 50 ;
21
23
}
22
24
23
25
performDetection ( data : WebRTCStatsParsed ) : IssueDetectorResult {
@@ -28,41 +30,83 @@ class VideoDecoderIssueDetector extends BaseIssueDetector {
28
30
}
29
31
30
32
private processData ( data : WebRTCStatsParsed ) : IssueDetectorResult {
31
- const currentIncomeVideoStreams = data . video . inbound ;
32
- const allLastProcessedStats = this
33
- . getAllLastProcessedStats ( data . connection . id ) ;
34
-
35
33
const issues : IssueDetectorResult = [ ] ;
36
34
37
- currentIncomeVideoStreams . forEach ( ( incomeVideoStream ) => {
38
- const lastIncomeVideoStreamStats = allLastProcessedStats
39
- . map ( ( connectionStats ) => connectionStats . video . inbound . find (
40
- ( videoStreamStats ) => videoStreamStats . id === incomeVideoStream . id ,
41
- ) )
42
- . filter ( ( stats ) => ( stats ?. framesDecoded || 0 ) > 0 && ( stats ?. totalDecodeTime || 0 ) > 0 ) ;
43
-
44
- if ( lastIncomeVideoStreamStats . length < this . #minDecodeTimePerFrameIncreaseCases) {
45
- return ;
46
- }
47
-
48
- const decodeTimePerFrame = lastIncomeVideoStreamStats
49
- . map ( ( stats ) => ( stats ! . totalDecodeTime * 1000 ) / stats ! . framesDecoded ) ;
50
-
51
- const currentDecodeTimePerFrame = ( incomeVideoStream . totalDecodeTime * 1000 ) / incomeVideoStream . framesDecoded ;
52
- decodeTimePerFrame . push ( currentDecodeTimePerFrame ) ;
53
-
54
- const mean = decodeTimePerFrame . reduce ( ( acc , val ) => acc + val , 0 ) / decodeTimePerFrame . length ;
55
- const squaredDiffs = decodeTimePerFrame . map ( ( val ) => ( val - mean ) ** 2 ) ;
56
- const variance = squaredDiffs . reduce ( ( acc , val ) => acc + val , 0 ) / squaredDiffs . length ;
57
- const volatility = Math . sqrt ( variance ) ;
58
-
59
- console . log ( {
60
- decodeTimePerFrame,
61
- mean,
62
- variance,
63
- volatility,
35
+ const allProcessedStats = [
36
+ ...this . getAllLastProcessedStats ( data . connection . id ) ,
37
+ data ,
38
+ ] ;
39
+
40
+ const throtthedStreams = data . video . inbound
41
+ . map ( ( incomeVideoStream ) => {
42
+ const allDecodeTimePerFrame : number [ ] = [ ] ;
43
+
44
+ // We need at least 4 elements to have enough representation
45
+ if ( allProcessedStats . length < 4 ) {
46
+ return ;
47
+ }
48
+
49
+ // exclude first element to calculate accurate delta
50
+ for ( let i = 1 ; i < allProcessedStats . length ; i += 1 ) {
51
+ let deltaFramesDecoded = 0 ;
52
+ let deltaTotalDecodeTime = 0 ;
53
+ let decodeTimePerFrame = 0 ;
54
+
55
+ const videoStreamStats = allProcessedStats [ i ] . video . inbound . find (
56
+ ( stream ) => stream . id === incomeVideoStream . id ,
57
+ ) ;
58
+
59
+ if ( ! videoStreamStats ) {
60
+ continue ;
61
+ }
62
+
63
+ const prevVideoStreamStats = allProcessedStats [ i - 1 ] . video . inbound . find (
64
+ ( stream ) => stream . id === incomeVideoStream . id ,
65
+ ) ;
66
+
67
+ if ( prevVideoStreamStats ) {
68
+ deltaFramesDecoded = videoStreamStats . framesDecoded - prevVideoStreamStats . framesDecoded ;
69
+ deltaTotalDecodeTime = videoStreamStats . totalDecodeTime - prevVideoStreamStats . totalDecodeTime ;
70
+ }
71
+
72
+ if ( deltaTotalDecodeTime > 0 && deltaFramesDecoded > 0 ) {
73
+ decodeTimePerFrame = deltaTotalDecodeTime * 1000 / deltaFramesDecoded ;
74
+ }
75
+
76
+ allDecodeTimePerFrame . push ( decodeTimePerFrame ) ;
77
+ }
78
+
79
+ // Calculate volatility
80
+ const mean = allDecodeTimePerFrame . reduce ( ( acc , val ) => acc + val , 0 ) / allDecodeTimePerFrame . length ;
81
+ const squaredDiffs = allDecodeTimePerFrame . map ( ( val ) => ( val - mean ) ** 2 ) ;
82
+ const variance = squaredDiffs . reduce ( ( acc , val ) => acc + val , 0 ) / squaredDiffs . length ;
83
+ const volatility = Math . sqrt ( variance ) ;
84
+
85
+ const isDecodeTimePerFrameIncrease = allDecodeTimePerFrame . every (
86
+ ( decodeTimePerFrame , index ) => {
87
+ return index === 0 || decodeTimePerFrame > allDecodeTimePerFrame [ index - 1 ] ;
88
+ } ,
89
+ ) ;
90
+
91
+ console . log ( {
92
+ allDecodeTimePerFrame,
93
+ isDecodeTimePerFrameIncrease,
94
+ mean,
95
+ volatility,
96
+ } ) ;
97
+
98
+ return volatility > this . #volatilityThreshold && isDecodeTimePerFrameIncrease ;
99
+ } )
100
+ . filter ( ( throttled ) => throttled ) ;
101
+
102
+
103
+ const affectedStreamsPercent = throtthedStreams . length / ( data . video . inbound . length / 100 ) ;
104
+ if ( affectedStreamsPercent > this . #affectedStreamsPercentThreshold) {
105
+ issues . push ( {
106
+ type : IssueType . CPU ,
107
+ reason : IssueReason . DecoderCPUThrottling ,
64
108
} ) ;
65
- } ) ;
109
+ }
66
110
67
111
return issues ;
68
112
}
0 commit comments