xref: /aosp_15_r20/external/webrtc/rtc_tools/py_event_log_analyzer/rtp_analyzer.py (revision d9f758449e529ab9291ac668be2861e7a55c2422)
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