* python 2.6+
* tornado
* jinja2
+ * PIL
* motion
* ffmpeg
* v4l2-utils
On a debian-based system you could run:
- apt-get install python-tornado python-jinja2 motion v4l2-utils
+ apt-get install python-tornado python-jinja2 python-imaging motion v4l2-utils
## Browser Compatibility ##
+-> test IE
-> layout seems to be too wide for a modern mobile phone
+-> add a mgmt command for generating thumbnails
+-> implement media files paging - find a solution for the many files
-> make camera frames positions configurable
-> add a view log functionality
'@enabled': ui.get('enabled', False),
'@proto': ui.get('proto', 'v4l2'),
'videodevice': ui.get('device', ''),
- 'lightswitch': int(ui.get('light_switch_detect', False)) * 5,
+ 'lightswitch': int(ui.get('light_switch_detect', True)) * 5,
'auto_brightness': ui.get('auto_brightness', False),
'width': int(ui['resolution'].split('x')[0]),
'height': int(ui['resolution'].split('x')[1]),
data.setdefault('@enabled', False)
data.setdefault('@proto', 'v4l2')
data.setdefault('videodevice', '/dev/video0')
- data.setdefault('lightswitch', 0)
+ data.setdefault('lightswitch', 5)
data.setdefault('auto_brightness', False)
data.setdefault('brightness', 0)
data.setdefault('contrast', 0)
camera_config.get('@username'),
camera_config.get('@password'),
camera_config.get('@remote_camera_id'), on_response,
- media_type='picture')
+ media_type='picture',
+ prefix=self.get_argument('prefix', None),
+ stat=self.get_argument('stat', None))
else:
- pictures = mediafiles.list_media(camera_config, media_type='picture')
+ pictures = mediafiles.list_media(camera_config, media_type='picture',
+ prefix=self.get_argument('prefix', None),
+ stat=self.get_argument('stat', None))
self.finish_json({
'mediaList': pictures,
camera_config.get('@username'),
camera_config.get('@password'),
camera_config.get('@remote_camera_id'), on_response,
- media_type='movie')
+ media_type='movie',
+ prefix=self.get_argument('prefix', None),
+ stat=self.get_argument('stat', None))
else:
- movies = mediafiles.list_media(camera_config, media_type='movie')
+ movies = mediafiles.list_media(camera_config, media_type='movie',
+ prefix=self.get_argument('prefix', None),
+ stat=self.get_argument('stat', None))
self.finish_json({
'mediaList': movies,
from PIL import Image
import config
+import utils
_PICTURE_EXTS = ['.jpg']
_MOVIE_EXTS = ['.avi', '.mp4']
-def _list_media_files(dir, exts):
+def _list_media_files(dir, exts, prefix=None):
full_paths = []
- for root, dirs, files in os.walk(dir): # @UnusedVariable
- for name in files:
+
+ if prefix is not None:
+ if prefix == 'ungrouped':
+ prefix = ''
+
+ root = os.path.join(dir, prefix)
+ for name in os.listdir(root):
full_path = os.path.join(root, name)
if not os.path.isfile(full_path):
continue
full_paths.append(full_path)
+ else:
+ for root, dirs, files in os.walk(dir): # @UnusedVariable
+ for name in files:
+ full_path = os.path.join(root, name)
+ if not os.path.isfile(full_path):
+ continue
+
+ full_path_lower = full_path.lower()
+ if not [e for e in exts if full_path_lower.endswith(e)]:
+ continue
+
+ full_paths.append(full_path)
+
return full_paths
logging.debug('all movies have preview')
-def list_media(camera_config, media_type):
+def list_media(camera_config, media_type, prefix=None, stat=False):
target_dir = camera_config.get('target_dir')
if media_type == 'picture':
elif media_type == 'movie':
exts = _MOVIE_EXTS
- full_paths = _list_media_files(target_dir, exts=exts)
+ full_paths = _list_media_files(target_dir, exts=exts, prefix=prefix)
media_files = []
for p in full_paths:
path = p[len(target_dir):]
if not path.startswith('/'):
path = '/' + path
-
-# try:
-# stat = os.stat(p)
-#
-# except Exception as e:
-# logging.error('stat call failed for file %(path)s: %(msg)s' % {
-# 'path': path, 'msg': unicode(e)})
-#
-# continue
-#
-# timestamp = stat.st_mtime
-# size = stat.st_size
+
+ timestamp = None
+ size = None
+
+ if stat:
+ try:
+ stat = os.stat(p)
+
+ except Exception as e:
+ logging.error('stat call failed for file %(path)s: %(msg)s' % {
+ 'path': path, 'msg': unicode(e)})
+
+ continue
+
+ timestamp = stat.st_mtime
+ size = stat.st_size
media_files.append({
'path': path,
- #'momentStr': utils.pretty_date_time(datetime.datetime.fromtimestamp(timestamp)),
- #'sizeStr': utils.pretty_size(size),
- #'timestamp': timestamp
+ 'momentStr': timestamp and utils.pretty_date_time(datetime.datetime.fromtimestamp(timestamp)),
+ 'sizeStr': size and utils.pretty_size(size),
+ 'timestamp': timestamp
})
# TODO files listed here may not belong to the given camera
http_client.fetch(request, on_response)
-def list_media(host, port, username, password, camera_id, callback, media_type):
+def list_media(host, port, username, password, camera_id, callback, media_type, prefix=None, stat=False):
logging.debug('getting media list for remote camera %(id)s on %(host)s:%(port)s' % {
'id': camera_id,
'host': host,
'port': port})
+ query = {}
+ if prefix is not None:
+ query['prefix'] = prefix
+
+ if stat:
+ query['stat'] = 'true'
+
request = _make_request(host, port, username, password, '/%(media_type)s/%(id)s/list/' % {
- 'id': camera_id, 'media_type': media_type})
+ 'id': camera_id, 'media_type': media_type}, query=query)
def on_response(response):
if response.error:
'host': host,
'port': port})
- uri = '/%(media_type)s/%(id)s/download/%(filename)s?' % {
+ uri = '/%(media_type)s/%(id)s/download/%(filename)s' % {
'media_type': media_type,
'id': camera_id,
'filename': filename}
'host': host,
'port': port})
- uri = '/%(media_type)s/%(id)s/preview/%(filename)s?' % {
+ uri = '/%(media_type)s/%(id)s/preview/%(filename)s' % {
'media_type': media_type,
'id': camera_id,
'filename': filename}
+ query = {}
+
if width:
- uri += 'width=' + str(width)
+ query['width'] = str(width)
if height:
- uri += 'height=' + str(height)
+ query['height'] = str(height)
- request = _make_request(host, port, username, password, uri)
+ request = _make_request(host, port, username, password, uri, query=query)
def on_response(response):
if response.error:
padding: 1em 0px 0.2em 0px;
}
+img.media-list-progress {
+ position: relative;
+ top: 35%;
+ display: block;
+ margin: auto;
+}
+
div.media-list-entry {
height: 4em;
background-color: #414141;
'path': path,
'group': key,
'name': parts[parts.length - 1],
- 'cameraId': cameraId,
- 'momentStr': media.momentStr,
- 'sizeStr': media.sizeStr,
- 'timestamp': media.timestamp
+ 'cameraId': cameraId
});
});
var nameDiv = $('<div class="media-list-entry-name">' + entry.name + '</div>');
entryDiv.append(nameDiv);
- if (entry.momentStr && entry.sizeStr) {
- var detailsDiv = $('<div class="media-list-entry-details"></div>');
- detailsDiv.html(entry.momentStr + ' | ' + entry.sizeStr);
- entryDiv.append(detailsDiv);
- }
- else {
- nameDiv.css('line-height', '2.3em');
- }
-
downloadButton[0]._onClick = function () {
window.location.href = '/picture/' + cameraId + '/download' + entry.path;
});
function showGroup(key) {
- groupsDiv.find('div.media-dialog-group-button').each(function () {
- var $this = $(this);
- if (this.key == key) {
- $this.addClass('current');
- }
- else {
- $this.removeClass('current');
- }
- });
+ if (mediaListDiv.find('img.media-list-progress').length) {
+ return; /* already in progress of loading */
+ }
- mediaListDiv.html('');
-
- var entries = groups[key];
- entries.forEach(function (entry) {
- mediaListDiv.append(entry.div);
- entry.div.click(entry.div[0]._onClick);
-
- var downloadButton = entry.div.find('div.media-list-download-button');
- downloadButton.click(downloadButton[0]._onClick);
- });
+ var previewImg = $('<img class="media-list-progress" src="' + staticUrl + 'img/modal-progress.gif"/>');
+ mediaListDiv.append(previewImg);
- setTimeout(function () {
- mediaListDiv.find('img.media-list-preview').each(function () {
- if (this._src) {
- this.src = this._src;
+ var url = '/' + mediaType + '/' + cameraId + '/list/?prefix=' + (key || 'ungrouped')+ '&stat=true';
+ ajax('GET', url, null, function (data) {
+ if (data == null || data.error) {
+ hideModalDialog();
+ showErrorMessage(data && data.error);
+ return;
+ }
+
+ var entries = groups[key];
+
+ /* index the media list by name */
+ var mediaListByName = {};
+ data.mediaList.forEach(function (media) {
+ var path = media.path;
+ var parts = path.split('/');
+ var name = parts[parts.length - 1];
+
+ mediaListByName[name] = media;
+ });
+
+ /* (re)set the current state of the group buttons */
+ groupsDiv.find('div.media-dialog-group-button').each(function () {
+ var $this = $(this);
+ if (this.key == key) {
+ $this.addClass('current');
+ }
+ else {
+ $this.removeClass('current');
}
+ });
+
+ /* add the entries to the media list */
+ mediaListDiv.children('div.media-list-entry').detach();
+ mediaListDiv.html('');
+ entries.forEach(function (entry) {
+ var entryDiv = entry.div;
+ var nameDiv = entryDiv.find('div.media-list-entry-name');
+ var detailsDiv = entryDiv.find('div.media-list-entry-details');
+ var downloadButton = entryDiv.find('div.media-list-download-button');
- delete this._src;
+ var media = mediaListByName[entry.name];
+ if (media) { /* if details are available, show them */
+ if (detailsDiv.length === 0) {
+ detailsDiv = $('<div class="media-list-entry-details"></div>');
+ entryDiv.append(detailsDiv);
+ }
+
+ detailsDiv.html(media.momentStr + ' | ' + media.sizeStr);
+ }
+ else {
+ nameDiv.css('line-height', '2.3em');
+ }
+
+ entryDiv.click(entryDiv[0]._onClick);
+ downloadButton.click(downloadButton[0]._onClick);
+
+ mediaListDiv.append(entryDiv);
});
- }, 1000);
+
+ setTimeout(function () {
+ mediaListDiv.find('img.media-list-preview').each(function () {
+ if (this._src) {
+ this.src = this._src;
+ }
+
+ delete this._src;
+ });
+ }, 1000);
+ });
}
if (keys.length) {
});
children.css('display', 'none');
+ updateModalDialogPosition();
container[0]._onClose = onClose; /* set the new onClose handler */
container.append(content);