import errno
import glob
import logging
+import math
import os.path
import re
import shlex
_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 = {}
# 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']),
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'] = (
ui['capture_mode'] = 'motion-triggered'
if picture_filename:
ui['image_file_name'] = picture_filename
-
+
if data['ffmpeg_output_movies']:
ui['movies'] = True
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']
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
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', '')
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()))
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):
_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 = {}
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)
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
'tmp_filename': tmp_filename,
'jpegs': ' '.join((('"' + p['path'] + '"') for p in pictures)),
'framerate': framerate,
+ 'codec': codec,
+ 'format': format,
'bitrate': bitrate
}
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;
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>
'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(),
$('#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']);
'<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>' +
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');
'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) {
<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>