blob: b13543d65a85ea1a57ccccf0b1b5397ddf02d0e5 [file] [log] [blame]
jansson1b0e3b82017-03-13 02:15:51 -07001#!/usr/bin/env python
2# Copyright (c) 2017 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
jansson80ff00c2017-04-11 07:40:26 -070010import glob
jansson1b0e3b82017-03-13 02:15:51 -070011import optparse
12import os
jansson80ff00c2017-04-11 07:40:26 -070013import shutil
jansson1b0e3b82017-03-13 02:15:51 -070014import subprocess
15import sys
16import time
jansson80ff00c2017-04-11 07:40:26 -070017
jansson1b0e3b82017-03-13 02:15:51 -070018
19# Used to time-stamp output files and directories
20CURRENT_TIME = time.strftime("%d_%m_%Y-%H:%M:%S")
21
jansson5c7a6232017-03-15 08:27:31 -070022
23class Error(Exception):
24 pass
25
26
27class FfmpegError(Error):
28 pass
29
30
31class MagewellError(Error):
32 pass
33
34
35class CompareVideosError(Error):
36 pass
37
38
jansson1b0e3b82017-03-13 02:15:51 -070039def _ParseArgs():
40 """Registers the command-line options."""
41 usage = 'usage: %prog [options]'
42 parser = optparse.OptionParser(usage=usage)
43
44 parser.add_option('--frame_width', type='string', default='1280',
45 help='Width of the recording. Default: %default')
46 parser.add_option('--frame_height', type='string', default='720',
47 help='Height of the recording. Default: %default')
48 parser.add_option('--framerate', type='string', default='60',
49 help='Recording framerate. Default: %default')
50 parser.add_option('--ref_duration', type='string', default='20',
51 help='Reference recording duration. Default: %default')
52 parser.add_option('--test_duration', type='string', default='10',
53 help='Test recording duration. Default: %default')
54 parser.add_option('--time_between_recordings', type=float, default=5,
55 help='Time between starting test recording after ref.'
56 'Default: %default')
57 parser.add_option('--ref_video_device', type='string', default='/dev/video0',
58 help='Reference recording device. Default: %default')
59 parser.add_option('--test_video_device', type='string', default='/dev/video1',
60 help='Test recording device. Default: %default')
61 parser.add_option('--app_name', type='string',
62 help='Name of the app under test.')
63 parser.add_option('--recording_api', type='string', default='Video4Linux2',
64 help='Recording API to use. Default: %default')
65 parser.add_option('--pixel_format', type='string', default='yuv420p',
66 help='Recording pixel format Default: %default')
67 parser.add_option('--ffmpeg', type='string',
68 help='Path to the ffmpeg executable for the reference '
69 'device.')
70 parser.add_option('--video_container', type='string', default='yuv',
71 help='Video container for the recordings.'
72 'Default: %default')
73 parser.add_option('--compare_videos_script', type='string',
74 default='compare_videos.py',
75 help='Path to script used to compare and generate metrics.'
76 'Default: %default')
77 parser.add_option('--frame_analyzer', type='string',
78 default='../../out/Default/frame_analyzer',
79 help='Path to the frame analyzer executable.'
80 'Default: %default')
81 parser.add_option('--zxing_path', type='string',
82 help='Path to the zebra xing barcode analyzer.')
83 parser.add_option('--ref_rec_dir', type='string', default='ref',
84 help='Path to where reference recordings will be created.'
85 'Ideally keep the ref and test directories on separate'
86 'drives. Default: %default')
87 parser.add_option('--test_rec_dir', type='string', default='test',
88 help='Path to where test recordings will be created.'
89 'Ideally keep the ref and test directories on separate '
90 'drives. Default: %default')
91 parser.add_option('--test_crop_parameters', type='string',
92 help='ffmpeg processing parameters for the test video.')
93 parser.add_option('--ref_crop_parameters', type='string',
94 help='ffmpeg processing parameters for the ref video.')
95
96 options, _ = parser.parse_args()
97
98 if not options.app_name:
99 parser.error('You must provide an application name!')
100
101 if not options.test_crop_parameters or not options.ref_crop_parameters:
102 parser.error('You must provide ref and test crop parameters!')
103
104 # Ensure the crop filter is included in the crop parameters used for ffmpeg.
105 if 'crop' not in options.ref_crop_parameters:
106 parser.error('You must provide a reference crop filter for ffmpeg.')
107 if 'crop' not in options.test_crop_parameters:
108 parser.error('You must provide a test crop filter for ffmpeg.')
109
110 if not options.ffmpeg:
111 parser.error('You most provide location for the ffmpeg executable.')
112 if not os.path.isfile(options.ffmpeg):
113 parser.error('Cannot find the ffmpeg executable.')
114
115 # compare_videos.py dependencies.
116 if not os.path.isfile(options.compare_videos_script):
117 parser.warning('Cannot find compare_videos.py script, no metrics will be '
118 'generated!')
119 if not os.path.isfile(options.frame_analyzer):
120 parser.warning('Cannot find frame_analyzer, no metrics will be generated!')
121 if not os.path.isfile(options.zxing_path):
122 parser.warning('Cannot find Zebra Xing, no metrics will be generated!')
123
124 return options
125
126
127def CreateRecordingDirs(options):
128 """Create root + sub directories for reference and test recordings.
129
130 Args:
131 options(object): Contains all the provided command line options.
jansson5c7a6232017-03-15 08:27:31 -0700132
133 Returns:
jansson1b0e3b82017-03-13 02:15:51 -0700134 record_paths(dict): key: value pair with reference and test file
135 absolute paths.
136 """
137
138 # Create root directories for the video recordings.
139 if not os.path.isdir(options.ref_rec_dir):
140 os.makedirs(options.ref_rec_dir)
141 if not os.path.isdir(options.test_rec_dir):
142 os.makedirs(options.test_rec_dir)
143
144 # Create and time-stamp directories for all the output files.
145 ref_rec_dir = os.path.join(options.ref_rec_dir, options.app_name + '_' + \
146 CURRENT_TIME)
147 test_rec_dir = os.path.join(options.test_rec_dir, options.app_name + '_' + \
148 CURRENT_TIME)
149
150 os.makedirs(ref_rec_dir)
151 os.makedirs(test_rec_dir)
152
153 record_paths = {
154 'ref_rec_location' : os.path.abspath(ref_rec_dir),
155 'test_rec_location' : os.path.abspath(test_rec_dir)
156 }
157
158 return record_paths
159
160
jansson80ff00c2017-04-11 07:40:26 -0700161def FindUsbPortForV4lDevices(ref_video_device, test_video_device):
162 """Tries to find the usb port for ref_video_device and test_video_device.
jansson1b0e3b82017-03-13 02:15:51 -0700163
164 Tries to find the provided ref_video_device and test_video_device devices
165 which use video4linux and then do a soft reset by using USB unbind and bind.
jansson80ff00c2017-04-11 07:40:26 -0700166
167 Args:
168 ref_device(string): reference recording device path.
169 test_device(string): test recording device path
170
171 Returns:
172 usb_ports(list): USB ports(string) for the devices found.
173 """
174
175 # Find the device location including USB and USB Bus ID's. Use the usb1
176 # in the path since the driver folder is a symlink which contains all the
177 # usb device port mappings and it's the same in all usbN folders. Tested
178 # on Ubuntu 14.04.
179 v4l_device_path = '/sys/bus/usb/devices/usb1/1-1/driver/**/**/video4linux/'
180 v4l_ref_device = glob.glob('%s%s' % (v4l_device_path, ref_video_device))
181 v4l_test_device = glob.glob('%s%s' % (v4l_device_path, test_video_device))
182 usb_ports = []
183 paths = []
184
185 # Split on the driver folder first since we are only interested in the
186 # folders thereafter.
187 ref_path = str(v4l_ref_device).split('driver')[1].split('/')
188 test_path = str(v4l_test_device).split('driver')[1].split('/')
189 paths.append(ref_path)
190 paths.append(test_path)
191
192 for path in paths:
193 for usb_id in path:
194 # Look for : separator and then use the first element in the list.
195 # E.g 3-3.1:1.0 split on : and [0] becomes 3-3.1 which can be used
196 # for bind/unbind.
197 if ':' in usb_id:
198 usb_ports.append(usb_id.split(':')[0])
199 return usb_ports
200
201
202def RestartMagewellDevices(ref_video_device_path, test_video_device_path):
203 """Reset the USB ports where Magewell capture devices are connected to.
204
205 Performs a soft reset by using USB unbind and bind.
jansson1b0e3b82017-03-13 02:15:51 -0700206 This is due to Magewell capture devices have proven to be unstable after the
207 first recording attempt.
208
jansson80ff00c2017-04-11 07:40:26 -0700209 Args:
210 ref_video_device_path(string): reference recording device path.
211 test_video_device_path(string): test recording device path
jansson5c7a6232017-03-15 08:27:31 -0700212
213 Raises:
214 MagewellError: If no magewell devices are found.
jansson1b0e3b82017-03-13 02:15:51 -0700215 """
216
217 # Get the dev/videoN device name from the command line arguments.
jansson80ff00c2017-04-11 07:40:26 -0700218 ref_magewell_path = ref_video_device_path.split('/')[2]
219 test_magewell_path = test_video_device_path.split('/')[2]
220 magewell_usb_ports = FindUsbPortForV4lDevices(ref_magewell_path,
221 test_magewell_path)
jansson1b0e3b82017-03-13 02:15:51 -0700222
jansson5c7a6232017-03-15 08:27:31 -0700223 # Abort early if no devices are found.
224 if len(magewell_usb_ports) == 0:
225 raise MagewellError('No magewell devices found.')
226 else:
227 print '\nResetting USB ports where magewell devices are connected...'
228 # Use the USB bus and port ID (e.g. 4-3) to unbind and bind the USB devices
229 # (i.e. soft eject and insert).
jansson1b0e3b82017-03-13 02:15:51 -0700230 for usb_port in magewell_usb_ports:
231 echo_cmd = ['echo', usb_port]
232 unbind_cmd = ['sudo', 'tee', '/sys/bus/usb/drivers/usb/unbind']
233 bind_cmd = ['sudo', 'tee', '/sys/bus/usb/drivers/usb/bind']
234
235 # TODO(jansson) Figure out a way to call on echo once for bind & unbind
236 # if possible.
237 echo_unbind = subprocess.Popen(echo_cmd, stdout=subprocess.PIPE)
238 unbind = subprocess.Popen(unbind_cmd, stdin=echo_unbind.stdout)
239 echo_unbind.stdout.close()
jansson1b0e3b82017-03-13 02:15:51 -0700240 unbind.wait()
241
242 echo_bind = subprocess.Popen(echo_cmd, stdout=subprocess.PIPE)
243 bind = subprocess.Popen(bind_cmd, stdin=echo_bind.stdout)
244 echo_bind.stdout.close()
jansson1b0e3b82017-03-13 02:15:51 -0700245 bind.wait()
jansson5c7a6232017-03-15 08:27:31 -0700246 if bind.returncode == 0:
247 print 'Reset done!\n'
jansson1b0e3b82017-03-13 02:15:51 -0700248
249
jansson5c7a6232017-03-15 08:27:31 -0700250def StartRecording(options, ref_file_location, test_file_location):
jansson1b0e3b82017-03-13 02:15:51 -0700251 """Starts recording from the two specified video devices.
252
253 Args:
254 options(object): Contains all the provided command line options.
255 record_paths(dict): key: value pair with reference and test file
256 absolute paths.
jansson5c7a6232017-03-15 08:27:31 -0700257
258 Returns:
259 recording_files_and_time(dict): key: value pair with the path to cropped
260 test and reference video files.
261
262 Raises:
263 FfmpegError: If the ffmpeg command fails.
jansson1b0e3b82017-03-13 02:15:51 -0700264 """
265 ref_file_name = '%s_%s_ref.%s' % (options.app_name, CURRENT_TIME,
266 options.video_container)
jansson5c7a6232017-03-15 08:27:31 -0700267 ref_file = os.path.join(ref_file_location, ref_file_name)
jansson1b0e3b82017-03-13 02:15:51 -0700268
269 test_file_name = '%s_%s_test.%s' % (options.app_name, CURRENT_TIME,
270 options.video_container)
jansson5c7a6232017-03-15 08:27:31 -0700271 test_file = os.path.join(test_file_location, test_file_name)
jansson1b0e3b82017-03-13 02:15:51 -0700272
273 # Reference video recorder command line.
274 ref_cmd = [
275 options.ffmpeg,
276 '-v', 'error',
277 '-s', options.frame_width + 'x' + options.frame_height,
278 '-framerate', options.framerate,
279 '-f', options.recording_api,
280 '-i', options.ref_video_device,
281 '-pix_fmt', options.pixel_format,
282 '-s', options.frame_width + 'x' + options.frame_height,
283 '-t', options.ref_duration,
284 '-framerate', options.framerate,
jansson5c7a6232017-03-15 08:27:31 -0700285 ref_file
jansson1b0e3b82017-03-13 02:15:51 -0700286 ]
287
288 # Test video recorder command line.
289 test_cmd = [
290 options.ffmpeg,
291 '-v', 'error',
292 '-s', options.frame_width + 'x' + options.frame_height,
293 '-framerate', options.framerate,
294 '-f', options.recording_api,
295 '-i', options.test_video_device,
296 '-pix_fmt', options.pixel_format,
297 '-s', options.frame_width + 'x' + options.frame_height,
298 '-t', options.test_duration,
299 '-framerate', options.framerate,
jansson5c7a6232017-03-15 08:27:31 -0700300 test_file
jansson1b0e3b82017-03-13 02:15:51 -0700301 ]
302 print 'Trying to record from reference recorder...'
303 ref_recorder = subprocess.Popen(ref_cmd, stderr=sys.stderr)
304
305 # Start the 2nd recording a little later to ensure the 1st one has started.
306 # TODO(jansson) Check that the ref_recorder output file exists rather than
307 # using sleep.
308 time.sleep(options.time_between_recordings)
309 print 'Trying to record from test recorder...'
310 test_recorder = subprocess.Popen(test_cmd, stderr=sys.stderr)
311 test_recorder.wait()
312 ref_recorder.wait()
313
314 # ffmpeg does not abort when it fails, need to check return code.
jansson5c7a6232017-03-15 08:27:31 -0700315 if ref_recorder.returncode != 0 or test_recorder.returncode != 0:
316 # Cleanup recording directories.
317 shutil.rmtree(ref_file_location)
318 shutil.rmtree(test_file_location)
319 raise FfmpegError('Recording failed, check ffmpeg output.')
320 else:
321 print 'Ref file recorded to: ' + os.path.abspath(ref_file)
322 print 'Test file recorded to: ' + os.path.abspath(test_file)
323 print 'Recording done!\n'
324 return FlipAndCropRecordings(options, test_file_name, test_file_location,
325 ref_file_name, ref_file_location)
jansson1b0e3b82017-03-13 02:15:51 -0700326
327
328def FlipAndCropRecordings(options, test_file_name, test_file_location,
329 ref_file_name, ref_file_location):
330 """Performs a horizontal flip of the reference video to match the test video.
331
332 This is done to the match orientation and then crops the ref and test videos
333 using the options.test_crop_parameters and options.ref_crop_parameters.
334
335 Args:
336 options(object): Contains all the provided command line options.
337 test_file_name(string): Name of the test video file recording.
338 test_file_location(string): Path to the test video file recording.
339 ref_file_name(string): Name of the reference video file recording.
340 ref_file_location(string): Path to the reference video file recording.
jansson5c7a6232017-03-15 08:27:31 -0700341
342 Returns:
jansson1b0e3b82017-03-13 02:15:51 -0700343 recording_files_and_time(dict): key: value pair with the path to cropped
344 test and reference video files.
jansson5c7a6232017-03-15 08:27:31 -0700345
346 Raises:
347 FfmpegError: If the ffmpeg command fails.
jansson1b0e3b82017-03-13 02:15:51 -0700348 """
349 print 'Trying to crop videos...'
350
351 # Ref file cropping.
352 cropped_ref_file_name = 'cropped_' + ref_file_name
353 cropped_ref_file = os.path.abspath(
354 os.path.join(ref_file_location, cropped_ref_file_name))
355
356 ref_video_crop_cmd = [
357 options.ffmpeg,
358 '-v', 'error',
359 '-s', options.frame_width + 'x' + options.frame_height,
360 '-i', os.path.join(ref_file_location, ref_file_name),
361 '-vf', options.ref_crop_parameters,
362 '-c:a', 'copy',
363 cropped_ref_file
364 ]
365
366 # Test file cropping.
367 cropped_test_file_name = 'cropped_' + test_file_name
368 cropped_test_file = os.path.abspath(
369 os.path.join(test_file_location, cropped_test_file_name))
370
371 test_video_crop_cmd = [
372 options.ffmpeg,
373 '-v', 'error',
374 '-s', options.frame_width + 'x' + options.frame_height,
375 '-i', os.path.join(test_file_location, test_file_name),
376 '-vf', options.test_crop_parameters,
377 '-c:a', 'copy',
378 cropped_test_file
379 ]
380
381 ref_crop = subprocess.Popen(ref_video_crop_cmd)
382 ref_crop.wait()
jansson5c7a6232017-03-15 08:27:31 -0700383 test_crop = subprocess.Popen(test_video_crop_cmd)
384 test_crop.wait()
jansson1b0e3b82017-03-13 02:15:51 -0700385
jansson5c7a6232017-03-15 08:27:31 -0700386 # ffmpeg does not abort when it fails, need to check return code.
387 if ref_crop.returncode != 0 or test_crop.returncode != 0:
388 # Cleanup recording directories.
389 shutil.rmtree(ref_file_location)
390 shutil.rmtree(test_file_location)
391 raise FfmpegError('Cropping failed, check ffmpeg output.')
392 else:
393 print 'Ref file cropped to: ' + cropped_ref_file
jansson1b0e3b82017-03-13 02:15:51 -0700394 print 'Test file cropped to: ' + cropped_test_file
395 print 'Cropping done!\n'
396
397 # Need to return these so they can be used by other parts.
398 cropped_recordings = {
399 'cropped_test_file' : cropped_test_file,
400 'cropped_ref_file' : cropped_ref_file
401 }
jansson1b0e3b82017-03-13 02:15:51 -0700402 return cropped_recordings
jansson1b0e3b82017-03-13 02:15:51 -0700403
404
jansson5c7a6232017-03-15 08:27:31 -0700405def CompareVideos(options, cropped_ref_file, cropped_test_file):
jansson1b0e3b82017-03-13 02:15:51 -0700406 """Runs the compare_video.py script from src/webrtc/tools using the file path.
407
408 Uses the path from recording_result and writes the output to a file named
409 <options.app_name + '_' + CURRENT_TIME + '_result.txt> in the reference video
410 recording folder taken from recording_result.
411
412 Args:
413 options(object): Contains all the provided command line options.
jansson5c7a6232017-03-15 08:27:31 -0700414 cropped_ref_file(string): Path to cropped reference video file.
415 cropped_test_file(string): Path to cropped test video file.
416
417 Raises:
418 CompareVideosError: If compare_videos.py fails.
jansson1b0e3b82017-03-13 02:15:51 -0700419 """
420 print 'Starting comparison...'
421 print 'Grab a coffee, this might take a few minutes...'
jansson1b0e3b82017-03-13 02:15:51 -0700422 compare_videos_script = os.path.abspath(options.compare_videos_script)
423 rec_path = os.path.abspath(os.path.join(
jansson5c7a6232017-03-15 08:27:31 -0700424 os.path.dirname(cropped_test_file)))
jansson1b0e3b82017-03-13 02:15:51 -0700425 result_file_name = os.path.join(rec_path, '%s_%s_result.txt') % (
426 options.app_name, CURRENT_TIME)
427
jansson5c7a6232017-03-15 08:27:31 -0700428 # Find the crop dimensions (e.g. 950 and 420) in the ref crop parameter
429 # string: 'hflip, crop=950:420:130:56'
jansson1b0e3b82017-03-13 02:15:51 -0700430 for param in options.ref_crop_parameters.split('crop'):
431 if param[0] == '=':
432 crop_width = param.split(':')[0].split('=')[1]
433 crop_height = param.split(':')[1]
434
435 compare_cmd = [
jansson1b0e3b82017-03-13 02:15:51 -0700436 compare_videos_script,
437 '--ref_video', cropped_ref_file,
438 '--test_video', cropped_test_file,
439 '--frame_analyzer', os.path.abspath(options.frame_analyzer),
440 '--zxing_path', options.zxing_path,
441 '--ffmpeg_path', options.ffmpeg,
442 '--stats_file_ref', os.path.join(os.path.dirname(cropped_ref_file),
443 cropped_ref_file + '_stats.txt'),
444 '--stats_file_test', os.path.join(os.path.dirname(cropped_test_file),
445 cropped_test_file + '_stats.txt'),
446 '--yuv_frame_height', crop_height,
447 '--yuv_frame_width', crop_width
448 ]
449
jansson5c7a6232017-03-15 08:27:31 -0700450 with open(result_file_name, 'w') as f:
451 compare_video_recordings = subprocess.Popen(compare_cmd, stdout=f)
452 compare_video_recordings.wait()
453 if compare_video_recordings.returncode != 0:
454 raise CompareVideosError('Failed to perform comparison.')
455 else:
456 print 'Result recorded to: ' + os.path.abspath(result_file_name)
457 print 'Comparison done!'
jansson1b0e3b82017-03-13 02:15:51 -0700458
459
460def main():
461 """The main function.
462
463 A simple invocation is:
464 ./run_video_analysis.py \
465 --app_name AppRTCMobile \
466 --ffmpeg ./ffmpeg --ref_video_device=/dev/video0 \
467 --test_video_device=/dev/video1 \
468 --zxing_path ./zxing \
469 --test_crop_parameters 'crop=950:420:130:56' \
470 --ref_crop_parameters 'hflip, crop=950:420:130:56' \
471 --ref_rec_dir /tmp/ref \
472 --test_rec_dir /tmp/test
473
474 This will produce the following files if successful:
475 # Original video recordings.
476 /tmp/ref/AppRTCMobile_<recording date and time>_ref.yuv
477 /tmp/test/AppRTCMobile_<recording date and time>_test.yuv
478
479 # Cropped video recordings according to the crop parameters.
480 /tmp/ref/cropped_AppRTCMobile_<recording date and time>_ref.yuv
481 /tmp/test/cropped_AppRTCMobile_<recording date and time>_ref.yuv
482
483 # Comparison metrics from cropped test and ref videos.
484 /tmp/test/AppRTCMobile_<recording date and time>_result.text
485
486 """
487 options = _ParseArgs()
488 RestartMagewellDevices(options.ref_video_device, options.test_video_device)
489 record_paths = CreateRecordingDirs(options)
jansson5c7a6232017-03-15 08:27:31 -0700490 recording_result = StartRecording(options, record_paths['ref_rec_location'],
491 record_paths['test_rec_location'])
jansson1b0e3b82017-03-13 02:15:51 -0700492
493 # Do not require compare_video.py script to run, no metrics will be generated.
494 if options.compare_videos_script:
jansson5c7a6232017-03-15 08:27:31 -0700495 CompareVideos(options, recording_result['cropped_ref_file'],
496 recording_result['cropped_test_file'])
jansson1b0e3b82017-03-13 02:15:51 -0700497 else:
498 print ('Skipping compare videos step due to compare_videos flag were not '
499 'passed.')
500
501
502if __name__ == '__main__':
503 sys.exit(main())