-> layout seems to be too wide for a modern mobile phone
--> add a previewer for movies
-> make camera frames positions configurable
-> add a view log functionality
-> browser compatibility test
-> requirements test
+
\ No newline at end of file
return
try:
- mediafiles.cleanup_pictures()
- mediafiles.cleanup_movies()
+ mediafiles.cleanup_media('picture')
+ mediafiles.cleanup_media('movie')
except Exception as e:
logging.error('failed to cleanup media files: %(msg)s' % {
import json
import logging
import os
-import StringIO
from tornado.web import RequestHandler, HTTPError, asynchronous
-from PIL import Image
import config
import mediafiles
self.finish_json(remote_list)
- remote.list_pictures(
+ remote.list_media(
camera_config.get('@host'),
camera_config.get('@port'),
camera_config.get('@username'),
camera_config.get('@password'),
- camera_config.get('@remote_camera_id'), on_response)
+ camera_config.get('@remote_camera_id'), on_response,
+ media_type='picture')
else:
- pictures = mediafiles.list_pictures(camera_config)
+ pictures = mediafiles.list_media(camera_config, media_type='picture')
self.finish_json({
'mediaList': pictures,
self.finish(response)
- remote.get_media(
+ remote.get_media_content(
camera_config.get('@host'),
camera_config.get('@port'),
camera_config.get('@username'),
media_type='picture')
else:
- content = mediafiles.get_media_content(camera_config, filename)
+ content = mediafiles.get_media_content(camera_config, filename, 'picture')
pretty_filename = camera_config['@name'] + '_' + os.path.basename(filename)
self.set_header('Content-Type', 'image/jpeg')
self.set_header('Content-Type', 'image/jpeg')
self.finish(response)
- remote.get_media(
+ remote.get_media_preview(
camera_config.get('@host'),
camera_config.get('@port'),
camera_config.get('@username'),
height=self.get_argument('height', None))
else:
- content = mediafiles.get_media_content(camera_config, filename)
- self.set_header('Content-Type', 'image/jpeg')
-
- width = self.get_argument('width', None)
- height = self.get_argument('height', None)
-
- if width is None and height is None:
- return self.finish(content)
-
- sio = StringIO.StringIO(content)
- image = Image.open(sio)
+ content = mediafiles.get_media_preview(camera_config, filename, 'picture',
+ width=self.get_argument('width', None),
+ height=self.get_argument('height', None))
- width = width and int(width) or image.size[0]
- height = height and int(height) or image.size[1]
-
- image.thumbnail((width, height), Image.ANTIALIAS)
-
- image.save(self, format='JPEG')
- self.finish()
+ self.set_header('Content-Type', 'image/jpeg')
+ self.finish(content)
class MovieHandler(BaseHandler):
@asynchronous
def get(self, camera_id, op, filename=None):
+ if camera_id is not None:
+ camera_id = int(camera_id)
+ if camera_id not in config.get_camera_ids():
+ raise HTTPError(404, 'no such camera')
+
if op == 'list':
self.list(camera_id)
elif op == 'download':
self.download(camera_id, filename)
+ elif op == 'preview':
+ self.preview(camera_id, filename)
+
else:
raise HTTPError(400, 'unknown operation')
-
- @BaseHandler.auth()
+
+ @BaseHandler.auth()
def list(self, camera_id):
logging.debug('listing movies for camera %(id)s' % {'id': camera_id})
+
+ if camera_id not in config.get_camera_ids():
+ raise HTTPError(404, 'no such camera')
+
+ camera_config = config.get_camera(camera_id)
+ if camera_config['@proto'] != 'v4l2':
+ def on_response(remote_list):
+ camera_url = remote.make_remote_camera_url(
+ camera_config.get('@host'),
+ camera_config.get('@port'),
+ camera_config.get('@remote_camera_id'))
+
+ camera_full_url = camera_config['@proto'] + '://' + camera_url
+
+ if remote_list is None:
+ return self.finish_json({'error': 'Failed to get movie list for %(url)s.' % {
+ 'url': camera_full_url}})
- # TODO implement me
+ self.finish_json(remote_list)
+
+ remote.list_media(
+ camera_config.get('@host'),
+ camera_config.get('@port'),
+ camera_config.get('@username'),
+ camera_config.get('@password'),
+ camera_config.get('@remote_camera_id'), on_response,
+ media_type='movie')
+
+ else:
+ movies = mediafiles.list_media(camera_config, media_type='movie')
+
+ self.finish_json({
+ 'mediaList': movies,
+ 'cameraName': camera_config['@name']
+ })
- self.finish_json()
-
@BaseHandler.auth()
def download(self, camera_id, filename):
logging.debug('downloading movie %(filename)s of camera %(id)s' % {
'filename': filename, 'id': camera_id})
+
+ if camera_id not in config.get_camera_ids():
+ raise HTTPError(404, 'no such camera')
+
+ camera_config = config.get_camera(camera_id)
+ if camera_config['@proto'] != 'v4l2':
+ def on_response(response):
+ camera_url = remote.make_remote_camera_url(
+ camera_config.get('@host'),
+ camera_config.get('@port'),
+ camera_config.get('@remote_camera_id'))
+
+ camera_full_url = camera_config['@proto'] + '://' + camera_url
+
+ if response is None:
+ return self.finish_json({'error': 'Failed to download movie from %(url)s.' % {
+ 'url': camera_full_url}})
+
+ pretty_filename = os.path.basename(filename) # no camera name available w/o additional request
+ self.set_header('Content-Type', 'video/mpeg')
+ self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + ';')
+
+ self.finish(response)
+
+ remote.get_media_content(
+ camera_config.get('@host'),
+ camera_config.get('@port'),
+ camera_config.get('@username'),
+ camera_config.get('@password'),
+ camera_config.get('@remote_camera_id'),
+ on_response,
+ filename=filename,
+ media_type='movie')
+
+ else:
+ content = mediafiles.get_media_content(camera_config, filename, 'movie')
+
+ pretty_filename = camera_config['@name'] + '_' + os.path.basename(filename)
+ self.set_header('Content-Type', 'video/mpeg')
+ self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + ';')
+
+ self.finish(content)
- # TODO implement me
+ @BaseHandler.auth()
+ def preview(self, camera_id, filename):
+ logging.debug('previewing movie %(filename)s of camera %(id)s' % {
+ 'filename': filename, 'id': camera_id})
- self.finish_json()
+ if camera_id not in config.get_camera_ids():
+ raise HTTPError(404, 'no such camera')
+
+ camera_config = config.get_camera(camera_id)
+ if camera_config['@proto'] != 'v4l2':
+ def on_response(response):
+ camera_url = remote.make_remote_camera_url(
+ camera_config.get('@host'),
+ camera_config.get('@port'),
+ camera_config.get('@remote_camera_id'))
+
+ camera_full_url = camera_config['@proto'] + '://' + camera_url
+
+ if response is None:
+ return self.finish_json({'error': 'Failed to get movie preview for %(url)s.' % {
+ 'url': camera_full_url}})
+
+ self.set_header('Content-Type', 'image/jpeg')
+ self.finish(response)
+
+ remote.get_media_preview(
+ camera_config.get('@host'),
+ camera_config.get('@port'),
+ camera_config.get('@username'),
+ camera_config.get('@password'),
+ camera_config.get('@remote_camera_id'),
+ on_response,
+ filename=filename,
+ media_type='movie',
+ width=self.get_argument('width', None),
+ height=self.get_argument('height', None))
+
+ else:
+ content = mediafiles.get_media_preview(camera_config, filename, 'movie',
+ width=self.get_argument('width', None),
+ height=self.get_argument('height', None))
+
+ self.set_header('Content-Type', 'image/jpeg')
+ self.finish(content)
class UpdateHandler(BaseHandler):
import datetime
import logging
import os.path
+import StringIO
+import subprocess
+
+from PIL import Image
import config
import utils
os.remove(full_path)
-def cleanup_pictures():
- logging.debug('cleaning up pictures...')
+def cleanup_media(media_type):
+ logging.debug('cleaning up %(media_type)ss...' % {'media_type': media_type})
+ if media_type == 'picture':
+ exts = _PICTURE_EXTS
+
+ elif media_type == 'movie':
+ exts = _MOVIE_EXTS
+
for camera_id in config.get_camera_ids():
camera_config = config.get_camera(camera_id)
if camera_config.get('@proto') != 'v4l2':
continue
- preserve_pictures = camera_config.get('@preserve_pictures')
- if preserve_pictures == 0:
+ preserve_media = camera_config.get('@preserve_%(media_type)ss' % {'media_type': media_type}, 0)
+ if preserve_media == 0:
return # preserve forever
- preserve_moment = datetime.datetime.now() - datetime.timedelta(days=preserve_pictures)
+ preserve_moment = datetime.datetime.now() - datetime.timedelta(days=preserve_media)
target_dir = camera_config.get('target_dir')
- _remove_older_files(target_dir, preserve_moment, exts=_PICTURE_EXTS)
+ _remove_older_files(target_dir, preserve_moment, exts=exts)
-def cleanup_movies():
- logging.debug('cleaning up movies...')
+def make_movie_preview(camera_config, full_path):
+ logging.debug('creating movie preview for %(path)s...' % {'path': full_path})
- for camera_id in config.get_camera_ids():
- camera_config = config.get_camera(camera_id)
- if camera_config.get('@proto') != 'v4l2':
- continue
-
- preserve_movies = camera_config.get('@preserve_movies')
- if preserve_movies == 0:
- return # preserve forever
+ framerate = camera_config['framerate']
+ pre_capture = camera_config['pre_capture']
+ offs = pre_capture / framerate
+
+ cmd = 'ffmpeg -i "%(path)s" -f mjpeg -vframes 1 -ss %(offs)s -y %(path)s.thumb' % {
+ 'path': full_path, 'offs': offs}
+
+ try:
+ subprocess.check_call(cmd, shell=True)
+
+ except subprocess.CalledProcessError as e:
+ logging.error('failed to create movie preview for %(path)s: %(msg)s' % {
+ 'path': full_path, 'msg': unicode(e)})
- preserve_moment = datetime.datetime.now() - datetime.timedelta(days=preserve_movies)
-
- target_dir = camera_config.get('target_dir')
- _remove_older_files(target_dir, preserve_moment, exts=_MOVIE_EXTS)
+ return None
+
+ return full_path + '.thumb'
-def list_pictures(camera_config):
+def list_media(camera_config, media_type):
target_dir = camera_config.get('target_dir')
-# output_all = camera_config.get('output_all')
-# output_normal = camera_config.get('output_normal')
-# jpeg_filename = camera_config.get('jpeg_filename')
-# snapshot_interval = camera_config.get('snapshot_interval')
-# snapshot_filename = camera_config.get('snapshot_filename')
-#
-# if (output_all or output_normal) and jpeg_filename:
-# filename = jpeg_filename
-#
-# elif snapshot_interval and snapshot_filename:
-# filename = snapshot_filename
-#
-# else:
-# return []
-
- full_paths = _list_media_files(target_dir, exts=_PICTURE_EXTS)
- picture_files = []
+
+ if media_type == 'picture':
+ exts = _PICTURE_EXTS
+
+ elif media_type == 'movie':
+ exts = _MOVIE_EXTS
+
+ full_paths = _list_media_files(target_dir, exts=exts)
+ media_files = []
for p in full_paths:
path = p[len(target_dir):]
if not path.startswith('/'):
path = '/' + path
- picture_files.append({
+ media_files.append({
'path': path,
'momentStr': utils.pretty_date_time(datetime.datetime.fromtimestamp(os.path.getmtime(p))),
'sizeStr': utils.pretty_size(os.path.getsize(p)),
# TODO files listed here may not belong to the given camera
- return picture_files
+ return media_files
-def list_movies(camera_config):
+def get_media_content(camera_config, path, media_type):
target_dir = camera_config.get('target_dir')
- full_paths = _list_media_files(target_dir, exts=_MOVIE_EXTS)
- movie_files = [{
- 'path': p[len(target_dir):],
- 'momentStr': utils.pretty_date_time(datetime.datetime.fromtimestamp(os.path.getmtime(p))),
- 'sizeStr': utils.pretty_size(os.path.getsize(p)),
- 'timestamp': os.path.getmtime(p)
- } for p in full_paths]
+ full_path = os.path.join(target_dir, path)
- # TODO files listed here may not belong to the given camera
+ try:
+ with open(full_path) as f:
+ return f.read()
- return movie_files
+ except Exception as e:
+ logging.error('failed to read file %(path)s: %(msg)s' % {
+ 'path': full_path, 'msg': unicode(e)})
+
+ return None
-def get_media_content(camera_config, path):
+def get_media_preview(camera_config, path, media_type, width, height):
target_dir = camera_config.get('target_dir')
-
full_path = os.path.join(target_dir, path)
+ if media_type == 'movie':
+ if not os.path.exists(full_path + '.thumb'):
+ if not make_movie_preview(camera_config, full_path):
+ return None
+
+ full_path += '.thumb'
+
try:
with open(full_path) as f:
- return f.read()
+ content = f.read()
except Exception as e:
logging.error('failed to read file %(path)s: %(msg)s' % {
'path': full_path, 'msg': unicode(e)})
return None
+
+ if width is height is None:
+ return content
+
+ sio = StringIO.StringIO(content)
+ image = Image.open(sio)
+ width = width and int(width) or image.size[0]
+ height = height and int(height) or image.size[1]
+
+ image.thumbnail((width, height), Image.LINEAR)
+
+ sio = StringIO.StringIO()
+ image.save(sio, format='JPEG')
+
+ return sio.getvalue()
http_client.fetch(request, on_response)
-def list_pictures(host, port, username, password, camera_id, callback):
- logging.debug('getting picture list for remote camera %(id)s on %(host)s:%(port)s' % {
+def list_media(host, port, username, password, camera_id, callback, media_type):
+ logging.debug('getting media list for remote camera %(id)s on %(host)s:%(port)s' % {
'id': camera_id,
'host': host,
'port': port})
- request = _make_request(host, port, username, password, '/picture/%(id)s/list/' % {'id': camera_id})
+ request = _make_request(host, port, username, password, '/%(media_type)s/%(id)s/list/' % {
+ 'id': camera_id, 'media_type': media_type})
def on_response(response):
if response.error:
- logging.error('failed to get picture list for remote camera %(id)s on %(host)s:%(port)s: %(msg)s' % {
+ logging.error('failed to get media list for remote camera %(id)s on %(host)s:%(port)s: %(msg)s' % {
'id': camera_id,
'host': host,
'port': port,
http_client.fetch(request, on_response)
-def get_media(host, port, username, password, camera_id, callback, filename, media_type, width=None, height=None):
- logging.debug('getting file %(filename)s of remote camera %(id)s on %(host)s:%(port)s' % {
+def get_media_content(host, port, username, password, camera_id, callback, filename, media_type):
+ logging.debug('downloading file %(filename)s of remote camera %(id)s on %(host)s:%(port)s' % {
'filename': filename,
'id': camera_id,
'host': host,
'port': port})
- uri = '/%(media_type)s/%(id)s/%(op)s/%(filename)s?' % {
+ uri = '/%(media_type)s/%(id)s/download/%(filename)s?' % {
+ 'media_type': media_type,
+ 'id': camera_id,
+ 'filename': filename}
+
+ request = _make_request(host, port, username, password, uri)
+
+ def on_response(response):
+ if response.error:
+ logging.error('failed to download file %(filename)s of remote camera %(id)s on %(host)s:%(port)s: %(msg)s' % {
+ 'filename': filename,
+ 'id': camera_id,
+ 'host': host,
+ 'port': port,
+ 'msg': unicode(response.error)})
+
+ return callback(None)
+
+ return callback(response.body)
+
+ http_client = AsyncHTTPClient()
+ http_client.fetch(request, on_response)
+
+
+def get_media_preview(host, port, username, password, camera_id, callback, filename, media_type, width, height):
+ logging.debug('getting file preview for %(filename)s of remote camera %(id)s on %(host)s:%(port)s' % {
+ 'filename': filename,
+ 'id': camera_id,
+ 'host': host,
+ 'port': port})
+
+ uri = '/%(media_type)s/%(id)s/preview/%(filename)s?' % {
'media_type': media_type,
- 'op': 'preview' if (width or height) else 'download',
'id': camera_id,
'filename': filename}
def on_response(response):
if response.error:
- logging.error('failed to get media file %(filename)s of remote camera %(id)s on %(host)s:%(port)s: %(msg)s' % {
+ logging.error('failed to get file preview for %(filename)s of remote camera %(id)s on %(host)s:%(port)s: %(msg)s' % {
'filename': filename,
'id': camera_id,
'host': host,
(r'^/picture/(?P<camera_id>\d+)/(?P<op>current|list)/?$', handlers.PictureHandler),
(r'^/picture/(?P<camera_id>\d+)/(?P<op>download|preview)/(?P<filename>.+)/?$', handlers.PictureHandler),
(r'^/movie/(?P<camera_id>\d+)/(?P<op>list)/?$', handlers.MovieHandler),
- (r'^/movie/(?P<camera_id>\d+)/(?P<op>download)/(?P<filename>.+)/?$', handlers.MovieHandler),
+ (r'^/movie/(?P<camera_id>\d+)/(?P<op>download|preview)/(?P<filename>.+)/?$', handlers.MovieHandler),
(r'^/update/?$', handlers.UpdateHandler),
],
debug=True, # enables autoreload
div.picture-dialog-next-arrow {
position: absolute;
top: 45%;
- background-color: rgba(0, 0, 0, 0.3);
+ background-color: rgba(0, 0, 0, 0.6);
background-image: url(../img/arrows.svg);
background-size: cover;
width: 3em;
runModalDialog({title: message, buttons: 'yesno', onYes: onYes});
}
-function runPictureDialog(entries, pos) {
+function runPictureDialog(entries, pos, mediaType) {
var content = $('<div class="picture-dialog-content"></div>');
var img = $('<img class="picture-dialog-content">');
progressImg.css('left', (img.parent().width() - progressImg.width()) / 2);
progressImg.css('top', (img.parent().height() - progressImg.height()) / 2);
- img.attr('src', '/picture/' + entry.cameraId + '/preview' + entry.path);
+ img.attr('src', '/' + mediaType + '/' + entry.cameraId + '/preview' + entry.path);
img.load(function () {
img.width(width);
updateModalDialogPosition();
buttons: [
{caption: 'Close'},
{caption: 'Download', isDefault: true, click: function () {
- window.location.href = img.attr('src').replace('preview', 'download');
+ var entry = entries[pos];
+ window.location.href = '/' + mediaType + '/' + entry.cameraId + '/download' + entry.path;
return false;
}}
};
entryDiv[0]._onClick = function () {
- if (mediaType === 'picture') {
- runPictureDialog(entries, pos);
- }
+ runPictureDialog(entries, pos, mediaType);
};
entry.div = entryDiv;
'<span class="camera-name"></span>' +
'<div class="camera-buttons">' +
'<div class="button camera-button mouse-effect media-pictures" title="pictures"></div>' +
-// '<div class="button camera-button mouse-effect media-movies" title="movies"></div>' +
+ '<div class="button camera-button mouse-effect media-movies" title="movies"></div>' +
'<div class="button camera-button mouse-effect configure" title="configure"></div>' +
// '<div class="button camera-button mouse-effect full-screen" title="full screen"></div>' +
'</div>' +