'group': group, 'id': camera_id, 'key': key})
self.finish_json({'key': key})
- mediafiles.get_zipped_content(camera_config, media_type='picture', callback=on_zip, prefix=group)
+ mediafiles.get_zipped_content(camera_config, media_type='picture', callback=on_zip, group=group)
else: # remote camera
def on_response(response=None, error=None):
logging.debug('prepared zip file for group %(group)s of camera %(id)s with key %(key)s' % {
'group': group, 'id': camera_id, 'key': key})
self.finish_json({'key': key})
-
+
remote.get_zipped_content(camera_config, media_type='picture', callback=on_response, group=group)
@BaseHandler.auth()
else:
interval = int(self.get_argument('interval'))
+ speed = int(self.get_argument('speed'))
- logging.debug('preparing timelapse movie for group %(group)s of camera %(id)s with interval %(int)s' % {
- 'group': group, 'id': camera_id, 'int': interval})
+ logging.debug('preparing timelapse movie for group %(group)s of camera %(id)s with rate %(speed)s/%(int)s' % {
+ 'group': group, 'id': camera_id, 'speed': speed, 'int': interval})
camera_config = config.get_camera(camera_id)
if utils.local_camera(camera_config):
'group': group, 'id': camera_id, 'key': key})
self.finish_json({'key': key})
- mediafiles.get_timelapse_movie(camera_config, interval, callback=on_timelapse, group=group)
+ mediafiles.get_timelapse_movie(camera_config, speed, interval, callback=on_timelapse, group=group)
else: # remote camera
def on_response(response=None, error=None):
'group': group, 'id': camera_id, 'key': key})
self.finish_json({'key': key})
- remote.get_timelapse_movie(camera_config, interval, callback=on_response, group=group)
+ remote.get_timelapse_movie(camera_config, speed, interval, callback=on_response, group=group)
def try_finish(self, content):
try:
now = datetime.datetime.now()
delta = now - started
if delta.seconds < 120:
- ioloop.add_timeout(datetime.timedelta(seconds=0.1), poll_process)
+ ioloop.add_timeout(datetime.timedelta(seconds=0.5), poll_process)
read_media_list()
else: # process did not finish within 2 minutes
return None
-def get_zipped_content(camera_config, media_type, callback, prefix):
+def get_zipped_content(camera_config, media_type, callback, group):
target_dir = camera_config.get('target_dir')
if media_type == 'picture':
# create a subprocess to add files to zip
def do_zip(pipe):
- mf = _list_media_files(target_dir, exts=exts, prefix=prefix)
+ mf = _list_media_files(target_dir, exts=exts, prefix=group)
paths = []
for (p, st) in mf: # @UnusedVariable
path = p[len(target_dir):]
poll_process()
-def get_timelapse_movie(camera_config, interval, callback, prefix):
+def get_timelapse_movie(camera_config, speed, interval, callback, group):
target_dir = camera_config.get('target_dir')
+
+ # create a subprocess to retrieve media files
+ def do_list_media(pipe):
+ mf = _list_media_files(target_dir, exts=_PICTURE_EXTS, prefix=group)
+ for (p, st) in mf:
+ timestamp = st.st_mtime
-# working = multiprocessing.Value('b')
-# working.value = True
-
-# # create a subprocess to create the files
-# def do_zip(pipe):
-# mf = _list_media_files(target_dir, exts=exts, prefix=prefix)
-# paths = []
-# for (p, st) in mf: # @UnusedVariable
-# path = p[len(target_dir):]
-# if path.startswith('/'):
-# path = path[1:]
-#
-# paths.append(path)
-#
-# zip_filename = os.path.join(settings.MEDIA_PATH, '.zip-%s' % int(time.time()))
-# logging.debug('adding %d files to zip file "%s"' % (len(paths), zip_filename))
-#
-# try:
-# with zipfile.ZipFile(zip_filename, mode='w') as f:
-# for path in paths:
-# full_path = os.path.join(target_dir, path)
-# f.write(full_path, path)
-#
-# except Exception as e:
-# logging.error('failed to create zip file "%s": %s' % (zip_filename, e))
-#
-# working.value = False
-# pipe.close()
-# return
-#
-# logging.debug('reading zip file "%s" into memory' % zip_filename)
-#
-# try:
-# with open(zip_filename, mode='r') as f:
-# data = f.read()
-#
-# working.value = False
-# pipe.send(data)
-# logging.debug('zip data ready')
-#
-# except Exception as e:
-# logging.error('failed to read zip file "%s": %s' % (zip_filename, e))
-# working.value = False
-#
-# finally:
-# os.remove(zip_filename)
-# pipe.close()
-#
-# logging.debug('starting zip process...')
-#
-# (parent_pipe, child_pipe) = multiprocessing.Pipe(duplex=False)
-# process = multiprocessing.Process(target=do_zip, args=(child_pipe, ))
-# process.start()
-#
-# # poll the subprocess to see when it has finished
-# started = datetime.datetime.now()
-#
-# def poll_process():
-# ioloop = tornado.ioloop.IOLoop.instance()
-# if working.value:
-# now = datetime.datetime.now()
-# delta = now - started
-# if delta.seconds < settings.ZIP_TIMEOUT:
-# ioloop.add_timeout(datetime.timedelta(seconds=0.5), poll_process)
-#
-# else: # process did not finish within 2 minutes
-# logging.error('timeout waiting for the zip process to finish')
-#
-# callback(None)
-#
-# else: # finished
-# try:
-# data = parent_pipe.recv()
-# logging.debug('zip process has returned %d bytes' % len(data))
-#
-# except:
-# data = None
-#
-# callback(data)
-#
-# poll_process()
+ pipe.send({
+ 'path': p,
+ 'timestamp': timestamp
+ })
+
+ pipe.close()
+
+ logging.debug('starting media listing process...')
+
+ (parent_pipe, child_pipe) = multiprocessing.Pipe(duplex=False)
+ process = [multiprocessing.Process(target=do_list_media, args=(child_pipe, ))]
+ process[0].start()
+
+ started = [datetime.datetime.now()]
+ media_list = []
+
+ tmp_filename = os.path.join(settings.MEDIA_PATH, '.%s.avi' % int(time.time()))
+
+ def read_media_list():
+ while parent_pipe.poll():
+ media_list.append(parent_pipe.recv())
+
+ def poll_media_list_process():
+ ioloop = tornado.ioloop.IOLoop.instance()
+ if process[0].is_alive(): # not finished yet
+ now = datetime.datetime.now()
+ delta = now - started[0]
+ if delta.seconds < 120:
+ ioloop.add_timeout(datetime.timedelta(seconds=0.5), poll_media_list_process)
+ read_media_list()
+
+ else: # process did not finish within 2 minutes
+ logging.error('timeout waiting for the media listing process to finish')
+
+ callback(None)
+
+ else: # finished
+ read_media_list()
+ logging.debug('media listing process has returned %(count)s files' % {'count': len(media_list)})
+
+ if not media_list:
+ return callback(None)
+
+ pictures = select_pictures(media_list)
+ make_movie(pictures)
+
+ def select_pictures(media_list):
+ media_list.sort(key=lambda e: e['timestamp'])
+ start = media_list[0]['timestamp']
+ slices = {}
+ max_idx = 0
+ for m in media_list:
+ offs = m['timestamp'] - start
+ pos = float(offs) / interval - 0.5
+ idx = int(round(pos))
+ max_idx = idx
+ m['delta'] = abs(pos - idx)
+ slices.setdefault(idx, []).append(m)
+
+ selected = []
+ for i in xrange(max_idx + 1):
+ slice = slices.get(i)
+ if not slice:
+ continue
+
+ selected.append(min(slice, key=lambda m: m['delta']))
+
+ return selected
+
+ def make_movie(pictures):
+ cmd = 'rm -f %(tmp_filename)s;'
+ cmd += 'cat %(jpegs)s | ffmpeg -framerate %(speed)s/%(interval)s -f image2pipe -vcodec mjpeg -i - -vcodec mpeg4 -b %(bitrate)s -f avi %(tmp_filename)s'
+
+ bitrate = 9999990
+
+ cmd = cmd % {
+ 'tmp_filename': tmp_filename,
+ 'jpegs': ' '.join((p['path'] for p in pictures)),
+ 'interval': interval,
+ 'speed': speed,
+ 'bitrate': bitrate
+ }
+ process[0] = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)
+ started[0] = datetime.datetime.now()
+
+ poll_movie_process()
+
+ def poll_movie_process():
+ ioloop = tornado.ioloop.IOLoop.instance()
+ if process[0].poll() is None: # not finished yet
+ now = datetime.datetime.now()
+ delta = now - started[0]
+ if delta.seconds < 120:
+ ioloop.add_timeout(datetime.timedelta(seconds=0.5), poll_movie_process)
+
+ else: # process did not finish within 2 minutes
+ logging.error('timeout waiting for the timelapse movie process to finish')
+
+ callback(None)
+
+ else: # finished
+ logging.debug('reading timelapse movie file "%s" into memory' % tmp_filename)
+
+ try:
+ with open(tmp_filename, mode='r') as f:
+ data = f.read()
+
+ logging.debug('timelapse movie process has returned %d bytes' % len(data))
+
+ except Exception as e:
+ logging.error('failed to read timelapse movie file "%s": %s' % (tmp_filename, e))
+ return callback(None)
+
+ finally:
+ os.remove(tmp_filename)
+
+ callback(data)
+
+ poll_media_list_process()
def get_media_preview(camera_config, path, media_type, width, height):
def get_prepared_cache(key):
- return _prepared_files.get(key)
+ return _prepared_files.pop(key, None)
def set_prepared_cache(data):
http_client.fetch(request, on_response)
-def get_zipped_content(local_config, callback, group, media_type):
+def get_zipped_content(local_config, media_type, callback, group):
host = local_config.get('@host', local_config.get('host'))
port = local_config.get('@port', local_config.get('port'))
username = local_config.get('@username', local_config.get('username'))
http_client.fetch(request, on_prepare)
-def get_timelapse_movie(local_config, interval, callback, group):
+def get_timelapse_movie(local_config, speed, interval, callback, group):
host = local_config.get('@host', local_config.get('host'))
port = local_config.get('@port', local_config.get('port'))
username = local_config.get('@username', local_config.get('username'))
uri = local_config.get('@uri', local_config.get('uri')) or ''
camera_id = local_config.get('@remote_camera_id', local_config.get('remote_camera_id'))
- logging.debug('downloading timelapse movie for group %(group)s of remote camera %(id)s with interval %(int)s on %(url)s' % {
+ logging.debug('downloading timelapse movie for group %(group)s of remote camera %(id)s with rate %(speed)s/%(int)s on %(url)s' % {
'group': group,
'id': camera_id,
+ 'speed': speed,
'int': interval,
'url': make_camera_url(local_config)})
- prepare_uri = uri + '/picture/%(id)s/timelapse/%(group)s/?interval=%(int)s' % {
+ prepare_uri = uri + '/picture/%(id)s/timelapse/%(group)s/?interval=%(int)s&speed=%(speed)s' % {
'id': camera_id,
'int': interval,
+ 'speed': speed,
'group': group}
# timeout here is 100 times larger than usual - we expect a big delay
def on_prepare(response):
if response.error:
- logging.error('failed to download timelapse movie for group %(group)s of remote camera %(id)s with interval %(int)s on %(url)s: %(msg)s' % {
+ logging.error('failed to download timelapse movie for group %(group)s of remote camera %(id)s with rate %(speed)s/%(int)s on %(url)s: %(msg)s' % {
'group': group,
'id': camera_id,
'url': make_camera_url(local_config),
'int': interval,
+ 'speed': speed,
'msg': unicode(response.error)})
return callback(error=unicode(response.error))
download_uri = uri + '/picture/%(id)s/timelapse/%(group)s/?key=%(key)s' % {
'id': camera_id,
'group': group,
- 'int': interval,
'key': key}
request = _make_request(host, port, username, password, download_uri, timeout=100 * settings.REMOTE_REQUEST_TIMEOUT)
def on_download(response):
if response.error:
- logging.error('failed to download timelapse movie for group %(group)s of remote camera %(id)s with interval %(int)s on %(url)s: %(msg)s' % {
+ logging.error('failed to download timelapse movie for group %(group)s of remote camera %(id)s with rate %(speed)s/%(int)s on %(url)s: %(msg)s' % {
'group': group,
'id': camera_id,
'url': make_camera_url(local_config),
'int': interval,
+ 'speed': speed,
'msg': unicode(response.error)})
return callback(error=unicode(response.error))
opacity: 0.7;
}
-table.timelapse-dialog select#intervalSelect {
+table.timelapse-dialog select {
width: 10em;
}
'<td class="dialog-item-value">' + groupKey + '</td>' +
'</tr>' +
'<tr>' +
- '<td class="dialog-item-label"><span class="dialog-item-label">Include a picture every</span></td>' +
+ '<td class="dialog-item-label"><span class="dialog-item-label">Include a picture taken every</span></td>' +
'<td class="dialog-item-value">' +
'<select class="styled timelapse" id="intervalSelect">' +
'<option value="1">second</option>' +
- '<option value="10">5 seconds</option>' +
+ '<option value="5">5 seconds</option>' +
'<option value="10">10 seconds</option>' +
'<option value="30">30 seconds</option>' +
'<option value="60">minute</option>' +
'<option value="3600">hour</option>' +
'</select>' +
'</td>' +
- '<td><span class="help-mark" title="select the interval of time between each two successive pictures included in the movie">?</span></td>' +
+ '<td><span class="help-mark" title="choose the interval of time between two selected pictures">?</span></td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td class="dialog-item-label"><span class="dialog-item-label">Timelapse speed factor</span></td>' +
+ '<td class="dialog-item-value">' +
+ '<select class="styled timelapse" id="speedSelect">' +
+ '<option value="1">1x</option>' +
+ '<option value="10">10x</option>' +
+ '<option value="100">100x</option>' +
+ '<option value="1000">1000x</option>' +
+ '<option value="10000">10000x</option>' +
+ '<option value="100000">100000x</option>' +
+ '</select>' +
+ '</td>' +
+ '<td><span class="help-mark" title="choose how fast you want the timelapse playback to be">?</span></td>' +
'</tr>' +
'</table>');
var intervalSelect = content.find('#intervalSelect');
+ var speedSelect = content.find('#speedSelect');
runModalDialog({
title: 'Create Timelapse Movie',
content: content,
onOk: function () {
showModalDialog('<div class="modal-progress"></div>', null, null, true);
- ajax('GET', '/picture/' + cameraId + '/timelapse/' + groupKey + '/', {interval: intervalSelect.val()}, function (data) {
+ ajax('GET', '/picture/' + cameraId + '/timelapse/' + groupKey + '/',
+ {interval: intervalSelect.val(), speed: speedSelect.val()}, function (data) {
+
hideModalDialog(); /* progress */
hideModalDialog(); /* timelapse dialog */
- downloadFile('/picture/' + cameraId + '/timelapse/' + groupKey + '/key=' + data.key);
+ downloadFile('/picture/' + cameraId + '/timelapse/' + groupKey + '/?key=' + data.key);
});
return false;
var dialogDiv = $('<div class="media-dialog"></div>');
var mediaListDiv = $('<div class="media-dialog-list"></div>');
var groupsDiv = $('<div class="media-dialog-groups"></div>');
+ var buttonsDiv = $('<div class="media-dialog-buttons"></div>');
var groups = {};
var groupKey = null;
dialogDiv.append(mediaListDiv);
if (mediaType == 'picture') {
- var buttonsDiv = $('<div class="media-dialog-buttons"></div>');
dialogDiv.append(buttonsDiv);
var zippedButton = $('<div class="media-dialog-button">Zipped Pictures</div>');
});
}
- function setDialogSize() {
+ function updateDialogSize() {
var windowWidth = $(window).width();
var windowHeight = $(window).height();
+ if (Object.keys(groups).length == 0) {
+ groupsDiv.width('auto');
+ groupsDiv.height('auto');
+ groupsDiv.addClass('small-screen');
+ mediaListDiv.width('auto');
+ mediaListDiv.height('auto');
+ buttonsDiv.hide();
+
+ return;
+ }
+
+ buttonsDiv.show();
+
if (windowWidth < 1000) {
groupsDiv.width(parseInt(windowWidth * 0.8));
groupsDiv.height('');
}
function onResize() {
- setDialogSize();
+ updateDialogSize();
updateModalDialogPosition();
}
$(window).resize(onResize);
- setDialogSize();
+ updateDialogSize();
showModalDialog('<div class="modal-progress"></div>');
});
});
+ updateDialogSize();
+
var keys = Object.keys(groups);
keys.sort();
keys.reverse();
var momentSpan = $('<span class="details-moment">' + entry.momentStr + ', </span>');
var momentShortSpan = $('<span class="details-moment-short">' + entry.momentStrShort + '</span>');
var sizeSpan = $('<span class="details-size">' + entry.sizeStr + '</span>');
+ detailsDiv.empty();
detailsDiv.append(momentSpan);
detailsDiv.append(momentShortSpan);
detailsDiv.append(sizeSpan);