Skip to content

Commit

Permalink
Initial commit of capture app.
Browse files Browse the repository at this point in the history
  • Loading branch information
Mark Cote committed Sep 19, 2011
0 parents commit 11461ac
Show file tree
Hide file tree
Showing 20 changed files with 3,185 additions and 0 deletions.
44 changes: 44 additions & 0 deletions README
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
Project Eideticker
==================

Project Eideticker is an automated test harness that captures and analyzes
browser output.

Required for capturing is a Linux system with a Blackmagic Design DeckLink card
and the appropriate drivers. Testing has been done with the DeckLink HD
Extreme 3D; no idea if other cards would work.

Capture
-------

### Installation

Simply run `make` in the `capture/decklink/` directory to compile the C++
capture app.


### Usage

Run `./controller.py <port>` from within the `capture/` directory. The default
port is 8888.

The capture device is controlled through a web interface. These command paths
are support:

* `/start/` Start recording.
* `/stop/` Stop recording and run the conversion script on the raw output.
* `/status/` Indicates if a job is running, and, if so, the name of the capture.
* `/captures/` Returns a JSON dictionary of the currently stored captures along
with URLs to the individual data files (raw video, avi, png
archive).
* `/captures/<timestamp>/` Returns a JSON dictionary of the URLs to the
individual data files for the given capture.
* `/captures/<timestamp>/<filename>` Access the raw video, avi, or png archive
of the given capture.

### To Do

* More configuration options (command line and/or config file).
* Packaging.
* Logging.
* Verify if the Capture program has been compiled.
18 changes: 18 additions & 0 deletions capture/.emacs.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
;; -*- mode: emacs-lisp; coding: emacs-mule; -*-
;; --------------------------------------------------------------------------
;; Desktop File for Emacs
;; --------------------------------------------------------------------------
;; Created Fri Sep 16 16:15:52 2011
;; Desktop file format version 206
;; Emacs version 23.2.1

;; Global section:
(setq desktop-missing-file-warning nil)
(setq tags-file-name nil)
(setq tags-table-list nil)
(setq search-ring '("true" "def C"))
(setq regexp-search-ring nil)
(setq register-alist nil)
(setq file-name-history nil)

;; Buffer section -- buffers listed in same order as in buffer list:
Empty file added capture/.raw
Empty file.
254 changes: 254 additions & 0 deletions capture/controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
#!/usr/bin/python

# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is Eideticker.
#
# The Initial Developer of the Original Code is
# Mozilla Foundation.
# Portions created by the Initial Developer are Copyright (C) 2011
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Mark Cote <mcote@mozilla.com>
#
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****

import BaseHTTPServer
import datetime
import json
import os
import re
import subprocess
import time

CAPTURE_DIR = 'captures'

class CaptureSubprocessController(object):

def __init__(self):
self.capture_proc = None
self.null_read = file('/dev/null', 'r')
self.null_write = file('/dev/null', 'w')
self.output_filename = ''

def launch(self, output_filename):
print 'launch requested'
if self.capture_proc:
print 'capture already running'
return
print 'launching'
self.output_filename = output_filename
args = ('decklink/Capture',
'-m',
'13',
'-p',
'0',
'-f',
self.output_filename + '.raw')
self.capture_proc = subprocess.Popen(args, close_fds=True)

def running(self):
if not self.capture_proc:
return False
running = self.capture_proc.poll()
if running != None:
self.capture_proc = None
return running == None

def terminate(self):
print 'terminate requested'
if not self.capture_proc:
print 'not running'
return

print 'terminating...'
self.capture_proc.terminate()
for i in range(0, 5):
rc = self.capture_proc.poll()
print 'rc: %s' % str(rc)
if rc != None:
print 'terminated'
self.capture_proc.wait() # necessary?
self.capture_proc = None
break
time.sleep(1)
if self.capture_proc:
print 'still running!'
# terminate failed; try forcibly killing it
try:
self.capture_proc.kill()
except:
pass
self.capture_proc.wait() # or poll and error out if still running?
self.capture_proc = None

print 'converting'
# convert raw file
# if this is too slow, we'll have to make this asynchronous and
# have multiple states
args = ('decklink/convert.sh', self.output_filename)
subprocess.Popen(args, close_fds=True).wait()


class CaptureControllerHTTPServer(BaseHTTPServer.HTTPServer):

def __init__(self, server_address, request_handler_class):
BaseHTTPServer.HTTPServer.__init__(self, server_address,
request_handler_class)
self.pcontroller = CaptureSubprocessController()

def get_filename(self):
return os.path.join(CAPTURE_DIR, datetime.datetime.now().isoformat())

def start(self):
if self.pcontroller.running():
return {'error': 'busy'}
output_filename = self.get_filename()
self.pcontroller.launch(output_filename)
return self.status()

def stop(self):
if self.pcontroller.running():
self.pcontroller.terminate()
return self.status()

def status(self):
if self.pcontroller.running():
return { 'status': 'running',
'output': self.pcontroller.output_filename }
return { 'status': 'idle' }


class CaptureControllerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):

