Ticket #12302: plotpackets.py

File plotpackets.py, 4.3 KB (added by Roger Siddons, 6 years ago)

Plots distribution of video & audio packets

Line 
1#!/usr/bin/env python3
2#
3import sys, os
4import shutil
5import argparse
6import subprocess
7
8# prefer C-based ElementTree
9try:
10    import xml.etree.cElementTree as etree
11except ImportError:
12    import xml.etree.ElementTree as etree
13
14# check for matplot lib
15try:
16    import numpy
17    import matplotlib.pyplot as matplot
18except ImportError:
19    sys.stderr.write("Error: Missing package 'python3-matplotlib'\n")
20    sys.exit(1)
21
22# check for ffprobe in path
23if not shutil.which("mythffprobe"):
24    sys.stderr.write("Error: Missing mythffprobe\n")
25    sys.exit(1)
26
27# get list of supported matplotlib formats
28format_list = list(
29    matplot.figure().canvas.get_supported_filetypes().keys())
30matplot.close()  # destroy test figure
31
32# parse command line arguments
33parser = argparse.ArgumentParser(
34    description="Graph bitrate for audio/video stream")
35parser.add_argument('input',  nargs='*',  help="input file", metavar="INPUT")
36parser.add_argument('-o', '--output', help="output file")
37parser.add_argument('-f', '--format', help="output file format",
38    choices=format_list)
39args = parser.parse_args()
40
41# select input file if no arg
42if not args.input:
43    from tkinter.filedialog import askopenfilenames
44    args.input = askopenfilenames()
45
46# check if format given w/o output file
47if args.format and not args.output:
48    sys.stderr.write("Error: Output format requires output file\n")
49    sys.exit(1)
50
51for filename in args.input:
52   
53    pkt_data = {}
54    discontinuity = []
55    peak_discontinuity = 0
56    pkt_count = 0
57    prev_pts = 0
58
59    # get packet data for the selected stream
60    with subprocess.Popen(
61        ["mythffprobe",
62            "-show_packets",
63            "-print_format", "xml",
64            filename
65        ],
66        stdout=subprocess.PIPE,
67        stderr=subprocess.DEVNULL) as proc:
68   
69        # process xml elements as they close
70        for event in etree.iterparse(proc.stdout):
71   
72            # skip non-packet elements
73            node = event[1]
74            if node.tag != 'packet':
75                continue
76   
77            # count number of packets
78            pkt_count += 1
79   
80            # collect packet data
81            pkt_type = node.get('codec_type')
82            pkt_start = float(node.get('pts_time'))
83            pkt_end = pkt_start + float(node.get('duration_time'))
84            packet = (pkt_count, pkt_start, pkt_end)
85   
86            # create new packet list if new type
87            if pkt_type not in pkt_data:
88                pkt_data[pkt_type] = []
89   
90            # append packet to list by type
91            pkt_data[pkt_type].append(packet)
92   
93            # calculate pts jump
94            pkt_discontinuity = pkt_start - prev_pts
95            if abs(pkt_discontinuity) > abs(peak_discontinuity):
96                peak_discontinuity = pkt_discontinuity
97            prev_pts = pkt_start
98           
99            discontinuity.append((pkt_count,  pkt_discontinuity))
100           
101        # check if ffprobe was successful
102        if pkt_count == 0:
103            sys.stderr.write("Error: No packet data, failed to execute mythffprobe\n")
104            sys.exit(1)
105   
106    # end packet subprocess
107   
108    # setup new figure
109    matplot.figure()
110    matplot.title("Packet Distribution of " + os.path.basename(filename))
111    matplot.xlabel("Packet Count")
112    matplot.ylabel("PTS Time (sec)")
113    matplot.grid(True)
114   
115    # map packet type to color
116    pkt_type_color = {
117        'audio': 'red',
118        'video': 'blue'
119    }
120   
121    for pkt_type in ['audio', 'video']:
122   
123        # skip packet type if missing
124        if pkt_type not in pkt_data:
125            continue
126   
127        # convert list of tuples to numpy 2d array
128        pkt_list = pkt_data[pkt_type]
129        pkt_array = numpy.array(pkt_list)
130   
131        # plot chart using gnuplot-like impulses
132        matplot.vlines(
133            pkt_array[:,0], pkt_array[:,1], pkt_array[:,2],
134            color=pkt_type_color[pkt_type],
135            label=pkt_type)
136   
137    # plot discontinuities
138    array = numpy.array(discontinuity)
139    matplot.plot(
140        array[:,0], array[:,1],
141        color='cyan',
142        label='Discontinuity (Peak {:.2f}s)'.format(peak_discontinuity))
143
144    matplot.legend(loc='upper left')
145   
146    # render graph to file (if requested) or screen
147    if args.output:
148        matplot.savefig(args.output, format=args.format)
149    else:
150        matplot.show(block=False)
151
152matplot.show()