1 #!/usr/bin/env python 2 # Copyright 2015 The Chromium Authors. All rights reserved. 3 # Use of this source code is governed by a BSD-style license that can be 4 # found in the LICENSE file. 5 6 """Captures a video from an Android device.""" 7 8 import argparse 9 import logging 10 import os 11 import threading 12 import time 13 import sys 14 15 if __name__ == '__main__': 16 sys.path.append(os.path.abspath(os.path.join( 17 os.path.dirname(__file__), '..', '..', '..'))) 18 from devil.android import device_signal 19 from devil.android import device_utils 20 from devil.android.tools import script_common 21 from devil.utils import cmd_helper 22 from devil.utils import reraiser_thread 23 from devil.utils import timeout_retry 24 25 26 class VideoRecorder(object): 27 """Records a screen capture video from an Android Device (KitKat or newer).""" 28 29 def __init__(self, device, megabits_per_second=4, size=None, 30 rotate=False): 31 """Creates a VideoRecorder instance. 32 33 Args: 34 device: DeviceUtils instance. 35 host_file: Path to the video file to store on the host. 36 megabits_per_second: Video bitrate in megabits per second. Allowed range 37 from 0.1 to 100 mbps. 38 size: Video frame size tuple (width, height) or None to use the device 39 default. 40 rotate: If True, the video will be rotated 90 degrees. 41 """ 42 self._bit_rate = megabits_per_second * 1000 * 1000 43 self._device = device 44 self._device_file = ( 45 '%s/screen-recording.mp4' % device.GetExternalStoragePath()) 46 self._recorder_thread = None 47 self._rotate = rotate 48 self._size = size 49 self._started = threading.Event() 50 51 def __enter__(self): 52 self.Start() 53 54 def Start(self, timeout=None): 55 """Start recording video.""" 56 def screenrecord_started(): 57 return bool(self._device.GetPids('screenrecord')) 58 59 if screenrecord_started(): 60 raise Exception("Can't run multiple concurrent video captures.") 61 62 self._started.clear() 63 self._recorder_thread = reraiser_thread.ReraiserThread(self._Record) 64 self._recorder_thread.start() 65 timeout_retry.WaitFor( 66 screenrecord_started, wait_period=1, max_tries=timeout) 67 self._started.wait(timeout) 68 69 def _Record(self): 70 cmd = ['screenrecord', '--verbose', '--bit-rate', str(self._bit_rate)] 71 if self._rotate: 72 cmd += ['--rotate'] 73 if self._size: 74 cmd += ['--size', '%dx%d' % self._size] 75 cmd += [self._device_file] 76 for line in self._device.adb.IterShell( 77 ' '.join(cmd_helper.SingleQuote(i) for i in cmd), None): 78 if line.startswith('Content area is '): 79 self._started.set() 80 81 def __exit__(self, _exc_type, _exc_value, _traceback): 82 self.Stop() 83 84 def Stop(self): 85 """Stop recording video.""" 86 if not self._device.KillAll('screenrecord', signum=device_signal.SIGINT, 87 quiet=True): 88 logging.warning('Nothing to kill: screenrecord was not running') 89 self._recorder_thread.join() 90 91 def Pull(self, host_file=None): 92 """Pull resulting video file from the device. 93 94 Args: 95 host_file: Path to the video file to store on the host. 96 Returns: 97 Output video file name on the host. 98 """ 99 # TODO(jbudorick): Merge filename generation with the logic for doing so in 100 # DeviceUtils. 101 host_file_name = ( 102 host_file 103 or 'screen-recording-%s-%s.mp4' % ( 104 str(self._device), 105 time.strftime('%Y%m%dT%H%M%S', time.localtime()))) 106 host_file_name = os.path.abspath(host_file_name) 107 self._device.PullFile(self._device_file, host_file_name) 108 self._device.RunShellCommand('rm -f "%s"' % self._device_file) 109 return host_file_name 110 111 112 def main(): 113 # Parse options. 114 parser = argparse.ArgumentParser(description=__doc__) 115 parser.add_argument('-d', '--device', dest='devices', action='append', 116 help='Serial number of Android device to use.') 117 parser.add_argument('--blacklist-file', help='Device blacklist JSON file.') 118 parser.add_argument('-f', '--file', metavar='FILE', 119 help='Save result to file instead of generating a ' 120 'timestamped file name.') 121 parser.add_argument('-v', '--verbose', action='store_true', 122 help='Verbose logging.') 123 parser.add_argument('-b', '--bitrate', default=4, type=float, 124 help='Bitrate in megabits/s, from 0.1 to 100 mbps, ' 125 '%default mbps by default.') 126 parser.add_argument('-r', '--rotate', action='store_true', 127 help='Rotate video by 90 degrees.') 128 parser.add_argument('-s', '--size', metavar='WIDTHxHEIGHT', 129 help='Frame size to use instead of the device ' 130 'screen size.') 131 parser.add_argument('host_file', nargs='?', 132 help='File to which the video capture will be written.') 133 134 args = parser.parse_args() 135 136 host_file = args.host_file or args.file 137 138 if args.verbose: 139 logging.getLogger().setLevel(logging.DEBUG) 140 141 size = (tuple(int(i) for i in args.size.split('x')) 142 if args.size 143 else None) 144 145 def record_video(device, stop_recording): 146 recorder = VideoRecorder( 147 device, megabits_per_second=args.bitrate, size=size, rotate=args.rotate) 148 with recorder: 149 stop_recording.wait() 150 151 f = None 152 if host_file: 153 root, ext = os.path.splitext(host_file) 154 f = '%s_%s%s' % (root, str(device), ext) 155 f = recorder.Pull(f) 156 print 'Video written to %s' % os.path.abspath(f) 157 158 parallel_devices = device_utils.DeviceUtils.parallel( 159 script_common.GetDevices(args.devices, args.blacklist_file), 160 async=True) 161 stop_recording = threading.Event() 162 running_recording = parallel_devices.pMap(record_video, stop_recording) 163 print 'Recording. Press Enter to stop.', 164 sys.stdout.flush() 165 raw_input() 166 stop_recording.set() 167 168 running_recording.pGet(None) 169 return 0 170 171 172 if __name__ == '__main__': 173 sys.exit(main()) 174