Compare commits

...

14 Commits

Author SHA1 Message Date
Blake Blackshear
7c60753ab0 Merge remote-tracking branch 'origin/master' into release-0.11.0 2022-09-22 18:42:20 -05:00
Nicolas Mowen
df40b96b44 BUG: Fixes and cleanup around region / bounding box calculation (#3879)
* -1 so ensure indexes are correct

* Catch case of zero division

* Due to the -1, it may be negative

* Ignore source of error

The error is occurring due to a detections bounding box starting beyond the frame, this should be immediately ignored

* Formatting

* Check horizontal placement as well

* Remove original frame clamping
2022-09-22 08:07:16 -05:00
Aksel Skauge Mellbye
faf583451f Recording: Fix z-index issue with recording playlist (#3762) 2022-09-20 06:05:43 -05:00
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
Blake Blackshear
0882e4a454 update recommended hardware links 2022-09-06 20:59:23 -05:00
Nicolas Mowen
699bd3748a Add note about special characters in passwords (#3719) 2022-09-06 20:51:49 -05:00
Nicolas Mowen
2ca59f0abe Fix stalebot (#3731)
* Reconfigure stalebot with new stale action

* remove old stalebot configuration
2022-09-06 20:51:29 -05:00
Nicolas Mowen
64b1b8e15c Add support request template for edgetpu and hwaccel 2022-08-28 16:58:43 -06:00
Nick Mowen
b6f799e641 Add camera specific for unify protect cameras 2022-08-28 11:14:01 -05:00
14 changed files with 327 additions and 80 deletions

View File

@@ -0,0 +1,84 @@
name: EdgeTpu Support Request
description: Support for setting up EdgeTPU in Frigate
title: "[EdgeTPU Support]: "
labels: ["support", "triage"]
assignees: []
body:
- type: textarea
id: description
attributes:
label: Describe the problem you are having
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: Visible on the Debug page in the Web UI
validations:
required: true
- type: textarea
id: config
attributes:
label: Frigate config file
description: This will be automatically formatted into code, so no need for backticks.
render: yaml
validations:
required: true
- type: textarea
id: docker
attributes:
label: docker-compose file or Docker CLI command
description: This will be automatically formatted into code, so no need for backticks.
render: yaml
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
validations:
required: true
- type: dropdown
id: os
attributes:
label: Operating system
options:
- HassOS
- Debian
- Other Linux
- Proxmox
- UNRAID
- Windows
- Other
validations:
required: true
- type: dropdown
id: install-method
attributes:
label: Install method
options:
- HassOS Addon
- Docker Compose
- Docker CLI
validations:
required: true
- type: dropdown
id: coral
attributes:
label: Coral version
options:
- USB
- PCIe
- M.2
- Dev Board
- Other
- CPU (no coral)
validations:
required: true
- type: textarea
id: other
attributes:
label: Any other information that may be helpful

View File

@@ -0,0 +1,96 @@
name: Hardware Acceleration Support Request
description: Support for setting up GPU hardware acceleration in Frigate
title: "[HW Accel Support]: "
labels: ["support", "triage"]
assignees: []
body:
- type: textarea
id: description
attributes:
label: Describe the problem you are having
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: Visible on the Debug page in the Web UI
validations:
required: true
- type: textarea
id: config
attributes:
label: Frigate config file
description: This will be automatically formatted into code, so no need for backticks.
render: yaml
validations:
required: true
- type: textarea
id: docker
attributes:
label: docker-compose file or Docker CLI command
description: This will be automatically formatted into code, so no need for backticks.
render: yaml
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
validations:
required: true
- type: textarea
id: ffprobe
attributes:
label: FFprobe output from your camera
description: Run `ffprobe <camera_url>` and provide output below
render: shell
validations:
required: true
- type: dropdown
id: os
attributes:
label: Operating system
options:
- HassOS
- Debian
- Other Linux
- Proxmox
- UNRAID
- Windows
- Other
validations:
required: true
- type: dropdown
id: install-method
attributes:
label: Install method
options:
- HassOS Addon
- Docker Compose
- Docker CLI
validations:
required: true
- type: dropdown
id: network
attributes:
label: Network connection
options:
- Wired
- Wireless
- Mixed
validations:
required: true
- type: input
id: camera
attributes:
label: Camera make and model
description: Dahua, hikvision, amcrest, reolink, etc and model number
validations:
required: true
- type: textarea
id: other
attributes:
label: Any other information that may be helpful

17
.github/stale.yml vendored
View File

@@ -1,17 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 30
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 3
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

25
.github/workflows/stale.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
# Close Stale Issues
# Warns and then closes issues and PRs that have had no activity for a specified amount of time.
# https://github.com/actions/stale
name: "Stalebot"
on:
schedule:
- cron: "0 0 * * *" # run stalebot once a day
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@main
id: stale
with:
stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.'
close-issue-message: ''
days-before-stale: 30
days-before-close: 3
exempt-draft-pr: true
exempt-issue-labels: 'pinned,security'
exempt-pr-labels: 'pinned,security'
- name: Print outputs
run: echo ${{ join(steps.stale.outputs.*, ',') }}

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

@@ -112,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

View File

@@ -45,6 +45,12 @@ More details on available detectors can be found [here](/configuration/detectors
Now let's add the first camera: Now let's add the first camera:
:::caution
Note that passwords that contain special characters often cause issues with ffmpeg connecting to the cameara. If recieving `end-of-file` or `unauthorized` errors with a verified correct password, try changing the password to something simple to rule out the possibility that the password is the issue.
:::
```yaml ```yaml
mqtt: mqtt:
host: <ip of your mqtt server> host: <ip of your mqtt server>

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 |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| 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. | | Odyssey X86 Blue J4125 (<a href="https://amzn.to/3oH4BKi" target="_blank" rel="nofollow noopener sponsored">Amazon</a>) (<a href="https://www.seeedstudio.com/Frigate-NVR-with-Odyssey-Blue-and-Coral-USB-Accelerator.html?utm_source=Frigate" target="_blank" rel="nofollow noopener sponsored">SeeedStudio</a>) | 9-10ms | M.2 B+M, 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. | | 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. |
| 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. | | 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. |
| 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. | | 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. |
| 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. | | 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. |
| 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. | | 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. |
| 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. | | 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

@@ -786,33 +786,38 @@ def recording_clip(camera_name, start_ts, end_ts):
file_name = f"clip_{camera_name}_{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_name}.", 500
response = make_response() response = make_response()
response.headers["Content-Description"] = "File Transfer" response.headers["Content-Description"] = "File Transfer"

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,15 @@ 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 - 1, (box[3] * size) + region[0]))
y_max = int((box[2] * size) + region[1]) y_max = int(min(detect_config.height - 1, (box[2] * size) + region[1]))
# ignore objects that were detected outside the frame
if (x_min >= detect_config.width - 1) or (y_min >= detect_config.height - 1):
continue
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 +631,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,
@@ -650,10 +662,10 @@ def process_frames(
# apply max/min to ensure values do not exceed the known frame size # apply max/min to ensure values do not exceed the known frame size
boxes = [ boxes = [
( (
max(o[2][0], 0), o[2][0],
max(o[2][1], 0), o[2][1],
min(o[2][2] - o[2][0], detect_config.width - 1), o[2][2] - o[2][0],
min(o[2][3] - o[2][1], detect_config.height - 1), o[2][3] - o[2][1],
) )
for o in group for o in group
] ]
@@ -679,6 +691,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

@@ -44,12 +44,12 @@ export default function RecordingPlaylist({ camera, recordings, selectedDate })
<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 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 z-10`}
> >
{active ? <Menu /> : <MenuOpen />} {active ? <Menu /> : <MenuOpen />}
</div> </div>
<div <div
className={`w-full h-full bg-gray-800 bg-opacity-70 border-l overflow-x-hidden overflow-y-auto${ className={`w-full h-full bg-gray-800 bg-opacity-70 border-l overflow-x-hidden overflow-y-auto z-10${
active ? '' : ' hidden' active ? '' : ' hidden'
}`} }`}
> >

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

@@ -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 />;
} }
@@ -495,9 +501,15 @@ 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',
@@ -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>
)} )}