]> www.vanbest.org Git - motioneye-debian/commitdiff
added arrow action buttons; added support for custom video format; the ffmpeg_variabl...
authorCalin Crisan <ccrisan@gmail.com>
Fri, 8 Jul 2016 16:02:38 +0000 (19:02 +0300)
committerCalin Crisan <ccrisan@gmail.com>
Fri, 8 Jul 2016 16:02:38 +0000 (19:02 +0300)
motioneye/config.py
motioneye/handlers.py
motioneye/mediafiles.py
motioneye/static/css/main.css
motioneye/static/img/camera-action-buttons.svg
motioneye/static/js/main.js
motioneye/templates/main.html

index 5292146f90a9759fe0fc4270d70ce85c50a2e812..ba590f0c75195fac2a70bf6af50e4f7feff7829c 100644 (file)
@@ -20,6 +20,7 @@ import datetime
 import errno
 import glob
 import logging
+import math
 import os.path
 import re
 import shlex
@@ -39,7 +40,7 @@ import v4l2ctl
 
 _CAMERA_CONFIG_FILE_NAME = 'thread-%(id)s.conf'
 _MAIN_CONFIG_FILE_NAME = 'motion.conf'
-_ACTIONS = ['lock', 'unlock', 'light_on', 'light_off', 'alarm_on', 'alarm_off']
+_ACTIONS = ['lock', 'unlock', 'light_on', 'light_off', 'alarm_on', 'alarm_off', 'up', 'right', 'down', 'left']
 
 _main_config_cache = None
 _camera_config_cache = {}
@@ -669,7 +670,6 @@ def motion_camera_ui_to_dict(ui, old_config=None):
         # movies
         'ffmpeg_output_movies': False,
         'movie_filename': ui['movie_file_name'],
-        'ffmpeg_bps': 44000, # a quality of about 85% for 320x240x2fps
         'max_movie_time': ui['max_movie_length'],
         '@preserve_movies': int(ui['preserve_movies']),
     
@@ -837,17 +837,11 @@ def motion_camera_ui_to_dict(ui, old_config=None):
 
         elif recording_mode == 'continuous':
             data['emulate_motion'] = True
+        
+        data['ffmpeg_video_codec'] = ui['movie_format']
 
-    if proto == 'v4l2':
-        max_val = data['width'] * data['height'] * data['framerate']
-    
-    else: # assume a netcam image size of 640x480, since we have no means to know it at this point
-        max_val = 640 * 480 * data['framerate']
-
-    max_val = min(max_val, 9999999)
+    data['ffmpeg_variable_bitrate'] = 1 + min(32766, int(math.ceil(327.66 * (100 - int(ui['movie_quality'])))))
 
