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 base64
import datetime from collections import OrderedDict
from datetime import datetime, timedelta
import json import json
import glob import glob
import logging import logging
import os import os
import re
import time import time
from functools import reduce from functools import reduce
from pathlib import Path from pathlib import Path
@ -449,6 +451,57 @@ def latest_frame(camera_name):
return "Camera named {} not found".format(camera_name), 404 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>") @bp.route("/vod/<path:path>")
def vod(path): def vod(path):
if not os.path.isdir(f"{RECORD_DIR}/{path}"): if not os.path.isdir(f"{RECORD_DIR}/{path}"):
@ -467,8 +520,13 @@ def vod(path):
) )
durations.append(duration) 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( return jsonify(
{ {
"cache": datetime.now() - timedelta(hours=2) > date,
"discontinuity": False, "discontinuity": False,
"durations": durations, "durations": durations,
"sequences": [{"clips": clips}], "sequences": [{"clips": clips}],

View File

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

388
web/package-lock.json generated
View File

@ -2873,7 +2873,6 @@
"version": "7.12.13", "version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.13.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.13.tgz",
"integrity": "sha512-8+3UMPBrjFa/6TtKi/7sehPKqfAm4g6K+YQjyyFOLUTxzOngcRZTlAVY8sc2CORJYqdHQY8gRPHmn+qo15rCBw==", "integrity": "sha512-8+3UMPBrjFa/6TtKi/7sehPKqfAm4g6K+YQjyyFOLUTxzOngcRZTlAVY8sc2CORJYqdHQY8gRPHmn+qo15rCBw==",
"dev": true,
"requires": { "requires": {
"regenerator-runtime": "^0.13.4" "regenerator-runtime": "^0.13.4"
} }
@ -4118,6 +4117,74 @@
"integrity": "sha512-GmVAWB+JuFKqSbzlofYK4qxk955gEv4Kd9/aj2hLOxneXMAm/J7OXcl5DlElS9tmkqwCcxGysSZGOrjzNvmjFQ==", "integrity": "sha512-GmVAWB+JuFKqSbzlofYK4qxk955gEv4Kd9/aj2hLOxneXMAm/J7OXcl5DlElS9tmkqwCcxGysSZGOrjzNvmjFQ==",
"dev": true "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": { "abab": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz",
@ -4163,6 +4230,28 @@
"integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
"dev": true "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": { "ajv": {
"version": "6.12.6", "version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@ -4763,6 +4852,14 @@
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
"dev": true "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": { "chalk": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@ -5103,6 +5200,11 @@
"whatwg-url": "^8.0.0" "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": { "debug": {
"version": "4.3.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
@ -5218,6 +5320,11 @@
"integrity": "sha512-9YLIBURXj4DJMFALxXw9K3Y3rwb5Fk0X5/8ipCzaN84+gKxoHK43tVKRNakCQbiEx07E8Uwhuq21BpUagFhZ8w==", "integrity": "sha512-9YLIBURXj4DJMFALxXw9K3Y3rwb5Fk0X5/8ipCzaN84+gKxoHK43tVKRNakCQbiEx07E8Uwhuq21BpUagFhZ8w==",
"dev": true "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": { "detect-newline": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@ -5271,6 +5378,11 @@
"integrity": "sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ==", "integrity": "sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ==",
"dev": true "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": { "domconstants": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/domconstants/-/domconstants-0.1.2.tgz", "resolved": "https://registry.npmjs.org/domconstants/-/domconstants-0.1.2.tgz",
@ -5964,12 +6076,22 @@
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"dev": true "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": { "esutils": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true "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": { "exec-sh": {
"version": "0.3.4", "version": "0.3.4",
"resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz",
@ -6259,6 +6381,14 @@
"path-exists": "^4.0.0" "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": { "flat-cache": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
@ -6275,6 +6405,19 @@
"integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==", "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==",
"dev": true "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": { "for-in": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@ -6385,6 +6528,11 @@
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
"dev": true "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": { "get-stdin": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz",
@ -6435,6 +6583,22 @@
"is-glob": "^4.0.1" "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": { "globals": {
"version": "11.12.0", "version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "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": { "himalaya": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/himalaya/-/himalaya-1.1.0.tgz", "resolved": "https://registry.npmjs.org/himalaya/-/himalaya-1.1.0.tgz",
@ -6693,6 +6865,11 @@
"integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=",
"dev": true "dev": true
}, },
"individual": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/individual/-/individual-2.0.0.tgz",
"integrity": "sha1-gzsJfa0jKU52EXqY+zjg2a1hu5c="
},
"inflight": { "inflight": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@ -6876,6 +7053,11 @@
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true "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": { "is-generator-fn": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz",
@ -8596,6 +8778,11 @@
"object.assign": "^4.1.2" "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": { "kind-of": {
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@ -8757,11 +8944,31 @@
"integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=",
"dev": true "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": { "magic-string": {
"version": "0.25.7", "version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
"integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
"dev": true,
"requires": { "requires": {
"sourcemap-codec": "^1.4.4" "sourcemap-codec": "^1.4.4"
} }
@ -8856,6 +9063,14 @@
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true "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": { "min-indent": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@ -8916,6 +9131,28 @@
"integrity": "sha512-Xm9jdWvqFrlV0k965eY5AlCpWIIUBY2ExzGbEG+byMs+mZI4J7zvaUOLpQ8MTFgkpgyEnu4qUhuZT/Or3QeRiA==", "integrity": "sha512-Xm9jdWvqFrlV0k965eY5AlCpWIIUBY2ExzGbEG+byMs+mZI4J7zvaUOLpQ8MTFgkpgyEnu4qUhuZT/Or3QeRiA==",
"dev": true "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": { "ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@ -8935,6 +9172,14 @@
"minimatch": "^3.0.4" "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": { "nanoid": {
"version": "3.1.20", "version": "3.1.20",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz",
@ -9381,6 +9626,14 @@
"node-modules-regexp": "^1.0.0" "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": { "pkg-dir": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
@ -9743,6 +9996,11 @@
"integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=",
"dev": true "dev": true
}, },
"process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI="
},
"progress": { "progress": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@ -9971,8 +10229,7 @@
"regenerator-runtime": { "regenerator-runtime": {
"version": "0.13.7", "version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
"dev": true
}, },
"regenerator-transform": { "regenerator-transform": {
"version": "0.14.5", "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": { "remove-trailing-separator": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "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": { "rsvp": {
"version": "4.8.5", "version": "4.8.5",
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
@ -10242,12 +10524,28 @@
"integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==", "integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==",
"dev": true "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": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true "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": { "safe-regex": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
@ -10490,6 +10788,15 @@
"lru-cache": "^6.0.0" "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": { "set-blocking": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@ -10809,8 +11116,7 @@
"sourcemap-codec": { "sourcemap-codec": {
"version": "1.4.8", "version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
"dev": true
}, },
"spdx-correct": { "spdx-correct": {
"version": "3.1.1", "version": "3.1.1",
@ -11291,6 +11597,11 @@
"punycode": "^2.1.1" "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": { "ts-morph": {
"version": "9.1.0", "version": "9.1.0",
"resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-9.1.0.tgz", "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-9.1.0.tgz",
@ -11478,6 +11789,14 @@
"integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==", "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==",
"dev": true "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": { "union-value": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@ -11490,6 +11809,14 @@
"set-value": "^2.0.1" "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": { "uniq": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
@ -11557,6 +11884,11 @@
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
"dev": true "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": { "use": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
@ -11631,6 +11963,45 @@
"extsprintf": "^1.2.0" "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": { "w3c-hr-time": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", "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==", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true "dev": true
}, },
"xmldom": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.4.0.tgz",
"integrity": "sha512-2E93k08T30Ugs+34HBSTQLVtpi6mCddaY8uO+pMNk1pqSjV5vElzn4mmh6KLxN3hki8rNcHSYzILoh3TEWORvA=="
},
"xtend": { "xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@ -11,11 +11,15 @@
"test": "jest" "test": "jest"
}, },
"dependencies": { "dependencies": {
"date-fns": "^2.21.3",
"flickity": "^2.2.2",
"idb-keyval": "^5.0.2", "idb-keyval": "^5.0.2",
"immer": "^8.0.1", "immer": "^8.0.1",
"preact": "^10.5.9", "preact": "^10.5.9",
"preact-async-route": "^2.2.1", "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": { "devDependencies": {
"@babel/eslint-parser": "^7.12.13", "@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="/cameras/:camera" getComponent={Routes.getCamera} />
<AsyncRoute path="/events/:eventId" getComponent={Routes.getEvent} /> <AsyncRoute path="/events/:eventId" getComponent={Routes.getEvent} />
<AsyncRoute path="/events" getComponent={Routes.getEvents} /> <AsyncRoute path="/events" getComponent={Routes.getEvents} />
<AsyncRoute path="/recordings/:camera/:date?/:hour?" getComponent={Routes.getRecording} />
<AsyncRoute path="/debug" getComponent={Routes.getDebug} /> <AsyncRoute path="/debug" getComponent={Routes.getDebug} />
<AsyncRoute path="/styleguide" getComponent={Routes.getStyleGuide} /> <AsyncRoute path="/styleguide" getComponent={Routes.getStyleGuide} />
<Cameras default path="/" /> <Cameras default path="/" />

View File

@ -27,6 +27,19 @@ export default function Sidebar() {
) : null ) : null
} }
</Match> </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="/events" text="Events" />
<Destination href="/debug" text="Debug" /> <Destination href="/debug" text="Debug" />
<Separator /> <Separator />

View File

@ -110,6 +110,11 @@ export function useEvent(eventId, fetchId) {
return useFetch(url, fetchId); return useFetch(url, fetchId);
} }
export function useRecording(camera, fetchId) {
const url = `/api/${camera}/recordings`;
return useFetch(url, fetchId);
}
export function useConfig(searchParams, fetchId) { export function useConfig(searchParams, fetchId) {
const url = `/api/config${searchParams ? `?${searchParams.toString()}` : ''}`; const url = `/api/config${searchParams ? `?${searchParams.toString()}` : ''}`;
return useFetch(url, fetchId); 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]} ${ let classes = `whitespace-nowrap flex items-center space-x-1 ${className} ${ButtonTypes[type]} ${
ButtonColors[disabled ? 'disabled' : color][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' 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: clipValue, send: sendClips } = useClipsState(name);
const { payload: snapshotValue, send: sendSnapshots } = useSnapshotsState(name); const { payload: snapshotValue, send: sendSnapshots } = useSnapshotsState(name);
const href = `/cameras/${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( 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; 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) { export async function getDebug(url, cb, props) {
const module = await import('./Debug.jsx'); const module = await import('./Debug.jsx');
return module.default; return module.default;