mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-03 01:35:22 +03:00
Merge branch 'release-0.11.0' of https://github.com/blakeblackshear/frigate into sub_label_filter
This commit is contained in:
commit
c943cb9f1d
@ -247,3 +247,16 @@ HTTP Live Streaming Video on Demand URL for the specified event. Can be viewed i
|
|||||||
### `GET /vod/<camera>/start/<start-timestamp>/end/<end-timestamp>/index.m3u8`
|
### `GET /vod/<camera>/start/<start-timestamp>/end/<end-timestamp>/index.m3u8`
|
||||||
|
|
||||||
HTTP Live Streaming Video on Demand URL for the camera with the specified time range. Can be viewed in an application like VLC.
|
HTTP Live Streaming Video on Demand URL for the camera with the specified time range. Can be viewed in an application like VLC.
|
||||||
|
|
||||||
|
### `GET /api/<camera_name>/recordings/summary`
|
||||||
|
|
||||||
|
Hourly summary of recordings data for a camera.
|
||||||
|
|
||||||
|
### `GET /api/<camera_name>/recordings`
|
||||||
|
|
||||||
|
Get recording segment details for the given timestamp range.
|
||||||
|
|
||||||
|
| param | Type | Description |
|
||||||
|
| -------- | ---- | ------------------------------------- |
|
||||||
|
| `after` | int | Unix timestamp for beginning of range |
|
||||||
|
| `before` | int | Unix timestamp for end of range |
|
||||||
|
|||||||
121
docs/package-lock.json
generated
121
docs/package-lock.json
generated
@ -8,8 +8,8 @@
|
|||||||
"name": "docs",
|
"name": "docs",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docusaurus/core": "^2.0.0-beta.15",
|
"@docusaurus/core": "^2.0.0-beta.20",
|
||||||
"@docusaurus/preset-classic": "^2.0.0-beta.15",
|
"@docusaurus/preset-classic": "^2.0.0-beta.20",
|
||||||
"@mdx-js/react": "^1.6.22",
|
"@mdx-js/react": "^1.6.22",
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
@ -3453,9 +3453,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
|
||||||
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
|
"integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@ -3563,9 +3563,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/async": {
|
"node_modules/async": {
|
||||||
"version": "2.6.3",
|
"version": "2.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lodash": "^4.17.14"
|
"lodash": "^4.17.14"
|
||||||
}
|
}
|
||||||
@ -3877,6 +3877,15 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bindings": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"file-uri-to-path": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bluebird": {
|
"node_modules/bluebird": {
|
||||||
"version": "3.7.2",
|
"version": "3.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
||||||
@ -6182,6 +6191,12 @@
|
|||||||
"webpack": "^4.0.0 || ^5.0.0"
|
"webpack": "^4.0.0 || ^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/file-uri-to-path": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/filesize": {
|
"node_modules/filesize": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
|
||||||
@ -6272,9 +6287,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.14.7",
|
"version": "1.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.0.tgz",
|
||||||
"integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==",
|
"integrity": "sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@ -8909,9 +8924,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minimist": {
|
"node_modules/minimist": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||||
},
|
},
|
||||||
"node_modules/mixin-deep": {
|
"node_modules/mixin-deep": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
@ -8974,6 +8989,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
|
||||||
"integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE="
|
"integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE="
|
||||||
},
|
},
|
||||||
|
"node_modules/nan": {
|
||||||
|
"version": "2.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
|
||||||
|
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
|
||||||
@ -10396,9 +10417,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prismjs": {
|
"node_modules/prismjs": {
|
||||||
"version": "1.25.0",
|
"version": "1.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.28.0.tgz",
|
||||||
"integrity": "sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg=="
|
"integrity": "sha512-8aaXdYvl1F7iC7Xm1spqSaY/OJBpYW3v+KJ+F17iYxvdc8sfjW194COK5wVhMZX45tGteiBQgdvD/nhxcRwylw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/process-nextick-args": {
|
"node_modules/process-nextick-args": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
@ -13394,9 +13418,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/url-parse": {
|
"node_modules/url-parse": {
|
||||||
"version": "1.5.3",
|
"version": "1.5.10",
|
||||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||||
"integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==",
|
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"querystringify": "^2.1.1",
|
"querystringify": "^2.1.1",
|
||||||
"requires-port": "^1.0.0"
|
"requires-port": "^1.0.0"
|
||||||
@ -17139,9 +17163,9 @@
|
|||||||
"integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw=="
|
"integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw=="
|
||||||
},
|
},
|
||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
|
||||||
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
|
"integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="
|
||||||
},
|
},
|
||||||
"ansi-styles": {
|
"ansi-styles": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
@ -17219,9 +17243,9 @@
|
|||||||
"integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c="
|
"integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c="
|
||||||
},
|
},
|
||||||
"async": {
|
"async": {
|
||||||
"version": "2.6.3",
|
"version": "2.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"lodash": "^4.17.14"
|
"lodash": "^4.17.14"
|
||||||
}
|
}
|
||||||
@ -17456,6 +17480,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA=="
|
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA=="
|
||||||
},
|
},
|
||||||
|
"bindings": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"file-uri-to-path": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"bluebird": {
|
"bluebird": {
|
||||||
"version": "3.7.2",
|
"version": "3.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
||||||
@ -19221,6 +19254,12 @@
|
|||||||
"schema-utils": "^3.0.0"
|
"schema-utils": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"file-uri-to-path": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"filesize": {
|
"filesize": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
|
||||||
@ -19292,9 +19331,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"follow-redirects": {
|
"follow-redirects": {
|
||||||
"version": "1.14.7",
|
"version": "1.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.0.tgz",
|
||||||
"integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ=="
|
"integrity": "sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ=="
|
||||||
},
|
},
|
||||||
"for-in": {
|
"for-in": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@ -21270,9 +21309,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||||
},
|
},
|
||||||
"mixin-deep": {
|
"mixin-deep": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
@ -21325,6 +21364,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
|
||||||
"integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE="
|
"integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE="
|
||||||
},
|
},
|
||||||
|
"nan": {
|
||||||
|
"version": "2.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
|
||||||
|
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"nanoid": {
|
"nanoid": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
|
||||||
@ -22285,9 +22330,9 @@
|
|||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"prismjs": {
|
"prismjs": {
|
||||||
"version": "1.25.0",
|
"version": "1.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.28.0.tgz",
|
||||||
"integrity": "sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg=="
|
"integrity": "sha512-8aaXdYvl1F7iC7Xm1spqSaY/OJBpYW3v+KJ+F17iYxvdc8sfjW194COK5wVhMZX45tGteiBQgdvD/nhxcRwylw=="
|
||||||
},
|
},
|
||||||
"process-nextick-args": {
|
"process-nextick-args": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
@ -24571,9 +24616,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"url-parse": {
|
"url-parse": {
|
||||||
"version": "1.5.3",
|
"version": "1.5.10",
|
||||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||||
"integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==",
|
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"querystringify": "^2.1.1",
|
"querystringify": "^2.1.1",
|
||||||
"requires-port": "^1.0.0"
|
"requires-port": "^1.0.0"
|
||||||
|
|||||||
@ -12,8 +12,8 @@
|
|||||||
"clear": "docusaurus clear"
|
"clear": "docusaurus clear"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docusaurus/core": "^2.0.0-beta.15",
|
"@docusaurus/core": "^2.0.0-beta.20",
|
||||||
"@docusaurus/preset-classic": "^2.0.0-beta.15",
|
"@docusaurus/preset-classic": "^2.0.0-beta.20",
|
||||||
"@mdx-js/react": "^1.6.22",
|
"@mdx-js/react": "^1.6.22",
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
|
|||||||
234
frigate/http.py
234
frigate/http.py
@ -136,12 +136,14 @@ def set_retain(id):
|
|||||||
|
|
||||||
@bp.route("/events/<id>/plus", methods=("POST",))
|
@bp.route("/events/<id>/plus", methods=("POST",))
|
||||||
def send_to_plus(id):
|
def send_to_plus(id):
|
||||||
if current_app.plus_api.is_active():
|
if not current_app.plus_api.is_active():
|
||||||
|
message = "PLUS_API_KEY environment variable is not set"
|
||||||
|
logger.error(message)
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify(
|
jsonify(
|
||||||
{
|
{
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "PLUS_API_KEY environment variable is not set",
|
"message": message,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
400,
|
400,
|
||||||
@ -150,14 +152,14 @@ def send_to_plus(id):
|
|||||||
try:
|
try:
|
||||||
event = Event.get(Event.id == id)
|
event = Event.get(Event.id == id)
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
return make_response(
|
message = f"Event {id} not found"
|
||||||
jsonify({"success": False, "message": "Event" + id + " not found"}), 404
|
logger.error(message)
|
||||||
)
|
return make_response(jsonify({"success": False, "message": message}), 404)
|
||||||
|
|
||||||
if event.plus_id:
|
if event.plus_id:
|
||||||
return make_response(
|
message = "Already submitted to plus"
|
||||||
jsonify({"success": False, "message": "Already submitted to plus"}), 400
|
logger.error(message)
|
||||||
)
|
return make_response(jsonify({"success": False, "message": message}), 400)
|
||||||
|
|
||||||
# load clean.png
|
# load clean.png
|
||||||
try:
|
try:
|
||||||
@ -175,6 +177,7 @@ def send_to_plus(id):
|
|||||||
try:
|
try:
|
||||||
plus_id = current_app.plus_api.upload_image(image, event.camera)
|
plus_id = current_app.plus_api.upload_image(image, event.camera)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
logger.exception(ex)
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify({"success": False, "message": str(ex)}),
|
jsonify({"success": False, "message": str(ex)}),
|
||||||
400,
|
400,
|
||||||
@ -501,6 +504,21 @@ def events():
|
|||||||
clauses = []
|
clauses = []
|
||||||
excluded_fields = []
|
excluded_fields = []
|
||||||
|
|
||||||
|
selected_columns = [
|
||||||
|
Event.id,
|
||||||
|
Event.camera,
|
||||||
|
Event.label,
|
||||||
|
Event.zones,
|
||||||
|
Event.start_time,
|
||||||
|
Event.end_time,
|
||||||
|
Event.has_clip,
|
||||||
|
Event.has_snapshot,
|
||||||
|
Event.plus_id,
|
||||||
|
Event.retain_indefinitely,
|
||||||
|
Event.sub_label,
|
||||||
|
Event.top_score,
|
||||||
|
]
|
||||||
|
|
||||||
if camera != "all":
|
if camera != "all":
|
||||||
clauses.append((Event.camera == camera))
|
clauses.append((Event.camera == camera))
|
||||||
|
|
||||||
@ -527,12 +545,14 @@ def events():
|
|||||||
|
|
||||||
if not include_thumbnails:
|
if not include_thumbnails:
|
||||||
excluded_fields.append(Event.thumbnail)
|
excluded_fields.append(Event.thumbnail)
|
||||||
|
else:
|
||||||
|
selected_columns.append(Event.thumbnail)
|
||||||
|
|
||||||
if len(clauses) == 0:
|
if len(clauses) == 0:
|
||||||
clauses.append((True))
|
clauses.append((True))
|
||||||
|
|
||||||
events = (
|
events = (
|
||||||
Event.select()
|
Event.select(*selected_columns)
|
||||||
.where(reduce(operator.and_, clauses))
|
.where(reduce(operator.and_, clauses))
|
||||||
.order_by(Event.start_time.desc())
|
.order_by(Event.start_time.desc())
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
@ -638,122 +658,100 @@ def latest_frame(camera_name):
|
|||||||
return "Camera named {} not found".format(camera_name), 404
|
return "Camera named {} not found".format(camera_name), 404
|
||||||
|
|
||||||
|
|
||||||
|
# return hourly summary for recordings of camera
|
||||||
|
@bp.route("/<camera_name>/recordings/summary")
|
||||||
|
def recordings_summary(camera_name):
|
||||||
|
recording_groups = (
|
||||||
|
Recordings.select(
|
||||||
|
fn.strftime(
|
||||||
|
"%Y-%m-%d %H",
|
||||||
|
fn.datetime(Recordings.start_time, "unixepoch", "localtime"),
|
||||||
|
).alias("hour"),
|
||||||
|
fn.SUM(Recordings.duration).alias("duration"),
|
||||||
|
fn.SUM(Recordings.motion).alias("motion"),
|
||||||
|
fn.SUM(Recordings.objects).alias("objects"),
|
||||||
|
)
|
||||||
|
.where(Recordings.camera == camera_name)
|
||||||
|
.group_by(
|
||||||
|
fn.strftime(
|
||||||
|
"%Y-%m-%d %H",
|
||||||
|
fn.datetime(Recordings.start_time, "unixepoch", "localtime"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by(
|
||||||
|
fn.strftime(
|
||||||
|
"%Y-%m-%d H",
|
||||||
|
fn.datetime(Recordings.start_time, "unixepoch", "localtime"),
|
||||||
|
).desc()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
event_groups = (
|
||||||
|
Event.select(
|
||||||
|
fn.strftime(
|
||||||
|
"%Y-%m-%d %H", fn.datetime(Event.start_time, "unixepoch", "localtime")
|
||||||
|
).alias("hour"),
|
||||||
|
fn.COUNT(Event.id).alias("count"),
|
||||||
|
)
|
||||||
|
.where(Event.camera == camera_name, Event.has_clip)
|
||||||
|
.group_by(
|
||||||
|
fn.strftime(
|
||||||
|
"%Y-%m-%d %H", fn.datetime(Event.start_time, "unixepoch", "localtime")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.objects()
|
||||||
|
)
|
||||||
|
|
||||||
|
event_map = {g.hour: g.count for g in event_groups}
|
||||||
|
|
||||||
|
days = {}
|
||||||
|
|
||||||
|
for recording_group in recording_groups.objects():
|
||||||
|
parts = recording_group.hour.split()
|
||||||
|
hour = parts[1]
|
||||||
|
day = parts[0]
|
||||||
|
events_count = event_map.get(recording_group.hour, 0)
|
||||||
|
hour_data = {
|
||||||
|
"hour": hour,
|
||||||
|
"events": events_count,
|
||||||
|
"motion": recording_group.motion,
|
||||||
|
"objects": recording_group.objects,
|
||||||
|
"duration": round(recording_group.duration),
|
||||||
|
}
|
||||||
|
if day not in days:
|
||||||
|
days[day] = {"events": events_count, "hours": [hour_data], "day": day}
|
||||||
|
else:
|
||||||
|
days[day]["events"] += events_count
|
||||||
|
days[day]["hours"].append(hour_data)
|
||||||
|
|
||||||
|
return jsonify(list(days.values()))
|
||||||
|
|
||||||
|
|
||||||
|
# return hour of recordings data for camera
|
||||||
@bp.route("/<camera_name>/recordings")
|
@bp.route("/<camera_name>/recordings")
|
||||||
def recordings(camera_name):
|
def recordings(camera_name):
|
||||||
dates = OrderedDict()
|
after = request.args.get(
|
||||||
|
"after", type=float, default=(datetime.now() - timedelta(hours=1)).timestamp()
|
||||||
|
)
|
||||||
|
before = request.args.get("before", type=float, default=datetime.now().timestamp())
|
||||||
|
|
||||||
# Retrieve all recordings for this camera
|
|
||||||
recordings = (
|
recordings = (
|
||||||
Recordings.select()
|
Recordings.select(
|
||||||
.where(Recordings.camera == camera_name)
|
Recordings.id,
|
||||||
.order_by(Recordings.start_time.asc())
|
Recordings.start_time,
|
||||||
)
|
Recordings.end_time,
|
||||||
|
Recordings.motion,
|
||||||
last_end = 0
|
Recordings.objects,
|
||||||
recording: Recordings
|
|
||||||
for recording in recordings:
|
|
||||||
date = datetime.fromtimestamp(recording.start_time)
|
|
||||||
key = date.strftime("%Y-%m-%d")
|
|
||||||
hour = date.strftime("%H")
|
|
||||||
|
|
||||||
# Create Day Record
|
|
||||||
if key not in dates:
|
|
||||||
dates[key] = OrderedDict()
|
|
||||||
|
|
||||||
# Create Hour Record
|
|
||||||
if hour not in dates[key]:
|
|
||||||
dates[key][hour] = {"delay": {}, "events": []}
|
|
||||||
|
|
||||||
# Check for delay
|
|
||||||
the_hour = datetime.strptime(f"{key} {hour}", "%Y-%m-%d %H").timestamp()
|
|
||||||
# diff current recording start time and the greater of the previous end time or top of the hour
|
|
||||||
diff = recording.start_time - max(last_end, the_hour)
|
|
||||||
# Determine seconds into recording
|
|
||||||
seconds = 0
|
|
||||||
if datetime.fromtimestamp(last_end).strftime("%H") == hour:
|
|
||||||
seconds = int(last_end - the_hour)
|
|
||||||
# Determine the delay
|
|
||||||
delay = min(int(diff), 3600 - seconds)
|
|
||||||
if delay > 1:
|
|
||||||
# Add an offset for any delay greater than a second
|
|
||||||
dates[key][hour]["delay"][seconds] = delay
|
|
||||||
|
|
||||||
last_end = recording.end_time
|
|
||||||
|
|
||||||
# Packing intervals to return all events with same label and overlapping times as one row.
|
|
||||||
# See: https://blogs.solidq.com/en/sqlserver/packing-intervals/
|
|
||||||
events = Event.raw(
|
|
||||||
"""WITH C1 AS
|
|
||||||
(
|
|
||||||
SELECT id, label, camera, top_score, start_time AS ts, +1 AS type, 1 AS sub
|
|
||||||
FROM event
|
|
||||||
WHERE camera = ?
|
|
||||||
UNION ALL
|
|
||||||
SELECT id, label, camera, top_score, end_time + 15 AS ts, -1 AS type, 0 AS sub
|
|
||||||
FROM event
|
|
||||||
WHERE camera = ?
|
|
||||||
),
|
|
||||||
C2 AS
|
|
||||||
(
|
|
||||||
SELECT C1.*,
|
|
||||||
SUM(type) OVER(PARTITION BY label ORDER BY ts, type DESC
|
|
||||||
ROWS BETWEEN UNBOUNDED PRECEDING
|
|
||||||
AND CURRENT ROW) - sub AS cnt
|
|
||||||
FROM C1
|
|
||||||
),
|
|
||||||
C3 AS
|
|
||||||
(
|
|
||||||
SELECT id, label, camera, top_score, ts,
|
|
||||||
(ROW_NUMBER() OVER(PARTITION BY label ORDER BY ts) - 1) / 2 + 1
|
|
||||||
AS grpnum
|
|
||||||
FROM C2
|
|
||||||
WHERE cnt = 0
|
|
||||||
)
|
)
|
||||||
SELECT id, label, camera, top_score, start_time, end_time
|
.where(
|
||||||
FROM event
|
Recordings.camera == camera_name,
|
||||||
WHERE camera = ? AND end_time IS NULL
|
Recordings.end_time >= after,
|
||||||
UNION ALL
|
Recordings.start_time <= before,
|
||||||
SELECT MIN(id) as id, label, camera, MAX(top_score) as top_score, MIN(ts) AS start_time, max(ts) AS end_time
|
)
|
||||||
FROM C3
|
.order_by(Recordings.start_time)
|
||||||
GROUP BY label, grpnum
|
|
||||||
ORDER BY start_time;""",
|
|
||||||
camera_name,
|
|
||||||
camera_name,
|
|
||||||
camera_name,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
event: Event
|
return jsonify([e for e in recordings.dicts()])
|
||||||
for event in events:
|
|
||||||
date = datetime.fromtimestamp(event.start_time)
|
|
||||||
key = date.strftime("%Y-%m-%d")
|
|
||||||
hour = date.strftime("%H")
|
|
||||||
if key in dates and hour in dates[key]:
|
|
||||||
dates[key][hour]["events"].append(
|
|
||||||
model_to_dict(
|
|
||||||
event,
|
|
||||||
exclude=[
|
|
||||||
Event.false_positive,
|
|
||||||
Event.zones,
|
|
||||||
Event.thumbnail,
|
|
||||||
Event.has_clip,
|
|
||||||
Event.has_snapshot,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return jsonify(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"date": date,
|
|
||||||
"events": sum([len(value["events"]) for value in hours.values()]),
|
|
||||||
"recordings": [
|
|
||||||
{"hour": hour, "delay": value["delay"], "events": value["events"]}
|
|
||||||
for hour, value in hours.items()
|
|
||||||
],
|
|
||||||
}
|
|
||||||
for date, hours in dates.items()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/<camera>/start/<int:start_ts>/end/<int:end_ts>/clip.mp4")
|
@bp.route("/<camera>/start/<int:start_ts>/end/<int:end_ts>/clip.mp4")
|
||||||
|
|||||||
@ -44,6 +44,8 @@ class PlusApi:
|
|||||||
raise Exception("Plus API not activated")
|
raise Exception("Plus API not activated")
|
||||||
parts = self.key.split(":")
|
parts = self.key.split(":")
|
||||||
r = requests.get(f"{self.host}/v1/auth/token", auth=(parts[0], parts[1]))
|
r = requests.get(f"{self.host}/v1/auth/token", auth=(parts[0], parts[1]))
|
||||||
|
if not r.ok:
|
||||||
|
raise Exception("Unable to refresh API token")
|
||||||
self._token_data = r.json()
|
self._token_data = r.json()
|
||||||
|
|
||||||
def _get_authorization_header(self) -> dict:
|
def _get_authorization_header(self) -> dict:
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import shutil
|
|||||||
import string
|
import string
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
import threading
|
import threading
|
||||||
import time
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@ -459,7 +458,13 @@ class RecordingCleanup(threading.Thread):
|
|||||||
deleted_recordings.add(recording.id)
|
deleted_recordings.add(recording.id)
|
||||||
|
|
||||||
logger.debug(f"Expiring {len(deleted_recordings)} recordings")
|
logger.debug(f"Expiring {len(deleted_recordings)} recordings")
|
||||||
Recordings.delete().where(Recordings.id << deleted_recordings).execute()
|
# delete up to 100,000 at a time
|
||||||
|
max_deletes = 100000
|
||||||
|
deleted_recordings_list = list(deleted_recordings)
|
||||||
|
for i in range(0, len(deleted_recordings_list), max_deletes):
|
||||||
|
Recordings.delete().where(
|
||||||
|
Recordings.id << deleted_recordings_list[i : i + max_deletes]
|
||||||
|
).execute()
|
||||||
|
|
||||||
logger.debug(f"End camera: {camera}.")
|
logger.debug(f"End camera: {camera}.")
|
||||||
|
|
||||||
@ -534,7 +539,12 @@ class RecordingCleanup(threading.Thread):
|
|||||||
logger.debug(
|
logger.debug(
|
||||||
f"Deleting {len(recordings_to_delete)} recordings with missing files"
|
f"Deleting {len(recordings_to_delete)} recordings with missing files"
|
||||||
)
|
)
|
||||||
Recordings.delete().where(Recordings.id << recordings_to_delete).execute()
|
# delete up to 100,000 at a time
|
||||||
|
max_deletes = 100000
|
||||||
|
for i in range(0, len(recordings_to_delete), max_deletes):
|
||||||
|
Recordings.delete().where(
|
||||||
|
Recordings.id << recordings_to_delete[i : i + max_deletes]
|
||||||
|
).execute()
|
||||||
|
|
||||||
logger.debug("End sync recordings.")
|
logger.debug("End sync recordings.")
|
||||||
|
|
||||||
|
|||||||
39
migrations/011_update_indexes.py
Normal file
39
migrations/011_update_indexes.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"""Peewee migrations -- 011_update_indexes.py.
|
||||||
|
|
||||||
|
Some examples (model - class or model name)::
|
||||||
|
|
||||||
|
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||||
|
|
||||||
|
> migrator.sql(sql) # Run custom SQL
|
||||||
|
> migrator.python(func, *args, **kwargs) # Run python code
|
||||||
|
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||||
|
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||||
|
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||||
|
> migrator.change_fields(model, **fields) # Change fields
|
||||||
|
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||||
|
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||||
|
> migrator.rename_table(model, new_table_name)
|
||||||
|
> migrator.add_index(model, *col_names, unique=False)
|
||||||
|
> migrator.drop_index(model, *col_names)
|
||||||
|
> migrator.add_not_null(model, *field_names)
|
||||||
|
> migrator.drop_not_null(model, *field_names)
|
||||||
|
> migrator.add_default(model, field_name, default)
|
||||||
|
|
||||||
|
"""
|
||||||
|
import peewee as pw
|
||||||
|
|
||||||
|
SQL = pw.SQL
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(migrator, database, fake=False, **kwargs):
|
||||||
|
migrator.sql(
|
||||||
|
'CREATE INDEX "event_start_time_end_time" ON "event" ("start_time" DESC, "end_time" DESC)'
|
||||||
|
)
|
||||||
|
migrator.sql("DROP INDEX recordings_start_time_end_time")
|
||||||
|
migrator.sql(
|
||||||
|
'CREATE INDEX "recordings_end_time_start_time" ON "recordings" ("end_time" DESC, "start_time" DESC)'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rollback(migrator, database, fake=False, **kwargs):
|
||||||
|
pass
|
||||||
@ -14,7 +14,10 @@
|
|||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"indent": ["error", 2, { "SwitchCase": 1 }],
|
"indent": ["error", 2, { "SwitchCase": 1 }],
|
||||||
"comma-dangle": ["error", { "objects": "always-multiline", "arrays": "always-multiline" }],
|
"comma-dangle": [
|
||||||
|
"error",
|
||||||
|
{ "objects": "always-multiline", "arrays": "always-multiline", "imports": "always-multiline" }
|
||||||
|
],
|
||||||
"no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
|
"no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
|
||||||
"no-console": "error"
|
"no-console": "error"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -31,7 +31,10 @@ export default function App() {
|
|||||||
<AsyncRoute path="/cameras/:camera" getComponent={cameraComponent} />
|
<AsyncRoute path="/cameras/:camera" getComponent={cameraComponent} />
|
||||||
<AsyncRoute path="/birdseye" getComponent={Routes.getBirdseye} />
|
<AsyncRoute path="/birdseye" getComponent={Routes.getBirdseye} />
|
||||||
<AsyncRoute path="/events" getComponent={Routes.getEvents} />
|
<AsyncRoute path="/events" getComponent={Routes.getEvents} />
|
||||||
<AsyncRoute path="/recording/:camera/:date?/:hour?/:seconds?" getComponent={Routes.getRecording} />
|
<AsyncRoute
|
||||||
|
path="/recording/:camera/:date?/:hour?/:minute?/:second?"
|
||||||
|
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="/" />
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export function ApiProvider({ children, options }) {
|
|||||||
return (
|
return (
|
||||||
<SWRConfig
|
<SWRConfig
|
||||||
value={{
|
value={{
|
||||||
fetcher: (path) => axios.get(path).then((res) => res.data),
|
fetcher: (path, params) => axios.get(path, { params }).then((res) => res.data),
|
||||||
...options,
|
...options,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,59 +1,39 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { useState } from 'preact/hooks';
|
import { useState, useMemo } from 'preact/hooks';
|
||||||
import {
|
import {
|
||||||
differenceInSeconds,
|
getUnixTime,
|
||||||
fromUnixTime,
|
fromUnixTime,
|
||||||
format,
|
format,
|
||||||
parseISO,
|
parseISO,
|
||||||
startOfHour,
|
intervalToDuration,
|
||||||
differenceInMinutes,
|
formatDuration,
|
||||||
differenceInHours
|
endOfDay,
|
||||||
|
startOfDay,
|
||||||
|
isSameDay,
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import ArrowDropdown from '../icons/ArrowDropdown';
|
import ArrowDropdown from '../icons/ArrowDropdown';
|
||||||
import ArrowDropup from '../icons/ArrowDropup';
|
import ArrowDropup from '../icons/ArrowDropup';
|
||||||
import Link from '../components/Link';
|
import Link from '../components/Link';
|
||||||
|
import ActivityIndicator from '../components/ActivityIndicator';
|
||||||
import Menu from '../icons/Menu';
|
import Menu from '../icons/Menu';
|
||||||
import MenuOpen from '../icons/MenuOpen';
|
import MenuOpen from '../icons/MenuOpen';
|
||||||
import { useApiHost } from '../api';
|
import { useApiHost } from '../api';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
export default function RecordingPlaylist({ camera, recordings, selectedDate }) {
|
export default function RecordingPlaylist({ camera, recordings, selectedDate }) {
|
||||||
const [active, setActive] = useState(true);
|
const [active, setActive] = useState(true);
|
||||||
const toggle = () => setActive(!active);
|
const toggle = () => setActive(!active);
|
||||||
|
|
||||||
const result = [];
|
const result = [];
|
||||||
for (const recording of recordings.slice().reverse()) {
|
for (const recording of recordings) {
|
||||||
const date = parseISO(recording.date);
|
const date = parseISO(recording.day);
|
||||||
result.push(
|
result.push(
|
||||||
<ExpandableList
|
<ExpandableList
|
||||||
title={format(date, 'MMM d, yyyy')}
|
title={format(date, 'MMM d, yyyy')}
|
||||||
events={recording.events}
|
events={recording.events}
|
||||||
selected={recording.date === selectedDate}
|
selected={isSameDay(date, selectedDate)}
|
||||||
>
|
>
|
||||||
{recording.recordings
|
<DayOfEvents camera={camera} day={recording.day} hours={recording.hours} />
|
||||||
.slice()
|
|
||||||
.reverse()
|
|
||||||
.map((item, i) => (
|
|
||||||
<div key={i} className="mb-2 w-full">
|
|
||||||
<div
|
|
||||||
className={`flex w-full text-md text-white px-8 py-2 mb-2 ${
|
|
||||||
i === 0 ? 'border-t border-white border-opacity-50' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex-1">
|
|
||||||
<Link href={`/recording/${camera}/${recording.date}/${item.hour}`} type="text">
|
|
||||||
{item.hour}:00
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 text-right">{item.events.length} Events</div>
|
|
||||||
</div>
|
|
||||||
{item.events
|
|
||||||
.slice()
|
|
||||||
.reverse()
|
|
||||||
.map((event) => (
|
|
||||||
<EventCard key={event.id} camera={camera} event={event} delay={item.delay} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</ExpandableList>
|
</ExpandableList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -79,6 +59,71 @@ export default function RecordingPlaylist({ camera, recordings, selectedDate })
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function DayOfEvents({ camera, day, hours }) {
|
||||||
|
const date = parseISO(day);
|
||||||
|
const { data: events } = useSWR([
|
||||||
|
`events`,
|
||||||
|
{
|
||||||
|
before: getUnixTime(endOfDay(date)),
|
||||||
|
after: getUnixTime(startOfDay(date)),
|
||||||
|
camera,
|
||||||
|
has_clip: '1',
|
||||||
|
include_thumbnails: 0,
|
||||||
|
limit: 5000,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// maps all the events under the keys for the hour by hour recordings view
|
||||||
|
const eventMap = useMemo(() => {
|
||||||
|
const eventMap = {};
|
||||||
|
for (const hour of hours) {
|
||||||
|
eventMap[`${day}-${hour.hour}`] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!events) {
|
||||||
|
return eventMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const event of events) {
|
||||||
|
const key = format(fromUnixTime(event.start_time), 'yyyy-MM-dd-HH');
|
||||||
|
// if the hour of recordings is missing for the event start time, skip it
|
||||||
|
if (key in eventMap) {
|
||||||
|
eventMap[key].push(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventMap;
|
||||||
|
}, [events, day, hours]);
|
||||||
|
|
||||||
|
if (!events) {
|
||||||
|
return <ActivityIndicator />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{hours.map((hour, i) => (
|
||||||
|
<div key={i} className="mb-2 w-full">
|
||||||
|
<div
|
||||||
|
className={`flex w-full text-md text-white px-8 py-2 mb-2 ${
|
||||||
|
i === 0 ? 'border-t border-white border-opacity-50' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex-1">
|
||||||
|
<Link href={`/recording/${camera}/${day}/${hour.hour}`} type="text">
|
||||||
|
{hour.hour}:00
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 text-right">{hour.events} Events</div>
|
||||||
|
</div>
|
||||||
|
{eventMap[`${day}-${hour.hour}`].map((event) => (
|
||||||
|
<EventCard key={event.id} camera={camera} event={event} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ExpandableList({ title, events = 0, children, selected = false }) {
|
export function ExpandableList({ title, events = 0, children, selected = false }) {
|
||||||
const [active, setActive] = useState(selected);
|
const [active, setActive] = useState(selected);
|
||||||
const toggle = () => setActive(!active);
|
const toggle = () => setActive(!active);
|
||||||
@ -89,35 +134,26 @@ export function ExpandableList({ title, events = 0, children, selected = false }
|
|||||||
<div className="flex-1 text-right mr-4">{events} Events</div>
|
<div className="flex-1 text-right mr-4">{events} Events</div>
|
||||||
<div className="w-6 md:w-10 h-6 md:h-10">{active ? <ArrowDropup /> : <ArrowDropdown />}</div>
|
<div className="w-6 md:w-10 h-6 md:h-10">{active ? <ArrowDropup /> : <ArrowDropdown />}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`bg-gray-800 bg-opacity-50 ${active ? '' : 'hidden'}`}>{children}</div>
|
{/* Only render the child when expanded to lazy load events for the day */}
|
||||||
|
{active && <div className={`bg-gray-800 bg-opacity-50`}>{children}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EventCard({ camera, event, delay }) {
|
export function EventCard({ camera, event }) {
|
||||||
const apiHost = useApiHost();
|
const apiHost = useApiHost();
|
||||||
const start = fromUnixTime(event.start_time);
|
const start = fromUnixTime(event.start_time);
|
||||||
|
const end = fromUnixTime(event.end_time);
|
||||||
let duration = 'In Progress';
|
let duration = 'In Progress';
|
||||||
if (event.end_time) {
|
if (event.end_time) {
|
||||||
const end = fromUnixTime(event.end_time);
|
duration = formatDuration(intervalToDuration({ start, end }));
|
||||||
const hours = differenceInHours(end, start);
|
|
||||||
const minutes = differenceInMinutes(end, start) - hours * 60;
|
|
||||||
const seconds = differenceInSeconds(end, start) - hours * 60 * 60 - minutes * 60;
|
|
||||||
duration = '';
|
|
||||||
if (hours) duration += `${hours}h `;
|
|
||||||
if (minutes) duration += `${minutes}m `;
|
|
||||||
duration += `${seconds}s`;
|
|
||||||
}
|
}
|
||||||
const position = differenceInSeconds(start, startOfHour(start));
|
|
||||||
const offset = Object.entries(delay)
|
|
||||||
.map(([p, d]) => (position > p ? d : 0))
|
|
||||||
.reduce((p, c) => p + c, 0);
|
|
||||||
const seconds = Math.max(position - offset - 10, 0);
|
|
||||||
return (
|
return (
|
||||||
<Link className="" href={`/recording/${camera}/${format(start, 'yyyy-MM-dd')}/${format(start, 'HH')}/${seconds}`}>
|
<Link className="" href={`/recording/${camera}/${format(start, 'yyyy-MM-dd/HH/mm/ss')}`}>
|
||||||
<div className="flex flex-row mb-2">
|
<div className="flex flex-row mb-2">
|
||||||
<div className="w-28 mr-4">
|
<div className="w-28 mr-4">
|
||||||
<img className="antialiased" src={`${apiHost}/api/events/${event.id}/thumbnail.jpg`} />
|
<img className="antialiased" loading="lazy" src={`${apiHost}/api/events/${event.id}/thumbnail.jpg`} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row w-full border-b">
|
<div className="flex flex-row w-full border-b">
|
||||||
<div className="w-full text-gray-700 font-semibold relative pt-0">
|
<div className="w-full text-gray-700 font-semibold relative pt-0">
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { closestTo, format, parseISO } from 'date-fns';
|
import { parseISO, endOfHour, startOfHour, getUnixTime } from 'date-fns';
|
||||||
|
import { useEffect, useMemo } from 'preact/hooks';
|
||||||
import ActivityIndicator from '../components/ActivityIndicator';
|
import ActivityIndicator from '../components/ActivityIndicator';
|
||||||
import Heading from '../components/Heading';
|
import Heading from '../components/Heading';
|
||||||
import RecordingPlaylist from '../components/RecordingPlaylist';
|
import RecordingPlaylist from '../components/RecordingPlaylist';
|
||||||
@ -7,15 +8,106 @@ import VideoPlayer from '../components/VideoPlayer';
|
|||||||
import { useApiHost } from '../api';
|
import { useApiHost } from '../api';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
export default function Recording({ camera, date, hour, seconds }) {
|
export default function Recording({ camera, date, hour = '00', minute = '00', second = '00' }) {
|
||||||
const apiHost = useApiHost();
|
const currentDate = useMemo(
|
||||||
const { data } = useSWR(`${camera}/recordings`);
|
() => (date ? parseISO(`${date}T${hour || '00'}:${minute || '00'}:${second || '00'}`) : new Date()),
|
||||||
|
[date, hour, minute, second]
|
||||||
|
);
|
||||||
|
|
||||||
if (!data) {
|
const apiHost = useApiHost();
|
||||||
|
const { data: recordingsSummary } = useSWR(`${camera}/recordings/summary`);
|
||||||
|
|
||||||
|
const recordingParams = {
|
||||||
|
before: getUnixTime(endOfHour(currentDate)),
|
||||||
|
after: getUnixTime(startOfHour(currentDate)),
|
||||||
|
};
|
||||||
|
const { data: recordings } = useSWR([`${camera}/recordings`, recordingParams]);
|
||||||
|
|
||||||
|
// calculates the seek seconds by adding up all the seconds in the segments prior to the playback time
|
||||||
|
const seekSeconds = useMemo(() => {
|
||||||
|
if (!recordings) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const currentUnix = getUnixTime(currentDate);
|
||||||
|
|
||||||
|
const hourStart = getUnixTime(startOfHour(currentDate));
|
||||||
|
let seekSeconds = 0;
|
||||||
|
recordings.every((segment) => {
|
||||||
|
// if the next segment is past the desired time, stop calculating
|
||||||
|
if (segment.start_time > currentUnix) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// if the segment starts before the hour, skip the seconds before the hour
|
||||||
|
const start = segment.start_time < hourStart ? hourStart : segment.start_time;
|
||||||
|
// if the segment ends after the selected time, use the selected time for end
|
||||||
|
const end = segment.end_time > currentUnix ? currentUnix : segment.end_time;
|
||||||
|
seekSeconds += end - start;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return seekSeconds;
|
||||||
|
}, [recordings, currentDate]);
|
||||||
|
|
||||||
|
const playlist = useMemo(() => {
|
||||||
|
if (!recordingsSummary) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedDayRecordingData = recordingsSummary.find((s) => !date || s.day === date);
|
||||||
|
|
||||||
|
const [year, month, day] = selectedDayRecordingData.day.split('-');
|
||||||
|
return selectedDayRecordingData.hours
|
||||||
|
.map((h) => {
|
||||||
|
return {
|
||||||
|
name: h.hour,
|
||||||
|
description: `${camera} recording @ ${h.hour}:00.`,
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
src: `${apiHost}/vod/${year}-${month}/${day}/${h.hour}/${camera}/index.m3u8`,
|
||||||
|
type: 'application/vnd.apple.mpegurl',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.reverse();
|
||||||
|
}, [apiHost, date, recordingsSummary, camera]);
|
||||||
|
|
||||||
|
const playlistIndex = useMemo(() => {
|
||||||
|
const index = playlist.findIndex((item) => item.name === hour);
|
||||||
|
if (index === -1) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}, [playlist, hour]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (this.player) {
|
||||||
|
this.player.playlist(playlist);
|
||||||
|
}
|
||||||
|
}, [playlist]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (this.player) {
|
||||||
|
this.player.playlist.currentItem(playlistIndex);
|
||||||
|
}
|
||||||
|
}, [playlistIndex]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (this.player) {
|
||||||
|
// if the playlist has moved on to the next item, then reset
|
||||||
|
if (this.player.playlist.currentItem() !== playlistIndex) {
|
||||||
|
this.player.playlist.currentItem(playlistIndex);
|
||||||
|
}
|
||||||
|
this.player.currentTime(seekSeconds);
|
||||||
|
// try and play since the user is likely to have interacted with the dom
|
||||||
|
this.player.play();
|
||||||
|
}
|
||||||
|
}, [seekSeconds, playlistIndex]);
|
||||||
|
|
||||||
|
if (!recordingsSummary) {
|
||||||
return <ActivityIndicator />;
|
return <ActivityIndicator />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.length === 0) {
|
if (recordingsSummary.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Heading>{camera} Recordings</Heading>
|
<Heading>{camera} Recordings</Heading>
|
||||||
@ -27,66 +119,18 @@ export default function Recording({ camera, date, hour, seconds }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 playlist = [];
|
|
||||||
const hours = [];
|
|
||||||
|
|
||||||
for (const item of data) {
|
|
||||||
if (item.date === selectedKey) {
|
|
||||||
for (const recording of item.recordings) {
|
|
||||||
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) {
|
|
||||||
this.player.playlist([]);
|
|
||||||
this.player.playlist(playlist);
|
|
||||||
this.player.playlist.autoadvance(0);
|
|
||||||
if (selectedHour !== -1) {
|
|
||||||
this.player.playlist.currentItem(selectedHour);
|
|
||||||
if (seconds !== undefined) {
|
|
||||||
this.player.currentTime(seconds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Force playback rate to be correct
|
|
||||||
const playbackRate = this.player.playbackRate();
|
|
||||||
this.player.defaultPlaybackRate(playbackRate);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 p-2 px-4">
|
<div className="space-y-4 p-2 px-4">
|
||||||
<Heading>{camera} Recordings</Heading>
|
<Heading>{camera} Recordings</Heading>
|
||||||
|
|
||||||
<VideoPlayer
|
<VideoPlayer
|
||||||
onReady={(player) => {
|
onReady={(player) => {
|
||||||
|
player.on('ratechange', () => player.defaultPlaybackRate(player.playbackRate()));
|
||||||
if (player.playlist) {
|
if (player.playlist) {
|
||||||
player.playlist(playlist);
|
player.playlist(playlist);
|
||||||
player.playlist.autoadvance(0);
|
player.playlist.autoadvance(0);
|
||||||
if (selectedHour !== -1) {
|
player.playlist.currentItem(playlistIndex);
|
||||||
player.playlist.currentItem(selectedHour);
|
player.currentTime(seekSeconds);
|
||||||
if (seconds !== undefined) {
|
|
||||||
player.currentTime(seconds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.player = player;
|
this.player = player;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -94,7 +138,7 @@ export default function Recording({ camera, date, hour, seconds }) {
|
|||||||
this.player = null;
|
this.player = null;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RecordingPlaylist camera={camera} recordings={data} selectedDate={selectedKey} selectedHour={hour} />
|
<RecordingPlaylist camera={camera} recordings={recordingsSummary} selectedDate={currentDate} />
|
||||||
</VideoPlayer>
|
</VideoPlayer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user