0
# Statistics
1
2
Connection statistics, media quality metrics, and transport performance monitoring for debugging and quality assurance.
3
4
## Capabilities
5
6
### RTCStatsReport Class
7
8
Container for WebRTC statistics providing dictionary-like access to stats objects.
9
10
```python { .api }
11
class RTCStatsReport:
12
"""Statistics report container."""
13
14
def __init__(self):
15
"""Create empty statistics report."""
16
17
def add(self, stats) -> None:
18
"""
19
Add statistics object to report.
20
21
Parameters:
22
- stats: Statistics object (RTCInboundRtpStreamStats, RTCOutboundRtpStreamStats, etc.)
23
"""
24
25
def __getitem__(self, key: str):
26
"""Get statistics object by ID."""
27
28
def __iter__(self):
29
"""Iterate over statistics IDs."""
30
31
def keys(self):
32
"""Get all statistics IDs."""
33
34
def values(self):
35
"""Get all statistics objects."""
36
37
def items(self):
38
"""Get (ID, stats) pairs."""
39
```
40
41
### Base Statistics Class
42
43
Base class for all statistics objects with common properties.
44
45
```python { .api }
46
class RTCStats:
47
"""Base class for statistics objects."""
48
49
@property
50
def id(self) -> str:
51
"""Unique statistics identifier"""
52
53
@property
54
def timestamp(self) -> float:
55
"""Timestamp when statistics were gathered (seconds since epoch)"""
56
57
@property
58
def type(self) -> str:
59
"""Statistics type identifier"""
60
```
61
62
### RTP Stream Statistics
63
64
Statistics for incoming and outgoing RTP media streams.
65
66
```python { .api }
67
class RTCInboundRtpStreamStats(RTCStats):
68
"""Statistics for inbound RTP streams."""
69
70
@property
71
def type(self) -> str:
72
"""Always "inbound-rtp" """
73
74
@property
75
def ssrc(self) -> int:
76
"""Synchronization source identifier"""
77
78
@property
79
def kind(self) -> str:
80
"""Media kind: "audio" or "video" """
81
82
@property
83
def trackId(self) -> str:
84
"""Associated track identifier"""
85
86
@property
87
def transportId(self) -> str:
88
"""Associated transport identifier"""
89
90
@property
91
def codecId(self) -> str:
92
"""Associated codec identifier"""
93
94
# Packet statistics
95
@property
96
def packetsReceived(self) -> int:
97
"""Total packets received"""
98
99
@property
100
def packetsLost(self) -> int:
101
"""Total packets lost"""
102
103
@property
104
def packetsDiscarded(self) -> int:
105
"""Total packets discarded"""
106
107
@property
108
def packetsRepaired(self) -> int:
109
"""Total packets repaired"""
110
111
# Byte statistics
112
@property
113
def bytesReceived(self) -> int:
114
"""Total bytes received"""
115
116
@property
117
def headerBytesReceived(self) -> int:
118
"""Total header bytes received"""
119
120
# Timing statistics
121
@property
122
def jitter(self) -> float:
123
"""Packet jitter in seconds"""
124
125
@property
126
def fractionLost(self) -> float:
127
"""Fraction of packets lost (0.0 to 1.0)"""
128
129
class RTCOutboundRtpStreamStats(RTCStats):
130
"""Statistics for outbound RTP streams."""
131
132
@property
133
def type(self) -> str:
134
"""Always "outbound-rtp" """
135
136
@property
137
def ssrc(self) -> int:
138
"""Synchronization source identifier"""
139
140
@property
141
def kind(self) -> str:
142
"""Media kind: "audio" or "video" """
143
144
@property
145
def trackId(self) -> str:
146
"""Associated track identifier"""
147
148
@property
149
def transportId(self) -> str:
150
"""Associated transport identifier"""
151
152
@property
153
def codecId(self) -> str:
154
"""Associated codec identifier"""
155
156
# Packet statistics
157
@property
158
def packetsSent(self) -> int:
159
"""Total packets sent"""
160
161
@property
162
def packetsLost(self) -> int:
163
"""Total packets lost (from RTCP feedback)"""
164
165
@property
166
def retransmittedPacketsSent(self) -> int:
167
"""Total retransmitted packets sent"""
168
169
# Byte statistics
170
@property
171
def bytesSent(self) -> int:
172
"""Total bytes sent"""
173
174
@property
175
def headerBytesSent(self) -> int:
176
"""Total header bytes sent"""
177
178
@property
179
def retransmittedBytesSent(self) -> int:
180
"""Total retransmitted bytes sent"""
181
182
# Quality statistics
183
@property
184
def targetBitrate(self) -> float:
185
"""Target bitrate in bits per second"""
186
187
@property
188
def framesEncoded(self) -> int:
189
"""Total frames encoded (video only)"""
190
191
@property
192
def keyFramesEncoded(self) -> int:
193
"""Total key frames encoded (video only)"""
194
195
class RTCRemoteInboundRtpStreamStats(RTCStats):
196
"""Statistics for remote inbound RTP streams (from RTCP feedback)."""
197
198
@property
199
def type(self) -> str:
200
"""Always "remote-inbound-rtp" """
201
202
@property
203
def ssrc(self) -> int:
204
"""Synchronization source identifier"""
205
206
@property
207
def kind(self) -> str:
208
"""Media kind: "audio" or "video" """
209
210
@property
211
def packetsLost(self) -> int:
212
"""Total packets lost at remote"""
213
214
@property
215
def fractionLost(self) -> float:
216
"""Fraction lost at remote (0.0 to 1.0)"""
217
218
@property
219
def roundTripTime(self) -> float:
220
"""Round trip time in seconds"""
221
222
@property
223
def jitter(self) -> float:
224
"""Jitter at remote in seconds"""
225
226
class RTCRemoteOutboundRtpStreamStats(RTCStats):
227
"""Statistics for remote outbound RTP streams (from RTCP feedback)."""
228
229
@property
230
def type(self) -> str:
231
"""Always "remote-outbound-rtp" """
232
233
@property
234
def ssrc(self) -> int:
235
"""Synchronization source identifier"""
236
237
@property
238
def kind(self) -> str:
239
"""Media kind: "audio" or "video" """
240
241
@property
242
def packetsSent(self) -> int:
243
"""Total packets sent by remote"""
244
245
@property
246
def bytesSent(self) -> int:
247
"""Total bytes sent by remote"""
248
249
@property
250
def remoteTimestamp(self) -> float:
251
"""Remote timestamp"""
252
```
253
254
### Transport Statistics
255
256
Statistics for transport layer performance.
257
258
```python { .api }
259
class RTCTransportStats(RTCStats):
260
"""Statistics for transport layer."""
261
262
@property
263
def type(self) -> str:
264
"""Always "transport" """
265
266
@property
267
def bytesSent(self) -> int:
268
"""Total bytes sent over transport"""
269
270
@property
271
def bytesReceived(self) -> int:
272
"""Total bytes received over transport"""
273
274
@property
275
def dtlsState(self) -> str:
276
"""DTLS connection state"""
277
278
@property
279
def iceRole(self) -> str:
280
"""ICE role: "controlling" or "controlled" """
281
282
@property
283
def iceState(self) -> str:
284
"""ICE connection state"""
285
286
@property
287
def localCandidateId(self) -> str:
288
"""Selected local candidate ID"""
289
290
@property
291
def remoteCandidateId(self) -> str:
292
"""Selected remote candidate ID"""
293
294
@property
295
def tlsVersion(self) -> str:
296
"""TLS/DTLS version used"""
297
298
@property
299
def dtlsCipher(self) -> str:
300
"""DTLS cipher suite"""
301
302
@property
303
def srtpCipher(self) -> str:
304
"""SRTP cipher suite"""
305
```
306
307
## Usage Examples
308
309
### Basic Statistics Collection
310
311
```python
312
import aiortc
313
import asyncio
314
315
async def collect_basic_stats():
316
pc = aiortc.RTCPeerConnection()
317
318
# Add media tracks
319
audio_track = aiortc.AudioStreamTrack()
320
video_track = aiortc.VideoStreamTrack()
321
322
audio_sender = pc.addTrack(audio_track)
323
video_sender = pc.addTrack(video_track)
324
325
# Simulate connection setup (simplified)
326
offer = await pc.createOffer()
327
await pc.setLocalDescription(offer)
328
329
# Get connection statistics
330
stats_report = await pc.getStats()
331
332
print(f"Total statistics objects: {len(list(stats_report.keys()))}")
333
334
# Iterate through all statistics
335
for stats_id, stats in stats_report.items():
336
print(f"Stats ID: {stats_id}")
337
print(f" Type: {stats.type}")
338
print(f" Timestamp: {stats.timestamp}")
339
340
# Print type-specific information
341
if hasattr(stats, 'ssrc'):
342
print(f" SSRC: {stats.ssrc}")
343
if hasattr(stats, 'kind'):
344
print(f" Kind: {stats.kind}")
345
```
346
347
### RTP Stream Statistics Monitoring
348
349
```python
350
async def monitor_rtp_stats():
351
pc = aiortc.RTCPeerConnection()
352
353
# Add tracks
354
audio_sender = pc.addTrack(aiortc.AudioStreamTrack())
355
video_sender = pc.addTrack(aiortc.VideoStreamTrack())
356
357
async def print_rtp_stats():
358
while True:
359
try:
360
# Get sender statistics
361
audio_stats = await audio_sender.getStats()
362
video_stats = await video_sender.getStats()
363
364
print("=== RTP Statistics ===")
365
366
# Process audio sender stats
367
for stats in audio_stats.values():
368
if isinstance(stats, aiortc.RTCOutboundRtpStreamStats):
369
print(f"Audio Outbound:")
370
print(f" Packets sent: {stats.packetsSent}")
371
print(f" Bytes sent: {stats.bytesSent}")
372
print(f" Packets lost: {stats.packetsLost}")
373
374
# Process video sender stats
375
for stats in video_stats.values():
376
if isinstance(stats, aiortc.RTCOutboundRtpStreamStats):
377
print(f"Video Outbound:")
378
print(f" Packets sent: {stats.packetsSent}")
379
print(f" Bytes sent: {stats.bytesSent}")
380
print(f" Frames encoded: {stats.framesEncoded}")
381
print(f" Key frames: {stats.keyFramesEncoded}")
382
383
await asyncio.sleep(5) # Update every 5 seconds
384
385
except Exception as e:
386
print(f"Error getting stats: {e}")
387
break
388
389
# Start monitoring
390
monitor_task = asyncio.create_task(print_rtp_stats())
391
392
# Let it run for a while
393
await asyncio.sleep(30)
394
monitor_task.cancel()
395
```
396
397
### Receiver Statistics Analysis
398
399
```python
400
async def analyze_receiver_stats():
401
pc = aiortc.RTCPeerConnection()
402
403
@pc.on("track")
404
def on_track(track):
405
print(f"Received {track.kind} track")
406
407
# Find the receiver for this track
408
receiver = None
409
for transceiver in pc.getTransceivers():
410
if transceiver.receiver.track == track:
411
receiver = transceiver.receiver
412
break
413
414
if receiver:
415
async def monitor_receiver():
416
while True:
417
try:
418
stats_report = await receiver.getStats()
419
420
for stats in stats_report.values():
421
if isinstance(stats, aiortc.RTCInboundRtpStreamStats):
422
print(f"{track.kind.capitalize()} Inbound:")
423
print(f" Packets received: {stats.packetsReceived}")
424
print(f" Packets lost: {stats.packetsLost}")
425
print(f" Bytes received: {stats.bytesReceived}")
426
print(f" Jitter: {stats.jitter:.4f}s")
427
print(f" Fraction lost: {stats.fractionLost:.2%}")
428
429
await asyncio.sleep(3)
430
431
except Exception as e:
432
print(f"Error monitoring receiver: {e}")
433
break
434
435
asyncio.create_task(monitor_receiver())
436
```
437
438
### Transport Layer Statistics
439
440
```python
441
async def monitor_transport_stats():
442
pc = aiortc.RTCPeerConnection()
443
444
async def print_transport_stats():
445
stats_report = await pc.getStats()
446
447
for stats in stats_report.values():
448
if isinstance(stats, aiortc.RTCTransportStats):
449
print("Transport Statistics:")
450
print(f" Bytes sent: {stats.bytesSent}")
451
print(f" Bytes received: {stats.bytesReceived}")
452
print(f" DTLS state: {stats.dtlsState}")
453
print(f" ICE state: {stats.iceState}")
454
print(f" ICE role: {stats.iceRole}")
455
print(f" TLS version: {stats.tlsVersion}")
456
print(f" DTLS cipher: {stats.dtlsCipher}")
457
print(f" SRTP cipher: {stats.srtpCipher}")
458
print(f" Local candidate: {stats.localCandidateId}")
459
print(f" Remote candidate: {stats.remoteCandidateId}")
460
461
# Monitor transport stats
462
await print_transport_stats()
463
```
464
465
### Quality Metrics Dashboard
466
467
```python
468
async def quality_metrics_dashboard():
469
"""Create a comprehensive quality metrics dashboard."""
470
471
pc = aiortc.RTCPeerConnection()
472
473
# Add tracks
474
audio_sender = pc.addTrack(aiortc.AudioStreamTrack())
475
video_sender = pc.addTrack(aiortc.VideoStreamTrack())
476
477
class QualityMetrics:
478
def __init__(self):
479
self.reset()
480
481
def reset(self):
482
self.total_packets_sent = 0
483
self.total_packets_lost = 0
484
self.total_bytes_sent = 0
485
self.average_jitter = 0.0
486
self.packet_loss_rate = 0.0
487
self.bitrate = 0.0
488
self.last_timestamp = None
489
self.last_bytes = 0
490
491
audio_metrics = QualityMetrics()
492
video_metrics = QualityMetrics()
493
494
async def update_quality_metrics():
495
nonlocal audio_metrics, video_metrics
496
497
try:
498
# Get all connection statistics
499
stats_report = await pc.getStats()
500
current_time = asyncio.get_event_loop().time()
501
502
for stats in stats_report.values():
503
if isinstance(stats, aiortc.RTCOutboundRtpStreamStats):
504
metrics = audio_metrics if stats.kind == "audio" else video_metrics
505
506
# Update packet statistics
507
metrics.total_packets_sent = stats.packetsSent
508
metrics.total_packets_lost = stats.packetsLost
509
metrics.total_bytes_sent = stats.bytesSent
510
511
# Calculate packet loss rate
512
total_packets = metrics.total_packets_sent + metrics.total_packets_lost
513
if total_packets > 0:
514
metrics.packet_loss_rate = metrics.total_packets_lost / total_packets
515
516
# Calculate bitrate
517
if metrics.last_timestamp:
518
time_diff = current_time - metrics.last_timestamp
519
byte_diff = stats.bytesSent - metrics.last_bytes
520
if time_diff > 0:
521
metrics.bitrate = (byte_diff * 8) / time_diff # bits per second
522
523
metrics.last_timestamp = current_time
524
metrics.last_bytes = stats.bytesSent
525
526
elif isinstance(stats, aiortc.RTCInboundRtpStreamStats):
527
metrics = audio_metrics if stats.kind == "audio" else video_metrics
528
metrics.average_jitter = stats.jitter
529
530
except Exception as e:
531
print(f"Error updating metrics: {e}")
532
533
def print_dashboard():
534
print("\n" + "="*50)
535
print("QUALITY METRICS DASHBOARD")
536
print("="*50)
537
538
print(f"AUDIO:")
539
print(f" Packets sent: {audio_metrics.total_packets_sent}")
540
print(f" Packets lost: {audio_metrics.total_packets_lost}")
541
print(f" Loss rate: {audio_metrics.packet_loss_rate:.2%}")
542
print(f" Bitrate: {audio_metrics.bitrate/1000:.1f} kbps")
543
print(f" Jitter: {audio_metrics.average_jitter*1000:.1f} ms")
544
545
print(f"VIDEO:")
546
print(f" Packets sent: {video_metrics.total_packets_sent}")
547
print(f" Packets lost: {video_metrics.total_packets_lost}")
548
print(f" Loss rate: {video_metrics.packet_loss_rate:.2%}")
549
print(f" Bitrate: {video_metrics.bitrate/1000:.1f} kbps")
550
print(f" Jitter: {video_metrics.average_jitter*1000:.1f} ms")
551
552
# Run dashboard updates
553
for i in range(10): # Run for 10 iterations
554
await update_quality_metrics()
555
print_dashboard()
556
await asyncio.sleep(2)
557
```
558
559
### Statistics Export
560
561
```python
562
async def export_statistics():
563
"""Export statistics to different formats."""
564
565
pc = aiortc.RTCPeerConnection()
566
567
# Add some tracks
568
pc.addTrack(aiortc.AudioStreamTrack())
569
pc.addTrack(aiortc.VideoStreamTrack())
570
571
# Get statistics
572
stats_report = await pc.getStats()
573
574
# Export to JSON
575
import json
576
577
stats_data = {}
578
for stats_id, stats in stats_report.items():
579
stats_dict = {
580
"id": stats.id,
581
"type": stats.type,
582
"timestamp": stats.timestamp
583
}
584
585
# Add type-specific fields
586
if hasattr(stats, 'ssrc'):
587
stats_dict['ssrc'] = stats.ssrc
588
if hasattr(stats, 'kind'):
589
stats_dict['kind'] = stats.kind
590
if hasattr(stats, 'packetsSent'):
591
stats_dict['packetsSent'] = stats.packetsSent
592
if hasattr(stats, 'bytesSent'):
593
stats_dict['bytesSent'] = stats.bytesSent
594
# Add more fields as needed
595
596
stats_data[stats_id] = stats_dict
597
598
# Save to file
599
with open("webrtc_stats.json", "w") as f:
600
json.dump(stats_data, f, indent=2)
601
602
print("Statistics exported to webrtc_stats.json")
603
604
# Export to CSV
605
import csv
606
607
with open("webrtc_stats.csv", "w", newline="") as f:
608
writer = csv.writer(f)
609
610
# Write header
611
writer.writerow(["ID", "Type", "Timestamp", "SSRC", "Kind", "PacketsSent", "BytesSent"])
612
613
# Write data
614
for stats_id, stats in stats_report.items():
615
row = [
616
stats.id,
617
stats.type,
618
stats.timestamp,
619
getattr(stats, 'ssrc', ''),
620
getattr(stats, 'kind', ''),
621
getattr(stats, 'packetsSent', ''),
622
getattr(stats, 'bytesSent', '')
623
]
624
writer.writerow(row)
625
626
print("Statistics exported to webrtc_stats.csv")
627
```
628
629
### Real-time Statistics Alerts
630
631
```python
632
async def statistics_alerts():
633
"""Monitor statistics and trigger alerts for issues."""
634
635
pc = aiortc.RTCPeerConnection()
636
audio_sender = pc.addTrack(aiortc.AudioStreamTrack())
637
video_sender = pc.addTrack(aiortc.VideoStreamTrack())
638
639
# Alert thresholds
640
PACKET_LOSS_THRESHOLD = 0.05 # 5%
641
HIGH_JITTER_THRESHOLD = 0.050 # 50ms
642
LOW_BITRATE_THRESHOLD = 50000 # 50 kbps
643
644
async def check_alerts():
645
while True:
646
try:
647
stats_report = await pc.getStats()
648
649
for stats in stats_report.values():
650
if isinstance(stats, aiortc.RTCOutboundRtpStreamStats):
651
# Check packet loss
652
total_packets = stats.packetsSent + stats.packetsLost
653
if total_packets > 0:
654
loss_rate = stats.packetsLost / total_packets
655
if loss_rate > PACKET_LOSS_THRESHOLD:
656
print(f"🚨 HIGH PACKET LOSS: {loss_rate:.2%} on {stats.kind}")
657
658
# Check bitrate (simplified calculation)
659
if hasattr(stats, 'targetBitrate') and stats.targetBitrate < LOW_BITRATE_THRESHOLD:
660
print(f"⚠️ LOW BITRATE: {stats.targetBitrate/1000:.1f} kbps on {stats.kind}")
661
662
elif isinstance(stats, aiortc.RTCInboundRtpStreamStats):
663
# Check jitter
664
if stats.jitter > HIGH_JITTER_THRESHOLD:
665
print(f"⚠️ HIGH JITTER: {stats.jitter*1000:.1f} ms on {stats.kind}")
666
667
# Check fraction lost
668
if stats.fractionLost > PACKET_LOSS_THRESHOLD:
669
print(f"🚨 HIGH LOSS FRACTION: {stats.fractionLost:.2%} on {stats.kind}")
670
671
await asyncio.sleep(5) # Check every 5 seconds
672
673
except Exception as e:
674
print(f"Error checking alerts: {e}")
675
break
676
677
# Start alert monitoring
678
await check_alerts()
679
```