-    data['ffmpeg_bps'] = int(int(ui['movie_quality']) * max_val / 100)
-    
     # working schedule
     if ui['working_schedule']:
         data['@working_schedule'] = (
@@ -1189,7 +1183,7 @@ def motion_camera_dict_to_ui(data):
         ui['capture_mode'] = 'motion-triggered'
         if picture_filename:
             ui['image_file_name'] = picture_filename
-        
+
     if data['ffmpeg_output_movies']:
         ui['movies'] = True
         
@@ -1198,18 +1192,14 @@ def motion_camera_dict_to_ui(data):
 
     else:
         ui['recording_mode'] = 'motion-triggered'
-            
-    ffmpeg_bps = data['ffmpeg_bps']
-    if ffmpeg_bps is not None:
-        if utils.v4l2_camera(data):
-            max_val = data['width'] * data['height'] * data['framerate']
         
-        else: # net camera
-            max_val = 640 * 480 * data['framerate']
-            
-        max_val = min(max_val, 9999999)
-        
-        ui['movie_quality'] = min(100, int(round(ffmpeg_bps * 100.0 / max_val)))
+    ui['movie_format'] = data['ffmpeg_video_codec']
+
+    bitrate = data['ffmpeg_variable_bitrate']
+    if not bitrate:
+        bitrate = 8193 # about 75%
+
+    ui['movie_quality'] = int(math.floor(100 - (bitrate - 2) * 100.0 / 32766))
 
     # working schedule
     working_schedule = data['@working_schedule']
@@ -1522,6 +1512,19 @@ def motion_mmal_support():
         return False
 
 
+def motion_new_movie_format_support():
+    import motionctl
+    try:
+        binary, version = motionctl.find_motion()  # @UnusedVariable
+
+        # any git version is supposed to accept newer formats
+        return version.lower().count('git') 
+    except:
+        return False
+
+
 def invalidate():
     global _main_config_cache
     global _camera_config_cache
@@ -1803,12 +1806,15 @@ def _set_default_motion_camera(camera_id, data):
     data.setdefault('quality', 85)
     data.setdefault('@preserve_pictures', 0)
     
-    data.setdefault('ffmpeg_variable_bitrate', 0)
-    data.setdefault('ffmpeg_bps', 44000) # a quality of about 85% 
+    data.setdefault('ffmpeg_variable_bitrate', 8193) # about 75%
     data.setdefault('movie_filename', '%Y-%m-%d/%H-%M-%S')
     data.setdefault('max_movie_time', 0)
     data.setdefault('ffmpeg_output_movies', False)
-    data.setdefault('ffmpeg_video_codec', 'msmpeg4')
+    if motion_new_movie_format_support():
+        data.setdefault('ffmpeg_video_codec', 'mp4') # will use h264 codec
+        
+    else:
+        data.setdefault('ffmpeg_video_codec', 'msmpeg4')
     data.setdefault('@preserve_movies', 0)
     
     data.setdefault('@working_schedule', '')
index 7ea04250028dc3dacd5f256fcd9082c29e93a33c..610317d46b7f381b49faa7374b8b479bfc40349e 100644 (file)
@@ -206,6 +206,7 @@ class MainHandler(BaseHandler):
                 title=self.get_argument('title', None),
                 admin_username=config.get_main().get('@admin_username'),
                 old_motion=config.is_old_motion(),
+                motion_new_movie_format_support=config.motion_new_movie_format_support(),
                 has_motion=bool(motionctl.find_motion()))
     
 
@@ -1215,9 +1216,10 @@ class PictureHandler(BaseHandler):
 
                 pretty_filename = camera_config['@name'] + '_' + group
                 pretty_filename = re.sub('[^a-zA-Z0-9]', '_', pretty_filename)
+                pretty_filename += '.' + mediafiles.FFMPEG_EXT_MAPPING.get(camera_config['ffmpeg_video_codec'], 'avi')
     
                 self.set_header('Content-Type', 'video/x-msvideo')
-                self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + '.avi;')
+                self.set_header('Content-Disposition', 'attachment; filename=' + pretty_filename + ';')
                 self.finish(data)
 
             elif utils.remote_camera(camera_config):
index 4e851f9df113debd36cbd562a9384866ad3435b6..c316b7515a893ba8fa8292e752024666736c650c 100644 (file)
@@ -41,7 +41,43 @@ import utils
 
 
 _PICTURE_EXTS = ['.jpg']
-_MOVIE_EXTS = ['.avi', '.mp4']
+_MOVIE_EXTS = ['.avi', '.mp4', '.mov', '.swf', '.flv', '.ogg', '.mkv']
+
+FFMPEG_CODEC_MAPPING = {
+    'mpeg4': 'mpeg4',
+    'msmpeg4': 'msmpeg4v2',
+    'swf': 'flv1',
+    'flv': 'flv1',
+    'mov': 'mpeg4',
+    'ogg': 'theora',
+    'mp4': 'h264',
+    'mkv': 'h264',
+    'hevc': 'h265'
+}
+
+FFMPEG_FORMAT_MAPPING = {
+    'mpeg4': 'avi',
+    'msmpeg4': 'avi',
+    'swf': 'swf',
+    'flv': 'flv',
+    'mov': 'mov',
+    'ogg': 'ogg',
+    'mp4': 'mp4',
+    'mkv': 'matroska',
+    'hevc': 'mp4'
+}
+
+FFMPEG_EXT_MAPPING = {
+    'mpeg4': 'avi',
+    'msmpeg4': 'avi',
+    'swf': 'swf',
+    'flv': 'flv',
+    'mov': 'mov',
+    'ogg': 'ogg',
+    'mp4': 'mp4',
+    'mkv': 'mkv',
+    'hevc': 'mp4'
+}
 
 # a cache of prepared files (whose preparing time is significant)
 _prepared_files = {}
