data['ffmpeg_variable_bitrate'] = int(vbr)
# motion detection
- if ui['mask'] and data.get('width') and data.get('height'):
+ if ui['mask']:
if ui['mask_type'] == 'smart':
data['smart_mask_speed'] = 10 - int(ui['smart_mask_slugginess'])
elif ui['mask_type'] == 'editable':
- data['mask_file'] = utils.build_editable_mask_file(old_config['@id'], data['width'], data['height'], ui['mask_lines'])
+ data['mask_file'] = utils.build_editable_mask_file(old_config['@id'], data.get('width'), data.get('height'), ui['mask_lines'])
# working schedule
if ui['working_schedule']:
ui['movie_quality'] = int(q)
# mask
- if data['mask_file'] and data.get('width') and data.get('height'):
+ if data['mask_file']:
ui['mask'] = True
ui['mask_type'] = 'editable'
- ui['mask_lines'] = utils.parse_editable_mask_file(data['@id'], data['width'], data['height'])
+ ui['mask_lines'] = utils.parse_editable_mask_file(data['@id'], data.get('width'), data.get('height'))
elif data['smart_mask_speed']:
ui['mask'] = True
has_streaming_auth=motionctl.has_streaming_auth(),
has_new_movie_format_support=motionctl.has_new_movie_format_support(),
has_motion=bool(motionctl.find_motion()),
- mask_width=utils.MASK_WIDTH)
+ mask_width=utils.MASK_WIDTH,
+ mask_default_resolution=utils.MASK_DEFAULT_RESOLUTION)
class ConfigHandler(BaseHandler):
div.update-button,
div.backup-button,
div.restore-button,
+div.edit-mask-button,
+div.save-mask-button,
+div.clear-mask-button,
div.test-button {
background: #317CAD;
}
+div.save-mask-button,
+div.clear-mask-button {
+ display: none;
+}
+
div.shut-down-button,
div.reboot-button {
background: #c0392b;
div.update-button:HOVER,
div.backup-button:HOVER,
-div.restore-button:HOVER {
+div.restore-button:HOVER,
+div.edit-mask-button:HOVER,
+div.save-mask-button:HOVER,
+div.clear-mask-button:HOVER,
+div.test-button:HOVER {
background: #3498db;
}
div.update-button:ACTIVE,
div.backup-button:ACTIVE,
-div.restore-button:ACTIVE {
+div.restore-button:ACTIVE,
+div.edit-mask-button:ACTIVE,
+div.save-mask-button:ACTIVE,
+div.clear-mask-button:ACTIVE,
+div.test-button:ACTIVE {
background: #317CAD;
}
width: 90%;
}
+#editableMaskEntry {
+ display: none;
+}
+
div.hidden,
tr.hidden {
display: none !important;
left: 0px;
width: 100%;
z-index: 1;
+ transition: height 0.2s ease;
}
div.camera-frame:HOVER div.camera-overlay-top,
height: 2.5em;
line-height: 2.5em;
white-space: nowrap;
+ overflow: hidden;
}
div.camera-overlay-mask {
- background: #888;
+ background: rgba(255, 255, 255, 0.2);
opacity: 0;
position: absolute;
top: 0px;
bottom: 0px;
left: 0px;
z-index: 0;
+ transition: opacity 0.2s ease;
+}
+
+div.camera-overlay.mask-edit > div.camera-overlay-top {
+ height: 0px;
+ bottom: auto;
}
-div.camera-overlay.mask-edit > div.camera-overlay-top,
div.camera-overlay.mask-edit > div.camera-overlay-bottom {
- display: none;
+ height: 0px;
+ top: auto;
}
div.camera-overlay.mask-edit > div.camera-overlay-mask {
- opacity: 0.3;
+ opacity: 1;
}
div.mask-element {
position: absolute;
box-sizing: border-box;
- border: 1px solid #aaa;
+ border: 1px solid rgba(255, 255, 255, 0.2);
border-width: 0px 1px 1px 0px;
background: transparent;
}
}
div.mask-element.on {
- background: #444;
+ background: rgba(0, 0, 0, 0.7);
}
div.camera-name {
this._prevSelectedIndex = this.selectedIndex;
beginProgress([$(this).val()]);
fetchCurrentCameraConfig(endProgress);
+ disableMaskEdit();
}
});
$('input.main-config, select.main-config, textarea.main-config').change(function () {
setTimeout(function () {
getCameraFrames().find('div.camera-overlay').addClass('visible');
}, 10);
+
+ overlayVisible = true;
}
function hideCameraOverlay() {
setTimeout(function () {
getCameraFrames().find('div.camera-overlay').css('display', 'none');
}, 300);
+
+ overlayVisible = false;
+
+ disableMaskEdit();
}
function enableMaskEdit(cameraId, width, height) {
var overlayDiv = cameraFrame.find('div.camera-overlay');
var maskDiv = cameraFrame.find('div.camera-overlay-mask');
+ if (overlayDiv.hasClass('mask-edit')) {
+ return; /* already enabled */
+ }
+
overlayDiv.addClass('mask-edit');
var nx = maskWidth; /* number of rectangles */
rh = parseInt(height / ny); /* rectangle height */
+ var mouseDown = false;
+ var currentState = false;
+
+ function handleMouseUp() {
+ mouseDown = false;
+ $('html').unbind('mouseup', handleMouseUp);
+ }
+
function makeMaskElement(x, y, px, py, pw, ph) {
px = px * 100 / width;
py = py * 100 / height;
el.addClass('last-line');
}
maskDiv.append(el);
+
+ el.mousedown(function () {
+ mouseDown = true;
+ el.toggleClass('on');
+ currentState = el.hasClass('on');
+ $('html').mouseup(handleMouseUp);
+ });
+
+ el.mouseenter(function () {
+ if (!mouseDown) {
+ return;
+ }
+
+ el.toggleClass('on', currentState);
+ });
}
-
+
/* make sure the mask is empty */
maskDiv.html('');
+
+ /* prevent editor closing by accidental click on mask container */
+ maskDiv.click(function () {
+ return false;
+ })
var x, y;
for (y = 0; y < ny; y++) {
makeMaskElement(x, y, nx * rw, ny * rh, rx, ry);
}
}
+
+ var selectedCameraId = $('#cameraSelect').val();
+ if (selectedCameraId && (!cameraId || cameraId == selectedCameraId)) {
+ $('#saveMaskButton, #clearMaskButton').css('display', 'inline-block');
+ $('#editMaskButton').css('display', 'none');
+ }
+
+ if (!overlayVisible) {
+ showCameraOverlay();
+ }
}
function disableMaskEdit(cameraId) {
+ var cameraFrames;
+ if (cameraId) {
+ cameraFrames = [getCameraFrame(cameraId)];
+ }
+ else { /* disable mask editor on any camera */
+ cameraFrames = getCameraFrames().toArray().map(function (f) {return $(f);});
+ }
+
+ cameraFrames.forEach(function (cameraFrame) {
+ var overlayDiv = cameraFrame.find('div.camera-overlay');
+ var maskDiv = cameraFrame.find('div.camera-overlay-mask');
+
+ overlayDiv.removeClass('mask-edit');
+ maskDiv.html('');
+ maskDiv.unbind('click');
+ });
+
+ var selectedCameraId = $('#cameraSelect').val();
+ if (selectedCameraId && (!cameraId || cameraId == selectedCameraId)) {
+ $('#editMaskButton').css('display', 'inline-block');
+ $('#saveMaskButton, #clearMaskButton').css('display', 'none');
+ }
+}
+
+function clearMask(cameraId) {
var cameraFrame = getCameraFrame(cameraId);
- var overlayDiv = cameraFrame.find('div.camera-overlay');
var maskDiv = cameraFrame.find('div.camera-overlay-mask');
-
- overlayDiv.removeClass('mask-edit');
- maskDiv.html('');
+
+ maskDiv.find('div.mask-element').removeClass('on');
}
'mask': $('#maskSwitch')[0].checked,
'mask_type': $('#maskTypeSelect').val(),
'smart_mask_slugginess': $('#smartMaskSlugginessSlider').val(),
- 'mask_lines': [], // TODO generate mask lines
-
+ 'mask_lines': $('#maskLinesEntry').val().split(','),
+
/* motion notifications */
'email_notifications_enabled': $('#emailNotificationsEnabledSwitch')[0].checked,
'email_notifications_from': $('#emailFromEntry').val(),
$('#maskSwitch')[0].checked = dict['mask']; markHideIfNull('mask', 'maskSwitch');
$('#maskTypeSelect').val(dict['mask_type']); markHideIfNull('mask_type', 'maskTypeSelect');
$('#smartMaskSlugginessSlider').val(dict['smart_mask_slugginess']); markHideIfNull('smart_mask_slugginess', 'smartMaskSlugginessSlider');
- //TODO use dict['mask_lines']; markHideIfNull('mask_file', 'maskFileEntry');
-
+ $('#maskLinesEntry').val((dict['mask_lines'] or []).join(',')); markHideIfNull('mask_lines', 'maskLinesEntry');
+
/* motion notifications */
$('#emailNotificationsEnabledSwitch')[0].checked = dict['email_notifications_enabled']; markHideIfNull('email_notifications_enabled', 'emailNotificationsEnabledSwitch');
$('#emailFromEntry').val(dict['email_notifications_from']);
cameraImg.click(function () {
showCameraOverlay();
- overlayVisible = true;
});
cameraOverlay.click(function () {
hideCameraOverlay();
- overlayVisible = false;
});
cameraOverlay.find('div.camera-overlay-top, div.camera-overlay-bottom').click(function () {
$('div#emailTestButton').click(doTestEmail);
$('div#networkShareTestButton').click(doTestNetworkShare);
+ /* mask editor buttons */
+ $('div#editMaskButton').click(function () {
+ var cameraId = $('#cameraSelect').val();
+ var resolution = $('#resolutionSelect').val();
+ if (!cameraId) {
+ return;
+ }
+
+ if (!resolution) {
+ /*
+ * TODO motion requires the mask file to be the same size as the
+ * captured images; however for netcams we have no means to know in
+ * advance the size of the stream; therefore, for netcams, we impose
+ * here a standard fixed mask size, which WILL NOT WORK for netcam
+ * streams of a different resolution
+ */
+ resolution = maskDefaultResolution;
+ }
+
+ resolution = resolution.split('x');
+ var width = resolution[0];
+ var height = resolution[1];
+
+ enableMaskEdit(cameraId, width, height);
+ });
+ $('div#saveMaskButton').click(function () {
+ disableMaskEdit();
+ });
+ $('div#clearMaskButton').click(function () {
+ var cameraId = $('#cameraSelect').val();
+ if (!cameraId) {
+ return;
+ }
+
+ clearMask(cameraId);
+ });
+
initUI();
beginProgress();
var frame = {% if frame %}true{% else %}false{% endif %};
var hasLocalCamSupport = {% if has_motion %}true{% else %}false{% endif %};
var hasNetCamSupport = {% if has_motion %}true{% else %}false{% endif %};
- {% if mask_width %}var maskWidth = {{mask_width}};{% endif %}
+ {% if mask_width %}
+ var maskWidth = {{mask_width}};
+ var maskDefaultResolution = '{{mask_default_resolution[0]}}x{{mask_default_resolution[1]}}';
+ {% endif %}
</script>
{% endblock %}
<td class="settings-item-value"><input type="text" class="range styled motion-detection camera-config" id="smartMaskSlugginessSlider"></td>
<td><span class="help-mark" title="lower values result in a longer-lasting smart mask with a slower building process">?</span></td>
</tr>
+ <tr class="settings-item advanced-setting" depends="mask maskType=editable">
+ <td class="settings-item-label"><span class="settings-item-label"></span></td>
+ <td class="settings-item-value">
+ <div class="button normal-button edit-mask-button" id="editMaskButton">Edit Mask</div>
+ <div class="button normal-button save-mask-button" id="saveMaskButton">Save Mask</div>
+ <input type="text" id="maskLinesEntry">
+ </td>
+ <td><span class="help-mark" title="click this button to enable/disable the mask editor">?</span></td>
+ </tr>
+ <tr class="settings-item advanced-setting" depends="mask maskType=editable">
+ <td class="settings-item-label"><span class="settings-item-label"></span></td>
+ <td class="settings-item-value"><div class="button normal-button clear-mask-button" id="clearMaskButton">Clear Mask</div></td>
+ <td><span class="help-mark" title="click this button to clear the current mask">?</span></td>
+ </tr>
{% for config in camera_sections.get('motion-detection', {}).get('configs', []) %}
{{config_item(config)}}
{% endfor %}
_SPECIAL_COOKIE_NAMES = {'expires', 'domain', 'path', 'secure', 'httponly'}
MASK_WIDTH = 32
+MASK_DEFAULT_RESOLUTION = (640, 480)
DEV_NULL = open('/dev/null', 'w')
def build_editable_mask_file(camera_id, width, height, mask_lines):
+ logging.debug('building editable mask for camera with id %s (%sx%s)' %
+ (camera_id, width, height))
+
+ width = width or MASK_DEFAULT_RESOLUTION[0]
+ height = height or MASK_DEFAULT_RESOLUTION[1]
+
# horizontal rectangles
nx = MASK_WIDTH # number of rectangles
if width % nx:
rw = width / nx # rectangle width
# vertical rectangles
- ny = height * MASK_WIDTH / width # number of rectangles
+ ny = mask_height = height * MASK_WIDTH / width # number of rectangles
if height % ny:
ny -= 1
ry = height % ny # remainder
# draw the actual mask image content
im = Image.new('L', (width, height), 255) # all white
dr = ImageDraw.Draw(im)
+
+ while len(mask_lines) < mask_height:
+ # add empty mask lines until mask is tall enough
+ mask_lines.append(0)
for y in xrange(ny):
line = mask_lines[y]
file_name = os.path.join(settings.CONF_PATH, 'mask_%s.pgm' % camera_id)
im.save(file_name, 'ppm')
+
+ return file_name
def parse_editable_mask_file(camera_id, width, height):
+ logging.debug('parsing editable mask for camera with id %s (%sx%s)' %
+ (camera_id, width, height))
+
# width and height arguments represent the current size of the camera image,
# as it might be different from that of the associated mask
+ width = width or MASK_DEFAULT_RESOLUTION[0]
+ height = height or MASK_DEFAULT_RESOLUTION[1]
+
# horizontal rectangles
nx = MASK_WIDTH # number of rectangles
if width % nx:
logging.error('failed to read mask file %s: %s' % (file_name, e))
# empty mask
- return [2 ** MASK_WIDTH - 1] * mask_height
+ return [0] * mask_height
# resize the image if necessary
if im.size != (width, height):
+ logging.debug('editable mask needs resizing from %sx%s to %sx%s' %
+ (im.size[0], im.size[1], width, height))
+
im = im.resize((width, height))
pixels = list(im.getdata())