1#!/usr/bin/env python3 2# Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 3# 4# Use of this source code is governed by a BSD-style license 5# that can be found in the LICENSE file in the root of the source 6# tree. An additional intellectual property rights grant can be found 7# in the file PATENTS. All contributing project authors may 8# be found in the AUTHORS file in the root of the source tree. 9"""Displays statistics and plots graphs from RTC protobuf dump.""" 10 11from __future__ import division 12from __future__ import print_function 13 14from __future__ import absolute_import 15import collections 16import optparse 17import os 18import sys 19from six.moves import range 20from six.moves import zip 21 22import matplotlib.pyplot as plt 23import numpy 24 25import misc 26import pb_parse 27 28 29class RTPStatistics: 30 """Has methods for calculating and plotting RTP stream statistics.""" 31 32 BANDWIDTH_SMOOTHING_WINDOW_SIZE = 10 33 PLOT_RESOLUTION_MS = 50 34 35 def __init__(self, data_points): 36 """Initializes object with data_points and computes simple statistics. 37 38 Computes percentages of number of packets and packet sizes by 39 SSRC. 40 41 Args: 42 data_points: list of pb_parse.DataPoints on which statistics are 43 calculated. 44 45 """ 46 47 self.data_points = data_points 48 self.ssrc_frequencies = misc.NormalizeCounter( 49 collections.Counter([pt.ssrc for pt in self.data_points])) 50 self.ssrc_size_table = misc.SsrcNormalizedSizeTable(self.data_points) 51 self.bandwidth_kbps = None 52 self.smooth_bw_kbps = None 53 54 def PrintHeaderStatistics(self): 55 print("{:>6}{:>14}{:>14}{:>6}{:>6}{:>3}{:>11}".format( 56 "SeqNo", "TimeStamp", "SendTime", "Size", "PT", "M", "SSRC")) 57 for point in self.data_points: 58 print("{:>6}{:>14}{:>14}{:>6}{:>6}{:>3}{:>11}".format( 59 point.sequence_number, point.timestamp, 60 int(point.arrival_timestamp_ms), point.size, point.payload_type, 61 point.marker_bit, "0x{:x}".format(point.ssrc))) 62 63 def PrintSsrcInfo(self, ssrc_id, ssrc): 64 """Prints packet and size statistics for a given SSRC. 65 66 Args: 67 ssrc_id: textual identifier of SSRC printed beside statistics for it. 68 ssrc: SSRC by which to filter data and display statistics 69 """ 70 filtered_ssrc = [point for point in self.data_points if point.ssrc == ssrc] 71 payloads = misc.NormalizeCounter( 72 collections.Counter([point.payload_type for point in filtered_ssrc])) 73 74 payload_info = "payload type(s): {}".format(", ".join( 75 str(payload) for payload in payloads)) 76 print("{} 0x{:x} {}, {:.2f}% packets, {:.2f}% data".format( 77 ssrc_id, ssrc, payload_info, self.ssrc_frequencies[ssrc] * 100, 78 self.ssrc_size_table[ssrc] * 100)) 79 print(" packet sizes:") 80 (bin_counts, 81 bin_bounds) = numpy.histogram([point.size for point in filtered_ssrc], 82 bins=5, 83 density=False) 84 bin_proportions = bin_counts / sum(bin_counts) 85 print("\n".join([ 86 " {:.1f} - {:.1f}: {:.2f}%".format(bin_bounds[i], bin_bounds[i + 1], 87 bin_proportions[i] * 100) 88 for i in range(len(bin_proportions)) 89 ])) 90 91 def ChooseSsrc(self): 92 """Queries user for SSRC.""" 93 94 if len(self.ssrc_frequencies) == 1: 95 chosen_ssrc = list(self.ssrc_frequencies.keys())[0] 96 self.PrintSsrcInfo("", chosen_ssrc) 97 return chosen_ssrc 98 99 ssrc_is_incoming = misc.SsrcDirections(self.data_points) 100 incoming = [ssrc for ssrc in ssrc_is_incoming if ssrc_is_incoming[ssrc]] 101 outgoing = [ssrc for ssrc in ssrc_is_incoming if not ssrc_is_incoming[ssrc]] 102 103 print("\nIncoming:\n") 104 for (i, ssrc) in enumerate(incoming): 105 self.PrintSsrcInfo(i, ssrc) 106 107 print("\nOutgoing:\n") 108 for (i, ssrc) in enumerate(outgoing): 109 self.PrintSsrcInfo(i + len(incoming), ssrc) 110 111 while True: 112 chosen_index = int(misc.get_input("choose one> ")) 113 if 0 <= chosen_index < len(self.ssrc_frequencies): 114 return (incoming + outgoing)[chosen_index] 115 print("Invalid index!") 116 117 def FilterSsrc(self, chosen_ssrc): 118 """Filters and wraps data points. 119 120 Removes data points with `ssrc != chosen_ssrc`. Unwraps sequence 121 numbers and timestamps for the chosen selection. 122 """ 123 self.data_points = [ 124 point for point in self.data_points if point.ssrc == chosen_ssrc 125 ] 126 unwrapped_sequence_numbers = misc.Unwrap( 127 [point.sequence_number for point in self.data_points], 2**16 - 1) 128 for (data_point, sequence_number) in zip(self.data_points, 129 unwrapped_sequence_numbers): 130 data_point.sequence_number = sequence_number 131 132 unwrapped_timestamps = misc.Unwrap( 133 [point.timestamp for point in self.data_points], 2**32 - 1) 134 135 for (data_point, timestamp) in zip(self.data_points, unwrapped_timestamps): 136 data_point.timestamp = timestamp 137 138 def PrintSequenceNumberStatistics(self): 139 seq_no_set = set(point.sequence_number for point in self.data_points) 140 missing_sequence_numbers = max(seq_no_set) - min(seq_no_set) + ( 141 1 - len(seq_no_set)) 142 print("Missing sequence numbers: {} out of {} ({:.2f}%)".format( 143 missing_sequence_numbers, len(seq_no_set), 144 100 * missing_sequence_numbers / len(seq_no_set))) 145 print("Duplicated packets: {}".format( 146 len(self.data_points) - len(seq_no_set))) 147 print("Reordered packets: {}".format( 148 misc.CountReordered( 149 [point.sequence_number for point in self.data_points]))) 150 151 def EstimateFrequency(self, always_query_sample_rate): 152 """Estimates frequency and updates data. 153 154 Guesses the most probable frequency by looking at changes in 155 timestamps (RFC 3550 section 5.1), calculates clock drifts and 156 sending time of packets. Updates `self.data_points` with changes 157 in delay and send time. 158 """ 159 delta_timestamp = (self.data_points[-1].timestamp - 160 self.data_points[0].timestamp) 161 delta_arr_timestamp = float((self.data_points[-1].arrival_timestamp_ms - 162 self.data_points[0].arrival_timestamp_ms)) 163 freq_est = delta_timestamp / delta_arr_timestamp 164 165 freq_vec = [8, 16, 32, 48, 90] 166 freq = None 167 for f in freq_vec: 168 if abs((freq_est - f) / f) < 0.05: 169 freq = f 170 171 print("Estimated frequency: {:.3f}kHz".format(freq_est)) 172 if freq is None or always_query_sample_rate: 173 if not always_query_sample_rate: 174 print("Frequency could not be guessed.", end=" ") 175 freq = int(misc.get_input("Input frequency (in kHz)> ")) 176 else: 177 print("Guessed frequency: {}kHz".format(freq)) 178 179 for point in self.data_points: 180 point.real_send_time_ms = (point.timestamp - 181 self.data_points[0].timestamp) / freq 182 point.delay = point.arrival_timestamp_ms - point.real_send_time_ms 183 184 def PrintDurationStatistics(self): 185 """Prints delay, clock drift and bitrate statistics.""" 186 187 min_delay = min(point.delay for point in self.data_points) 188 189 for point in self.data_points: 190 point.absdelay = point.delay - min_delay 191 192 stream_duration_sender = self.data_points[-1].real_send_time_ms / 1000 193 print("Stream duration at sender: {:.1f} seconds".format( 194 stream_duration_sender)) 195 196 arrival_timestamps_ms = [ 197 point.arrival_timestamp_ms for point in self.data_points 198 ] 199 stream_duration_receiver = (max(arrival_timestamps_ms) - 200 min(arrival_timestamps_ms)) / 1000 201 print("Stream duration at receiver: {:.1f} seconds".format( 202 stream_duration_receiver)) 203 204 print("Clock drift: {:.2f}%".format( 205 100 * (stream_duration_receiver / stream_duration_sender - 1))) 206 207 total_size = sum(point.size for point in self.data_points) * 8 / 1000 208 print("Send average bitrate: {:.2f} kbps".format(total_size / 209 stream_duration_sender)) 210 211 print("Receive average bitrate: {:.2f} kbps".format( 212 total_size / stream_duration_receiver)) 213 214 def RemoveReordered(self): 215 last = self.data_points[0] 216 data_points_ordered = [last] 217 for point in self.data_points[1:]: 218 if point.sequence_number > last.sequence_number and ( 219 point.real_send_time_ms > last.real_send_time_ms): 220 data_points_ordered.append(point) 221 last = point 222 self.data_points = data_points_ordered 223 224 def ComputeBandwidth(self): 225 """Computes bandwidth averaged over several consecutive packets. 226 227 The number of consecutive packets used in the average is 228 BANDWIDTH_SMOOTHING_WINDOW_SIZE. Averaging is done with 229 numpy.correlate. 230 """ 231 start_ms = self.data_points[0].real_send_time_ms 232 stop_ms = self.data_points[-1].real_send_time_ms 233 (self.bandwidth_kbps, _) = numpy.histogram( 234 [point.real_send_time_ms for point in self.data_points], 235 bins=numpy.arange(start_ms, stop_ms, RTPStatistics.PLOT_RESOLUTION_MS), 236 weights=[ 237 point.size * 8 / RTPStatistics.PLOT_RESOLUTION_MS 238 for point in self.data_points 239 ]) 240 correlate_filter = ( 241 numpy.ones(RTPStatistics.BANDWIDTH_SMOOTHING_WINDOW_SIZE) / 242 RTPStatistics.BANDWIDTH_SMOOTHING_WINDOW_SIZE) 243 self.smooth_bw_kbps = numpy.correlate(self.bandwidth_kbps, correlate_filter) 244 245 def PlotStatistics(self): 246 """Plots changes in delay and average bandwidth.""" 247 248 start_ms = self.data_points[0].real_send_time_ms 249 stop_ms = self.data_points[-1].real_send_time_ms 250 time_axis = numpy.arange(start_ms / 1000, stop_ms / 1000, 251 RTPStatistics.PLOT_RESOLUTION_MS / 1000) 252 253 delay = CalculateDelay(start_ms, stop_ms, RTPStatistics.PLOT_RESOLUTION_MS, 254 self.data_points) 255 256 plt.figure(1) 257 plt.plot(time_axis, delay[:len(time_axis)]) 258 plt.xlabel("Send time [s]") 259 plt.ylabel("Relative transport delay [ms]") 260 261 plt.figure(2) 262 plt.plot(time_axis[:len(self.smooth_bw_kbps)], self.smooth_bw_kbps) 263 plt.xlabel("Send time [s]") 264 plt.ylabel("Bandwidth [kbps]") 265 266 plt.show() 267 268 269def CalculateDelay(start, stop, step, points): 270 """Quantizes the time coordinates for the delay. 271 272 Quantizes points by rounding the timestamps downwards to the nearest 273 point in the time sequence start, start+step, start+2*step... Takes 274 the average of the delays of points rounded to the same. Returns 275 masked array, in which time points with no value are masked. 276 277 """ 278 grouped_delays = [[] for _ in numpy.arange(start, stop + step, step)] 279 rounded_value_index = lambda x: int((x - start) / step) 280 for point in points: 281 grouped_delays[rounded_value_index(point.real_send_time_ms)].append( 282 point.absdelay) 283 regularized_delays = [ 284 numpy.average(arr) if arr else -1 for arr in grouped_delays 285 ] 286 return numpy.ma.masked_values(regularized_delays, -1) 287 288 289def main(): 290 usage = "Usage: %prog [options] <filename of rtc event log>" 291 parser = optparse.OptionParser(usage=usage) 292 parser.add_option("--dump_header_to_stdout", 293 default=False, 294 action="store_true", 295 help="print header info to stdout; similar to rtp_analyze") 296 parser.add_option("--query_sample_rate", 297 default=False, 298 action="store_true", 299 help="always query user for real sample rate") 300 301 parser.add_option("--working_directory", 302 default=None, 303 action="store", 304 help="directory in which to search for relative paths") 305 306 (options, args) = parser.parse_args() 307 308 if len(args) < 1: 309 parser.print_help() 310 sys.exit(0) 311 312 input_file = args[0] 313 314 if options.working_directory and not os.path.isabs(input_file): 315 input_file = os.path.join(options.working_directory, input_file) 316 317 data_points = pb_parse.ParseProtobuf(input_file) 318 rtp_stats = RTPStatistics(data_points) 319 320 if options.dump_header_to_stdout: 321 print("Printing header info to stdout.", file=sys.stderr) 322 rtp_stats.PrintHeaderStatistics() 323 sys.exit(0) 324 325 chosen_ssrc = rtp_stats.ChooseSsrc() 326 print("Chosen SSRC: 0X{:X}".format(chosen_ssrc)) 327 328 rtp_stats.FilterSsrc(chosen_ssrc) 329 330 print("Statistics:") 331 rtp_stats.PrintSequenceNumberStatistics() 332 rtp_stats.EstimateFrequency(options.query_sample_rate) 333 rtp_stats.PrintDurationStatistics() 334 rtp_stats.RemoveReordered() 335 rtp_stats.ComputeBandwidth() 336 rtp_stats.PlotStatistics() 337 338 339if __name__ == "__main__": 340 main() 341