Initial Recordings UI

This commit is contained in:
Jason Hunter 2021-05-28 13:13:48 -04:00 committed by Blake Blackshear
parent abbc608ee4
commit 5461308d30
14 changed files with 732 additions and 14 deletions

View File

@ -1,9 +1,11 @@
import base64
import datetime
from collections import OrderedDict
from datetime import datetime, timedelta
import json
import glob
import logging
import os
import re
import time
from functools import reduce
from pathlib import Path
@ -449,6 +451,57 @@ def latest_frame(camera_name):
return "Camera named {} not found".format(camera_name), 404
@bp.route("/<camera_name>/recordings")
def recordings(camera_name):
files = glob.glob(f"{RECORD_DIR}/*/*/*/{camera_name}")
if len(files) == 0:
return "No recordings found.", 404
files.sort()
dates = OrderedDict()
for path in files:
search = re.search(r".+/(\d{4}[-]\d{2})/(\d{2})/(\d{2}).+", path)
if not search:
continue
date = f"{search.group(1)}-{search.group(2)}"
if date not in dates:
dates[date] = OrderedDict()
dates[date][search.group(3)] = 0
events = (
Event.select(
fn.DATE(Event.start_time, "unixepoch", "localtime"),
fn.STRFTIME("%H", Event.start_time, "unixepoch", "localtime"),
fn.COUNT(Event.id),
)
.where(Event.camera == camera_name)
.group_by(
fn.DATE(Event.start_time, "unixepoch", "localtime"),
fn.STRFTIME("%H", Event.start_time, "unixepoch", "localtime"),
)
.tuples()
)
for date, hour, count in events:
key = date.strftime("%Y-%m-%d")
if key in dates and hour in dates[key]:
dates[key][hour] = count
return jsonify(
[
{
"date": date,
"recordings": [
{"hour": hour, "events": events} for hour, events in hours.items()
],
}
for date, hours in dates.items()
]
)
@bp.route("/vod/<path:path>")
def vod(path):
if not os.path.isdir(f"{RECORD_DIR}/{path}"):
@ -467,8 +520,13 @@ def vod(path):
)
durations.append(duration)
# Should we cache?
parts = path.split("/", 4)
date = datetime.strptime(f"{parts[0]}-{parts[1]} {parts[2]}", "%Y-%m-%d %H")
return jsonify(
{
"cache": datetime.now() - timedelta(hours=2) > date,
"discontinuity": False,
"durations": durations,
"sequences": [{"clips": clips}],

View File

@ -39,12 +39,9 @@ http {
vod_mode mapped;
vod_max_mapping_response_size 1m;
vod_upstream_location /api;
vod_last_modified 'Sun, 19 Nov 2000 08:52:00 GMT';
vod_last_modified_types *;
# vod caches
vod_metadata_cache metadata_cache 512m;
vod_response_cache response_cache 128m;
vod_mapping_cache mapping_cache 5m;
# gzip manifests
@ -65,7 +62,7 @@ http {
add_header Access-Control-Expose-Headers 'Server,range,Content-Length,Content-Range';
add_header Access-Control-Allow-Methods 'GET, HEAD, OPTIONS';
add_header Access-Control-Allow-Origin '*';
expires 100d;
expires -1;
}
location /stream/ {

388
web/package-lock.json generated
View File

@ -2873,7 +2873,6 @@
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.13.tgz",
"integrity": "sha512-8+3UMPBrjFa/6TtKi/7sehPKqfAm4g6K+YQjyyFOLUTxzOngcRZTlAVY8sc2CORJYqdHQY8gRPHmn+qo15rCBw==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.4"
}
@ -4118,6 +4117,74 @@
"integrity": "sha512-GmVAWB+JuFKqSbzlofYK4qxk955gEv4Kd9/aj2hLOxneXMAm/J7OXcl5DlElS9tmkqwCcxGysSZGOrjzNvmjFQ==",
"dev": true
},
"@videojs/http-streaming": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.6.4.tgz",
"integrity": "sha512-sFVE0MVXhawAkET8EgiUSMvDDv6u3uGidtO0BvNXG0/qKWlze/zEzhvLsyPU4HmLFRnffKeHK5RE2XpO5vHY8Q==",
"requires": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.0",
"aes-decrypter": "3.1.2",
"global": "^4.4.0",
"m3u8-parser": "4.5.2",
"mpd-parser": "0.15.4",
"mux.js": "5.10.0",
"video.js": "^6 || ^7"
},
"dependencies": {
"global": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
"requires": {
"min-document": "^2.19.0",
"process": "^0.11.10"
}
}
}
},
"@videojs/vhs-utils": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.2.tgz",
"integrity": "sha512-r8Yas1/tNGsGRNoIaDJuiWiQgM0P2yaEnobgzw5JcBiEqxnS8EXoUm4QtKH7nJtnppZ1yqBx1agBZCvBMKXA2w==",
"requires": {
"@babel/runtime": "^7.12.5",
"global": "^4.4.0",
"url-toolkit": "^2.2.1"
},
"dependencies": {
"global": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
"requires": {
"min-document": "^2.19.0",
"process": "^0.11.10"
}
}
}
},
"@videojs/xhr": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.5.1.tgz",
"integrity": "sha512-wV9nGESHseSK+S9ePEru2+OJZ1jq/ZbbzniGQ4weAmTIepuBMSYPx5zrxxQA0E786T5ykpO8ts+LayV+3/oI2w==",
"requires": {
"@babel/runtime": "^7.5.5",
"global": "~4.4.0",
"is-function": "^1.0.1"
},
"dependencies": {
"global": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
"requires": {
"min-document": "^2.19.0",
"process": "^0.11.10"
}
}
}
},
"abab": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz",
@ -4163,6 +4230,28 @@
"integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
"dev": true
},
"aes-decrypter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.2.tgz",
"integrity": "sha512-42nRwfQuPRj9R1zqZBdoxnaAmnIFyDi0MNyTVhjdFOd8fifXKKRfwIHIZ6AMn1or4x5WONzjwRTbTWcsIQ0O4A==",
"requires": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.0",
"global": "^4.4.0",
"pkcs7": "^1.0.4"
},
"dependencies": {
"global": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
"requires": {
"min-document": "^2.19.0",
"process": "^0.11.10"
}
}
}
},
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@ -4763,6 +4852,14 @@
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
"dev": true
},
"chainsaw": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.0.9.tgz",
"integrity": "sha1-EaBRAtHEx4W20EFdM21aOhYSkT4=",
"requires": {
"traverse": ">=0.3.0 <0.4"
}
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@ -5103,6 +5200,11 @@
"whatwg-url": "^8.0.0"
}
},
"date-fns": {
"version": "2.21.3",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.21.3.tgz",
"integrity": "sha512-HeYdzCaFflc1i4tGbj7JKMjM4cKGYoyxwcIIkHzNgCkX8xXDNJDZXgDDVchIWpN4eQc3lH37WarduXFZJOtxfw=="
},
"debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
@ -5218,6 +5320,11 @@
"integrity": "sha512-9YLIBURXj4DJMFALxXw9K3Y3rwb5Fk0X5/8ipCzaN84+gKxoHK43tVKRNakCQbiEx07E8Uwhuq21BpUagFhZ8w==",
"dev": true
},
"desandro-matches-selector": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/desandro-matches-selector/-/desandro-matches-selector-2.0.2.tgz",
"integrity": "sha1-cXvu1NwT59jzdi9wem1YpndCGOE="
},
"detect-newline": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@ -5271,6 +5378,11 @@
"integrity": "sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ==",
"dev": true
},
"dom-walk": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
},
"domconstants": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/domconstants/-/domconstants-0.1.2.tgz",
@ -5964,12 +6076,22 @@
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"dev": true
},
"estree-walker": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
"integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="
},
"esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true
},
"ev-emitter": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ev-emitter/-/ev-emitter-1.1.1.tgz",
"integrity": "sha512-ipiDYhdQSCZ4hSbX4rMW+XzNKMD1prg/sTvoVmSLkuQ1MVlwjJQQA+sW8tMYR3BLUr9KjodFV4pvzunvRhd33Q=="
},
"exec-sh": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz",
@ -6259,6 +6381,14 @@
"path-exists": "^4.0.0"
}
},
"fizzy-ui-utils": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/fizzy-ui-utils/-/fizzy-ui-utils-2.0.7.tgz",
"integrity": "sha512-CZXDVXQ1If3/r8s0T+v+qVeMshhfcuq0rqIFgJnrtd+Bu8GmDmqMjntjUePypVtjHXKJ6V4sw9zeyox34n9aCg==",
"requires": {
"desandro-matches-selector": "^2.0.0"
}
},
"flat-cache": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
@ -6275,6 +6405,19 @@
"integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==",
"dev": true
},
"flickity": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/flickity/-/flickity-2.2.2.tgz",
"integrity": "sha512-yiPMuP8tw/zN7ARgeSLZNvzK11GkzI2mp/zlYBsyttguSCROAqxj6wiN2sSfPfW3xMG3hcUHxWUXNQMlk/wYcg==",
"requires": {
"desandro-matches-selector": "^2.0.0",
"ev-emitter": "^1.1.1",
"fizzy-ui-utils": "^2.0.7",
"get-size": "^2.0.3",
"unidragger": "^2.3.0",
"unipointer": "^2.3.0"
}
},
"for-in": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@ -6385,6 +6528,11 @@
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
"dev": true
},
"get-size": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/get-size/-/get-size-2.0.3.tgz",
"integrity": "sha512-lXNzT/h/dTjTxRbm9BXb+SGxxzkm97h/PCIKtlN/CBCxxmkkIVV21udumMS93MuVTDX583gqc94v3RjuHmI+2Q=="
},
"get-stdin": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz",
@ -6435,6 +6583,22 @@
"is-glob": "^4.0.1"
}
},
"global": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/global/-/global-4.3.2.tgz",
"integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=",
"requires": {
"min-document": "^2.19.0",
"process": "~0.5.1"
},
"dependencies": {
"process": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz",
"integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8="
}
}
},
"globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@ -6537,6 +6701,14 @@
}
}
},
"hashish": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/hashish/-/hashish-0.0.4.tgz",
"integrity": "sha1-bWC8b/r3Ebav1g5CbQd5iAFOZVQ=",
"requires": {
"traverse": ">=0.2.4"
}
},
"himalaya": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/himalaya/-/himalaya-1.1.0.tgz",
@ -6693,6 +6865,11 @@
"integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=",
"dev": true
},
"individual": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/individual/-/individual-2.0.0.tgz",
"integrity": "sha1-gzsJfa0jKU52EXqY+zjg2a1hu5c="
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@ -6876,6 +7053,11 @@
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true
},
"is-function": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz",
"integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ=="
},
"is-generator-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz",
@ -8596,6 +8778,11 @@
"object.assign": "^4.1.2"
}
},
"keycode": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.0.tgz",
"integrity": "sha1-PQr1bce4uOXLqNCpfxByBO7CKwQ="
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@ -8757,11 +8944,31 @@
"integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=",
"dev": true
},
"m3u8-parser": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.5.2.tgz",
"integrity": "sha512-sN/lu3TiRxmG2RFjZxo5c0/7Dr4RrEztl43jXrWwj5gFZ7vfa2iIxGfiPx485dm5QCazaIcKk+vNkUso8Aq0Ag==",
"requires": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.0",
"global": "^4.4.0"
},
"dependencies": {
"global": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
"requires": {
"min-document": "^2.19.0",
"process": "^0.11.10"
}
}
}
},
"magic-string": {
"version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
"integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
"dev": true,
"requires": {
"sourcemap-codec": "^1.4.4"
}
@ -8856,6 +9063,14 @@
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true
},
"min-document": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
"integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=",
"requires": {
"dom-walk": "^0.1.0"
}
},
"min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@ -8916,6 +9131,28 @@
"integrity": "sha512-Xm9jdWvqFrlV0k965eY5AlCpWIIUBY2ExzGbEG+byMs+mZI4J7zvaUOLpQ8MTFgkpgyEnu4qUhuZT/Or3QeRiA==",
"dev": true
},
"mpd-parser": {
"version": "0.15.4",
"resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.15.4.tgz",
"integrity": "sha512-YcOclxKc5gnT87UQYwRoPJpWOFvQORwN+bXYmTWCJ4U2pCSS7jjtPrIhoOLHFAyekj48CHTX4hjGBV/VSNsUsg==",
"requires": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.0",
"global": "^4.4.0",
"xmldom": "^0.4.0"
},
"dependencies": {
"global": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
"requires": {
"min-document": "^2.19.0",
"process": "^0.11.10"
}
}
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@ -8935,6 +9172,14 @@
"minimatch": "^3.0.4"
}
},
"mux.js": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-5.10.0.tgz",
"integrity": "sha512-kLzvYsHYBwNa+ckkmpxWV3eImwntJbrwd1KbN4WR0hLe+dK/KB82aCuC0fQzAI2hkjYszdlSGsAWFgYdiFBUuA==",
"requires": {
"@babel/runtime": "^7.11.2"
}
},
"nanoid": {
"version": "3.1.20",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz",
@ -9381,6 +9626,14 @@
"node-modules-regexp": "^1.0.0"
}
},
"pkcs7": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz",
"integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==",
"requires": {
"@babel/runtime": "^7.5.5"
}
},
"pkg-dir": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
@ -9743,6 +9996,11 @@
"integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=",
"dev": true
},
"process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI="
},
"progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@ -9971,8 +10229,7 @@
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
"dev": true
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
},
"regenerator-transform": {
"version": "0.14.5",
@ -10046,6 +10303,14 @@
}
}
},
"remove": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/remove/-/remove-0.1.5.tgz",
"integrity": "sha1-CV/9gn1lyfQa2X0z5BanWBEHmVU=",
"requires": {
"seq": ">= 0.3.5"
}
},
"remove-trailing-separator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
@ -10230,6 +10495,23 @@
}
}
},
"rollup-plugin-replace": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz",
"integrity": "sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==",
"requires": {
"magic-string": "^0.25.2",
"rollup-pluginutils": "^2.6.0"
}
},
"rollup-pluginutils": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
"integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
"requires": {
"estree-walker": "^0.6.1"
}
},
"rsvp": {
"version": "4.8.5",
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
@ -10242,12 +10524,28 @@
"integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==",
"dev": true
},
"rust-result": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/rust-result/-/rust-result-1.0.0.tgz",
"integrity": "sha1-NMdbLm3Dn+WHXlveyFteD5FTb3I=",
"requires": {
"individual": "^2.0.0"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
},
"safe-json-parse": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-4.0.0.tgz",
"integrity": "sha1-fA9XjPzNEtM6ccDgVBPi7KFx6qw=",
"requires": {
"rust-result": "^1.0.0"
}
},
"safe-regex": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
@ -10490,6 +10788,15 @@
"lru-cache": "^6.0.0"
}
},
"seq": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/seq/-/seq-0.3.5.tgz",
"integrity": "sha1-rgKvOkJHk9jMvyEtaRdODFTf/jg=",
"requires": {
"chainsaw": ">=0.0.7 <0.1",
"hashish": ">=0.0.2 <0.1"
}
},
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@ -10809,8 +11116,7 @@
"sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
},
"spdx-correct": {
"version": "3.1.1",
@ -11291,6 +11597,11 @@
"punycode": "^2.1.1"
}
},
"traverse": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
"integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk="
},
"ts-morph": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-9.1.0.tgz",
@ -11478,6 +11789,14 @@
"integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==",
"dev": true
},
"unidragger": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/unidragger/-/unidragger-2.3.1.tgz",
"integrity": "sha512-u+IgG7AG0MXJTKcdzAIYxCm+W5FcnA9M28203Awl6jIcE3/+9OtEyUX4Wv64y7XNKEVRKPot52IV4V6x7FlF5Q==",
"requires": {
"unipointer": "^2.3.0"
}
},
"union-value": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@ -11490,6 +11809,14 @@
"set-value": "^2.0.1"
}
},
"unipointer": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/unipointer/-/unipointer-2.3.0.tgz",
"integrity": "sha512-m85sAoELCZhogI1owtJV3Dva7GxkHk2lI7A0otw3o0OwCuC/Q9gi7ehddigEYIAYbhkqNdri+dU1QQkrcBvirQ==",
"requires": {
"ev-emitter": "^1.0.1"
}
},
"uniq": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
@ -11557,6 +11884,11 @@
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
"dev": true
},
"url-toolkit": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.2.tgz",
"integrity": "sha512-l25w6Sy+Iy3/IbogunxhWwljPaDnqpiKvrQRoLBm6DfISco7NyRIS7Zf6+Oxhy1T8kHxWdwLND7ZZba6NjXMug=="
},
"use": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
@ -11631,6 +11963,45 @@
"extsprintf": "^1.2.0"
}
},
"video.js": {
"version": "7.11.8",
"resolved": "https://registry.npmjs.org/video.js/-/video.js-7.11.8.tgz",
"integrity": "sha512-iQmNYB+pdgu8b45Za1AKSa5J7uDyHIqfJy+picw4voKfjErXK/BEvs+A3f99Ck7SCZU4cmMmX/s17AwaaNs+1w==",
"requires": {
"@babel/runtime": "^7.9.2",
"@videojs/http-streaming": "2.6.4",
"@videojs/xhr": "2.5.1",
"global": "4.3.2",
"keycode": "^2.2.0",
"remove": "^0.1.5",
"rollup-plugin-replace": "^2.2.0",
"safe-json-parse": "4.0.0",
"videojs-font": "3.2.0",
"videojs-vtt.js": "^0.15.2"
}
},
"videojs-font": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-3.2.0.tgz",
"integrity": "sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA=="
},
"videojs-playlist": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/videojs-playlist/-/videojs-playlist-4.3.1.tgz",
"integrity": "sha512-fxI3T6mWHKaXRwTQyJeq5I0b8GM9Q4S/p92Aq7O1xAT+X8jYxYSIN15xi32a1F5adEGPRqct+yMl5MkXO9x9cQ==",
"requires": {
"global": "^4.3.2",
"video.js": "^6 || ^7"
}
},
"videojs-vtt.js": {
"version": "0.15.3",
"resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.3.tgz",
"integrity": "sha512-5FvVsICuMRx6Hd7H/Y9s9GDeEtYcXQWzGMS+sl4UX3t/zoHp3y+isSfIPRochnTH7h+Bh1ILyC639xy9Z6kPag==",
"requires": {
"global": "^4.3.1"
}
},
"w3c-hr-time": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
@ -11784,6 +12155,11 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true
},
"xmldom": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.4.0.tgz",
"integrity": "sha512-2E93k08T30Ugs+34HBSTQLVtpi6mCddaY8uO+pMNk1pqSjV5vElzn4mmh6KLxN3hki8rNcHSYzILoh3TEWORvA=="
},
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@ -11,11 +11,15 @@
"test": "jest"
},
"dependencies": {
"date-fns": "^2.21.3",
"flickity": "^2.2.2",
"idb-keyval": "^5.0.2",
"immer": "^8.0.1",
"preact": "^10.5.9",
"preact-async-route": "^2.2.1",
"preact-router": "^3.2.1"
"preact-router": "^3.2.1",
"video.js": "^7.11.8",
"videojs-playlist": "^4.3.1"
},
"devDependencies": {
"@babel/eslint-parser": "^7.12.13",

View File

@ -29,6 +29,7 @@ export default function App() {
<AsyncRoute path="/cameras/:camera" getComponent={Routes.getCamera} />
<AsyncRoute path="/events/:eventId" getComponent={Routes.getEvent} />
<AsyncRoute path="/events" getComponent={Routes.getEvents} />
<AsyncRoute path="/recordings/:camera/:date?/:hour?" getComponent={Routes.getRecording} />
<AsyncRoute path="/debug" getComponent={Routes.getDebug} />
<AsyncRoute path="/styleguide" getComponent={Routes.getStyleGuide} />
<Cameras default path="/" />

View File

@ -27,6 +27,19 @@ export default function Sidebar() {
) : null
}
</Match>
<Match path="/recordings/:camera/:date?/:hour?">
{({ matches }) =>
matches ? (
<Fragment>
<Separator />
{cameras.map((camera) => (
<Destination href={`/recordings/${camera}`} text={camera} />
))}
<Separator />
</Fragment>
) : null
}
</Match>
<Destination href="/events" text="Events" />
<Destination href="/debug" text="Debug" />
<Separator />

View File

@ -110,6 +110,11 @@ export function useEvent(eventId, fetchId) {
return useFetch(url, fetchId);
}
export function useRecording(camera, fetchId) {
const url = `/api/${camera}/recordings`;
return useFetch(url, fetchId);
}
export function useConfig(searchParams, fetchId) {
const url = `/api/config${searchParams ? `?${searchParams.toString()}` : ''}`;
return useFetch(url, fetchId);

View File

@ -66,7 +66,7 @@ export default function Button({
let classes = `whitespace-nowrap flex items-center space-x-1 ${className} ${ButtonTypes[type]} ${
ButtonColors[disabled ? 'disabled' : color][type]
} font-sans inline-flex font-bold uppercase text-xs px-2 py-2 rounded outline-none focus:outline-none ring-opacity-50 transition-shadow transition-colors ${
} font-sans inline-flex font-bold uppercase text-xs px-1.5 md:px-2 py-2 rounded outline-none focus:outline-none ring-opacity-50 transition-shadow transition-colors ${
disabled ? 'cursor-not-allowed' : 'focus:ring-2 cursor-pointer'
}`;

View File

@ -0,0 +1,26 @@
import { h } from 'preact';
import { format } from 'date-fns';
export default function Calendar({ date, hours = 0, events = 0, selected = false }) {
const bg = selected ? 'bg-blue-500 bg-opacity-80' : 'bg-gray-500';
return (
<div className="min-w-20 min-h-20 md:min-w-32 md:min-h-32 p-1.5 mb-1 font-medium text-xs md:text-base">
<div className="w-20 md:w-32 flex-none rounded-lg text-center shadow-md">
<div className="block rounded-lg overflow-hidden text-center text-black">
<div className={`${bg} text-white py-0.5`}>{format(date, 'MMM yyyy')}</div>
<div className="pt-0.5 bg-white">
<span className="text-2xl md:text-5xl font-bold leading-tight">{format(date, 'd')}</span>
</div>
<div className="text-center bg-white pt-0.5">
<span className="md:text-sm">{format(date, 'EEEE')}</span>
</div>
<div className="pb-0.5 border-l border-r border-b border-white text-center bg-white hidden md:block">
<span className="md:text-xs leading-normal">
{hours} hrs, {events} events
</span>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,65 @@
import { h, Component } from 'preact';
import Flickity from 'flickity';
import 'flickity/css/flickity.css';
export default class Carousel extends Component {
constructor(props) {
super(props);
this.carousel = null;
this.flkty = null;
}
create() {
if (this.carousel) {
this.flkty = new Flickity(this.carousel, this.props.options);
if (this.props.flickityRef) {
this.props.flickityRef(this.flkty);
}
}
}
destroy() {
if (this.flkty) {
this.flkty.destroy();
this.flkty = null;
this.carousel = null;
}
}
componentWillUpdate() {
this.destroy();
}
componentDidUpdate() {
this.create();
}
componentWillUnmount() {
this.destroy();
}
componentDidMount() {
this.create();
}
render(props) {
return h(
this.props.elementType,
{
className: this.props.className,
ref: (c) => {
this.carousel = c;
},
},
this.props.children
);
}
}
Carousel.defaultProps = {
options: {},
className: '',
elementType: 'div',
};

View File

@ -0,0 +1,51 @@
import { h, Component } from 'preact';
import videojs from 'video.js';
import 'videojs-playlist';
import 'video.js/dist/video-js.css';
const defaultOptions = {
controls: true,
fluid: true,
};
export default class VideoPlayer extends Component {
componentDidMount() {
const { options, onReady = () => {} } = this.props;
const videoJsOptions = {
...defaultOptions,
...options,
};
const self = this;
this.player = videojs(this.videoNode, videoJsOptions, function onPlayerReady() {
onReady(this);
this.on('error', () => {
console.error('VIDEOJS: ERROR: currentSources:', this.currentSources());
});
this.on('play', () => {
console.log('VIDEOJS: currentSources:', this.currentSources());
});
});
}
componentWillUnmount() {
if (this.player) {
this.player.dispose();
}
}
shouldComponentUpdate() {
return false;
}
render() {
const { style } = this.props;
return (
<div style={style}>
<div data-vjs-player>
<video playsinline ref={(node) => (this.videoNode = node)} className="video-js" />
<div className="vjs-playlist" />
</div>
</div>
);
}
}

View File

@ -28,7 +28,10 @@ function Camera({ name }) {
const { payload: clipValue, send: sendClips } = useClipsState(name);
const { payload: snapshotValue, send: sendSnapshots } = useSnapshotsState(name);
const href = `/cameras/${name}`;
const buttons = useMemo(() => [{ name: 'Events', href: `/events?camera=${name}` }], [name]);
const buttons = useMemo(() => [
{ name: 'Events', href: `/events?camera=${name}` },
{ name: 'Recordings', href: `/recordings/${name}` }
], [name]);
const icons = useMemo(
() => [
{

View File

@ -0,0 +1,114 @@
import { h } from 'preact';
import { Link } from 'preact-router/match';
import { closestTo, format, isEqual, parseISO } from 'date-fns';
import ActivityIndicator from '../components/ActivityIndicator';
import Button from '../components/Button';
import Calendar from '../components/Calendar';
import Carousel from '../components/Carousel';
import Heading from '../components/Heading';
import VideoPlayer from '../components/VideoPlayer';
import { FetchStatus, useApiHost, useRecording } from '../api';
export default function Recording({ camera, date, hour }) {
const apiHost = useApiHost();
const { data, status } = useRecording(camera);
if (status !== FetchStatus.LOADED) {
return <ActivityIndicator />;
}
const recordingDates = data.map((item) => item.date);
const selectedDate = closestTo(
date ? parseISO(date) : new Date(),
recordingDates.map((i) => parseISO(i))
);
const selectedKey = format(selectedDate, 'yyyy-MM-dd');
const [year, month, day] = selectedKey.split('-');
const calendar = [];
const buttons = [];
const playlist = [];
const hours = [];
for (const item of data) {
const date = parseISO(item.date);
const events = item.recordings.map((i) => i.events);
calendar.push(
<Link href={`/recordings/${camera}/${item.date}`}>
<Calendar
date={date}
hours={events.length}
events={events.reduce((a, b) => a + b)}
selected={isEqual(selectedDate, date)}
/>
</Link>
);
if (item.date == selectedKey) {
for (const recording of item.recordings) {
buttons.push(
<Button href={`/recordings/${camera}/${item.date}/${recording.hour}`} type="text">
{recording.hour}:00
</Button>
);
playlist.push({
name: `${selectedKey} ${recording.hour}:00`,
description: `${camera} recording @ ${recording.hour}:00.`,
sources: [
{
src: `${apiHost}/vod/${year}-${month}/${day}/${recording.hour}/${camera}/index.m3u8`,
type: 'application/vnd.apple.mpegurl',
},
],
});
hours.push(recording.hour);
}
}
}
const selectedHour = hours.indexOf(hour);
if (this.player !== undefined) {
this.player.playlist([]);
this.player.playlist(playlist);
this.player.playlist.autoadvance(0);
if (selectedHour !== -1) {
this.player.playlist.currentItem(selectedHour);
}
}
const selectDate = (flkty) => {
flkty.select(recordingDates.indexOf(selectedKey), false, true);
};
const selectHour = (flkty) => {
flkty.select(selectedHour, false, true);
};
return (
<div className="space-y-4">
<Heading>{camera} Recordings</Heading>
<Carousel flickityRef={selectDate} options={{ pageDots: false }}>
{calendar}
</Carousel>
<VideoPlayer
date={selectedKey}
onReady={(player) => {
if (player.playlist) {
player.playlist(playlist);
player.playlist.autoadvance(0);
if (selectedHour !== -1) {
player.playlist.currentItem(selectedHour);
}
this.player = player;
}
}}
/>
<Carousel flickityRef={selectHour} options={{ pageDots: false }}>
{buttons}
</Carousel>
</div>
);
}

View File

@ -18,6 +18,11 @@ export async function getEvents(url, cb, props) {
return module.default;
}
export async function getRecording(url, cb, props) {
const module = await import('./Recording.jsx');
return module.default;
}
export async function getDebug(url, cb, props) {
const module = await import('./Debug.jsx');
return module.default;