@@ -458,7 +494,11 @@ def make_timelapse_movie(camera_config, framerate, interval, group):
     global _timelapse_data
     
     target_dir = camera_config.get('target_dir')
+    codec = camera_config.get('ffmpeg_video_codec')
     
+    codec = FFMPEG_CODEC_MAPPING.get(codec, codec)
+    format = FFMPEG_FORMAT_MAPPING.get(codec, codec)
+
     # create a subprocess to retrieve media files
     def do_list_media(pipe):
         mf = _list_media_files(target_dir, exts=_PICTURE_EXTS, prefix=group)
@@ -549,7 +589,7 @@ def make_timelapse_movie(camera_config, framerate, interval, group):
         global _timelapse_process
 
         cmd =  'rm -f %(tmp_filename)s;'
-        cmd += 'cat %(jpegs)s | ffmpeg -framerate %(framerate)s -f image2pipe -vcodec mjpeg -i - -vcodec mpeg4 -b:v %(bitrate)s -qscale:v 0.1 -f avi %(tmp_filename)s'
+        cmd += 'cat %(jpegs)s | ffmpeg -framerate %(framerate)s -f image2pipe -vcodec mjpeg -i - -vcodec %(codec)s -format %(format)s -b:v %(bitrate)s -qscale:v 0.1 -f avi %(tmp_filename)s'
 
         bitrate = 9999999
 
@@ -557,6 +597,8 @@ def make_timelapse_movie(camera_config, framerate, interval, group):
             'tmp_filename': tmp_filename,
             'jpegs': ' '.join((('"' + p['path'] + '"') for p in pictures)),
             'framerate': framerate,
+            'codec': codec,
+            'format': format,
             'bitrate': bitrate
         }
         
index 7922d0f77b9fd730d6d931d045b89445b4912c70..fd81a9cb0dec58fa63e1415b7dbb539824cb6509 100644 (file)
@@ -1080,6 +1080,22 @@ div.camera-action-button.record-stop {
     background-position: -800% 0px;
 }
 
