Compare commits

..

23 Commits

Author SHA1 Message Date
Blake Blackshear
37325c70ba fix plus enabled for addons 2022-07-22 08:12:58 -05:00
Blake Blackshear
3c46a33992 revert false warning messages 2022-07-20 06:55:06 -05:00
deviant77
ed1897db71 Add log message when discarding recording segments in cache (#3439)
* Add log message when discarding recording segments in cache

Currently Frigate silently discards recording segments in cache if there's more than "keep_count" for a camera, which can happen on high load/busy/slow systems.
This results in recording segments being lost with no apparent cause in the logs (even when set to debug).
This PR adds a warning log entry when discarding segments, in the same way as discarding corrupted segments

* Add explanatory warning and properly format cache_path warning

* lint fixes

Co-authored-by: Blake Blackshear <blakeb@blakeshome.com>
2022-07-19 07:24:44 -05:00
Caros2017
dfbebb63ff Update hardware_acceleration.md
Added the solution for the problem in https://github.com/blakeblackshear/frigate/issues/3227
2022-07-19 07:06:40 -05:00
JohnMark Sill
a67a768e89 improvement: better play/pause 2022-07-19 07:04:33 -05:00
JohnMark Sill
43f05c18d6 chore: remove unused import 2022-07-19 07:04:33 -05:00
JohnMark Sill
3b076c28c2 chore: removed unused properties interface 2022-07-19 07:04:33 -05:00
JohnMark Sill
cbf12e3f90 fix: removed unused state 2022-07-19 07:04:33 -05:00
JohnMark Sill
17b745434c improvement: migrated to videojs 2022-07-19 07:04:33 -05:00
JohnMark Sill
37011c2fda improvement: use useCallback instead of setting ref auto-magically 2022-07-19 07:04:33 -05:00
JohnMark Sill
fa95a041dd fix: height of video is now constant in history viewer 2022-07-19 07:04:33 -05:00
JohnMark Sill
0879d7a2d1 fix: marker time image 2022-07-19 07:04:33 -05:00
Nick Mowen
061fb15a80 Reset motion to false on startup 2022-07-19 06:59:20 -05:00
Blake Blackshear
3246fcce22 document buildx setup 2022-07-19 06:56:23 -05:00
Blake Blackshear
f2a3797b46 add additional render group 2022-07-19 06:44:11 -05:00
Blake Blackshear
b80080ac52 don't refetch data on refocus 2022-07-07 07:05:05 -05:00
Blake Blackshear
b36b63599b update stimeout to timeout 2022-07-05 11:55:55 -05:00
Blake Blackshear
5d8c0e43c2 try ffmpeg5 again 2022-07-05 11:54:21 -05:00
Josh Hawkins
7845995dfd Adjust threshold and contour_area with mqtt 2022-07-05 08:46:10 -05:00
Blake Blackshear
afe88d6e3a switch back to upgraded numpy 2022-07-04 16:51:48 -05:00
Blake Blackshear
560ee0104d arm32 compat 2022-07-04 09:06:26 -05:00
Blake Blackshear
dc8b625d55 sync numpy version 2022-07-03 10:12:14 -05:00
Blake Blackshear
162c0147d2 use jellyfin for all arch 2022-07-03 10:12:00 -05:00
21 changed files with 258 additions and 123 deletions

View File

@@ -6,6 +6,7 @@ services:
# add groups from host for render, plugdev, video
group_add:
- "109" # render
- "110" # render
- "44" # video
- "46" # plugdev
shm_size: "256mb"

View File

@@ -11,8 +11,8 @@ RUN apt-get -qq update \
apt-transport-https \
gnupg \
wget \
&& wget -O - http://archive.raspberrypi.org/debian/raspberrypi.gpg.key | apt-key add - \
&& echo "deb http://archive.raspberrypi.org/debian/ bullseye main" | tee /etc/apt/sources.list.d/raspi.list \
&& apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 9165938D90FDDD2E \
&& echo "deb http://raspbian.raspberrypi.org/raspbian/ bullseye main contrib non-free rpi" | tee /etc/apt/sources.list.d/raspi.list \
&& apt-get -qq update \
&& apt-get -qq install -y \
python3 \
@@ -46,7 +46,7 @@ RUN pip3 wheel --wheel-dir=/wheels -r requirements-wheels.txt
FROM debian:11-slim
ARG TARGETARCH
ARG JELLYFIN_FFMPEG_VERSION=4.4.1-4
ARG JELLYFIN_FFMPEG_VERSION=5.0.1-7
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
# http://stackoverflow.com/questions/48162574/ddg#49462622
@@ -67,8 +67,8 @@ RUN apt-get -qq update \
unzip tzdata libxml2 xz-utils \
python3-pip \
# add raspberry pi repo
&& wget -O - http://archive.raspberrypi.org/debian/raspberrypi.gpg.key | apt-key add - \
&& echo "deb http://archive.raspberrypi.org/debian/ bullseye main" | tee /etc/apt/sources.list.d/raspi.list \
&& apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 9165938D90FDDD2E \
&& echo "deb http://raspbian.raspberrypi.org/raspbian/ bullseye main contrib non-free rpi" | tee /etc/apt/sources.list.d/raspi.list \
# add coral repo
&& apt-key adv --fetch-keys https://packages.cloud.google.com/apt/doc/apt-key.gpg \
&& echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" > /etc/apt/sources.list.d/coral-edgetpu.list \
@@ -80,15 +80,24 @@ RUN apt-get -qq update \
# coral drivers
libedgetpu1-max python3-tflite-runtime python3-pycoral \
&& pip3 install -U /wheels/*.whl \
# jellyfin-ffmpeg
&& 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" \
&& apt-get -qq install --no-install-recommends --no-install-suggests -y ./jellyfin.deb \
&& rm jellyfin.deb \
# arch specific packages
&& if [ "${TARGETARCH}" = "amd64" ]; then \
# jellyfin-ffmpeg
wget -O jellyfin.deb "https://repo.jellyfin.org/releases/server/debian/versions/jellyfin-ffmpeg/${JELLYFIN_FFMPEG_VERSION}/jellyfin-ffmpeg_${JELLYFIN_FFMPEG_VERSION}-$( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release )_$( dpkg --print-architecture ).deb" \
&& apt-get -qq install --no-install-recommends --no-install-suggests -y \
mesa-va-drivers intel-media-va-driver-non-free ./jellyfin.deb \
&& rm jellyfin.deb; else \
apt-get -qq install --no-install-recommends --no-install-suggests -y \
ffmpeg; \
mesa-va-drivers intel-media-va-driver-non-free; \
fi \
# not sure why 32bit arm requires all these
&& if [ "${TARGETARCH}" = "arm" ]; then \
apt-get -qq install --no-install-recommends --no-install-suggests -y \
libgtk-3-dev \
libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \
libxvidcore-dev libx264-dev libjpeg-dev libpng-dev libtiff-dev \
gfortran openexr libatlas-base-dev libssl-dev\
libtbb2 libtbb-dev libdc1394-22-dev libopenexr-dev \
libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev; \
fi \
&& rm -rf /wheels \
&& apt-get remove gnupg apt-transport-https -y \

View File

@@ -102,7 +102,7 @@ You will need to remove `nobuffer` flag for Blue Iris RTSP cameras
```yaml
ffmpeg:
input_args: -avoid_negative_ts make_zero -flags low_delay -strict experimental -fflags +genpts+discardcorrupt -rtsp_transport tcp -stimeout 5000000 -use_wallclock_as_timestamps 1
input_args: -avoid_negative_ts make_zero -flags low_delay -strict experimental -fflags +genpts+discardcorrupt -rtsp_transport tcp -timeout 5000000 -use_wallclock_as_timestamps 1
```
### UDP Only Cameras
@@ -111,5 +111,5 @@ If your cameras do not support TCP connections for RTSP, you can use UDP.
```yaml
ffmpeg:
input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport udp -stimeout 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
```

View File

@@ -21,6 +21,7 @@ ffmpeg:
ffmpeg:
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.
### Intel-based CPUs (>=10th Generation) via Quicksync

View File

@@ -135,7 +135,7 @@ ffmpeg:
# NOTE: See hardware acceleration docs for your specific device
hwaccel_args: []
# Optional: global input args (default: shown below)
input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport tcp -stimeout 5000000 -use_wallclock_as_timestamps 1
input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport tcp -timeout 5000000 -use_wallclock_as_timestamps 1
# Optional: global output args
output_args:
# Optional: output args for detect streams (default: shown below)

View File

@@ -208,3 +208,16 @@ npm run build
```
This command generates static content into the `build` directory and can be served using any static contents hosting service.
## Official builds
Setup buildx for multiarch
```
docker buildx stop builder && docker buildx rm builder # <---- if existing
docker run --privileged --rm tonistiigi/binfmt --install all
docker buildx create --name builder --driver docker-container --driver-opt network=host --use
docker buildx inspect builder --bootstrap
make build_web
make push
```

View File

@@ -140,3 +140,19 @@ Topic to turn improve_contrast for a camera on and off. Expected values are `ON`
### `frigate/<camera_name>/improve_contrast/state`
Topic with current state of improve_contrast for a camera. Published values are `ON` and `OFF`.
### `frigate/<camera_name>/motion_threshold/set`
Topic to adjust motion threshold for a camera. Expected value is an integer.
### `frigate/<camera_name>/motion_threshold/state`
Topic with current motion threshold for a camera. Published value is an integer.
### `frigate/<camera_name>/motion_contour_area/set`
Topic to adjust motion contour area for a camera. Expected value is an integer.
### `frigate/<camera_name>/motion_contour_area/state`
Topic with current motion contour area for a camera. Published value is an integer.

View File

@@ -95,6 +95,12 @@ class FrigateApp:
"improve_contrast_enabled": mp.Value(
"i", self.config.cameras[camera_name].motion.improve_contrast
),
"motion_threshold": mp.Value(
"i", self.config.cameras[camera_name].motion.threshold
),
"motion_contour_area": mp.Value(
"i", self.config.cameras[camera_name].motion.contour_area
),
"detection_fps": mp.Value("d", 0.0),
"detection_frame": mp.Value("d", 0.0),
"read_start": mp.Value("d", 0.0),

View File

@@ -346,7 +346,7 @@ FFMPEG_INPUT_ARGS_DEFAULT = [
"+genpts+discardcorrupt",
"-rtsp_transport",
"tcp",
"-stimeout",
"-timeout",
"5000000",
"-use_wallclock_as_timestamps",
"1",

View File

@@ -25,7 +25,7 @@ from flask import (
from peewee import SqliteDatabase, operator, fn, DoesNotExist
from playhouse.shortcuts import model_to_dict
from frigate.const import CLIPS_DIR, PLUS_ENV_VAR
from frigate.const import CLIPS_DIR
from frigate.models import Event, Recordings
from frigate.stats import stats_snapshot
from frigate.version import VERSION
@@ -571,7 +571,7 @@ def config():
for cmd in camera_dict["ffmpeg_cmds"]:
cmd["cmd"] = " ".join(cmd["cmd"])
config["plus"] = {"enabled": PLUS_ENV_VAR in os.environ}
config["plus"] = {"enabled": current_app.plus_api.is_active()}
return jsonify(config)

View File

@@ -5,7 +5,14 @@ from frigate.config import MotionConfig
class MotionDetector:
def __init__(self, frame_shape, config: MotionConfig, improve_contrast_enabled):
def __init__(
self,
frame_shape,
config: MotionConfig,
improve_contrast_enabled,
motion_threshold,
motion_contour_area,
):
self.config = config
self.frame_shape = frame_shape
self.resize_factor = frame_shape[0] / config.frame_height
@@ -25,6 +32,8 @@ class MotionDetector:
self.mask = np.where(resized_mask == [0])
self.save_images = False
self.improve_contrast = improve_contrast_enabled
self.threshold = motion_threshold
self.contour_area = motion_contour_area
def detect(self, frame):
motion_boxes = []
@@ -69,7 +78,7 @@ class MotionDetector:
# compute the threshold image for the current frame
current_thresh = cv2.threshold(
frameDelta, self.config.threshold, 255, cv2.THRESH_BINARY
frameDelta, self.threshold.value, 255, cv2.THRESH_BINARY
)[1]
# black out everything in the avg_delta where there isnt motion in the current frame
@@ -79,7 +88,7 @@ class MotionDetector:
# then look for deltas above the threshold, but only in areas where there is a delta
# in the current frame. this prevents deltas from previous frames from being included
thresh = cv2.threshold(
avg_delta_image, self.config.threshold, 255, cv2.THRESH_BINARY
avg_delta_image, self.threshold.value, 255, cv2.THRESH_BINARY
)[1]
# dilate the thresholded image to fill in holes, then find contours
@@ -94,7 +103,7 @@ class MotionDetector:
for c in cnts:
# if the contour is big enough, count it as motion
contour_area = cv2.contourArea(c)
if contour_area > self.config.contour_area:
if contour_area > self.contour_area.value:
x, y, w, h = cv2.boundingRect(c)
motion_boxes.append(
(
@@ -111,8 +120,7 @@ class MotionDetector:
# print(self.frame_counter)
for c in cnts:
contour_area = cv2.contourArea(c)
# print(contour_area)
if contour_area > self.config.contour_area:
if contour_area > self.contour_area.value:
x, y, w, h = cv2.boundingRect(c)
cv2.rectangle(
thresh_dilated,

View File

@@ -145,6 +145,52 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
state_topic = f"{message.topic[:-4]}/state"
client.publish(state_topic, payload, retain=True)
def on_motion_threshold_command(client, userdata, message):
try:
payload = int(message.payload.decode())
except ValueError:
logger.warning(
f"Received unsupported value at {message.topic}: {message.payload.decode()}"
)
return
logger.debug(f"on_motion_threshold_toggle: {message.topic} {payload}")
camera_name = message.topic.split("/")[-3]
motion_settings = config.cameras[camera_name].motion
logger.info(f"Setting motion threshold for {camera_name} via mqtt: {payload}")
camera_metrics[camera_name]["motion_threshold"].value = payload
motion_settings.threshold = payload
state_topic = f"{message.topic[:-4]}/state"
client.publish(state_topic, payload, retain=True)
def on_motion_contour_area_command(client, userdata, message):
try:
payload = int(message.payload.decode())
except ValueError:
logger.warning(
f"Received unsupported value at {message.topic}: {message.payload.decode()}"
)
return
logger.debug(f"on_motion_contour_area_toggle: {message.topic} {payload}")
camera_name = message.topic.split("/")[-3]
motion_settings = config.cameras[camera_name].motion
logger.info(
f"Setting motion contour area for {camera_name} via mqtt: {payload}"
)
camera_metrics[camera_name]["motion_contour_area"].value = payload
motion_settings.contour_area = payload
state_topic = f"{message.topic[:-4]}/state"
client.publish(state_topic, payload, retain=True)
def on_restart_command(client, userdata, message):
restart_frigate()
@@ -195,6 +241,14 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
f"{mqtt_config.topic_prefix}/{name}/improve_contrast/set",
on_improve_contrast_command,
)
client.message_callback_add(
f"{mqtt_config.topic_prefix}/{name}/motion_threshold/set",
on_motion_threshold_command,
)
client.message_callback_add(
f"{mqtt_config.topic_prefix}/{name}/motion_contour_area/set",
on_motion_contour_area_command,
)
client.message_callback_add(
f"{mqtt_config.topic_prefix}/restart", on_restart_command
@@ -250,6 +304,21 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
"ON" if config.cameras[name].motion.improve_contrast else "OFF",
retain=True,
)
client.publish(
f"{mqtt_config.topic_prefix}/{name}/motion_threshold/state",
config.cameras[name].motion.threshold,
retain=True,
)
client.publish(
f"{mqtt_config.topic_prefix}/{name}/motion_contour_area/state",
config.cameras[name].motion.contour_area,
retain=True,
)
client.publish(
f"{mqtt_config.topic_prefix}/{name}/motion",
"OFF",
retain=False,
)
return client

View File

@@ -99,11 +99,23 @@ class RecordingMaintainer(threading.Thread):
# delete all cached files past the most recent 5
keep_count = 5
for camera in grouped_recordings.keys():
if len(grouped_recordings[camera]) > keep_count:
segment_count = len(grouped_recordings[camera])
if segment_count > keep_count:
####
# Need to find a way to tell if these are aging out based on retention settings or if the system is overloaded.
####
# logger.warning(
# f"Too many recording segments in cache for {camera}. Keeping the {keep_count} most recent segments out of {segment_count}, discarding the rest..."
# )
to_remove = grouped_recordings[camera][:-keep_count]
for f in to_remove:
Path(f["cache_path"]).unlink(missing_ok=True)
self.end_time_cache.pop(f["cache_path"], None)
cache_path = f["cache_path"]
####
# Need to find a way to tell if these are aging out based on retention settings or if the system is overloaded.
####
# logger.warning(f"Discarding a recording segment: {cache_path}")
Path(cache_path).unlink(missing_ok=True)
self.end_time_cache.pop(cache_path, None)
grouped_recordings[camera] = grouped_recordings[camera][-keep_count:]
for camera, recordings in grouped_recordings.items():

View File

@@ -13,6 +13,7 @@ from playhouse.shortcuts import model_to_dict
from frigate.config import FrigateConfig
from frigate.http import create_app
from frigate.models import Event, Recordings
from frigate.plus import PlusApi
from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS
@@ -113,7 +114,7 @@ class TestHttp(unittest.TestCase):
def test_get_event_list(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, None
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
id2 = "7890.random"
@@ -142,7 +143,7 @@ class TestHttp(unittest.TestCase):
def test_get_good_event(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, None
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
@@ -156,7 +157,7 @@ class TestHttp(unittest.TestCase):
def test_get_bad_event(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, None
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
bad_id = "654321.other"
@@ -169,7 +170,7 @@ class TestHttp(unittest.TestCase):
def test_delete_event(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, None
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
@@ -184,7 +185,7 @@ class TestHttp(unittest.TestCase):
def test_event_retention(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, None
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
@@ -203,7 +204,7 @@ class TestHttp(unittest.TestCase):
def test_set_delete_sub_label(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, None
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
sub_label = "sub"
@@ -231,7 +232,7 @@ class TestHttp(unittest.TestCase):
def test_sub_label_list(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, None
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
sub_label = "sub"
@@ -253,7 +254,7 @@ class TestHttp(unittest.TestCase):
self.db,
None,
None,
None,
PlusApi(),
)
with app.test_client() as client:
@@ -267,7 +268,7 @@ class TestHttp(unittest.TestCase):
self.db,
None,
None,
None,
PlusApi(),
)
id = "123456.random"
@@ -284,7 +285,7 @@ class TestHttp(unittest.TestCase):
self.db,
None,
None,
None,
PlusApi(),
)
mock_stats.return_value = self.test_stats

View File

@@ -16,6 +16,8 @@ class CameraMetricsTypes(TypedDict):
frame_queue: Queue
motion_enabled: Synchronized
improve_contrast_enabled: Synchronized
motion_threshold: Synchronized
motion_contour_area: Synchronized
process: Optional[Process]
process_fps: Synchronized
read_start: Synchronized

View File

@@ -363,13 +363,19 @@ def track_camera(
detection_enabled = process_info["detection_enabled"]
motion_enabled = process_info["motion_enabled"]
improve_contrast_enabled = process_info["improve_contrast_enabled"]
motion_threshold = process_info["motion_threshold"]
motion_contour_area = process_info["motion_contour_area"]
frame_shape = config.frame_shape
objects_to_track = config.objects.track
object_filters = config.objects.filters
motion_detector = MotionDetector(
frame_shape, config.motion, improve_contrast_enabled
frame_shape,
config.motion,
improve_contrast_enabled,
motion_threshold,
motion_contour_area,
)
object_detector = RemoteObjectDetector(
name, labelmap, detection_queue, result_connection, model_shape

13
web/package-lock.json generated
View File

@@ -30,6 +30,7 @@
"@testing-library/preact": "^2.0.1",
"@testing-library/preact-hooks": "^1.1.0",
"@testing-library/user-event": "^13.5.0",
"@types/video.js": "^7.3.42",
"@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.18.0",
"autoprefixer": "^10.4.2",
@@ -3234,6 +3235,12 @@
"@types/jest": "*"
}
},
"node_modules/@types/video.js": {
"version": "7.3.42",
"resolved": "https://registry.npmjs.org/@types/video.js/-/video.js-7.3.42.tgz",
"integrity": "sha512-AD6AQNMgLTqrgoayC6SshKh8EDkDd9x5pmEuiY9YsniHlhn5jPXdkVqrzKLwviapaRhQF15TQYxo1JWpqXCUBg==",
"dev": true
},
"node_modules/@types/yargs": {
"version": "16.0.4",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
@@ -14504,6 +14511,12 @@
"@types/jest": "*"
}
},
"@types/video.js": {
"version": "7.3.42",
"resolved": "https://registry.npmjs.org/@types/video.js/-/video.js-7.3.42.tgz",
"integrity": "sha512-AD6AQNMgLTqrgoayC6SshKh8EDkDd9x5pmEuiY9YsniHlhn5jPXdkVqrzKLwviapaRhQF15TQYxo1JWpqXCUBg==",
"dev": true
},
"@types/yargs": {
"version": "16.0.4",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",

View File

@@ -32,6 +32,7 @@
"@testing-library/preact": "^2.0.1",
"@testing-library/preact-hooks": "^1.1.0",
"@testing-library/user-event": "^13.5.0",
"@types/video.js": "^7.3.42",
"@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.18.0",
"autoprefixer": "^10.4.2",

View File

@@ -3,17 +3,17 @@ import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import { useApiHost } from '../../api';
import { isNullOrUndefined } from '../../utils/objectUtils';
import 'videojs-seek-buttons';
import 'video.js/dist/video-js.css';
import 'videojs-seek-buttons/dist/videojs-seek-buttons.css';
import videojs, { VideoJsPlayer } from 'video.js';
interface OnTimeUpdateEvent {
timestamp: number;
isPlaying: boolean;
}
interface VideoProperties {
posterUrl: string;
videoUrl: string;
height: number;
}
interface HistoryVideoProps {
id?: string;
isPlaying: boolean;
@@ -32,68 +32,39 @@ export const HistoryVideo = ({
onPlay,
}: HistoryVideoProps) => {
const apiHost = useApiHost();
const videoRef = useRef<HTMLVideoElement|null>(null);
const [videoHeight, setVideoHeight] = useState<number>(0);
const [videoProperties, setVideoProperties] = useState<VideoProperties>({
posterUrl: '',
videoUrl: '',
height: 0,
});
const videoRef = useRef<HTMLVideoElement>(null);
const currentVideo = videoRef.current;
if (currentVideo && !videoHeight) {
const currentVideoHeight = currentVideo.offsetHeight;
if (currentVideoHeight > 0) {
setVideoHeight(currentVideoHeight);
}
}
const [video, setVideo] = useState<VideoJsPlayer>();
useEffect(() => {
const idExists = !isNullOrUndefined(id);
if (idExists) {
if (videoRef.current && !videoRef.current.paused) {
videoRef.current = null;
}
setVideoProperties({
posterUrl: `${apiHost}/api/events/${id}/snapshot.jpg`,
videoUrl: `${apiHost}/vod/event/${id}/index.m3u8`,
height: videoHeight,
});
} else {
setVideoProperties({
posterUrl: '',
videoUrl: '',
height: 0,
});
let video: VideoJsPlayer
if (videoRef.current) {
video = videojs(videoRef.current, {})
setVideo(video)
}
}, [id, videoHeight, videoRef, apiHost]);
() => video?.dispose()
}, [videoRef]);
useEffect(() => {
const playVideo = (video: HTMLMediaElement) => video.play();
const attemptPlayVideo = (video: HTMLMediaElement) => {
const videoHasNotLoaded = video.readyState <= 1;
if (videoHasNotLoaded) {
video.oncanplay = () => {
playVideo(video);
};
video.load();
} else {
playVideo(video);
}
};
const video = videoRef.current;
const videoExists = !isNullOrUndefined(video);
if (video && videoExists) {
if (videoIsPlaying) {
attemptPlayVideo(video);
} else {
video.pause();
}
if (!video) {
return
}
}, [videoIsPlaying, videoRef]);
if (!id) {
video.pause()
return
}
video.src({
src: `${apiHost}/vod/event/${id}/index.m3u8`,
type: 'application/vnd.apple.mpegurl',
});
video.poster(`${apiHost}/api/events/${id}/snapshot.jpg`);
if (videoIsPlaying) {
video.play();
}
}, [video, id]);
useEffect(() => {
const video = videoRef.current;
@@ -111,32 +82,38 @@ export const HistoryVideo = ({
isPlaying: videoIsPlaying,
timestamp: target.currentTime,
};
onTimeUpdate && onTimeUpdate(timeUpdateEvent);
},
[videoIsPlaying, onTimeUpdate]
);
const videoPropertiesIsUndefined = isNullOrUndefined(videoProperties);
if (videoPropertiesIsUndefined) {
return <div style={{ height: `${videoHeight}px`, width: '100%' }} />;
}
useEffect(() => {
if (video && video.readyState() >= 1) {
if (videoIsPlaying) {
video.play()
} else {
video.pause()
}
}
}, [video, videoIsPlaying])
const { posterUrl, videoUrl, height } = videoProperties;
const onLoad = useCallback(() => {
if (video && video.readyState() >= 1 && videoIsPlaying) {
video.play()
}
}, [video, videoIsPlaying])
return (
<video
ref={videoRef}
key={posterUrl}
onTimeUpdate={onTimeUpdateHandler}
onPause={onPause}
onPlay={onPlay}
poster={posterUrl}
preload='metadata'
controls
style={height ? { minHeight: `${height}px` } : {}}
playsInline
>
<source type='application/vnd.apple.mpegurl' src={videoUrl} />
</video>
<div data-vjs-player>
<video
ref={videoRef}
onTimeUpdate={onTimeUpdateHandler}
onLoadedMetadata={onLoad}
onPause={onPause}
onPlay={onPlay}
className="video-js vjs-fluid"
data-setup="{}"
/>
</div>
);
};

View File

@@ -20,11 +20,11 @@ export const TimelineBlocks = ({ timeline, firstBlockOffset, onEventClick }: Tim
const timelineBlockOffset = (timelineContainerHeight - largestYOffsetInBlocks) / 2;
return (
<div
className='relative'
className="relative"
style={{
height: `${timelineContainerHeight}px`,
width: `${timelineContainerWidth}px`,
background: "url('/marker.png')",
background: "url('/images/marker.png')",
backgroundPosition: 'center',
backgroundSize: '30px',
backgroundRepeat: 'repeat',
@@ -41,7 +41,7 @@ export const TimelineBlocks = ({ timeline, firstBlockOffset, onEventClick }: Tim
</div>
);
}
return <div />
return <div />;
}, [timeline, onEventClick, firstBlockOffset]);
return timelineEventBlocks;

View File

@@ -15,13 +15,13 @@ export default function Recording({ camera, date, hour = '00', minute = '00', se
);
const apiHost = useApiHost();
const { data: recordingsSummary } = useSWR(`${camera}/recordings/summary`);
const { data: recordingsSummary } = useSWR(`${camera}/recordings/summary`, { revalidateOnFocus: false });
const recordingParams = {
before: getUnixTime(endOfHour(currentDate)),
after: getUnixTime(startOfHour(currentDate)),
};
const { data: recordings } = useSWR([`${camera}/recordings`, recordingParams]);
const { data: recordings } = useSWR([`${camera}/recordings`, recordingParams], { revalidateOnFocus: false });
// calculates the seek seconds by adding up all the seconds in the segments prior to the playback time
const seekSeconds = useMemo(() => {