aboutsummaryrefslogtreecommitdiff
path: root/catapult/devil/devil/android/tools/video_recorder.py
blob: 08432640b740cd2bc819a7d4d3bf9d269a7c0e60 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
#!/usr/bin/env python
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Captures a video from an Android device."""

import argparse
import logging
import os
import threading
import time
import sys

if __name__ == '__main__':
  sys.path.append(os.path.abspath(os.path.join(
      os.path.dirname(__file__), '..', '..', '..')))
from devil.android import device_signal
from devil.android import device_utils
from devil.android.tools import script_common
from devil.utils import cmd_helper
from devil.utils import reraiser_thread
from devil.utils import timeout_retry

logger = logging.getLogger(__name__)


class VideoRecorder(object):
  """Records a screen capture video from an Android Device (KitKat or newer)."""

  def __init__(self, device, megabits_per_second=4, size=None,
               rotate=False):
    """Creates a VideoRecorder instance.

    Args:
      device: DeviceUtils instance.
      host_file: Path to the video file to store on the host.
      megabits_per_second: Video bitrate in megabits per second. Allowed range
                           from 0.1 to 100 mbps.
      size: Video frame size tuple (width, height) or None to use the device
            default.
      rotate: If True, the video will be rotated 90 degrees.
    """
    self._bit_rate = megabits_per_second * 1000 * 1000
    self._device = device
    self._device_file = (
        '%s/screen-recording.mp4' % device.GetExternalStoragePath())
    self._recorder_thread = None
    self._rotate = rotate
    self._size = size
    self._started = threading.Event()

  def __enter__(self):
    self.Start()

  def Start(self, timeout=None):
    """Start recording video."""
    def screenrecord_started():
      return bool(self._device.GetPids('screenrecord'))

    if screenrecord_started():
      raise Exception("Can't run multiple concurrent video captures.")

    self._started.clear()
    self._recorder_thread = reraiser_thread.ReraiserThread(self._Record)
    self._recorder_thread.start()
    timeout_retry.WaitFor(
        screenrecord_started, wait_period=1, max_tries=timeout)
    self._started.wait(timeout)

  def _Record(self):
    cmd = ['screenrecord', '--verbose', '--bit-rate', str(self._bit_rate)]
    if self._rotate:
      cmd += ['--rotate']
    if self._size:
      cmd += ['--size', '%dx%d' % self._size]
    cmd += [self._device_file]
    for line in self._device.adb.IterShell(
        ' '.join(cmd_helper.SingleQuote(i) for i in cmd), None):
      if line.startswith('Content area is '):
        self._started.set()

  def __exit__(self, _exc_type, _exc_value, _traceback):
    self.Stop()

  def Stop(self):
    """Stop recording video."""
    if not self._device.KillAll('screenrecord', signum=device_signal.SIGINT,
                                quiet=True):
      logger.warning('Nothing to kill: screenrecord was not running')
    self._recorder_thread.join()

  def Pull(self, host_file=None):
    """Pull resulting video file from the device.

    Args:
      host_file: Path to the video file to store on the host.
    Returns:
      Output video file name on the host.
    """
    # TODO(jbudorick): Merge filename generation with the logic for doing so in
    # DeviceUtils.
    host_file_name = (
        host_file
        or 'screen-recording-%s-%s.mp4' % (
            str(self._device),
            time.strftime('%Y%m%dT%H%M%S', time.localtime())))
    host_file_name = os.path.abspath(host_file_name)
    self._device.PullFile(self._device_file, host_file_name)
    self._device.RemovePath(self._device_file, force=True)
    return host_file_name


def main():
  # Parse options.
  parser = argparse.ArgumentParser(description=__doc__)
  parser.add_argument('-d', '--device', dest='devices', action='append',
                      help='Serial number of Android device to use.')
  parser.add_argument('--blacklist-file', help='Device blacklist JSON file.')
  parser.add_argument('-f', '--file', metavar='FILE',
                      help='Save result to file instead of generating a '
                           'timestamped file name.')
  parser.add_argument('-v', '--verbose', action='store_true',
                      help='Verbose logging.')
  parser.add_argument('-b', '--bitrate', default=4, type=float,
                      help='Bitrate in megabits/s, from 0.1 to 100 mbps, '
                           '%(default)d mbps by default.')
  parser.add_argument('-r', '--rotate', action='store_true',
                      help='Rotate video by 90 degrees.')
  parser.add_argument('-s', '--size', metavar='WIDTHxHEIGHT',
                      help='Frame size to use instead of the device '
                           'screen size.')
  parser.add_argument('host_file', nargs='?',
                      help='File to which the video capture will be written.')

  args = parser.parse_args()

  host_file = args.host_file or args.file

  if args.verbose:
    logging.getLogger().setLevel(logging.DEBUG)

  size = (tuple(int(i) for i in args.size.split('x'))
          if args.size
          else None)

  def record_video(device, stop_recording):
    recorder = VideoRecorder(
        device, megabits_per_second=args.bitrate, size=size, rotate=args.rotate)
    with recorder:
      stop_recording.wait()

    f = None
    if host_file:
      root, ext = os.path.splitext(host_file)
      f = '%s_%s%s' % (root, str(device), ext)
    f = recorder.Pull(f)
    print 'Video written to %s' % os.path.abspath(f)

  parallel_devices = device_utils.DeviceUtils.parallel(
      script_common.GetDevices(args.devices, args.blacklist_file),
      async=True)
  stop_recording = threading.Event()
  running_recording = parallel_devices.pMap(record_video, stop_recording)
  print 'Recording. Press Enter to stop.',
  sys.stdout.flush()
  raw_input()
  stop_recording.set()

  running_recording.pGet(None)
  return 0


if __name__ == '__main__':
  sys.exit(main())