Compare commits

...

35 Commits

Author SHA1 Message Date
Nicolas Mowen
be7b858cbd Remove JELLYFIN from intel driver name hwaccel docs (#3855) 2022-09-16 10:47:22 -06:00
Aksel Skauge Mellbye
a6a0e4d1de Bring recording playlist in front of video.js modals
Ensure that the playlist doesn't get covered by video.js error messages,
preventing interaction.
2022-09-15 06:36:32 -05:00
Aksel
14faf0b2f6 Show snapshots on events page (#3763)
* Add tabs to show snapshot or thumbnail as part of event details,
  even if event has a clip available.
* Add ability for TextTab to render as disabled.
2022-09-15 06:35:51 -05:00
Nicolas Mowen
bdfe4a961a Add documentation for custom ffmpeg builds (#3771)
* Add instructions for custom ffmpeg build

* Add note about mapping /bin
2022-09-15 06:34:07 -05:00
Nick Mowen
1bc8d94312 Fix edge of frame after region adjustments 2022-09-15 06:33:32 -05:00
Nick Mowen
7e9f913ff6 Fix edge case where browser sends multiple requests to download file. 2022-09-15 06:32:54 -05:00
Nick Mowen
b6f799e641 Add camera specific for unify protect cameras 2022-08-28 11:14:01 -05:00
uvjustin
c461c9e700 Set vod_ignore_edit_list option (#3710) 2022-08-26 06:33:16 -05:00
uvjustin
9df415b3f2 Fix VOD issues with longer keyframe intervals (#3671)
* Fix VOD issues with longer keyframe intervals

* Move probe function to util
Update comment

* Use recording duration for keyFrameDurations

* Remove unused early return

* Avoid clipping first clip
2022-08-26 06:32:34 -05:00
Blake Blackshear
f7c5e02a35 Merge remote-tracking branch 'origin/master' into release-0.11.0 2022-08-25 07:40:26 -05:00
Nicolas Mowen
656e6a2a89 Events setup docs (#3164)
* Add events setup to guides

* Add to sidebar

* Fix relative docs references
2022-08-25 07:34:39 -05:00
Jason
afefa3ec02 Update camera_specific.md (#3292)
Change fflag arg to remove `nobuffer`, which causes stream errors in 0.11-beta2
2022-08-25 07:33:47 -05:00
uvjustin
8c45dab9b8 Handle spaces in labels (#3679)
* Unquote label in flask routes
Encode label in Camera.jsx

* Don't use preprocessor

* Update nginx location for cacheable images
2022-08-25 07:32:30 -05:00
Nicolas Mowen
0d6dd1ed0f Cleanup names of cameras, zones, & labels in the UI (#3708)
* Cleanup names of cameras, zones, & labels in the UI

* Fix tests to include camera name
2022-08-25 06:44:34 -05:00
Nicolas Mowen
911d6fdfa7 Ensure bounding box is within camera frame (#3702)
* Ensure bounding box is within camera frame

* Account for 0 index
2022-08-24 07:39:47 -05:00
Blake Blackshear
0cf759acad Merge remote-tracking branch 'origin' into release-0.11.0 2022-08-24 07:22:57 -05:00
Justin Wong
7c57c8c2da Use master playlist for videojs 2022-08-24 07:21:39 -05:00
Justin Wong
89c04acdff Bump videojs to 7.20.2
Bump videojs-seek-buttons to 2.2.1
Bump types/video.js to 7.3.44
2022-08-24 07:21:39 -05:00
Nick Mowen
05d5f13f0e Remove outdated section on events 2022-08-24 06:52:18 -05:00
Blake Blackshear
4682af81fb update recommended hardware links 2022-08-24 06:43:42 -05:00
Nick Mowen
e649a1eb98 Use btbn build for ffmpeg since jellyfin has compatibility issues with rtsp-simple-server 2022-08-24 06:43:42 -05:00
Blake Blackshear
01482d791b Merge remote-tracking branch 'origin/master' into release-0.11.0 2022-08-13 06:50:21 -05:00
Nick Mowen
6e2e297aeb Add clean copy to configuration docs 2022-08-13 06:47:58 -05:00
Stéphane Graber
5577ef081f docker: Add VAAPI to arm64
Signed-off-by: Stéphane Graber <stgraber@ubuntu.com>
2022-08-13 06:47:29 -05:00
Nicolas Mowen
2d5d3bdaf4 Wait for recordingSummary and recordings to be valid before playing (#3558)
* Wait for recordingSummary and recordings to be valid before playing

* Wait for recordingSummary and recordings to be valid before playing
2022-08-13 06:46:08 -05:00
Nicolas Mowen
3376e85be6 Update docs to reflect new image multiarch tag (#3570)
* Update docs to reflect new image multiarch

* simplify

Co-authored-by: Blake Blackshear <blake@frigate.video>
2022-08-13 06:45:32 -05:00
ZuluWhiskey
7a1215d581 Adding configuration example for retain modes (#3589)
* Adding configuration example for retain modes

Reading the documentation on its own didn't help me but when I found https://github.com/blakeblackshear/frigate/discussions/2447 I was able to understand how to add this to my configuration. I've added the example given in that discussion to help future readers of the page.

* Update record.md

Added suggested changes and have also added wording beneath the example mentioning the configuration can be added on a per camera basis.
Have also built on the example to add object specific retentions timings - Not sure if it would be preferred to have it all within one example to not complicate things?

Let me know your thoughts

* Update record.md

Created Object Specific Retention header

* Typo

Co-authored-by: Blake Blackshear <blake@frigate.video>
2022-08-13 06:42:25 -05:00
Nick Mowen
a94297ac93 Add link to supported encoding for each GPU 2022-08-13 06:32:24 -05:00
Matthew Parlane
e68f80b44a Use relative path without .. for rtmp documentation to fix #3481
../faq was used in rtmp.md which leads to the wrong URL when
the current URL ends in a slash due to 301 redirect of netlify
2022-07-19 07:06:27 -05:00
Nick Mowen
653c2274e1 Add FAQ about database locked error 2022-07-19 06:59:50 -05:00
Nicolas Mowen
93cd973e59 Update index.md (#3267) 2022-05-29 09:48:55 -05:00
Nick Mowen
53bf3cd2e6 Use tip for casting notes and specific note for no audio cameras 2022-05-14 07:32:31 -05:00
Nick Mowen
f59871a189 Fix bolding 2022-05-14 07:32:31 -05:00
Nick Mowen
2dda7608bb Clarify camera streaming 2022-05-14 07:32:31 -05:00
Nick Mowen
10db9faff9 Add docs about casting 2022-05-14 07:32:31 -05:00
29 changed files with 337 additions and 215 deletions

View File

@@ -46,7 +46,6 @@ RUN pip3 wheel --wheel-dir=/wheels -r requirements-wheels.txt
FROM debian:11-slim FROM debian:11-slim
ARG TARGETARCH ARG TARGETARCH
ARG JELLYFIN_FFMPEG_VERSION=5.0.1-7
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive" ARG DEBIAN_FRONTEND="noninteractive"
# http://stackoverflow.com/questions/48162574/ddg#49462622 # http://stackoverflow.com/questions/48162574/ddg#49462622
@@ -80,14 +79,25 @@ RUN apt-get -qq update \
# coral drivers # coral drivers
libedgetpu1-max python3-tflite-runtime python3-pycoral \ libedgetpu1-max python3-tflite-runtime python3-pycoral \
&& pip3 install -U /wheels/*.whl \ && pip3 install -U /wheels/*.whl \
# jellyfin-ffmpeg # btbn-ffmpeg -> amd64 / arm64
&& wget -O jellyfin.deb "https://repo.jellyfin.org/releases/server/debian/versions/jellyfin-ffmpeg/${JELLYFIN_FFMPEG_VERSION}/jellyfin-ffmpeg5_${JELLYFIN_FFMPEG_VERSION}-$( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release )_$( dpkg --print-architecture ).deb" \ && if [ "${TARGETARCH}" = "amd64" ] || [ "${TARGETARCH}" = "arm64" ]; then \
&& apt-get -qq install --no-install-recommends --no-install-suggests -y ./jellyfin.deb \ mkdir -p /usr/lib/btbn-ffmpeg \
&& rm jellyfin.deb \ && wget -O btbn-ffmpeg.tar.xz "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2022-07-31-12-37/ffmpeg-n5.1-2-g915ef932a3-linux$( [ "$TARGETARCH" = "amd64" ] && echo "64" || echo "arm64" )-gpl-5.1.tar.xz" \
&& tar -xf btbn-ffmpeg.tar.xz -C /usr/lib/btbn-ffmpeg --strip-components 1 \
&& rm btbn-ffmpeg.tar.xz; \
fi \
# ffmpeg -> arm32
&& if [ "${TARGETARCH}" = "arm" ]; then \
apt-get -qq install --no-install-recommends --no-install-suggests -y ffmpeg; \
fi \
# arch specific packages # arch specific packages
&& if [ "${TARGETARCH}" = "amd64" ]; then \ && if [ "${TARGETARCH}" = "amd64" ]; then \
apt-get -qq install --no-install-recommends --no-install-suggests -y \ apt-get -qq install --no-install-recommends --no-install-suggests -y \
mesa-va-drivers intel-media-va-driver-non-free; \ mesa-va-drivers libva-drm2 intel-media-va-driver-non-free i965-va-driver libmfx1; \
fi \
&& if [ "${TARGETARCH}" = "arm64" ]; then \
apt-get -qq install --no-install-recommends --no-install-suggests -y \
libva-drm2 mesa-va-drivers; \
fi \ fi \
# not sure why 32bit arm requires all these # not sure why 32bit arm requires all these
&& if [ "${TARGETARCH}" = "arm" ]; then \ && if [ "${TARGETARCH}" = "arm" ]; then \
@@ -105,7 +115,7 @@ RUN apt-get -qq update \
&& apt-get autoremove -y \ && apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
ENV PATH=$PATH:/usr/lib/jellyfin-ffmpeg ENV PATH=$PATH:/usr/lib/btbn-ffmpeg/bin
COPY --from=nginx /usr/local/nginx/ /usr/local/nginx/ COPY --from=nginx /usr/local/nginx/ /usr/local/nginx/

View File

@@ -55,6 +55,7 @@ http {
vod_upstream_location /api; vod_upstream_location /api;
vod_align_segments_to_key_frames on; vod_align_segments_to_key_frames on;
vod_manifest_segment_durations_mode accurate; vod_manifest_segment_durations_mode accurate;
vod_ignore_edit_list on;
# vod caches # vod caches
vod_metadata_cache metadata_cache 512m; vod_metadata_cache metadata_cache 512m;
@@ -172,10 +173,11 @@ http {
proxy_set_header Host $host; proxy_set_header Host $host;
} }
location ~* /api/(.*\.(jpg|jpeg|png)$) { location ~* /api/.*\.(jpg|jpeg|png)$ {
add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS'; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
proxy_pass http://frigate_api/$1$is_args$args; rewrite ^/api/(.*)$ $1 break;
proxy_pass http://frigate_api;
proxy_pass_request_headers on; proxy_pass_request_headers on;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View File

@@ -67,3 +67,14 @@ model:
``` ```
Note that if you rename objects in the labelmap, you will also need to update your `objects -> track` list as well. Note that if you rename objects in the labelmap, you will also need to update your `objects -> track` list as well.
## Custom ffmpeg build
Included with Frigate is a build of ffmpeg that works for the vast majority of users. However, there exists some hardware setups which have incompatibilities with the included build. In this case, a docker volume mapping can be used to overwrite the included ffmpeg build with an ffmpeg build that works for your specific hardware setup.
To do this:
1. Download your ffmpeg build and uncompress to a folder on the host (let's use `/home/appdata/frigate/custom-ffmpeg` for this example).
2. Update your docker-compose or docker CLI to include `'/home/appdata/frigate/custom-ffmpeg':'/usr/lib/btbn-ffmpeg':'ro'` in the volume mappings.
3. Restart frigate and the custom version will be used if the mapping was done correctly.
NOTE: The folder that is mapped from the host needs to be the folder that contains `/bin`. So if the full structure is `/home/appdata/frigate/custom-ffmpeg/bin/ffmpeg` then `/home/appdata/frigate/custom-ffmpeg` needs to be mapped to `/usr/lib/btbn-ffmpeg`.

View File

@@ -58,18 +58,17 @@ ffmpeg:
### Reolink 410/520 (possibly others) ### Reolink 410/520 (possibly others)
According to [this discussion](https://github.com/blakeblackshear/frigate/issues/1713#issuecomment-932976305), the http video streams seem to be the most reliable for Reolink. According to [this discussion](https://github.com/blakeblackshear/frigate/issues/3235#issuecomment-1135876973), the http video streams seem to be the most reliable for Reolink.
```yaml ```yaml
cameras: cameras:
reolink: reolink:
ffmpeg: ffmpeg:
hwaccel_args:
input_args: input_args:
- -avoid_negative_ts - -avoid_negative_ts
- make_zero - make_zero
- -fflags - -fflags
- nobuffer+genpts+discardcorrupt - +genpts+discardcorrupt
- -flags - -flags
- low_delay - low_delay
- -strict - -strict
@@ -113,3 +112,14 @@ If your cameras do not support TCP connections for RTSP, you can use UDP.
ffmpeg: ffmpeg:
input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport udp -timeout 5000000 -use_wallclock_as_timestamps 1 input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport udp -timeout 5000000 -use_wallclock_as_timestamps 1
``` ```
### Unifi Protect Cameras
In the Unifi 2.0 update Unifi Protect Cameras had a change in audio sample rate which causes issues for ffmpeg. The input rate needs to be set for record and rtmp.
```yaml
ffmpeg:
output_args:
record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c:v copy -ar 44100 -c:a aac
rtmp: -c:v copy -f flv -ar 44100 -c:a aac
```

View File

@@ -21,7 +21,7 @@ ffmpeg:
ffmpeg: ffmpeg:
hwaccel_args: -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p hwaccel_args: -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p
``` ```
**NOTICE**: With some of the processors, like the J4125, the default driver `iHD` doesn't seem to work correctly for hardware acceleration. You may need to change the driver to `i965` by adding the following environment variable `LIBVA_DRIVER_NAME_JELLYFIN=i965` to your docker-compose file. **NOTICE**: With some of the processors, like the J4125, the default driver `iHD` doesn't seem to work correctly for hardware acceleration. You may need to change the driver to `i965` by adding the following environment variable `LIBVA_DRIVER_NAME=i965` to your docker-compose file.
### Intel-based CPUs (>=10th Generation) via Quicksync ### Intel-based CPUs (>=10th Generation) via Quicksync
@@ -41,6 +41,8 @@ ffmpeg:
### NVIDIA GPU ### NVIDIA GPU
[Supported Nvidia GPUs for Decoding](https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new)
These instructions are based on the [jellyfin documentation](https://jellyfin.org/docs/general/administration/hardware-acceleration.html#nvidia-hardware-acceleration-on-docker-linux) These instructions are based on the [jellyfin documentation](https://jellyfin.org/docs/general/administration/hardware-acceleration.html#nvidia-hardware-acceleration-on-docker-linux)
Add `--gpus all` to your docker run command or update your compose file. Add `--gpus all` to your docker run command or update your compose file.

View File

@@ -25,6 +25,10 @@ cameras:
height: 720 height: 720
``` ```
### VSCode Configuration Schema
VSCode (and VSCode addon) supports the JSON schemas which will automatically validate the config. This can be added by adding `# yaml-language-server: $schema=http://frigate_host:5000/api/config/schema` to the top of the config file. `frigate_host` being the IP address of frigate or `ccab4aaf-frigate` if running in the addon.
### Full configuration reference: ### Full configuration reference:
:::caution :::caution
@@ -311,6 +315,8 @@ snapshots:
# Optional: Enable writing jpg snapshot to /media/frigate/clips (default: shown below) # Optional: Enable writing jpg snapshot to /media/frigate/clips (default: shown below)
# This value can be set via MQTT and will be updated in startup based on retained value # This value can be set via MQTT and will be updated in startup based on retained value
enabled: False enabled: False
# Optional: save a clean PNG copy of the snapshot image (default: shown below)
clean_copy: True
# Optional: print a timestamp on the snapshots (default: shown below) # Optional: print a timestamp on the snapshots (default: shown below)
timestamp: False timestamp: False
# Optional: draw bounding box on the snapshots (default: shown below) # Optional: draw bounding box on the snapshots (default: shown below)

View File

@@ -42,3 +42,35 @@ The same options are available with events. Let's consider a scenario where you
- With the `all` option all segments for the duration of the event would be saved for the event. This event would have 4 hours of footage. - With the `all` option all segments for the duration of the event would be saved for the event. This event would have 4 hours of footage.
- With the `motion` option all segments for the duration of the event with motion would be saved. This means any segment where a car drove by in the street, person walked by, lighting changed, etc. would be saved. - With the `motion` option all segments for the duration of the event with motion would be saved. This means any segment where a car drove by in the street, person walked by, lighting changed, etc. would be saved.
- With the `active_objects` it would only keep segments where the object was active. In this case the only segments that would be saved would be the ones where the car was driving up, you going inside, you coming outside, and the car driving away. Essentially reducing the 4 hours to a minute or two of event footage. - With the `active_objects` it would only keep segments where the object was active. In this case the only segments that would be saved would be the ones where the car was driving up, you going inside, you coming outside, and the car driving away. Essentially reducing the 4 hours to a minute or two of event footage.
A configuration example of the above retain modes where all `motion` segments are stored for 7 days and `active objects` are stored for 14 days would be as follows:
```yaml
record:
enabled: True
retain:
days: 7
mode: motion
events:
retain:
default: 14
mode: active_objects
```
The above configuration example can be added globally or on a per camera basis.
### Object Specific Retention
You can also set specific retention length for an object type. The below configuration example builds on from above but also specifies that recordings of dogs only need to be kept for 2 days and recordings of cars should be kept for 7 days.
```yaml
record:
enabled: True
retain:
days: 7
mode: motion
events:
retain:
default: 14
mode: active_objects
objects:
dog: 2
car: 7
```

View File

@@ -5,4 +5,4 @@ title: RTMP
Frigate can re-stream your video feed as a RTMP feed for other applications such as Home Assistant to utilize it at `rtmp://<frigate_host>/live/<camera_name>`. Port 1935 must be open. This allows you to use a video feed for detection in frigate and Home Assistant live view at the same time without having to make two separate connections to the camera. The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate. Frigate can re-stream your video feed as a RTMP feed for other applications such as Home Assistant to utilize it at `rtmp://<frigate_host>/live/<camera_name>`. Port 1935 must be open. This allows you to use a video feed for detection in frigate and Home Assistant live view at the same time without having to make two separate connections to the camera. The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate.
Some video feeds are not compatible with RTMP. If you are experiencing issues, check to make sure your camera feed is h264 with AAC audio. If your camera doesn't support a compatible format for RTMP, you can use the ffmpeg args to re-encode it on the fly at the expense of increased CPU utilization. Some more information about it can be found [here](../faqs#audio-in-recordings). Some video feeds are not compatible with RTMP. If you are experiencing issues, check to make sure your camera feed is h264 with AAC audio. If your camera doesn't support a compatible format for RTMP, you can use the ffmpeg args to re-encode it on the fly at the expense of increased CPU utilization. Some more information about it can be found [here](/faqs#audio-in-recordings).

View File

@@ -47,3 +47,7 @@ These messages in the logs are expected in certain situations. Frigate checks th
### "On connect called" ### "On connect called"
If you see repeated "On connect called" messages in your config, check for another instance of frigate. This happens when multiple frigate containers are trying to connect to mqtt with the same client_id. If you see repeated "On connect called" messages in your config, check for another instance of frigate. This happens when multiple frigate containers are trying to connect to mqtt with the same client_id.
### Error: Database Is Locked
sqlite does not work well on a network share, if the `/media` folder is mapped to a network share then [this guide](/configuration/advanced#database) should be used to move the database to a location on the internal drive.

View File

@@ -0,0 +1,10 @@
---
id: events_setup
title: Setting Up Events
---
[Snapshots](../configuration/snapshots.md) and/or [Recordings](../configuration/record.md) must be enabled for events to be created for detected objects.
## Limiting Events to Areas of Interest
The best way to limit events to areas of interest is to use [zones](../configuration/zones.md) along with `required_zones` for events and snapshots to only have events created in areas of interest.

View File

@@ -3,7 +3,7 @@ id: stationary_objects
title: Avoiding stationary objects title: Avoiding stationary objects
--- ---
Many people use Frigate to detect cars entering their driveway, and they often run into an issue with repeated events of a parked car being repeatedly detected. This is because object tracking stops when motion ends and the event ends. Motion detection works by determining if a sufficient number of pixels have changed between frames. Shadows or other lighting changes will be detected as motion. This will often cause a new event for a parked car. Many people use Frigate to detect cars entering their driveway, and they often run into an issue with repeated events of a parked car being repeatedly detected over the course of multiple days (for example if the car is lost at night and detected again the following morning.
You can use zones to restrict events and notifications to objects that have entered specific areas. You can use zones to restrict events and notifications to objects that have entered specific areas.

View File

@@ -23,15 +23,15 @@ I may earn a small commission for my endorsement, recommendation, testimonial, o
My current favorite is the Minisforum GK41 because of the dual NICs that allow you to setup a dedicated private network for your cameras where they can be blocked from accessing the internet. There are many used workstation options on eBay that work very well. Anything with an Intel CPU and capable of running Debian should work fine. As a bonus, you may want to look for devices with a M.2 or PCIe express slot that is compatible with the Google Coral. I may earn a small commission for my endorsement, recommendation, testimonial, or link to any products or services from this website. My current favorite is the Minisforum GK41 because of the dual NICs that allow you to setup a dedicated private network for your cameras where they can be blocked from accessing the internet. There are many used workstation options on eBay that work very well. Anything with an Intel CPU and capable of running Debian should work fine. As a bonus, you may want to look for devices with a M.2 or PCIe express slot that is compatible with the Google Coral. I may earn a small commission for my endorsement, recommendation, testimonial, or link to any products or services from this website.
| Name | Inference Speed | Coral Compatibility | Notes | | Name | Inference Speed | Coral Compatibility | Notes |
| ------------------------------------------------------------------------------------------------------------------------------- | --------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| <a href="https://amzn.to/3oH4BKi" target="_blank" rel="nofollow noopener sponsored">Odyssey X86 Blue J4125</a> (affiliate link) | 9-10ms | M.2 B+M | Dual gigabit NICs for easy isolated camera network. Easily handles several 1080p cameras. | | Odyssey X86 Blue J4125 (<a href="https://amzn.to/3oH4BKi" target="_blank" rel="nofollow noopener sponsored">Amazon</a>) (<a href="https://www.seeedstudio.com/Odyssey-Blue-J4125-128GB-p-4921.html?utm_source=Frigate" target="_blank" rel="nofollow noopener sponsored">SeeedStudio</a>) | 9-10ms | M.2 B+M | Dual gigabit NICs for easy isolated camera network. Easily handles several 1080p cameras. |
| <a href="https://amzn.to/3ptnb8D" target="_blank" rel="nofollow noopener sponsored">Minisforum GK41</a> (affiliate link) | 9-10ms | USB | Dual gigabit NICs for easy isolated camera network. Easily handles several 1080p cameras. | | Minisforum GK41 (<a href="https://amzn.to/3ptnb8D" target="_blank" rel="nofollow noopener sponsored">Amazon</a>) | 9-10ms | USB | Dual gigabit NICs for easy isolated camera network. Easily handles several 1080p cameras. |
| <a href="https://amzn.to/35E79BC" target="_blank" rel="nofollow noopener sponsored">Beelink GK55</a> (affiliate link) | 9-10ms | USB | Dual gigabit NICs for easy isolated camera network. Easily handles several 1080p cameras. | | Beelink GK55 (<a href="https://amzn.to/35E79BC" target="_blank" rel="nofollow noopener sponsored">Amazon</a>) | 9-10ms | USB | Dual gigabit NICs for easy isolated camera network. Easily handles several 1080p cameras. |
| <a href="https://amzn.to/3psFlHi" target="_blank" rel="nofollow noopener sponsored">Intel NUC</a> (affiliate link) | 8-10ms | USB | Overkill for most, but great performance. Can handle many cameras at 5fps depending on typical amounts of motion. Requires extra parts. | | Intel NUC (<a href="https://amzn.to/3psFlHi" target="_blank" rel="nofollow noopener sponsored">Amazon</a>) | 8-10ms | USB | Overkill for most, but great performance. Can handle many cameras at 5fps depending on typical amounts of motion. Requires extra parts. |
| <a href="https://amzn.to/3a6TBh8" target="_blank" rel="nofollow noopener sponsored">BMAX B2 Plus</a> (affiliate link) | 10-12ms | USB | Good balance of performance and cost. Also capable of running many other services at the same time as frigate. | | BMAX B2 Plus (<a href="https://amzn.to/3a6TBh8" target="_blank" rel="nofollow noopener sponsored">Amazon</a>) | 10-12ms | USB | Good balance of performance and cost. Also capable of running many other services at the same time as frigate. |
| <a href="https://amzn.to/2YjpY9m" target="_blank" rel="nofollow noopener sponsored">Atomic Pi</a> (affiliate link) | 16ms | USB | Good option for a dedicated low power board with a small number of cameras. Can leverage Intel QuickSync for stream decoding. | | Atomic Pi (<a href="https://amzn.to/2YjpY9m" target="_blank" rel="nofollow noopener sponsored">Amazon</a>) | 16ms | USB | Good option for a dedicated low power board with a small number of cameras. Can leverage Intel QuickSync for stream decoding. |
| <a href="https://amzn.to/2YhSGHH" target="_blank" rel="nofollow noopener sponsored">Raspberry Pi 4 (64bit)</a> (affiliate link) | 10-15ms | USB | Can handle a small number of cameras. | | Raspberry Pi 4 (64bit) (<a href="https://amzn.to/2YhSGHH" target="_blank" rel="nofollow noopener sponsored">Amazon</a>) | 10-15ms | USB | Can handle a small number of cameras. |
## Google Coral TPU ## Google Coral TPU

View File

@@ -100,18 +100,7 @@ Additionally, the USB Coral draws a considerable amount of power. If using any o
## Docker ## Docker
Running in Docker directly is the recommended install method. Running in Docker with compose is the recommended install method:
Make sure you choose the right image for your architecture:
| Arch | Image Name |
| ----------- | ------------------------------------------ |
| amd64 | blakeblackshear/frigate:stable-amd64 |
| amd64nvidia | blakeblackshear/frigate:stable-amd64nvidia |
| armv7 | blakeblackshear/frigate:stable-armv7 |
| aarch64 | blakeblackshear/frigate:stable-aarch64 |
It is recommended to run with docker-compose:
```yaml ```yaml
version: "3.9" version: "3.9"
@@ -120,7 +109,7 @@ services:
container_name: frigate container_name: frigate
privileged: true # this may not be necessary for all setups privileged: true # this may not be necessary for all setups
restart: unless-stopped restart: unless-stopped
image: blakeblackshear/frigate:<specify_version_tag> image: blakeblackshear/frigate:stable
shm_size: "64mb" # update for your cameras based on calculation above shm_size: "64mb" # update for your cameras based on calculation above
devices: devices:
- /dev/bus/usb:/dev/bus/usb # passes the USB Coral, needs to be modified for other versions - /dev/bus/usb:/dev/bus/usb # passes the USB Coral, needs to be modified for other versions
@@ -157,7 +146,7 @@ docker run -d \
-e FRIGATE_RTSP_PASSWORD='password' \ -e FRIGATE_RTSP_PASSWORD='password' \
-p 5000:5000 \ -p 5000:5000 \
-p 1935:1935 \ -p 1935:1935 \
blakeblackshear/frigate:<specify_version_tag> blakeblackshear/frigate:stable
``` ```
## Home Assistant Operating System (HassOS) ## Home Assistant Operating System (HassOS)

View File

@@ -85,6 +85,17 @@ The integration provides:
This is accessible via "Media Browser" on the left menu panel in Home Assistant. This is accessible via "Media Browser" on the left menu panel in Home Assistant.
## Casting Clips To Media Devices
The integration supports casting clips and camera streams to supported media devices.
:::tip
For clips to be castable to media devices, audio is required and may need to be [enabled for recordings](../faqs.md#audio-in-recordings).
**NOTE: Even if you camera does not support audio, audio will need to be enabled for Casting to be accepted.**
:::
<a name="api"></a> <a name="api"></a>
## Notification API ## Notification API

View File

@@ -4,6 +4,7 @@ module.exports = {
Guides: [ Guides: [
"guides/camera_setup", "guides/camera_setup",
"guides/getting_started", "guides/getting_started",
"guides/events_setup",
"guides/false_positives", "guides/false_positives",
"guides/ha_notifications", "guides/ha_notifications",
"guides/stationary_objects", "guides/stationary_objects",

View File

@@ -8,6 +8,7 @@ import subprocess as sp
import time import time
from functools import reduce from functools import reduce
from pathlib import Path from pathlib import Path
from urllib.parse import unquote
import cv2 import cv2
@@ -341,6 +342,7 @@ def event_thumbnail(id, max_cache_age=2592000):
@bp.route("/<camera_name>/<label>/best.jpg") @bp.route("/<camera_name>/<label>/best.jpg")
@bp.route("/<camera_name>/<label>/thumbnail.jpg") @bp.route("/<camera_name>/<label>/thumbnail.jpg")
def label_thumbnail(camera_name, label): def label_thumbnail(camera_name, label):
label = unquote(label)
if label == "any": if label == "any":
event_query = ( event_query = (
Event.select() Event.select()
@@ -424,6 +426,7 @@ def event_snapshot(id):
@bp.route("/<camera_name>/<label>/snapshot.jpg") @bp.route("/<camera_name>/<label>/snapshot.jpg")
def label_snapshot(camera_name, label): def label_snapshot(camera_name, label):
label = unquote(label)
if label == "any": if label == "any":
event_query = ( event_query = (
Event.select() Event.select()
@@ -491,7 +494,7 @@ def event_clip(id):
def events(): def events():
limit = request.args.get("limit", 100) limit = request.args.get("limit", 100)
camera = request.args.get("camera", "all") camera = request.args.get("camera", "all")
label = request.args.get("label", "all") label = unquote(request.args.get("label", "all"))
sub_label = request.args.get("sub_label", "all") sub_label = request.args.get("sub_label", "all")
zone = request.args.get("zone", "all") zone = request.args.get("zone", "all")
after = request.args.get("after", type=float) after = request.args.get("after", type=float)
@@ -753,9 +756,9 @@ def recordings(camera_name):
return jsonify([e for e in recordings.dicts()]) return jsonify([e for e in recordings.dicts()])
@bp.route("/<camera>/start/<int:start_ts>/end/<int:end_ts>/clip.mp4") @bp.route("/<camera_name>/start/<int:start_ts>/end/<int:end_ts>/clip.mp4")
@bp.route("/<camera>/start/<float:start_ts>/end/<float:end_ts>/clip.mp4") @bp.route("/<camera_name>/start/<float:start_ts>/end/<float:end_ts>/clip.mp4")
def recording_clip(camera, start_ts, end_ts): def recording_clip(camera_name, start_ts, end_ts):
download = request.args.get("download", type=bool) download = request.args.get("download", type=bool)
recordings = ( recordings = (
@@ -765,7 +768,7 @@ def recording_clip(camera, start_ts, end_ts):
| (Recordings.end_time.between(start_ts, end_ts)) | (Recordings.end_time.between(start_ts, end_ts))
| ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time)) | ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time))
) )
.where(Recordings.camera == camera) .where(Recordings.camera == camera_name)
.order_by(Recordings.start_time.asc()) .order_by(Recordings.start_time.asc())
) )
@@ -780,36 +783,41 @@ def recording_clip(camera, start_ts, end_ts):
if clip.end_time > end_ts: if clip.end_time > end_ts:
playlist_lines.append(f"outpoint {int(end_ts - clip.start_time)}") playlist_lines.append(f"outpoint {int(end_ts - clip.start_time)}")
file_name = f"clip_{camera}_{start_ts}-{end_ts}.mp4" file_name = f"clip_{camera_name}_{start_ts}-{end_ts}.mp4"
path = f"/tmp/cache/{file_name}" path = f"/tmp/cache/{file_name}"
ffmpeg_cmd = [ if not os.path.exists(path):
"ffmpeg", ffmpeg_cmd = [
"-y", "ffmpeg",
"-protocol_whitelist", "-y",
"pipe,file", "-protocol_whitelist",
"-f", "pipe,file",
"concat", "-f",
"-safe", "concat",
"0", "-safe",
"-i", "0",
"/dev/stdin", "-i",
"-c", "/dev/stdin",
"copy", "-c",
"-movflags", "copy",
"+faststart", "-movflags",
path, "+faststart",
] path,
]
p = sp.run(
ffmpeg_cmd,
input="\n".join(playlist_lines),
encoding="ascii",
capture_output=True,
)
p = sp.run( if p.returncode != 0:
ffmpeg_cmd, logger.error(p.stderr)
input="\n".join(playlist_lines), return f"Could not create clip from recordings for {camera_name}.", 500
encoding="ascii", else:
capture_output=True, logger.debug(
) f"Ignoring subsequent request for {path} as it already exists in the cache."
if p.returncode != 0: )
logger.error(p.stderr)
return f"Could not create clip from recordings for {camera}.", 500
response = make_response() response = make_response()
response.headers["Content-Description"] = "File Transfer" response.headers["Content-Description"] = "File Transfer"
@@ -825,9 +833,9 @@ def recording_clip(camera, start_ts, end_ts):
return response return response
@bp.route("/vod/<camera>/start/<int:start_ts>/end/<int:end_ts>") @bp.route("/vod/<camera_name>/start/<int:start_ts>/end/<int:end_ts>")
@bp.route("/vod/<camera>/start/<float:start_ts>/end/<float:end_ts>") @bp.route("/vod/<camera_name>/start/<float:start_ts>/end/<float:end_ts>")
def vod_ts(camera, start_ts, end_ts): def vod_ts(camera_name, start_ts, end_ts):
recordings = ( recordings = (
Recordings.select() Recordings.select()
.where( .where(
@@ -835,7 +843,7 @@ def vod_ts(camera, start_ts, end_ts):
| Recordings.end_time.between(start_ts, end_ts) | Recordings.end_time.between(start_ts, end_ts)
| ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time)) | ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time))
) )
.where(Recordings.camera == camera) .where(Recordings.camera == camera_name)
.order_by(Recordings.start_time.asc()) .order_by(Recordings.start_time.asc())
) )
@@ -846,16 +854,13 @@ def vod_ts(camera, start_ts, end_ts):
for recording in recordings: for recording in recordings:
clip = {"type": "source", "path": recording.path} clip = {"type": "source", "path": recording.path}
duration = int(recording.duration * 1000) duration = int(recording.duration * 1000)
# Determine if offset is needed for first clip
if recording.start_time < start_ts:
offset = int((start_ts - recording.start_time) * 1000)
clip["clipFrom"] = offset
duration -= offset
# Determine if we need to end the last clip early # Determine if we need to end the last clip early
if recording.end_time > end_ts: if recording.end_time > end_ts:
duration -= int((recording.end_time - end_ts) * 1000) duration -= int((recording.end_time - end_ts) * 1000)
if duration > 0: if duration > 0:
clip["keyFrameDurations"] = [duration]
clips.append(clip) clips.append(clip)
durations.append(duration) durations.append(duration)
else: else:
@@ -876,14 +881,14 @@ def vod_ts(camera, start_ts, end_ts):
) )
@bp.route("/vod/<year_month>/<day>/<hour>/<camera>") @bp.route("/vod/<year_month>/<day>/<hour>/<camera_name>")
def vod_hour(year_month, day, hour, camera): def vod_hour(year_month, day, hour, camera_name):
start_date = datetime.strptime(f"{year_month}-{day} {hour}", "%Y-%m-%d %H") start_date = datetime.strptime(f"{year_month}-{day} {hour}", "%Y-%m-%d %H")
end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1) end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1)
start_ts = start_date.timestamp() start_ts = start_date.timestamp()
end_ts = end_date.timestamp() end_ts = end_date.timestamp()
return vod_ts(camera, start_ts, end_ts) return vod_ts(camera_name, start_ts, end_ts)
@bp.route("/vod/event/<id>") @bp.route("/vod/event/<id>")

View File

@@ -440,7 +440,13 @@ def intersects_any(box_a, boxes):
def detect( def detect(
object_detector, frame, model_shape, region, objects_to_track, object_filters detect_config: DetectConfig,
object_detector,
frame,
model_shape,
region,
objects_to_track,
object_filters,
): ):
tensor_input = create_tensor_input(frame, model_shape, region) tensor_input = create_tensor_input(frame, model_shape, region)
@@ -449,10 +455,10 @@ def detect(
for d in region_detections: for d in region_detections:
box = d[2] box = d[2]
size = region[2] - region[0] size = region[2] - region[0]
x_min = int((box[1] * size) + region[0]) x_min = int(max(0, (box[1] * size) + region[0]))
y_min = int((box[0] * size) + region[1]) y_min = int(max(0, (box[0] * size) + region[1]))
x_max = int((box[3] * size) + region[0]) x_max = int(min(detect_config.width, (box[3] * size) + region[0]))
y_max = int((box[2] * size) + region[1]) y_max = int(min(detect_config.height, (box[2] * size) + region[1]))
width = x_max - x_min width = x_max - x_min
height = y_max - y_min height = y_max - y_min
area = width * height area = width * height
@@ -620,6 +626,7 @@ def process_frames(
for region in regions: for region in regions:
detections.extend( detections.extend(
detect( detect(
detect_config,
object_detector, object_detector,
frame, frame,
model_shape, model_shape,
@@ -647,12 +654,13 @@ def process_frames(
# apply non-maxima suppression to suppress weak, overlapping bounding boxes # apply non-maxima suppression to suppress weak, overlapping bounding boxes
# o[2] is the box of the object: xmin, ymin, xmax, ymax # o[2] is the box of the object: xmin, ymin, xmax, ymax
# apply max/min to ensure values do not exceed the known frame size
boxes = [ boxes = [
( (
o[2][0], max(o[2][0], 0),
o[2][1], max(o[2][1], 0),
o[2][2] - o[2][0], min(o[2][2] - o[2][0], detect_config.width - 1),
o[2][3] - o[2][1], min(o[2][3] - o[2][1], detect_config.height - 1),
) )
for o in group for o in group
] ]
@@ -678,6 +686,7 @@ def process_frames(
selected_objects.extend( selected_objects.extend(
detect( detect(
detect_config,
object_detector, object_detector,
frame, frame,
model_shape, model_shape,

View File

@@ -68,6 +68,7 @@ export const handlers = [
top_score: Math.random(), top_score: Math.random(),
zones: ['front_patio'], zones: ['front_patio'],
thumbnail: '/9j/4aa...', thumbnail: '/9j/4aa...',
camera: 'camera_name',
})) }))
) )
); );

176
web/package-lock.json generated
View File

@@ -17,9 +17,9 @@
"preact-async-route": "^2.2.1", "preact-async-route": "^2.2.1",
"preact-router": "^4.0.1", "preact-router": "^4.0.1",
"swr": "^1.2.2", "swr": "^1.2.2",
"video.js": "^7.17.0", "video.js": "^7.20.2",
"videojs-playlist": "^5.0.0", "videojs-playlist": "^5.0.0",
"videojs-seek-buttons": "^2.2.0" "videojs-seek-buttons": "^2.2.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-env": "^7.16.11", "@babel/preset-env": "^7.16.11",
@@ -30,7 +30,7 @@
"@testing-library/preact": "^2.0.1", "@testing-library/preact": "^2.0.1",
"@testing-library/preact-hooks": "^1.1.0", "@testing-library/preact-hooks": "^1.1.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"@types/video.js": "^7.3.42", "@types/video.js": "^7.3.44",
"@typescript-eslint/eslint-plugin": "^5.18.0", "@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.18.0", "@typescript-eslint/parser": "^5.18.0",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
@@ -3236,9 +3236,9 @@
} }
}, },
"node_modules/@types/video.js": { "node_modules/@types/video.js": {
"version": "7.3.42", "version": "7.3.44",
"resolved": "https://registry.npmjs.org/@types/video.js/-/video.js-7.3.42.tgz", "resolved": "https://registry.npmjs.org/@types/video.js/-/video.js-7.3.44.tgz",
"integrity": "sha512-AD6AQNMgLTqrgoayC6SshKh8EDkDd9x5pmEuiY9YsniHlhn5jPXdkVqrzKLwviapaRhQF15TQYxo1JWpqXCUBg==", "integrity": "sha512-ov1HXNOjUkt38al/ybw8cj1181I5P3sOXdrqBR8AkDCqQX6GYwxOCzdmsGn/LDwKHTZ/3veNC9Ad6BjR5wSq4g==",
"dev": true "dev": true
}, },
"node_modules/@types/yargs": { "node_modules/@types/yargs": {
@@ -3874,17 +3874,17 @@
} }
}, },
"node_modules/@videojs/http-streaming": { "node_modules/@videojs/http-streaming": {
"version": "2.12.0", "version": "2.14.2",
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.12.0.tgz", "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.14.2.tgz",
"integrity": "sha512-vdQA0lDYBXGJqV2T02AGqg1w4dcgyRoN+bYG+G8uF4DpCEMhEtUI0BA4tRu4/Njar8w/9D5k0a1KX40pcvM3fA==", "integrity": "sha512-K1raSfO/pq5r8iUas3OSYni0kXOj91n8ealIpV02khghzGv9LQ6O3YUqYd/eAhJ1HIrmZWOnrYpK/P+mhUExXQ==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "3.0.4", "@videojs/vhs-utils": "3.0.5",
"aes-decrypter": "3.1.2", "aes-decrypter": "3.1.3",
"global": "^4.4.0", "global": "^4.4.0",
"m3u8-parser": "4.7.0", "m3u8-parser": "4.7.1",
"mpd-parser": "0.19.2", "mpd-parser": "0.21.1",
"mux.js": "5.14.1", "mux.js": "6.0.1",
"video.js": "^6 || ^7" "video.js": "^6 || ^7"
}, },
"engines": { "engines": {
@@ -3896,9 +3896,9 @@
} }
}, },
"node_modules/@videojs/vhs-utils": { "node_modules/@videojs/vhs-utils": {
"version": "3.0.4", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.4.tgz", "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz",
"integrity": "sha512-hui4zOj2I1kLzDgf8QDVxD3IzrwjS/43KiS8IHQO0OeeSsb4pB/lgNt1NG7Dv0wMQfCccUpMVLGcK618s890Yg==", "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"global": "^4.4.0", "global": "^4.4.0",
@@ -3985,12 +3985,12 @@
} }
}, },
"node_modules/aes-decrypter": { "node_modules/aes-decrypter": {
"version": "3.1.2", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.2.tgz", "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.3.tgz",
"integrity": "sha512-42nRwfQuPRj9R1zqZBdoxnaAmnIFyDi0MNyTVhjdFOd8fifXKKRfwIHIZ6AMn1or4x5WONzjwRTbTWcsIQ0O4A==", "integrity": "sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.0", "@videojs/vhs-utils": "^3.0.5",
"global": "^4.4.0", "global": "^4.4.0",
"pkcs7": "^1.0.4" "pkcs7": "^1.0.4"
} }
@@ -9416,12 +9416,12 @@
} }
}, },
"node_modules/m3u8-parser": { "node_modules/m3u8-parser": {
"version": "4.7.0", "version": "4.7.1",
"resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.7.0.tgz", "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.7.1.tgz",
"integrity": "sha512-48l/OwRyjBm+QhNNigEEcRcgbRvnUjL7rxs597HmW9QSNbyNvt+RcZ9T/d9vxi9A9z7EZrB1POtZYhdRlwYQkQ==", "integrity": "sha512-pbrQwiMiq+MmI9bl7UjtPT3AK603PV9bogNlr83uC+X9IoxqL5E4k7kU7fMQ0dpRgxgeSMygqUa0IMLQNXLBNA==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.0", "@videojs/vhs-utils": "^3.0.5",
"global": "^4.4.0" "global": "^4.4.0"
} }
}, },
@@ -9552,12 +9552,12 @@
"dev": true "dev": true
}, },
"node_modules/mpd-parser": { "node_modules/mpd-parser": {
"version": "0.19.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.19.2.tgz", "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.21.1.tgz",
"integrity": "sha512-M5tAIdtBM2TN+OSTz/37T7V+h9ZLvhyNqq4TNIdtjAQ/Hg8UnMRf5nJQDjffcXag3POXi31yUJQEKOXdcAM/nw==", "integrity": "sha512-BxlSXWbKE1n7eyEPBnTEkrzhS3PdmkkKdM1pgKbPnPOH0WFZIc0sPOWi7m0Uo3Wd2a4Or8Qf4ZbS7+ASqQ49fw==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.2", "@videojs/vhs-utils": "^3.0.5",
"@xmldom/xmldom": "^0.7.2", "@xmldom/xmldom": "^0.7.2",
"global": "^4.4.0" "global": "^4.4.0"
}, },
@@ -9722,11 +9722,12 @@
"dev": true "dev": true
}, },
"node_modules/mux.js": { "node_modules/mux.js": {
"version": "5.14.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-5.14.1.tgz", "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.0.1.tgz",
"integrity": "sha512-38kA/xjWRDzMbcpHQfhKbJAME8eTZVsb9U2Puk890oGvGqnyu8B/AkKdICKPHkigfqYX9MY20vje88TP14nhog==", "integrity": "sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.11.2" "@babel/runtime": "^7.11.2",
"global": "^4.4.0"
}, },
"bin": { "bin": {
"muxjs-transmux": "bin/transmux.js" "muxjs-transmux": "bin/transmux.js"
@@ -11762,20 +11763,20 @@
} }
}, },
"node_modules/video.js": { "node_modules/video.js": {
"version": "7.17.0", "version": "7.20.2",
"resolved": "https://registry.npmjs.org/video.js/-/video.js-7.17.0.tgz", "resolved": "https://registry.npmjs.org/video.js/-/video.js-7.20.2.tgz",
"integrity": "sha512-8RbLu9+Pdpep9OTPncUHIvZXFgn/7hKdPnSTE/lGSnlFSucXtTUBp41R7NDwncscMLQ0WgazUbmFlvr4MNWMbA==", "integrity": "sha512-hdvAHKAyaL6bCDkeu0pPtFYKi1EDaOUovm7FN1xqBDolUxgH8FKy1WIgTS+Ouuaw7R54SCTcSeXjZEizhy9ouQ==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@videojs/http-streaming": "2.12.0", "@videojs/http-streaming": "2.14.2",
"@videojs/vhs-utils": "^3.0.3", "@videojs/vhs-utils": "^3.0.4",
"@videojs/xhr": "2.6.0", "@videojs/xhr": "2.6.0",
"aes-decrypter": "3.1.2", "aes-decrypter": "3.1.3",
"global": "^4.4.0", "global": "^4.4.0",
"keycode": "^2.2.0", "keycode": "^2.2.0",
"m3u8-parser": "4.7.0", "m3u8-parser": "4.7.1",
"mpd-parser": "0.19.2", "mpd-parser": "0.21.1",
"mux.js": "5.14.1", "mux.js": "6.0.1",
"safe-json-parse": "4.0.0", "safe-json-parse": "4.0.0",
"videojs-font": "3.2.0", "videojs-font": "3.2.0",
"videojs-vtt.js": "^0.15.3" "videojs-vtt.js": "^0.15.3"
@@ -11799,9 +11800,9 @@
} }
}, },
"node_modules/videojs-seek-buttons": { "node_modules/videojs-seek-buttons": {
"version": "2.2.0", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/videojs-seek-buttons/-/videojs-seek-buttons-2.2.0.tgz", "resolved": "https://registry.npmjs.org/videojs-seek-buttons/-/videojs-seek-buttons-2.2.1.tgz",
"integrity": "sha512-yjCA6ntq+8fRKgZi/H6QJlghQWgA1x9oSRl6wfLODAcujhynDXetwMgRKGgl4NlV5af2bKY6erNtJ0kOBko/nQ==", "integrity": "sha512-tXInD8ElUddyc1vxkGsVNhxlFtz8JC4VagPOSyL8mj7LI7oXjLGxni2pjx2wqzsV3PaIBMMH47MBtsNWf+wbtw==",
"dependencies": { "dependencies": {
"global": "^4.4.0", "global": "^4.4.0",
"video.js": "^6 || ^7" "video.js": "^6 || ^7"
@@ -14512,9 +14513,9 @@
} }
}, },
"@types/video.js": { "@types/video.js": {
"version": "7.3.42", "version": "7.3.44",
"resolved": "https://registry.npmjs.org/@types/video.js/-/video.js-7.3.42.tgz", "resolved": "https://registry.npmjs.org/@types/video.js/-/video.js-7.3.44.tgz",
"integrity": "sha512-AD6AQNMgLTqrgoayC6SshKh8EDkDd9x5pmEuiY9YsniHlhn5jPXdkVqrzKLwviapaRhQF15TQYxo1JWpqXCUBg==", "integrity": "sha512-ov1HXNOjUkt38al/ybw8cj1181I5P3sOXdrqBR8AkDCqQX6GYwxOCzdmsGn/LDwKHTZ/3veNC9Ad6BjR5wSq4g==",
"dev": true "dev": true
}, },
"@types/yargs": { "@types/yargs": {
@@ -14894,24 +14895,24 @@
} }
}, },
"@videojs/http-streaming": { "@videojs/http-streaming": {
"version": "2.12.0", "version": "2.14.2",
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.12.0.tgz", "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.14.2.tgz",
"integrity": "sha512-vdQA0lDYBXGJqV2T02AGqg1w4dcgyRoN+bYG+G8uF4DpCEMhEtUI0BA4tRu4/Njar8w/9D5k0a1KX40pcvM3fA==", "integrity": "sha512-K1raSfO/pq5r8iUas3OSYni0kXOj91n8ealIpV02khghzGv9LQ6O3YUqYd/eAhJ1HIrmZWOnrYpK/P+mhUExXQ==",
"requires": { "requires": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "3.0.4", "@videojs/vhs-utils": "3.0.5",
"aes-decrypter": "3.1.2", "aes-decrypter": "3.1.3",
"global": "^4.4.0", "global": "^4.4.0",
"m3u8-parser": "4.7.0", "m3u8-parser": "4.7.1",
"mpd-parser": "0.19.2", "mpd-parser": "0.21.1",
"mux.js": "5.14.1", "mux.js": "6.0.1",
"video.js": "^6 || ^7" "video.js": "^6 || ^7"
} }
}, },
"@videojs/vhs-utils": { "@videojs/vhs-utils": {
"version": "3.0.4", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.4.tgz", "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz",
"integrity": "sha512-hui4zOj2I1kLzDgf8QDVxD3IzrwjS/43KiS8IHQO0OeeSsb4pB/lgNt1NG7Dv0wMQfCccUpMVLGcK618s890Yg==", "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==",
"requires": { "requires": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"global": "^4.4.0", "global": "^4.4.0",
@@ -14980,12 +14981,12 @@
"dev": true "dev": true
}, },
"aes-decrypter": { "aes-decrypter": {
"version": "3.1.2", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.2.tgz", "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.3.tgz",
"integrity": "sha512-42nRwfQuPRj9R1zqZBdoxnaAmnIFyDi0MNyTVhjdFOd8fifXKKRfwIHIZ6AMn1or4x5WONzjwRTbTWcsIQ0O4A==", "integrity": "sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==",
"requires": { "requires": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.0", "@videojs/vhs-utils": "^3.0.5",
"global": "^4.4.0", "global": "^4.4.0",
"pkcs7": "^1.0.4" "pkcs7": "^1.0.4"
} }
@@ -18886,12 +18887,12 @@
"dev": true "dev": true
}, },
"m3u8-parser": { "m3u8-parser": {
"version": "4.7.0", "version": "4.7.1",
"resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.7.0.tgz", "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.7.1.tgz",
"integrity": "sha512-48l/OwRyjBm+QhNNigEEcRcgbRvnUjL7rxs597HmW9QSNbyNvt+RcZ9T/d9vxi9A9z7EZrB1POtZYhdRlwYQkQ==", "integrity": "sha512-pbrQwiMiq+MmI9bl7UjtPT3AK603PV9bogNlr83uC+X9IoxqL5E4k7kU7fMQ0dpRgxgeSMygqUa0IMLQNXLBNA==",
"requires": { "requires": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.0", "@videojs/vhs-utils": "^3.0.5",
"global": "^4.4.0" "global": "^4.4.0"
} }
}, },
@@ -18992,12 +18993,12 @@
"dev": true "dev": true
}, },
"mpd-parser": { "mpd-parser": {
"version": "0.19.2", "version": "0.21.1",
"resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.19.2.tgz", "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.21.1.tgz",
"integrity": "sha512-M5tAIdtBM2TN+OSTz/37T7V+h9ZLvhyNqq4TNIdtjAQ/Hg8UnMRf5nJQDjffcXag3POXi31yUJQEKOXdcAM/nw==", "integrity": "sha512-BxlSXWbKE1n7eyEPBnTEkrzhS3PdmkkKdM1pgKbPnPOH0WFZIc0sPOWi7m0Uo3Wd2a4Or8Qf4ZbS7+ASqQ49fw==",
"requires": { "requires": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.2", "@videojs/vhs-utils": "^3.0.5",
"@xmldom/xmldom": "^0.7.2", "@xmldom/xmldom": "^0.7.2",
"global": "^4.4.0" "global": "^4.4.0"
} }
@@ -19120,11 +19121,12 @@
"dev": true "dev": true
}, },
"mux.js": { "mux.js": {
"version": "5.14.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-5.14.1.tgz", "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.0.1.tgz",
"integrity": "sha512-38kA/xjWRDzMbcpHQfhKbJAME8eTZVsb9U2Puk890oGvGqnyu8B/AkKdICKPHkigfqYX9MY20vje88TP14nhog==", "integrity": "sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w==",
"requires": { "requires": {
"@babel/runtime": "^7.11.2" "@babel/runtime": "^7.11.2",
"global": "^4.4.0"
} }
}, },
"nanoid": { "nanoid": {
@@ -20612,20 +20614,20 @@
} }
}, },
"video.js": { "video.js": {
"version": "7.17.0", "version": "7.20.2",
"resolved": "https://registry.npmjs.org/video.js/-/video.js-7.17.0.tgz", "resolved": "https://registry.npmjs.org/video.js/-/video.js-7.20.2.tgz",
"integrity": "sha512-8RbLu9+Pdpep9OTPncUHIvZXFgn/7hKdPnSTE/lGSnlFSucXtTUBp41R7NDwncscMLQ0WgazUbmFlvr4MNWMbA==", "integrity": "sha512-hdvAHKAyaL6bCDkeu0pPtFYKi1EDaOUovm7FN1xqBDolUxgH8FKy1WIgTS+Ouuaw7R54SCTcSeXjZEizhy9ouQ==",
"requires": { "requires": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@videojs/http-streaming": "2.12.0", "@videojs/http-streaming": "2.14.2",
"@videojs/vhs-utils": "^3.0.3", "@videojs/vhs-utils": "^3.0.4",
"@videojs/xhr": "2.6.0", "@videojs/xhr": "2.6.0",
"aes-decrypter": "3.1.2", "aes-decrypter": "3.1.3",
"global": "^4.4.0", "global": "^4.4.0",
"keycode": "^2.2.0", "keycode": "^2.2.0",
"m3u8-parser": "4.7.0", "m3u8-parser": "4.7.1",
"mpd-parser": "0.19.2", "mpd-parser": "0.21.1",
"mux.js": "5.14.1", "mux.js": "6.0.1",
"safe-json-parse": "4.0.0", "safe-json-parse": "4.0.0",
"videojs-font": "3.2.0", "videojs-font": "3.2.0",
"videojs-vtt.js": "^0.15.3" "videojs-vtt.js": "^0.15.3"
@@ -20646,9 +20648,9 @@
} }
}, },
"videojs-seek-buttons": { "videojs-seek-buttons": {
"version": "2.2.0", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/videojs-seek-buttons/-/videojs-seek-buttons-2.2.0.tgz", "resolved": "https://registry.npmjs.org/videojs-seek-buttons/-/videojs-seek-buttons-2.2.1.tgz",
"integrity": "sha512-yjCA6ntq+8fRKgZi/H6QJlghQWgA1x9oSRl6wfLODAcujhynDXetwMgRKGgl4NlV5af2bKY6erNtJ0kOBko/nQ==", "integrity": "sha512-tXInD8ElUddyc1vxkGsVNhxlFtz8JC4VagPOSyL8mj7LI7oXjLGxni2pjx2wqzsV3PaIBMMH47MBtsNWf+wbtw==",
"requires": { "requires": {
"global": "^4.4.0", "global": "^4.4.0",
"video.js": "^6 || ^7" "video.js": "^6 || ^7"

View File

@@ -19,9 +19,9 @@
"preact-async-route": "^2.2.1", "preact-async-route": "^2.2.1",
"preact-router": "^4.0.1", "preact-router": "^4.0.1",
"swr": "^1.2.2", "swr": "^1.2.2",
"video.js": "^7.17.0", "video.js": "^7.20.2",
"videojs-playlist": "^5.0.0", "videojs-playlist": "^5.0.0",
"videojs-seek-buttons": "^2.2.0" "videojs-seek-buttons": "^2.2.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-env": "^7.16.11", "@babel/preset-env": "^7.16.11",
@@ -32,7 +32,7 @@
"@testing-library/preact": "^2.0.1", "@testing-library/preact": "^2.0.1",
"@testing-library/preact-hooks": "^1.1.0", "@testing-library/preact-hooks": "^1.1.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"@types/video.js": "^7.3.42", "@types/video.js": "^7.3.44",
"@typescript-eslint/eslint-plugin": "^5.18.0", "@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.18.0", "@typescript-eslint/parser": "^5.18.0",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",

View File

@@ -65,7 +65,7 @@ function CameraSection({ sortedCameras }) {
<Fragment> <Fragment>
<Separator /> <Separator />
{sortedCameras.map(([camera]) => ( {sortedCameras.map(([camera]) => (
<Destination key={camera} href={`/cameras/${camera}`} text={camera} /> <Destination key={camera} href={`/cameras/${camera}`} text={camera.replaceAll('_', ' ')} />
))} ))}
<Separator /> <Separator />
</Fragment> </Fragment>
@@ -83,7 +83,7 @@ function RecordingSection({ sortedCameras }) {
key={camera} key={camera}
path={`/recording/${camera}/:date?/:hour?/:seconds?`} path={`/recording/${camera}/:date?/:hour?/:seconds?`}
href={`/recording/${camera}`} href={`/recording/${camera}`}
text={camera} text={camera.replaceAll('_', ' ')}
/> />
); );
})} })}

View File

@@ -57,7 +57,7 @@ export const HistoryVideo = ({
} }
video.src({ video.src({
src: `${apiHost}/vod/event/${id}/index.m3u8`, src: `${apiHost}/vod/event/${id}/master.m3u8`,
type: 'application/vnd.apple.mpegurl', type: 'application/vnd.apple.mpegurl',
}); });
video.poster(`${apiHost}/api/events/${id}/snapshot.jpg`); video.poster(`${apiHost}/api/events/${id}/snapshot.jpg`);

View File

@@ -41,7 +41,7 @@ export default function RecordingPlaylist({ camera, recordings, selectedDate })
const openClass = active ? '-left-6' : 'right-0'; const openClass = active ? '-left-6' : 'right-0';
return ( return (
<div className="flex absolute inset-y-0 right-0 w-9/12 md:w-1/2 lg:w-3/5 max-w-md text-base text-white font-sans"> <div className="flex absolute z-10 inset-y-0 right-0 w-9/12 md:w-1/2 lg:w-3/5 max-w-md text-base text-white font-sans">
<div <div
onClick={toggle} onClick={toggle}
className={`absolute ${openClass} cursor-pointer items-center self-center rounded-tl-lg rounded-bl-lg border border-r-0 w-6 h-20 py-7 bg-gray-800 bg-opacity-70`} className={`absolute ${openClass} cursor-pointer items-center self-center rounded-tl-lg rounded-bl-lg border border-r-0 w-6 h-20 py-7 bg-gray-800 bg-opacity-70`}

View File

@@ -27,12 +27,14 @@ export function Tabs({ children, selectedIndex: selectedIndexProp, onChange, cla
); );
} }
export function TextTab({ selected, text, onClick }) { export function TextTab({ selected, text, onClick, disabled }) {
const selectedStyle = selected const selectedStyle = disabled
? 'text-white bg-blue-500 dark:text-black dark:bg-white' ? 'text-gray-400 dark:text-gray-600 bg-transparent'
: 'text-black dark:text-white bg-transparent'; : selected
? 'text-white bg-blue-500 dark:text-black dark:bg-white'
: 'text-black dark:text-white bg-transparent';
return ( return (
<button onClick={onClick} className={`rounded-full px-4 py-2 ${selectedStyle}`}> <button onClick={onClick} disabled={disabled} className={`rounded-full px-4 py-2 ${selectedStyle}`}>
<span>{text}</span> <span>{text}</span>
</button> </button>
); );

View File

@@ -120,7 +120,7 @@ export default function Camera({ camera }) {
return ( return (
<div className="space-y-4 p-2 px-4"> <div className="space-y-4 p-2 px-4">
<Heading size="2xl">{camera}</Heading> <Heading size="2xl">{camera.replaceAll('_', ' ')}</Heading>
<ButtonsTabbed viewModes={['live', 'debug']} setViewMode={setViewMode} /> <ButtonsTabbed viewModes={['live', 'debug']} setViewMode={setViewMode} />
{player} {player}
@@ -133,8 +133,8 @@ export default function Camera({ camera }) {
className="mb-4 mr-4" className="mb-4 mr-4"
key={objectType} key={objectType}
header={objectType} header={objectType}
href={`/events?camera=${camera}&label=${objectType}`} href={`/events?camera=${camera}&label=${encodeURIComponent(objectType)}`}
media={<img src={`${apiHost}/api/${camera}/${objectType}/thumbnail.jpg`} />} media={<img src={`${apiHost}/api/${camera}/${encodeURIComponent(objectType)}/thumbnail.jpg`} />}
/> />
))} ))}
</div> </div>

View File

@@ -50,6 +50,10 @@ function Camera({ name }) {
{ name: 'Recordings', href: `/recording/${name}` }, { name: 'Recordings', href: `/recording/${name}` },
]; ];
}, [name]); }, [name]);
const cleanName = useMemo(
() => { return `${name.replaceAll('_', ' ')}` },
[name]
);
const icons = useMemo( const icons = useMemo(
() => [ () => [
{ {
@@ -81,6 +85,6 @@ function Camera({ name }) {
); );
return ( return (
<Card buttons={buttons} href={href} header={name} icons={icons} media={<CameraImage camera={name} stretch />} /> <Card buttons={buttons} href={href} header={cleanName} icons={icons} media={<CameraImage camera={name} stretch />} />
); );
} }

View File

@@ -81,7 +81,7 @@ export default function Debug() {
{cameraNames.map((camera, i) => ( {cameraNames.map((camera, i) => (
<Tr key={i} index={i}> <Tr key={i} index={i}>
<Td> <Td>
<Link href={`/cameras/${camera}`}>{camera}</Link> <Link href={`/cameras/${camera}`}>{camera.replaceAll('_', ' ')}</Link>
</Td> </Td>
{cameraDataKeys.map((name) => ( {cameraDataKeys.map((name) => (
<Td key={`${name}-${camera}`}>{cameras[camera][name]}</Td> <Td key={`${name}-${camera}`}>{cameras[camera][name]}</Td>

View File

@@ -2,6 +2,7 @@ import { h, Fragment } from 'preact';
import { route } from 'preact-router'; import { route } from 'preact-router';
import ActivityIndicator from '../components/ActivityIndicator'; import ActivityIndicator from '../components/ActivityIndicator';
import Heading from '../components/Heading'; import Heading from '../components/Heading';
import { Tabs, TextTab } from '../components/Tabs';
import { useApiHost } from '../api'; import { useApiHost } from '../api';
import useSWR from 'swr'; import useSWR from 'swr';
import useSWRInfinite from 'swr/infinite'; import useSWRInfinite from 'swr/infinite';
@@ -54,6 +55,7 @@ export default function Events({ path, ...props }) {
}); });
const [uploading, setUploading] = useState([]); const [uploading, setUploading] = useState([]);
const [viewEvent, setViewEvent] = useState(); const [viewEvent, setViewEvent] = useState();
const [eventDetailType, setEventDetailType] = useState('clip');
const [downloadEvent, setDownloadEvent] = useState({ const [downloadEvent, setDownloadEvent] = useState({
id: null, id: null,
has_clip: false, has_clip: false,
@@ -235,6 +237,10 @@ export default function Events({ path, ...props }) {
} }
}; };
const handleEventDetailTabChange = (index) => {
setEventDetailType(index == 0 ? 'clip' : 'image');
};
if (!config) { if (!config) {
return <ActivityIndicator />; return <ActivityIndicator />;
} }
@@ -251,7 +257,7 @@ export default function Events({ path, ...props }) {
<option value="all">all cameras</option> <option value="all">all cameras</option>
{filterValues.cameras.map((item) => ( {filterValues.cameras.map((item) => (
<option key={item} value={item}> <option key={item} value={item}>
{item} {item.replaceAll('_', ' ')}
</option> </option>
))} ))}
</select> </select>
@@ -262,7 +268,7 @@ export default function Events({ path, ...props }) {
> >
<option value="all">all labels</option> <option value="all">all labels</option>
{filterValues.labels.map((item) => ( {filterValues.labels.map((item) => (
<option key={item} value={item}> <option key={item.replaceAll('_', ' ')} value={item}>
{item} {item}
</option> </option>
))} ))}
@@ -275,7 +281,7 @@ export default function Events({ path, ...props }) {
<option value="all">all zones</option> <option value="all">all zones</option>
{filterValues.zones.map((item) => ( {filterValues.zones.map((item) => (
<option key={item} value={item}> <option key={item} value={item}>
{item} {item.replaceAll('_', ' ')}
</option> </option>
))} ))}
</select> </select>
@@ -457,11 +463,11 @@ export default function Events({ path, ...props }) {
</div> </div>
<div className="capitalize text-sm flex align-center mt-1"> <div className="capitalize text-sm flex align-center mt-1">
<Camera className="h-5 w-5 mr-2 inline" /> <Camera className="h-5 w-5 mr-2 inline" />
{event.camera} {event.camera.replaceAll('_', ' ')}
</div> </div>
<div className="capitalize text-sm flex align-center"> <div className="capitalize text-sm flex align-center">
<Zone className="w-5 h-5 mr-2 inline" /> <Zone className="w-5 h-5 mr-2 inline" />
{event.zones.join(',')} {event.zones.join(', ').replaceAll('_', ' ')}
</div> </div>
</div> </div>
<div class="hidden sm:flex flex-col justify-end mr-2"> <div class="hidden sm:flex flex-col justify-end mr-2">
@@ -495,16 +501,22 @@ export default function Events({ path, ...props }) {
{viewEvent !== event.id ? null : ( {viewEvent !== event.id ? null : (
<div className="space-y-4"> <div className="space-y-4">
<div className="mx-auto max-w-7xl"> <div className="mx-auto max-w-7xl">
{event.has_clip ? ( <div className='flex justify-center w-full py-2'>
<> <Tabs selectedIndex={event.has_clip && eventDetailType == 'clip' ? 0 : 1} onChange={handleEventDetailTabChange} className='justify'>
<Heading size="lg">Clip</Heading> <TextTab text='Clip' disabled={!event.has_clip} />
<TextTab text={event.has_snapshot ? 'Snapshot' : 'Thumbnail'} />
</Tabs>
</div>
<div>
{((eventDetailType == 'clip') && event.has_clip) ? (
<VideoPlayer <VideoPlayer
options={{ options={{
preload: 'auto', preload: 'auto',
autoplay: true, autoplay: true,
sources: [ sources: [
{ {
src: `${apiHost}/vod/event/${event.id}/index.m3u8`, src: `${apiHost}/vod/event/${event.id}/master.m3u8`,
type: 'application/vnd.apple.mpegurl', type: 'application/vnd.apple.mpegurl',
}, },
], ],
@@ -512,23 +524,22 @@ export default function Events({ path, ...props }) {
seekOptions={{ forward: 10, back: 5 }} seekOptions={{ forward: 10, back: 5 }}
onReady={() => {}} onReady={() => {}}
/> />
</> ) : null }
) : (
<div className="flex justify-center"> {((eventDetailType == 'image') || !event.has_clip) ? (
<div> <div className="flex justify-center">
<Heading size="sm">{event.has_snapshot ? 'Best Image' : 'Thumbnail'}</Heading>
<img <img
className="flex-grow-0" className="flex-grow-0"
src={ src={
event.has_snapshot event.has_snapshot
? `${apiHost}/api/events/${event.id}/snapshot.jpg` ? `${apiHost}/api/events/${event.id}/snapshot.jpg`
: `data:image/jpeg;base64,${event.thumbnail}` : `${apiHost}/api/events/${event.id}/thumbnail.jpg`
} }
alt={`${event.label} at ${(event.top_score * 100).toFixed(0)}% confidence`} alt={`${event.label} at ${(event.top_score * 100).toFixed(0)}% confidence`}
/> />
</div> </div>
</div> ) : null }
)} </div>
</div> </div>
</div> </div>
)} )}

View File

@@ -62,7 +62,7 @@ export default function Recording({ camera, date, hour = '00', minute = '00', se
description: `${camera} recording @ ${h.hour}:00.`, description: `${camera} recording @ ${h.hour}:00.`,
sources: [ sources: [
{ {
src: `${apiHost}/vod/${year}-${month}/${day}/${h.hour}/${camera}/index.m3u8`, src: `${apiHost}/vod/${year}-${month}/${day}/${h.hour}/${camera}/master.m3u8`,
type: 'application/vnd.apple.mpegurl', type: 'application/vnd.apple.mpegurl',
}, },
], ],
@@ -103,7 +103,7 @@ export default function Recording({ camera, date, hour = '00', minute = '00', se
} }
}, [seekSeconds, playlistIndex]); }, [seekSeconds, playlistIndex]);
if (!recordingsSummary) { if (!recordingsSummary || !recordings) {
return <ActivityIndicator />; return <ActivityIndicator />;
} }
@@ -121,7 +121,7 @@ export default function Recording({ camera, date, hour = '00', minute = '00', se
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.replaceAll('_', ' ')} Recordings</Heading>
<VideoPlayer <VideoPlayer
onReady={(player) => { onReady={(player) => {