video_recorder.py revision cef7893435aa41160dd1255c43cb8498279738cc
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
8import argparse
9import logging
10import os
11import threading
12import time
13import sys
14
15if __name__ == '__main__':
16  sys.path.append(os.path.abspath(os.path.join(
17      os.path.dirname(__file__), '..', '..', '..')))
18from devil.android import device_signal
19from devil.android import device_utils
20from devil.android.tools import script_common
21from devil.utils import cmd_helper
22from devil.utils import reraiser_thread
23from devil.utils import timeout_retry
24
25
26class 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
112def 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
172if __name__ == '__main__':
173  sys.exit(main())
174