path_handlers = { '/test/': 'test',
'/start/': 'start',
'/stop/': 'stop',
'/status/': 'status',
'/captures/': 'captures' }

def do_GET(self):
code = 404
data = None
for path_prefix, func_name in self.path_handlers.iteritems():
if self.path.startswith(path_prefix):
response = getattr(self, func_name)()
break
if not response:
print 'no response'
return
code, data = response
self.send_response(code)
if data != None:
json_data = json.dumps(data)
self.send_header('Content-Type', 'application/json; charset=utf-8')
self.send_header('Content-Length', len(json_data))
self.end_headers()
self.wfile.write(json_data)

def test(self):
return (200, {'test': True})

def start(self):
return (200, self.server.start())

def stop(self):
return (200, self.server.stop())

def status(self):
return (200, self.server.status())

def write_file(self, content_type, path):
self.send_response(200)
f = file(path, 'rb')
self.send_header('Content-Type', '%s; name="%s"' %
(content_type, os.path.basename(path)))
self.send_header('Content-Length', str(os.fstat(f.fileno()).st_size))
self.end_headers()
while True:
buf = f.read(64*1024)
if not buf:
break
self.wfile.write(buf)

def capture_data(self, capture_name):
raw_url = avi_url = imgs_url = ''
raw_path = os.path.join(CAPTURE_DIR, capture_name + '.raw')
if os.path.exists(raw_path):
raw_url = '/captures/%s' % os.path.basename(raw_path)
avi_path = os.path.join(CAPTURE_DIR, capture_name + '.avi')
if os.path.exists(avi_path):
avi_url = '/captures/%s' % os.path.basename(avi_path)
imgs_path = os.path.join(CAPTURE_DIR, capture_name + '-pngs.zip')
if os.path.exists(imgs_path):
imgs_url = '/captures/%s' % os.path.basename(imgs_path)
return { capture_name: { 'imgs': imgs_url,
'raw': raw_url,
'avi': avi_url } }

def captures(self):
if self.path[-1] == '/':
capture_data = {}
if self.path == '/captures/':
captures = set()
r = '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}'
for f in os.listdir(CAPTURE_DIR):
m = re.match(r, f)
if m:
captures.add(m.group(0))
else:
comps = filter(lambda x: x, self.path.split('/'))
captures = set(comps[1])
for capture_name in captures:
capture_data.update(self.capture_data(capture_name))
return (200, capture_data)
filename = os.path.basename(self.path)
filepath = os.path.join(CAPTURE_DIR, filename)
if not os.path.exists(filepath):
return (404, {})
if filename.endswith('.raw'):
self.write_file('video/x-raw-yuv; format=UYVY', filepath)
elif filename.endswith('.avi'):
self.write_file('video/x-msvideo', filepath)
elif filename.endswith('.zip'):
self.write_file('application/zip', filepath)
else:
return (404, {})


def main(port):
server = CaptureControllerHTTPServer(('', port),
CaptureControllerRequestHandler)
server.serve_forever()


if __name__ == '__main__':
import sys
port = 8888
if len(sys.argv) > 1:
port = int(sys.argv[1])
main(port)
29 changes: 29 additions & 0 deletions capture/decklink/.emacs.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
;; -*- mode: emacs-lisp; coding: emacs-mule; -*-
;; --------------------------------------------------------------------------
;; Desktop File for Emacs
;; --------------------------------------------------------------------------
;; Created Wed Sep 14 23:18:36 2011
;; Desktop file format version 206
;; Emacs version 23.2.1

;; Global section:
(setq desktop-missing-file-warning nil)
(setq tags-file-name nil)
(setq tags-table-list nil)
(setq search-ring nil)
(setq regexp-search-ring nil)
(setq register-alist nil)
(setq file-name-history nil)

;; Buffer section -- buffers listed in same order as in buffer list:
(desktop-create-buffer 206
"/home/mozauto/projects/eideticker/capture/decklink/Makefile"
"Makefile"
'makefile-gmake-mode
nil
1454
'(nil nil)
nil
nil
'((indent-tabs-mode . t) (buffer-file-coding-system . undecided-unix)))

1 change: 1 addition & 0 deletions capture/decklink/.emacs.desktop.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
4903
Binary file added capture/decklink/Capture
Binary file not shown.
Loading

0 comments on commit 11461ac

Please sign in to comment.