]> www.vanbest.org Git - motioneye-debian/commitdiff
added a media previewer for movies
authorCalin Crisan <ccrisan@gmail.com>
Sat, 16 Nov 2013 17:11:35 +0000 (19:11 +0200)
committerCalin Crisan <ccrisan@gmail.com>
Sat, 16 Nov 2013 17:19:17 +0000 (19:19 +0200)
doc/todo.txt
motioneye.py
src/handlers.py
src/mediafiles.py
src/remote.py
src/server.py
static/css/main.css
static/js/main.js

index d9bf54532f4de0ccb6dd6247b74a56e4989d3d52..34719ed47e9c2d845b3d429abc5dea97f2b01d0a 100644 (file)
@@ -1,5 +1,4 @@
 -> 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
 
@@ -16,3 +15,4 @@
 
 -> browser compatibility test
 -> requirements test
+   
\ No newline at end of file
index 830a432e39e4dcffa7b49589c8d6d265766e6608..9a89434d4cd144b4e3f20df449c947f49bd6b4b2 100755 (executable)
@@ -186,8 +186,8 @@ def _start_cleanup():
             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' % {
index 5bd95e8a288998d2f70768ff851ab4e9f34ed6c0..f32b62f76528d0a139146f21c491e723edd39882 100644 (file)
@@ -19,10 +19,8 @@ import base64
 import json
 import logging
 import os
-import StringIO
 
 from tornado.web import RequestHandler, HTTPError, asynchronous
-from PIL import Image
 
 import config
 import mediafiles
@@ -590,15 +588,16 @@ class PictureHandler(BaseHandler):
 
                 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,
@@ -633,7 +632,7 @@ class PictureHandler(BaseHandler):
                 
                 self.finish(response)
 
-            remote.get_media(
+            remote.get_media_content(
                     camera_config.get('@host'),
                     camera_config.get('@port'),
                     camera_config.get('@username'),
@@ -644,7 +643,7 @@ class PictureHandler(BaseHandler):
                     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')
@@ -678,7 +677,7 @@ class PictureHandler(BaseHandler):
                 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'),
@@ -691,55 +690,164 @@ class PictureHandler(BaseHandler):
                     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):
index c346b361b317dfa21eeb0439f301d86ce77a8080..518637025651bc4efd280e485fee085bb2a9bfe9 100644 (file)
 import datetime
 import logging
 import os.path
+import StringIO
+import subprocess
+
+from PIL import Image
 
 import config
 import utils
@@ -56,68 +60,70 @@ def _remove_older_files(dir, moment, exts):
             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)),
@@ -126,36 +132,57 @@ def list_pictures(camera_config):
     
     # 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()
index 7766293634d088e1a4e16e29adbe172c12774b5d..6a976a0e4aa730fcccfedcc76df9a72356f45ca4 100644 (file)
@@ -208,17 +208,18 @@ def current_picture(host, port, username, password, camera_id, callback):
     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,
@@ -243,16 +244,46 @@ def list_pictures(host, port, username, password, camera_id, callback):
     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}
     
@@ -266,7 +297,7 @@ def get_media(host, port, username, password, camera_id, callback, filename, med
     
     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,
index f7ead25a4ea9d4bc270410c3766041e5986cbf81..2dd02885e70e0435e8049261c6cc451a76de97a0 100644 (file)
@@ -47,7 +47,7 @@ application = Application(
         (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
index b70020e841917accd499d0b307868800c786b11b..5b6fde4262d21010c580d36fb7d99b81a07553b8 100644 (file)
@@ -477,7 +477,7 @@ div.picture-dialog-prev-arrow,
 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;
index 55fff9271c4047bd982d26b4f9a6c11af9dbfb00..c34c3720be8fbbb013868097da0e89b8195cb86d 100644 (file)
@@ -1012,7 +1012,7 @@ function runConfirmDialog(message, onYes) {
     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">');
@@ -1045,7 +1045,7 @@ function runPictureDialog(entries, pos) {
         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();
@@ -1082,7 +1082,8 @@ function runPictureDialog(entries, pos) {
         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;
             }}
@@ -1396,9 +1397,7 @@ function runMediaDialog(cameraId, mediaType) {
                 };
                 
                 entryDiv[0]._onClick = function () {
-                    if (mediaType === 'picture') {
-                        runPictureDialog(entries, pos);
-                    }
+                    runPictureDialog(entries, pos, mediaType);
                 };
                 
                 entry.div = entryDiv;
@@ -1500,7 +1499,7 @@ function addCameraFrameUi(cameraId, cameraName, framerate) {
                     '<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>' +