+div.camera-action-button.up {
+    background-position: -900% 0px;
+}
+
+div.camera-action-button.right {
+    background-position: -1000% 0px;
+}
+
+div.camera-action-button.down {
+    background-position: -1100% 0px;
+}
+
+div.camera-action-button.left {
+    background-position: -1200% 0px;
+}
+
 div.camera-container {
     position: relative;
     padding: 0px;
index ee25bc593a1efd246203f516cce405e463e287c8..c3bd52d01e3f5e9085e4ab033317ab95c83c3515 100644 (file)
@@ -9,7 +9,7 @@
    xmlns="http://www.w3.org/2000/svg"
    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   width="360"
+   width="640"
    height="40"
    id="svg2"
    version="1.1"
      borderopacity="1.0"
      inkscape:pageopacity="0.74509804"
      inkscape:pageshadow="2"
-     inkscape:zoom="7.9999999"
-     inkscape:cx="56.963083"
-     inkscape:cy="9.783066"
+     inkscape:zoom="1"
+     inkscape:cx="449.26208"
+     inkscape:cy="22.330471"
      inkscape:document-units="px"
      inkscape:current-layer="layer1"
      showgrid="true"
      inkscape:window-width="1920"
-     inkscape:window-height="1018"
+     inkscape:window-height="1025"
      inkscape:window-x="0"
-     inkscape:window-y="25"
+     inkscape:window-y="27"
      inkscape:window-maximized="1"
      inkscape:snap-bbox="true"
      inkscape:bbox-nodes="true"
        d="m 258,1025.3622 -1,2 -2,0 c -1.108,0 -2,0.892 -2,2 l 0,6 c 0,1.108 0.892,2 2,2 l 10,0 c 1.108,0 2,-0.892 2,-2 l 0,-6 c 0,-1.108 -0.892,-2 -2,-2 l -2,0 -1,-2 -4,0 z m 2,3 a 4,4 0 0 1 4,4 4,4 0 0 1 -4,4 4,4 0 0 1 -4,-4 4,4 0 0 1 4,-4 z"
        id="path4266"
        inkscape:connector-curvature="0" />
+    <circle
+       r="13"
+       cy="1032.3623"
+       cx="380"
+       id="circle4156"
+       style="fill:none;stroke:#3498db;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    <path
+       style="fill:#3498db;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m 380,1023.3622 -8,8 5,0 0,9 6,0 0.0312,-9 4.96875,0 z"
+       id="path4158"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cccccccc" />
+    <circle
+       style="fill:none;stroke:#3498db;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="circle4160"
+       cx="420"
+       cy="1032.3623"
+       r="13" />
+    <path
+       sodipodi:nodetypes="cccccccc"
+       inkscape:connector-curvature="0"
+       id="path4162"
+       d="m 429,1032.3623 -8,-8 0,5 -9,0 0,6 9,0.031 0,4.9688 z"
+       style="fill:#3498db;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <circle
+       r="13"
+       cy="1032.3623"
+       cx="460"
+       id="circle4164"
+       style="fill:none;stroke:#3498db;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    <path
+       style="fill:#3498db;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m 460,1041.3622 -8,-8 5,0 0,-9 6,0 0.0312,9 4.96875,0 z"
+       id="path4166"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cccccccc" />
+    <circle
+       style="fill:none;stroke:#3498db;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="circle4168"
+       cx="500"
+       cy="1032.3623"
+       r="13" />
+    <path
+       style="fill:#3498db;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m 491,1032.3623 8,-8 0,5 9,0 0,6 -9,0.031 0,4.9688 z"
+       id="path4174"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cccccccc" />
   </g>
 </svg>
index a990bad0cf8725201d6af2cd7f63a2930fccdafd..d0d4b65c353053ca3b5633beb00029fb0c57404a 100644 (file)
@@ -1538,6 +1538,7 @@ function cameraUi2Dict() {
         'movies': $('#moviesEnabledSwitch')[0].checked,
         'movie_file_name': $('#movieFileNameEntry').val(),
         'movie_quality': $('#movieQualitySlider').val(),
+        'movie_format': $('#movieFormatSelect').val(),
         'recording_mode': $('#recordingModeSelect').val(),
         'max_movie_length': $('#maxMovieLengthEntry').val(),
         'preserve_movies': $('#preserveMoviesSelect').val() >= 0 ? $('#preserveMoviesSelect').val() : $('#moviesLifetimeEntry').val(),
@@ -1872,6 +1873,7 @@ function dict2CameraUi(dict) {
     $('#moviesEnabledSwitch')[0].checked = dict['movies']; markHideIfNull('movies', 'moviesEnabledSwitch');
     $('#movieFileNameEntry').val(dict['movie_file_name']); markHideIfNull('movie_file_name', 'movieFileNameEntry');
     $('#movieQualitySlider').val(dict['movie_quality']); markHideIfNull('movie_quality', 'movieQualitySlider');
+    $('#movieFormatSelect').val(dict['movie_format']); markHideIfNull('movie_format', 'movieFormatSelect');
     $('#recordingModeSelect').val(dict['recording_mode']); markHideIfNull('recording_mode', 'recordingModeSelect');
     $('#maxMovieLengthEntry').val(dict['max_movie_length']); markHideIfNull('max_movie_length', 'maxMovieLengthEntry');
     $('#preserveMoviesSelect').val(dict['preserve_movies']);
@@ -3968,6 +3970,10 @@ function addCameraFrameUi(cameraConfig) {
                                 '<div class="button icon camera-action-button mouse-effect alarm-off" title="turn alarm off"></div>' +
                                 '<div class="button icon camera-action-button mouse-effect snapshot" title="take a snapshot"></div>' +
                                 '<div class="button icon camera-action-button mouse-effect record-start" title="toggle continuous recording mode"></div>' +
+                                '<div class="button icon camera-action-button mouse-effect up" title="up"></div>' +
+                                '<div class="button icon camera-action-button mouse-effect down" title="down"></div>' +
+                                '<div class="button icon camera-action-button mouse-effect left" title="left"></div>' +
+                                '<div class="button icon camera-action-button mouse-effect right" title="right"></div>' +
                             '</div>' +
                         '</div>' +
                     '</div>' +
@@ -3992,6 +3998,10 @@ function addCameraFrameUi(cameraConfig) {
     var alarmOffButton = cameraFrameDiv.find('div.camera-action-button.alarm-off');
     var snapshotButton = cameraFrameDiv.find('div.camera-action-button.snapshot');
     var recordButton = cameraFrameDiv.find('div.camera-action-button.record-start');
+    var upButton = cameraFrameDiv.find('div.camera-action-button.up');
+    var rightButton = cameraFrameDiv.find('div.camera-action-button.right');
+    var downButton = cameraFrameDiv.find('div.camera-action-button.down');
+    var leftButton = cameraFrameDiv.find('div.camera-action-button.left');
     
     var cameraOverlay = cameraFrameDiv.find('div.camera-overlay');
     var cameraPlaceholder = cameraFrameDiv.find('div.camera-placeholder');
@@ -4094,7 +4104,11 @@ function addCameraFrameUi(cameraConfig) {
         'alarm_on': alarmOnButton,
         'alarm_off': alarmOffButton,
         'snapshot': snapshotButton,
-        'record': recordButton
+        'record': recordButton,
+        'up': upButton,
+        'right': rightButton,
+        'down': downButton,
+        'left': leftButton,
     };
     
     cameraConfig.actions.forEach(function (action) {
index 5a230ecb0a5cfe05714f8300875517055e9d1504..2e82f01716a2e6da97eec7bce9473e3792569ad1 100644 (file)
                     <tr class="settings-item advanced-setting" required="true" strip="true">
                         <td class="settings-item-label"><span class="settings-item-label">Movie File Name</span></td>
                         <td class="settings-item-value"><input type="text" class="styled movies camera-config" id="movieFileNameEntry" placeholder="file name pattern..."></td>
-                        <td><span class="help-mark" title="sets the name pattern for the movie (MPEG) files; the following special tokens are accepted: %Y = year, %m = month, %d = date, %H = hour, %M = minute, %S = second, %q = frame number, / = subfolder">?</span></td>
+                        <td><span class="help-mark" title="sets the name pattern for the movie files; the following special tokens are accepted: %Y = year, %m = month, %d = date, %H = hour, %M = minute, %S = second, %q = frame number, / = subfolder">?</span></td>
+                    </tr>
+                    <tr class="settings-item advanced-setting">
+                        <td class="settings-item-label"><span class="settings-item-label">Movie Format</span></td>
+                        <td class="settings-item-value">
+                            <select class="styled movies camera-config" id="movieFormatSelect">
+                                <option value="mpeg4">MPEG4 (.avi)</option>
+                                <option value="msmpeg4">MSMPEG4v2 (.avi)</option>
+                                <option value="swf">Small Web Format (.swf)</option>
+                                <option value="flv">Flash Video (.flv)</option>
+                                <option value="mov">QuickTime (.mov)</option>
+                                {% if motion_new_movie_format_support %}
+                                <option value="ogg">Ogg (.ogg)</option>
+                                <option value="mp4">H.264 (.mp4)</option>
+                                <option value="hevc">HEVC (.mp4)</option>
+                                <option value="mkv">Matroska Video (.mkv)</option>
+                                {% endif %}
+                            </select>
+                        </td>
+                        <td><span class="help-mark" title="sets the movie file format; not all formats are guaranteed to work on all systems; if in doubt, test each format and select the one that works best with your video player">?</span></td>
                     </tr>
                     <tr class="settings-item advanced-setting" min="0" max="100" snap="2" ticksnum="5" decimals="0" unit="%">
                         <td class="settings-item-label"><span class="settings-item-label">Movie Quality</span></td>