blob: 34524070c8ee6177b72b4627967d86476b9e23fa [file] [log] [blame]
#!/usr/bin/python2.7
# Copyright 2016 The Vanadium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
"""Continuously capture screenshots from an Android emulator instance.
Launches Xvfb with supplied display number, launches android emulator with
supplied avd name on that display, and captures the Xvfb framebuffer in a loop
as JPEG images and outputs in through a TCP socket in a format similar to
Minicap (https://github.com/openstf/minicap).
"""
import argparse
import atexit
import os
import re
import socket
import struct
from subprocess import PIPE
from subprocess import Popen
import time
import traceback
# The depth needed for the emulator is minimum of 24
# The size should be changed if you change the display associated with
# the AVD that you're using with the emulator.
SCREEN_SIZE = "1000x900x24"
# The number of seconds we wait for XVFB to start.
XVFB_WAIT_TIME = 5
# The number of seconds we wait for the emulator to start.
# If we don't wait long enough, we might grab wrong dimension
# for the emulator window (initially it is smaller when it is loading).
EMULATOR_WAIT_TIME = 300
# Each display is assigned a port number that starts from this port
# Display :0 will be port 6100, display :1 will be 6101 etc.
BASE_PORT_NUM = 6100
# To be compatible with Minicap, we use a similar header format as specified at
# https://github.com/openstf/minicap (under "Usage")
HEADER_SIZE = 24
HEADER_VERSION = 1
procs = []
@atexit.register
def kill_subprocesses():
"""Auto kill subprocesses (Xvfb and emulator) when this script is killed.
Details at: http://sharats.me/the-ever-useful-and-neat-subprocess-module.html
"""
for process in procs:
process.kill()
def run_shell_cmd(command):
"""Runs the command in a shell and returns a tuple with output and error."""
try:
process = Popen(command, shell=True, stdout=PIPE, stderr=PIPE)
except OSError:
print "could not run command: " + command
traceback.print_exc()
return process.communicate()
def get_window_names(phrase, display_number):
"""Names of all windows on given display whose names contain the phrase."""
out = run_shell_cmd("xwininfo -display localhost:" + str(display_number) +
" -tree -root | grep " + phrase)[0]
window_names = []
for line in out.split("\n"):
try:
window_names.append(line.split("\"")[1])
except IndexError:
print "Window name without a \" was found."
return window_names
def get_window_details(win_name, display_number):
"""Return the x/y coordinates as well as width/height of a window."""
out = run_shell_cmd("xwininfo -display localhost:" + str(display_number) +
" -name " + win_name)[0]
lines = out.split("\n")
x = y = w = h = 0
xt = yt = wt = ht = False
for line in lines:
if "Absolute upper-left X:" in line:
x = int(re.sub("[^0-9]", "", line))
xt = True
elif "Absolute upper-left Y:" in line:
y = int(re.sub("[^0-9]", "", line))
yt = True
elif "Width:" in line:
w = int(re.sub("[^0-9]", "", line))
wt = True
elif "Height:" in line:
h = int(re.sub("[^0-9]", "", line))
ht = True
if xt and yt and wt and ht:
return (x, y, w, h)
else:
raise RuntimeError("Could not find position or size of specified window.")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("display_num", help="The display number to use with Xvfb",
type=int)
parser.add_argument("avd_name", help="The name of the AVD instance to use")
args = parser.parse_args()
display_num = args.display_num
avd_name = args.avd_name
# Folder where xvfb will store its framebuffer as a memory mapped file.
# We create a separate subdirectory under /var/tmp for each display.
fbdir = os.path.join("/var/tmp", str(display_num))
if not os.path.exists(fbdir):
os.mkdir(fbdir)
# Start xvfb and give it a few seconds to start.
cmd = ["Xvfb", ":"+str(display_num), "-ac", "-fbdir", fbdir, "-screen", "0",
SCREEN_SIZE]
proc_xvfb = Popen(cmd, stdout=PIPE, stderr=PIPE)
procs.append(proc_xvfb)
time.sleep(XVFB_WAIT_TIME)
print "display: " + str(display_num)
# Run the emulator on the display created by Xvfb.
cmd = ("DISPLAY=:" + str(display_num) + " emulator64-x86 -avd " + avd_name +
" -noaudio -nojni -netfast -no-boot-anim -qemu -enable-kvm -snapshot")
proc_emulator = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE)
procs.append(proc_emulator)
time.sleep(EMULATOR_WAIT_TIME)
# We assume that each display will only have one window with an emulator.
# We locate it by using the fact that the window name contains the avd_name
# that we specify when running the emulator.
window_name = get_window_names(avd_name, display_num)[0]
window_x, window_y, window_width, window_height = get_window_details(
window_name, display_num)
# This is where we start the server that dumps the screenshots as JPEG images
# The port number assigned is dependent on the diplay number
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
port = BASE_PORT_NUM + display_num
server_address = ("localhost", port)
sock.bind(server_address)
sock.listen(1)
# This command grabs the pixels corresponding to the emulator window from the
# Xvfb framebuffer (that is mapped to a file) and converts it to a JPEG image
# The - argument at the end means that the JPEG image is output to stdout
cmd = ("convert /var/tmp/" + str(display_num) +
"/Xvfb_screen0 -crop {}x{}+{}+{} -").format(window_width,
window_height, window_x,
window_y)
# We use a similar header format as Minicap -- specified here
# https://github.com/openstf/minicap (under "Usage").
# The only difference is that instead of the real PID we have a number that
# helps identify the emulator this server is associated with.
# We grab that number (say "5554") from the window name (which in this case
# will be 5554:avd_name). We also have the same virtual display width/height
# as the real display width/height.
pid = int(window_name.split(":")[0])
print "emulator: " + str(pid)
print "port: " + str(port)
# The struct python library is used to pack different data as bytes.
# More info here: https://docs.python.org/3/library/struct.html
global_header = (struct.pack("B", HEADER_VERSION) +
struct.pack("B", HEADER_SIZE) +
struct.pack("<I", pid) +
struct.pack("<I", window_width) +
struct.pack("<I", window_height) +
struct.pack("<I", window_width) +
struct.pack("<I", window_height) +
struct.pack("B", 0) +
struct.pack("B", 1))
while True:
connection, client_address = sock.accept()
# Every time a new client connects, we send the global header first.
connection.sendall(global_header)
try:
while True:
proc = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE)
jpeg, err = proc.communicate()
# Each frame consist of the frame size as a 4 byte uint32 followed by
# the actual JPEG bytes.
frame_size = struct.pack("<I", len(jpeg) + 4)
frame = frame_size + jpeg
connection.sendall(frame)
except socket.error, e:
# This handles the case when the client disconnects unexpectedly.
# We just go back to accepting another connection.
print e
finally:
connection.close()