From 834a68233f8c162b0c77049cbd177ee053876f27 Mon Sep 17 00:00:00 2001 From: Calin Crisan Date: Fri, 8 Jul 2016 19:02:38 +0300 Subject: [PATCH] added arrow action buttons; added support for custom video format; the ffmpeg_variable_bitrate is now used to control movie quality --- motioneye/config.py | 58 ++++++++++-------- motioneye/handlers.py | 4 +- motioneye/mediafiles.py | 46 +++++++++++++- motioneye/static/css/main.css | 16 +++++ .../static/img/camera-action-buttons.svg | 60 +++++++++++++++++-- motioneye/static/js/main.js | 16 ++++- motioneye/templates/main.html | 21 ++++++- 7 files changed, 184 insertions(+), 37 deletions(-) diff --git a/motioneye/config.py b/motioneye/config.py index 5292146..ba590f0 100644 --- a/motioneye/config.py +++ b/motioneye/config.py @@ -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', '') diff --git a/motioneye/handlers.py b/motioneye/handlers.py index 7ea0425..610317d 100644 --- a/motioneye/handlers.py +++ b/motioneye/handlers.py @@ -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): diff --git a/motioneye/mediafiles.py b/motioneye/mediafiles.py index 4e851f9..c316b75 100644 --- a/motioneye/mediafiles.py +++ b/motioneye/mediafiles.py @@ -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 } diff --git a/motioneye/static/css/main.css b/motioneye/static/css/main.css index 7922d0f..fd81a9c 100644 --- a/motioneye/static/css/main.css +++ b/motioneye/static/css/main.css @@ -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; diff --git a/motioneye/static/img/camera-action-buttons.svg b/motioneye/static/img/camera-action-buttons.svg index ee25bc5..c3bd52d 100644 --- a/motioneye/static/img/camera-action-buttons.svg +++ b/motioneye/static/img/camera-action-buttons.svg @@ -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" @@ -27,16 +27,16 @@ 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" @@ -206,5 +206,53 @@ 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" /> + + + + + + + + diff --git a/motioneye/static/js/main.js b/motioneye/static/js/main.js index a990bad..d0d4b65 100644 --- a/motioneye/static/js/main.js +++ b/motioneye/static/js/main.js @@ -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) { '
' + '
' + '
' + + '
' + + '
' + + '
' + + '
' + '' + '' + '' + @@ -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) { diff --git a/motioneye/templates/main.html b/motioneye/templates/main.html index 5a230ec..2e82f01 100644 --- a/motioneye/templates/main.html +++ b/motioneye/templates/main.html @@ -691,7 +691,26 @@ Movie File Name - ? + ? + + + Movie Format + + + + ? Movie Quality -- 2.39.5