Home | History | Annotate | Download | only in tools
      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