@@ -45,20 +45,28 @@ class StatsTracer:
4545 - Delta compression to reduce size by ~90%
4646 - Performance stats calculation (encode/decode metrics)
4747 - Frame time and FPS history for averaging
48+ - Integration with VideoFrameTracker/BufferedMediaTrack for frame metrics
4849 """
4950
50- def __init__ (self , pc , peer_type : str ):
51+ def __init__ (self , pc , peer_type : str , interval_s : float = 8.0 ):
5152 """Initialize StatsTracer for a peer connection.
5253
5354 Args:
5455 pc: The RTCPeerConnection to collect stats from
5556 peer_type: "publisher" or "subscriber"
57+ interval_s: Interval between stats collections in seconds (for FPS calculation)
5658 """
5759 self ._pc = pc
5860 self ._peer_type = peer_type
61+ self ._interval_s = interval_s
5962 self ._previous_stats : Dict [str , Dict ] = {}
6063 self ._frame_time_history : List [float ] = []
6164 self ._fps_history : List [float ] = []
65+ self ._frame_tracker : Optional [Any ] = None
66+
67+ def set_frame_tracker (self , tracker : Any ) -> None :
68+ """Set the frame tracker for publisher stats (video track wrapper)."""
69+ self ._frame_tracker = tracker
6270
6371 async def get (self ) -> ComputedStats :
6472 """Get stats with delta compression and performance metrics.
@@ -131,6 +139,9 @@ def _report_to_dict(self, report) -> Dict[str, Dict]:
131139 # Add ICE candidate stats (not provided by aiortc getStats)
132140 self ._add_ice_candidate_stats (result )
133141
142+ # Inject frame stats from tracker (not provided by aiortc)
143+ self ._inject_frame_stats (result )
144+
134145 return result
135146
136147 def _delta_compress (
@@ -579,3 +590,85 @@ def _codec_id(self, codec, mid: Optional[str]) -> str:
579590 clock_rate = getattr (codec , "clockRate" , 0 )
580591 key = f"{ mime_type } :{ payload_type } :{ clock_rate } :{ mid } "
581592 return hashlib .md5 (key .encode ()).hexdigest ()[:8 ]
593+
594+ def _inject_frame_stats (self , result : Dict [str , Dict ]) -> None :
595+ """Inject frame stats from trackers into RTP stats.
596+
597+ aiortc doesn't provide frame metrics (dimensions, frame count, encode/decode time).
598+ We inject these from our frame trackers into the appropriate RTP stats entries.
599+ """
600+ if self ._peer_type == "publisher" :
601+ self ._inject_publisher_stats (result )
602+ else :
603+ self ._inject_subscriber_stats (result )
604+
605+ def _inject_publisher_stats (self , result : Dict [str , Dict ]) -> None :
606+ """Inject stats for publisher (outbound-rtp)."""
607+ if not self ._frame_tracker :
608+ return
609+
610+ try :
611+ frame_stats = self ._frame_tracker .get_frame_stats ()
612+ if frame_stats .get ("framesSent" , 0 ) == 0 :
613+ return
614+
615+ for stat in result .values ():
616+ if not isinstance (stat , dict ):
617+ continue
618+ if stat .get ("kind" ) != "video" or stat .get ("type" ) != "outbound-rtp" :
619+ continue
620+
621+ stat ["framesSent" ] = frame_stats ["framesSent" ]
622+ stat ["frameWidth" ] = frame_stats ["frameWidth" ]
623+ stat ["frameHeight" ] = frame_stats ["frameHeight" ]
624+ stat ["totalEncodeTime" ] = frame_stats ["totalEncodeTime" ]
625+
626+ if self ._previous_stats :
627+ prev = self ._previous_stats .get (stat .get ("id" , "" ), {})
628+ delta = frame_stats ["framesSent" ] - prev .get ("framesSent" , 0 )
629+ if delta > 0 :
630+ stat ["framesPerSecond" ] = delta / self ._interval_s
631+
632+ except Exception as e :
633+ logger .debug (f"Failed to inject publisher stats: { e } " )
634+
635+ def _inject_subscriber_stats (self , result : Dict [str , Dict ]) -> None :
636+ """Inject frame stats for subscriber (inbound-rtp video).
637+
638+ Note: When multiple video tracks exist (e.g., webcam + screenshare),
639+ get_video_frame_tracker() returns the first by insertion order, which
640+ may not match the actively consumed track. This is a known limitation;
641+ _get_decode_stats() mitigates by selecting the highest-resolution track
642+ for performance calculations.
643+ """
644+ # Get video tracker from PC if not set
645+ if not self ._frame_tracker and hasattr (self ._pc , "get_video_frame_tracker" ):
646+ self ._frame_tracker = self ._pc .get_video_frame_tracker ()
647+
648+ if not self ._frame_tracker :
649+ return
650+
651+ try :
652+ frame_stats = self ._frame_tracker .get_frame_stats ()
653+ if frame_stats .get ("framesDecoded" , 0 ) == 0 :
654+ return
655+
656+ for stat in result .values ():
657+ if not isinstance (stat , dict ):
658+ continue
659+ if stat .get ("type" ) != "inbound-rtp" or stat .get ("kind" ) != "video" :
660+ continue
661+
662+ stat ["framesDecoded" ] = frame_stats ["framesDecoded" ]
663+ stat ["frameWidth" ] = frame_stats ["frameWidth" ]
664+ stat ["frameHeight" ] = frame_stats ["frameHeight" ]
665+ stat ["totalDecodeTime" ] = frame_stats ["totalDecodeTime" ]
666+
667+ if self ._previous_stats :
668+ prev = self ._previous_stats .get (stat .get ("id" , "" ), {})
669+ delta = frame_stats ["framesDecoded" ] - prev .get ("framesDecoded" , 0 )
670+ if delta > 0 :
671+ stat ["framesPerSecond" ] = delta / self ._interval_s
672+
673+ except Exception as e :
674+ logger .debug (f"Failed to inject subscriber stats: { e } " )
0 commit comments