1 | #!/usr/bin/env python3 |
---|
2 | # |
---|
3 | import sys, os |
---|
4 | import shutil |
---|
5 | import argparse |
---|
6 | import subprocess |
---|
7 | |
---|
8 | # prefer C-based ElementTree |
---|
9 | try: |
---|
10 | import xml.etree.cElementTree as etree |
---|
11 | except ImportError: |
---|
12 | import xml.etree.ElementTree as etree |
---|
13 | |
---|
14 | # check for matplot lib |
---|
15 | try: |
---|
16 | import numpy |
---|
17 | import matplotlib.pyplot as matplot |
---|
18 | except ImportError: |
---|
19 | sys.stderr.write("Error: Missing package 'python3-matplotlib'\n") |
---|
20 | sys.exit(1) |
---|
21 | |
---|
22 | # check for ffprobe in path |
---|
23 | if not shutil.which("mythffprobe"): |
---|
24 | sys.stderr.write("Error: Missing mythffprobe\n") |
---|
25 | sys.exit(1) |
---|
26 | |
---|
27 | # get list of supported matplotlib formats |
---|
28 | format_list = list( |
---|
29 | matplot.figure().canvas.get_supported_filetypes().keys()) |
---|
30 | matplot.close() # destroy test figure |
---|
31 | |
---|
32 | # parse command line arguments |
---|
33 | parser = argparse.ArgumentParser( |
---|
34 | description="Graph bitrate for audio/video stream") |
---|
35 | parser.add_argument('input', nargs='*', help="input file", metavar="INPUT") |
---|
36 | parser.add_argument('-o', '--output', help="output file") |
---|
37 | parser.add_argument('-f', '--format', help="output file format", |
---|
38 | choices=format_list) |
---|
39 | args = parser.parse_args() |
---|
40 | |
---|
41 | # select input file if no arg |
---|
42 | if not args.input: |
---|
43 | from tkinter.filedialog import askopenfilenames |
---|
44 | args.input = askopenfilenames() |
---|
45 | |
---|
46 | # check if format given w/o output file |
---|
47 | if args.format and not args.output: |
---|
48 | sys.stderr.write("Error: Output format requires output file\n") |
---|
49 | sys.exit(1) |
---|
50 | |
---|
51 | for 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 | |
---|
152 | matplot.show() |
---|