mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Initial Recordings UI
This commit is contained in:
parent
abbc608ee4
commit
5461308d30
@ -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}],
|
||||
|
@ -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
388
web/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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="/" />
|
||||
|
@ -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 />
|
||||
|
@ -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);
|
||||
|
@ -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'
|
||||
}`;
|
||||
|
||||
|
26
web/src/components/Calendar.jsx
Normal file
26
web/src/components/Calendar.jsx
Normal 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>
|
||||
);
|
||||
}
|
65
web/src/components/Carousel.jsx
Normal file
65
web/src/components/Carousel.jsx
Normal 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',
|
||||
};
|
51
web/src/components/VideoPlayer.jsx
Normal file
51
web/src/components/VideoPlayer.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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(
|
||||
() => [
|
||||
{
|
||||
|
114
web/src/routes/Recording.jsx
Normal file
114
web/src/routes/Recording.jsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user