mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Add go2rtc and add restream role / live source (#4082)
* Pull go2rtc dependency * Add go2rtc to local services and add to s6 * Add relay controller for go2rtc * Add restream role * Add restream role * Add restream to nginx * Add camera live source config * Disable RTMP by default and use restream * Use go2rtc for camera config * Fix go2rtc move * Start restream on frigate start * Send restream to camera level * Fix restream * Make sure jsmpeg works as expected * Make view rspect live size config * Tweak player options to fit live view * Adjust VideoPlayer to accept live option which disables irrelevant controls * Add multiple options from restream live view * Add base for webrtc option * Setup specific restream modules * Make mp4 the default streaming for now * Expose 8554 for rtsp relay from go2rtc * Formatting * Update docs to suggest new restream method. * Update docs to reflect restream role * Update docs to reflect restream role * Add webrtc player * Improvements to webRTC * Support webrtc * Cleanup * Adjust rtmp test and add restream test * Fix tests * Add restream tests * Add live view docs and show different options * Small docs tweak * Support all stream types * Update to beta 9 of go2rtc * Formatting * Make jsmpeg the default * Support wss if made from https * Support wss if made from https * Use onEffect * Set url outside onEffect * Fix passed deps * Update docs about required host mode * Try memo instead * Close websocket on changing camera * Formatting * Close pc connection * Set video source to null on cleanup * Use full path since go2rtc can't see PATH var * Adjust audio codec to enable browser audio by default * Cleanup stream creation * Add restream tests * Format tests * Mock requests * Adjust paths * Move stream configs to restream * Remove live source * Remove live config * Use live persistence for which view to use on each camera * Fix live sizes * Only use jsmpeg sizes for jsmpeg live * Set max live size * Remove access of live config * Add selector for live view source in web view * Remove RTMP from default list of roles * Update docs * Fix tests * Fix docs for live view modes * make default undefined to avoid race condition * Wait until camera source is loaded to avoid race condition * Fix tests * Add config to go2rtc * Work with config * Set full path for config * Set to use stun * Check for mounted file * Look for frigate-go2rtc * Update docs to reflect webRTC configuration. * Add link to go2rtc config * Update docs to be more clear * Update docs to be more clear * Update format Co-authored-by: Felipe Santos <felipecassiors@gmail.com> * Update live docs * Improve bash startup script * Add option to force audio compatibility * Formatting * Fix mapping * Fix broken link * Update go2rtc version * Get go2rtc webui working * Add support for mse * Remove mp4 option * Undo changes to video player * Update docs for new live view options * Make separate path for mse * Remove unused * Remove mp4 path * Try to get go2rtc proxy working * Try to get go2rtc proxy working * Remove unused callback * Allow websocket on restrea dashboard * Make mse default stream option * Fix mse sizing * don't assume roles is defined * Remove nginx mapping to go2rtc ui Co-authored-by: Felipe Santos <felipecassiors@gmail.com> Co-authored-by: Blake Blackshear <blakeb@blakeshome.com>
This commit is contained in:
parent
b4d4adb75b
commit
d8123d2497
@ -28,6 +28,7 @@ services:
|
||||
- "5000:5000"
|
||||
- "5001:5001"
|
||||
- "8080:8080"
|
||||
- "8554:8554"
|
||||
entrypoint: ["sudo", "/init"]
|
||||
command: /bin/sh -c "while sleep 1000; do :; done"
|
||||
mqtt:
|
||||
|
@ -117,6 +117,12 @@ RUN apt-get -qq update \
|
||||
|
||||
ENV PATH=$PATH:/usr/lib/btbn-ffmpeg/bin
|
||||
|
||||
# install go2rtc
|
||||
RUN wget -O go2rtc "https://github.com/AlexxIT/go2rtc/releases/download/v0.1-beta.10/go2rtc_linux_${TARGETARCH}" \
|
||||
&& chmod +x go2rtc \
|
||||
&& mkdir -p /usr/local/go2rtc/sbin/ \
|
||||
&& mv go2rtc /usr/local/go2rtc/sbin/go2rtc
|
||||
|
||||
COPY --from=nginx /usr/local/nginx/ /usr/local/nginx/
|
||||
|
||||
# get model and labels
|
||||
@ -142,6 +148,8 @@ RUN S6_ARCH="${TARGETARCH}" \
|
||||
|
||||
EXPOSE 5000
|
||||
EXPOSE 1935
|
||||
EXPOSE 8554
|
||||
EXPOSE 8555
|
||||
|
||||
ENTRYPOINT ["/init"]
|
||||
|
||||
|
5
docker/rootfs/etc/services.d/go2rtc/finish
Normal file
5
docker/rootfs/etc/services.d/go2rtc/finish
Normal file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/execlineb -S1
|
||||
if { s6-test ${1} -ne 0 }
|
||||
if { s6-test ${1} -ne 256 }
|
||||
|
||||
s6-svscanctl -t /var/run/s6/services
|
12
docker/rootfs/etc/services.d/go2rtc/run
Normal file
12
docker/rootfs/etc/services.d/go2rtc/run
Normal file
@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
# https://gist.github.com/mohanpedala/1e2ff5661761d3abd0385e8223e16425?permalink_comment_id=3945021
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -f "/config/frigate-go2rtc.yaml" ]]; then
|
||||
CONFIG_PATH=/config/frigate-go2rtc.yaml
|
||||
else
|
||||
CONFIG_PATH=/usr/local/go2rtc/sbin/go2rtc.yaml
|
||||
fi
|
||||
|
||||
exec /usr/local/go2rtc/sbin/go2rtc -config="$CONFIG_PATH"
|
4
docker/rootfs/usr/local/go2rtc/sbin/go2rtc.yaml
Normal file
4
docker/rootfs/usr/local/go2rtc/sbin/go2rtc.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
webrtc:
|
||||
listen: ":8555"
|
||||
candidates:
|
||||
- stun:8555
|
@ -44,6 +44,11 @@ http {
|
||||
keepalive 1024;
|
||||
}
|
||||
|
||||
upstream go2rtc {
|
||||
server 127.0.0.1:1984;
|
||||
keepalive 1024;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 5000;
|
||||
|
||||
@ -165,7 +170,7 @@ http {
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /live/ {
|
||||
location /live/jsmpeg/ {
|
||||
proxy_pass http://jsmpeg/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
@ -173,6 +178,22 @@ http {
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /live/mse/ {
|
||||
proxy_pass http://go2rtc/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /live/webrtc/ {
|
||||
proxy_pass http://go2rtc/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location ~* /api/.*\.(jpg|jpeg|png)$ {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
|
||||
|
@ -10,10 +10,11 @@ Several inputs can be configured for each camera and the role of each input can
|
||||
Each role can only be assigned to one input per camera. The options for roles are as follows:
|
||||
|
||||
| Role | Description |
|
||||
| -------- | ----------------------------------------------------------------------------------------------- |
|
||||
| ---------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| `detect` | Main feed for object detection |
|
||||
| `record` | Saves segments of the video feed based on configuration settings. [docs](/configuration/record) |
|
||||
| `rtmp` | Broadcast as an RTMP feed for other services to consume. [docs](/configuration/rtmp) |
|
||||
| `restream` | Broadcast as RTSP feed and use the full res stream for live view. [docs](/configuration/restream) |
|
||||
| `rtmp` | Deprecated: Broadcast as an RTMP feed for other services to consume. [docs](/configuration/restream) |
|
||||
|
||||
```yaml
|
||||
mqtt:
|
||||
|
@ -19,7 +19,7 @@ cameras:
|
||||
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
|
||||
roles:
|
||||
- detect
|
||||
- rtmp
|
||||
- restream
|
||||
detect:
|
||||
width: 1280
|
||||
height: 720
|
||||
@ -336,19 +336,26 @@ snapshots:
|
||||
person: 15
|
||||
|
||||
# Optional: RTMP configuration
|
||||
# NOTE: RTMP is deprecated in favor of restream
|
||||
# NOTE: Can be overridden at the camera level
|
||||
rtmp:
|
||||
# Optional: Enable the RTMP stream (default: True)
|
||||
enabled: True
|
||||
# Optional: Enable the RTMP stream (default: False)
|
||||
enabled: False
|
||||
|
||||
# Optional: Live stream configuration for WebUI
|
||||
# Optional: Restream configuration
|
||||
# NOTE: Can be overridden at the camera level
|
||||
live:
|
||||
# Optional: Set the height of the live stream. (default: 720)
|
||||
restream:
|
||||
# Optional: Enable the restream (default: True)
|
||||
enabled: True
|
||||
# Optional: Force audio compatibility with browsers (default: shown below)
|
||||
force_audio: False
|
||||
# Optional: jsmpeg stream configuration for WebUI
|
||||
jsmpeg:
|
||||
# Optional: Set the height of the jsmpeg stream. (default: 720)
|
||||
# This must be less than or equal to the height of the detect stream. Lower resolutions
|
||||
# reduce bandwidth required for viewing the live stream. Width is computed to match known aspect ratio.
|
||||
# reduce bandwidth required for viewing the jsmpeg stream. Width is computed to match known aspect ratio.
|
||||
height: 720
|
||||
# Optional: Set the encode quality of the live stream (default: shown below)
|
||||
# Optional: Set the encode quality of the jsmpeg stream (default: shown below)
|
||||
# 1 is the highest quality, and 31 is the lowest. Lower quality feeds utilize less CPU resources.
|
||||
quality: 8
|
||||
|
||||
@ -387,11 +394,12 @@ cameras:
|
||||
# Required: the path to the stream
|
||||
# NOTE: path may include environment variables, which must begin with 'FRIGATE_' and be referenced in {}
|
||||
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
|
||||
# Required: list of roles for this stream. valid values are: detect,record,rtmp
|
||||
# NOTICE: In addition to assigning the record, and rtmp roles,
|
||||
# Required: list of roles for this stream. valid values are: detect,record,restream,rtmp
|
||||
# NOTICE: In addition to assigning the record, restream, and rtmp roles,
|
||||
# they must also be enabled in the camera config.
|
||||
roles:
|
||||
- detect
|
||||
- restream
|
||||
- rtmp
|
||||
# Optional: stream specific global args (default: inherit)
|
||||
# global_args:
|
||||
|
41
docs/docs/configuration/live.md
Normal file
41
docs/docs/configuration/live.md
Normal file
@ -0,0 +1,41 @@
|
||||
---
|
||||
id: live
|
||||
title: Live View
|
||||
---
|
||||
|
||||
Frigate has different live view options, some of which require [restream](restream.md) to be enabled.
|
||||
|
||||
## Live View Options
|
||||
|
||||
Live view options can be selected while viewing the live stream. The options are:
|
||||
|
||||
| Source | Latency | Frame Rate | Resolution | Audio | Requires Restream | Other Limitations |
|
||||
| ------ | ------- | -------------------------------------- | -------------- | ---------------------------- | ----------------- | --------------------- |
|
||||
| jsmpeg | low | same as `detect -> fps`, capped at 10 | same as detect | no | no | none |
|
||||
| mse | low | native | native | yes (depends on audio codec) | yes | none |
|
||||
| webrtc | lowest | native | native | yes (depends on audio codec) | yes | requires extra config |
|
||||
|
||||
### WebRTC extra configuration:
|
||||
|
||||
webRTC works by creating a websocket connection on extra ports. One of the following is required for webRTC to work:
|
||||
* Frigate is run with `network_mode: host` to support automatic UDP port pass through locally and remotely. See https://github.com/AlexxIT/go2rtc#module-webrtc for more details
|
||||
* Frigate is run with `network_mode: bridge` and has:
|
||||
* Router setup to forward port `8555` to port `8555` on the frigate device.
|
||||
* For local webRTC, you will need to create your own go2rtc config:
|
||||
|
||||
```yaml
|
||||
webrtc:
|
||||
listen: ":8555"
|
||||
candidates:
|
||||
- <frigate host ip address>:8555 # <--- enter frigate host IP here
|
||||
- stun:8555
|
||||
```
|
||||
|
||||
and pass that config to frigate via docker or `frigate-go2rtc.yaml` for addon users:
|
||||
|
||||
See https://github.com/AlexxIT/go2rtc#module-webrtc for more details
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- /path/to/your/go2rtc.yaml:/config/frigate-go2rtc.yaml:ro
|
||||
```
|
12
docs/docs/configuration/restream.md
Normal file
12
docs/docs/configuration/restream.md
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
id: restream
|
||||
title: Restream
|
||||
---
|
||||
|
||||
### RTSP
|
||||
|
||||
Frigate can restream your video feed as an RTSP feed for other applications such as Home Assistant to utilize it at `rtsp://<frigate_host>:8554/<camera_name>`. Port 8554 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.
|
||||
|
||||
### RTMP (Deprecated)
|
||||
|
||||
In previous Frigate versions RTMP was used for re-streaming. RTMP has disadvantages however including being incompatible with H.265, high bitrates, and certain audio codecs. RTMP is deprecated and it is recommended to move to the new restream role.
|
@ -1,8 +0,0 @@
|
||||
---
|
||||
id: rtmp
|
||||
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.
|
||||
|
||||
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).
|
@ -17,7 +17,8 @@ module.exports = {
|
||||
"configuration/record",
|
||||
"configuration/snapshots",
|
||||
"configuration/objects",
|
||||
"configuration/rtmp",
|
||||
"configuration/restream",
|
||||
"configuration/live",
|
||||
"configuration/zones",
|
||||
"configuration/birdseye",
|
||||
"configuration/stationary_objects",
|
||||
|
@ -25,6 +25,7 @@ from frigate.object_processing import TrackedObjectProcessor
|
||||
from frigate.output import output_frames
|
||||
from frigate.plus import PlusApi
|
||||
from frigate.record import RecordingCleanup, RecordingMaintainer
|
||||
from frigate.restream import RestreamApi
|
||||
from frigate.stats import StatsEmitter, stats_init
|
||||
from frigate.storage import StorageMaintainer
|
||||
from frigate.version import VERSION
|
||||
@ -163,6 +164,10 @@ class FrigateApp:
|
||||
self.plus_api,
|
||||
)
|
||||
|
||||
def init_restream(self) -> None:
|
||||
self.restream = RestreamApi(self.config)
|
||||
self.restream.add_cameras()
|
||||
|
||||
def init_mqtt(self) -> None:
|
||||
self.mqtt_client = create_mqtt_client(self.config, self.camera_metrics)
|
||||
|
||||
@ -363,6 +368,7 @@ class FrigateApp:
|
||||
self.start_camera_capture_processes()
|
||||
self.init_stats()
|
||||
self.init_web_server()
|
||||
self.init_restream()
|
||||
self.start_mqtt_relay()
|
||||
self.start_event_processor()
|
||||
self.start_event_cleanup()
|
||||
|
@ -403,6 +403,7 @@ class FfmpegConfig(FrigateBaseModel):
|
||||
|
||||
class CameraRoleEnum(str, Enum):
|
||||
record = "record"
|
||||
restream = "restream"
|
||||
rtmp = "rtmp"
|
||||
detect = "detect"
|
||||
|
||||
@ -513,12 +514,22 @@ class CameraMqttConfig(FrigateBaseModel):
|
||||
|
||||
|
||||
class RtmpConfig(FrigateBaseModel):
|
||||
enabled: bool = Field(default=True, title="RTMP restreaming enabled.")
|
||||
enabled: bool = Field(default=False, title="RTMP restreaming enabled.")
|
||||
|
||||
|
||||
class CameraLiveConfig(FrigateBaseModel):
|
||||
height: int = Field(default=720, title="Live camera view height")
|
||||
quality: int = Field(default=8, ge=1, le=31, title="Live camera view quality")
|
||||
class JsmpegStreamConfig(FrigateBaseModel):
|
||||
height: int = Field(default=720, title="Live camera view height.")
|
||||
quality: int = Field(default=8, ge=1, le=31, title="Live camera view quality.")
|
||||
|
||||
|
||||
class RestreamConfig(FrigateBaseModel):
|
||||
enabled: bool = Field(default=True, title="Restreaming enabled.")
|
||||
force_audio: bool = Field(
|
||||
default=False, title="Force audio compatibility with the browser."
|
||||
)
|
||||
jsmpeg: JsmpegStreamConfig = Field(
|
||||
default_factory=JsmpegStreamConfig, title="Jsmpeg Stream Configuration."
|
||||
)
|
||||
|
||||
|
||||
class CameraUiConfig(FrigateBaseModel):
|
||||
@ -544,8 +555,8 @@ class CameraConfig(FrigateBaseModel):
|
||||
rtmp: RtmpConfig = Field(
|
||||
default_factory=RtmpConfig, title="RTMP restreaming configuration."
|
||||
)
|
||||
live: CameraLiveConfig = Field(
|
||||
default_factory=CameraLiveConfig, title="Live playback settings."
|
||||
restream: RestreamConfig = Field(
|
||||
default_factory=RestreamConfig, title="Restreaming configuration."
|
||||
)
|
||||
snapshots: SnapshotsConfig = Field(
|
||||
default_factory=SnapshotsConfig, title="Snapshot configuration."
|
||||
@ -582,7 +593,16 @@ class CameraConfig(FrigateBaseModel):
|
||||
|
||||
# add roles to the input if there is only one
|
||||
if len(config["ffmpeg"]["inputs"]) == 1:
|
||||
config["ffmpeg"]["inputs"][0]["roles"] = ["record", "rtmp", "detect"]
|
||||
has_rtmp = "rtmp" in config["ffmpeg"]["inputs"][0].get("roles", [])
|
||||
|
||||
config["ffmpeg"]["inputs"][0]["roles"] = [
|
||||
"record",
|
||||
"detect",
|
||||
"restream",
|
||||
]
|
||||
|
||||
if has_rtmp:
|
||||
config["ffmpeg"]["inputs"][0]["roles"].append("rtmp")
|
||||
|
||||
super().__init__(**config)
|
||||
|
||||
@ -763,12 +783,12 @@ class FrigateConfig(FrigateBaseModel):
|
||||
snapshots: SnapshotsConfig = Field(
|
||||
default_factory=SnapshotsConfig, title="Global snapshots configuration."
|
||||
)
|
||||
live: CameraLiveConfig = Field(
|
||||
default_factory=CameraLiveConfig, title="Global live configuration."
|
||||
)
|
||||
rtmp: RtmpConfig = Field(
|
||||
default_factory=RtmpConfig, title="Global RTMP restreaming configuration."
|
||||
)
|
||||
restream: RestreamConfig = Field(
|
||||
default_factory=RestreamConfig, title="Global restream configuration."
|
||||
)
|
||||
birdseye: BirdseyeConfig = Field(
|
||||
default_factory=BirdseyeConfig, title="Birdseye configuration."
|
||||
)
|
||||
@ -805,8 +825,8 @@ class FrigateConfig(FrigateBaseModel):
|
||||
"birdseye": ...,
|
||||
"record": ...,
|
||||
"snapshots": ...,
|
||||
"live": ...,
|
||||
"rtmp": ...,
|
||||
"restream": ...,
|
||||
"objects": ...,
|
||||
"motion": ...,
|
||||
"detect": ...,
|
||||
@ -893,6 +913,11 @@ class FrigateConfig(FrigateBaseModel):
|
||||
f"Camera {name} has rtmp enabled, but rtmp is not assigned to an input."
|
||||
)
|
||||
|
||||
if camera_config.restream.enabled and not "restream" in assigned_roles:
|
||||
raise ValueError(
|
||||
f"Camera {name} has restream enabled, but restream is not assigned to an input."
|
||||
)
|
||||
|
||||
# backwards compatibility for retain_days
|
||||
if not camera_config.record.retain_days is None:
|
||||
logger.warning(
|
||||
|
@ -366,15 +366,15 @@ def output_frames(config: FrigateConfig, video_output_queue):
|
||||
|
||||
for camera, cam_config in config.cameras.items():
|
||||
width = int(
|
||||
cam_config.live.height
|
||||
cam_config.restream.jsmpeg.height
|
||||
* (cam_config.frame_shape[1] / cam_config.frame_shape[0])
|
||||
)
|
||||
converters[camera] = FFMpegConverter(
|
||||
cam_config.frame_shape[1],
|
||||
cam_config.frame_shape[0],
|
||||
width,
|
||||
cam_config.live.height,
|
||||
cam_config.live.quality,
|
||||
cam_config.restream.jsmpeg.height,
|
||||
cam_config.restream.jsmpeg.quality,
|
||||
)
|
||||
broadcasters[camera] = BroadcastThread(
|
||||
camera, converters[camera], websocket_server
|
||||
|
44
frigate/restream.py
Normal file
44
frigate/restream.py
Normal file
@ -0,0 +1,44 @@
|
||||
"""Controls go2rtc restream."""
|
||||
|
||||
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from frigate.config import FrigateConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_manual_go2rtc_stream(camera_url: str) -> str:
|
||||
"""Get a manual stream for go2rtc."""
|
||||
return f"exec: /usr/lib/btbn-ffmpeg/bin/ffmpeg -i {camera_url} -c:v copy -c:a libopus -rtsp_transport tcp -f rtsp {{output}}"
|
||||
|
||||
|
||||
class RestreamApi:
|
||||
"""Control go2rtc relay API."""
|
||||
|
||||
def __init__(self, config: FrigateConfig) -> None:
|
||||
self.config: FrigateConfig = config
|
||||
|
||||
def add_cameras(self) -> None:
|
||||
"""Add cameras to go2rtc."""
|
||||
self.relays: dict[str, str] = {}
|
||||
|
||||
for cam_name, camera in self.config.cameras.items():
|
||||
if not camera.restream.enabled:
|
||||
continue
|
||||
|
||||
for input in camera.ffmpeg.inputs:
|
||||
if "restream" in input.roles:
|
||||
if (
|
||||
input.path.startswith("rtsp")
|
||||
and not camera.restream.force_audio
|
||||
):
|
||||
self.relays[cam_name] = input.path
|
||||
else:
|
||||
# go2rtc only supports rtsp for direct relay, otherwise ffmpeg is used
|
||||
self.relays[cam_name] = get_manual_go2rtc_stream(input.path)
|
||||
|
||||
for name, path in self.relays.items():
|
||||
params = {"src": path, "name": name}
|
||||
requests.put("http://127.0.0.1:1984/api/streams", params=params)
|
@ -575,7 +575,7 @@ class TestConfig(unittest.TestCase):
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect", "rtmp"],
|
||||
"roles": ["detect", "rtmp", "restream"],
|
||||
},
|
||||
{"path": "rtsp://10.0.0.1:554/record", "roles": ["record"]},
|
||||
]
|
||||
@ -837,7 +837,7 @@ class TestConfig(unittest.TestCase):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"rtmp": {"enabled": False},
|
||||
"restream": {"enabled": False},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
@ -1050,11 +1050,11 @@ class TestConfig(unittest.TestCase):
|
||||
assert runtime_config.cameras["back"].snapshots.height == 150
|
||||
assert runtime_config.cameras["back"].snapshots.enabled
|
||||
|
||||
def test_global_rtmp(self):
|
||||
def test_global_restream(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"rtmp": {"enabled": True},
|
||||
"restream": {"enabled": True},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
@ -1072,9 +1072,32 @@ class TestConfig(unittest.TestCase):
|
||||
assert config == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert runtime_config.cameras["back"].rtmp.enabled
|
||||
assert runtime_config.cameras["back"].restream.enabled
|
||||
|
||||
def test_default_rtmp(self):
|
||||
def test_global_rtmp_disabled(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect"],
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
frigate_config = FrigateConfig(**config)
|
||||
assert config == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert not runtime_config.cameras["back"].rtmp.enabled
|
||||
|
||||
def test_default_not_rtmp(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
@ -1095,7 +1118,57 @@ class TestConfig(unittest.TestCase):
|
||||
assert config == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert runtime_config.cameras["back"].rtmp.enabled
|
||||
assert not runtime_config.cameras["back"].rtmp.enabled
|
||||
|
||||
def test_default_restream(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect"],
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
frigate_config = FrigateConfig(**config)
|
||||
assert config == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert runtime_config.cameras["back"].restream.enabled
|
||||
|
||||
def test_global_restream_merge(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"restream": {"enabled": False},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect"],
|
||||
},
|
||||
]
|
||||
},
|
||||
"restream": {
|
||||
"enabled": True,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
frigate_config = FrigateConfig(**config)
|
||||
assert config == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert runtime_config.cameras["back"].restream.enabled
|
||||
|
||||
def test_global_rtmp_merge(self):
|
||||
|
||||
@ -1108,7 +1181,7 @@ class TestConfig(unittest.TestCase):
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect"],
|
||||
"roles": ["detect", "rtmp"],
|
||||
},
|
||||
]
|
||||
},
|
||||
@ -1128,7 +1201,7 @@ class TestConfig(unittest.TestCase):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"rtmp": {"enabled": False},
|
||||
"restream": {"enabled": False},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
@ -1152,11 +1225,11 @@ class TestConfig(unittest.TestCase):
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert not runtime_config.cameras["back"].rtmp.enabled
|
||||
|
||||
def test_global_live(self):
|
||||
def test_global_jsmpeg(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"live": {"quality": 4},
|
||||
"restream": {"jsmpeg": {"quality": 4}},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
@ -1174,7 +1247,7 @@ class TestConfig(unittest.TestCase):
|
||||
assert config == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert runtime_config.cameras["back"].live.quality == 4
|
||||
assert runtime_config.cameras["back"].restream.jsmpeg.quality == 4
|
||||
|
||||
def test_default_live(self):
|
||||
|
||||
@ -1197,13 +1270,13 @@ class TestConfig(unittest.TestCase):
|
||||
assert config == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert runtime_config.cameras["back"].live.quality == 8
|
||||
assert runtime_config.cameras["back"].restream.jsmpeg.quality == 8
|
||||
|
||||
def test_global_live_merge(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"live": {"quality": 4, "height": 480},
|
||||
"restream": {"jsmpeg": {"quality": 4, "height": 480}},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
@ -1214,8 +1287,10 @@ class TestConfig(unittest.TestCase):
|
||||
},
|
||||
]
|
||||
},
|
||||
"live": {
|
||||
"restream": {
|
||||
"jsmpeg": {
|
||||
"quality": 7,
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
@ -1224,8 +1299,8 @@ class TestConfig(unittest.TestCase):
|
||||
assert config == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert runtime_config.cameras["back"].live.quality == 7
|
||||
assert runtime_config.cameras["back"].live.height == 480
|
||||
assert runtime_config.cameras["back"].restream.jsmpeg.quality == 7
|
||||
assert runtime_config.cameras["back"].restream.jsmpeg.height == 480
|
||||
|
||||
def test_global_timestamp_style(self):
|
||||
|
||||
|
64
frigate/test/test_restream.py
Normal file
64
frigate/test/test_restream.py
Normal file
@ -0,0 +1,64 @@
|
||||
"""Test restream.py."""
|
||||
|
||||
from unittest import TestCase, main
|
||||
from unittest.mock import patch
|
||||
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.restream import RestreamApi
|
||||
|
||||
|
||||
class TestRestream(TestCase):
|
||||
def setUp(self) -> None:
|
||||
"""Setup the tests."""
|
||||
self.config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"restream": {"enabled": False},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect", "restream"],
|
||||
},
|
||||
]
|
||||
},
|
||||
"restream": {
|
||||
"enabled": True,
|
||||
},
|
||||
},
|
||||
"front": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": "http://10.0.0.1:554/video/stream",
|
||||
"roles": ["detect", "restream"],
|
||||
},
|
||||
]
|
||||
},
|
||||
"restream": {
|
||||
"enabled": True,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@patch("frigate.restream.requests")
|
||||
def test_rtsp_stream(self, mock_requests) -> None:
|
||||
"""Test that the normal rtsp stream is sent plainly."""
|
||||
frigate_config = FrigateConfig(**self.config)
|
||||
restream = RestreamApi(frigate_config)
|
||||
restream.add_cameras()
|
||||
assert restream.relays["back"].startswith("rtsp")
|
||||
|
||||
@patch("frigate.restream.requests")
|
||||
def test_http_stream(self, mock_requests) -> None:
|
||||
"""Test that the http stream is sent via ffmpeg."""
|
||||
frigate_config = FrigateConfig(**self.config)
|
||||
restream = RestreamApi(frigate_config)
|
||||
restream.add_cameras()
|
||||
assert not restream.relays["front"].startswith("rtsp")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(verbosity=2)
|
BIN
test.db-journal
Normal file
BIN
test.db-journal
Normal file
Binary file not shown.
@ -19,7 +19,7 @@ export const handlers = [
|
||||
record: { enabled: true },
|
||||
detect: { width: 1280, height: 720 },
|
||||
snapshots: {},
|
||||
live: { height: 720 },
|
||||
restream: { enabled: true, jsmpeg: { height: 720 } },
|
||||
ui: { dashboard: true, order: 0 },
|
||||
},
|
||||
side: {
|
||||
@ -28,7 +28,7 @@ export const handlers = [
|
||||
record: { enabled: false },
|
||||
detect: { width: 1280, height: 720 },
|
||||
snapshots: {},
|
||||
live: { height: 720 },
|
||||
restream: { enabled: true, jsmpeg: { height: 720 } },
|
||||
ui: { dashboard: true, order: 1 },
|
||||
},
|
||||
},
|
||||
|
@ -5,7 +5,7 @@ import JSMpeg from '@cycjimmy/jsmpeg-player';
|
||||
|
||||
export default function JSMpegPlayer({ camera, width, height }) {
|
||||
const playerRef = useRef();
|
||||
const url = `${baseUrl.replace(/^http/, 'ws')}live/${camera}`;
|
||||
const url = `${baseUrl.replace(/^http/, 'ws')}live/jsmpeg/${camera}`;
|
||||
|
||||
useEffect(() => {
|
||||
const video = new JSMpeg.VideoElement(
|
||||
|
93
web/src/components/MsePlayer.jsx
Normal file
93
web/src/components/MsePlayer.jsx
Normal file
@ -0,0 +1,93 @@
|
||||
import { h } from 'preact';
|
||||
import { baseUrl } from '../api/baseUrl';
|
||||
import { useEffect } from 'preact/hooks';
|
||||
|
||||
export default function MsePlayer({ camera, width, height }) {
|
||||
const url = `${baseUrl.replace(/^http/, 'ws')}live/mse/api/ws?src=${camera}`;
|
||||
|
||||
useEffect(() => {
|
||||
const video = document.querySelector('#video');
|
||||
|
||||
// support api_path
|
||||
const ws = new WebSocket(url);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
|
||||
let mediaSource;
|
||||
|
||||
ws.onopen = () => {
|
||||
// https://web.dev/i18n/en/fast-playback-with-preload/#manual_buffering
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API
|
||||
mediaSource = new MediaSource();
|
||||
video.src = URL.createObjectURL(mediaSource);
|
||||
mediaSource.onsourceopen = () => {
|
||||
mediaSource.onsourceopen = null;
|
||||
URL.revokeObjectURL(video.src);
|
||||
ws.send(JSON.stringify({ type: 'mse' }));
|
||||
};
|
||||
};
|
||||
|
||||
let sourceBuffer,
|
||||
queueBuffer = [];
|
||||
|
||||
ws.onmessage = (ev) => {
|
||||
if (typeof ev.data === 'string') {
|
||||
const data = JSON.parse(ev.data);
|
||||
|
||||
if (data.type === 'mse') {
|
||||
sourceBuffer = mediaSource.addSourceBuffer(data.value);
|
||||
// important: segments supports TrackFragDecodeTime
|
||||
// sequence supports only TrackFragRunEntry Duration
|
||||
sourceBuffer.mode = 'segments';
|
||||
sourceBuffer.onupdateend = () => {
|
||||
if (!sourceBuffer.updating && queueBuffer.length > 0) {
|
||||
sourceBuffer.appendBuffer(queueBuffer.shift());
|
||||
}
|
||||
};
|
||||
}
|
||||
} else if (sourceBuffer.updating) {
|
||||
queueBuffer.push(ev.data);
|
||||
} else {
|
||||
sourceBuffer.appendBuffer(ev.data);
|
||||
}
|
||||
};
|
||||
|
||||
let offsetTime = 1,
|
||||
noWaiting = 0;
|
||||
|
||||
setInterval(() => {
|
||||
if (video.paused || video.seekable.length === 0) return;
|
||||
|
||||
if (noWaiting < 0) {
|
||||
offsetTime = Math.min(offsetTime * 1.1, 5);
|
||||
} else if (noWaiting >= 30) {
|
||||
noWaiting = 0;
|
||||
offsetTime = Math.max(offsetTime * 0.9, 0.5);
|
||||
}
|
||||
noWaiting += 1;
|
||||
|
||||
const endTime = video.seekable.end(video.seekable.length - 1);
|
||||
let playbackRate = (endTime - video.currentTime) / offsetTime;
|
||||
if (playbackRate < 0.1) {
|
||||
// video.currentTime = endTime - offsetTime;
|
||||
playbackRate = 0.1;
|
||||
} else if (playbackRate > 10) {
|
||||
// video.currentTime = endTime - offsetTime;
|
||||
playbackRate = 10;
|
||||
}
|
||||
// https://github.com/GoogleChrome/developer.chrome.com/issues/135
|
||||
video.playbackRate = playbackRate;
|
||||
}, 1000);
|
||||
|
||||
video.onwaiting = () => {
|
||||
const endTime = video.seekable.end(video.seekable.length - 1);
|
||||
video.currentTime = endTime - offsetTime;
|
||||
noWaiting = -1;
|
||||
};
|
||||
}, [url]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<video id="video" autoplay playsinline controls muted width={width} height={height} />
|
||||
</div>
|
||||
);
|
||||
}
|
72
web/src/components/WebRtcPlayer.jsx
Normal file
72
web/src/components/WebRtcPlayer.jsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { h } from 'preact';
|
||||
import { baseUrl } from '../api/baseUrl';
|
||||
import { useEffect } from 'preact/hooks';
|
||||
|
||||
export default function WebRtcPlayer({ camera, width, height }) {
|
||||
const url = `${baseUrl.replace(/^http/, 'ws')}live/webrtc/api/ws?src=${camera}`;
|
||||
|
||||
useEffect(() => {
|
||||
const ws = new WebSocket(url);
|
||||
ws.onopen = () => {
|
||||
pc.createOffer().then((offer) => {
|
||||
pc.setLocalDescription(offer).then(() => {
|
||||
const msg = { type: 'webrtc/offer', value: pc.localDescription.sdp };
|
||||
ws.send(JSON.stringify(msg));
|
||||
});
|
||||
});
|
||||
};
|
||||
ws.onmessage = (ev) => {
|
||||
const msg = JSON.parse(ev.data);
|
||||
|
||||
if (msg.type === 'webrtc/candidate') {
|
||||
pc.addIceCandidate({ candidate: msg.value, sdpMid: '' });
|
||||
} else if (msg.type === 'webrtc/answer') {
|
||||
pc.setRemoteDescription({ type: 'answer', sdp: msg.value });
|
||||
}
|
||||
};
|
||||
|
||||
const pc = new RTCPeerConnection({
|
||||
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
|
||||
});
|
||||
pc.onicecandidate = (ev) => {
|
||||
if (ev.candidate !== null) {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'webrtc/candidate',
|
||||
value: ev.candidate.toJSON().candidate,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
pc.ontrack = (ev) => {
|
||||
const video = document.getElementById('video');
|
||||
|
||||
// when audio track not exist in Chrome
|
||||
if (ev.streams.length === 0) return;
|
||||
// when audio track not exist in Firefox
|
||||
if (ev.streams[0].id[0] === '{') return;
|
||||
// when stream already init
|
||||
if (video.srcObject !== null) return;
|
||||
|
||||
video.srcObject = ev.streams[0];
|
||||
};
|
||||
|
||||
// Safari don't support "offerToReceiveVideo"
|
||||
// so need to create transeivers manually
|
||||
pc.addTransceiver('video', { direction: 'recvonly' });
|
||||
pc.addTransceiver('audio', { direction: 'recvonly' });
|
||||
|
||||
return () => {
|
||||
const video = document.getElementById('video');
|
||||
video.srcObject = null;
|
||||
pc.close();
|
||||
ws.close();
|
||||
};
|
||||
}, [url]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<video id="video" autoplay playsinline controls muted width={width} height={height} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -13,6 +13,8 @@ import { usePersistence } from '../context';
|
||||
import { useCallback, useMemo, useState } from 'preact/hooks';
|
||||
import { useApiHost } from '../api';
|
||||
import useSWR from 'swr';
|
||||
import WebRtcPlayer from '../components/WebRtcPlayer';
|
||||
import MsePlayer from '../components/MsePlayer';
|
||||
|
||||
const emptyObject = Object.freeze({});
|
||||
|
||||
@ -23,9 +25,11 @@ export default function Camera({ camera }) {
|
||||
const [viewMode, setViewMode] = useState('live');
|
||||
|
||||
const cameraConfig = config?.cameras[camera];
|
||||
const liveWidth = cameraConfig
|
||||
? Math.round(cameraConfig.live.height * (cameraConfig.detect.width / cameraConfig.detect.height))
|
||||
const jsmpegWidth = cameraConfig
|
||||
? Math.round(cameraConfig.restream.jsmpeg.height * (cameraConfig.detect.width / cameraConfig.detect.height))
|
||||
: 0;
|
||||
const [viewSource, setViewSource, sourceIsLoaded] = usePersistence(`${camera}-source`, 'mse');
|
||||
const sourceValues = cameraConfig && cameraConfig.restream.enabled ? ['mse', 'webrtc', 'jsmpeg'] : ['mse'];
|
||||
const [options, setOptions] = usePersistence(`${camera}-feed`, emptyObject);
|
||||
|
||||
const handleSetOption = useCallback(
|
||||
@ -51,7 +55,7 @@ export default function Camera({ camera }) {
|
||||
setShowSettings(!showSettings);
|
||||
}, [showSettings, setShowSettings]);
|
||||
|
||||
if (!cameraConfig) {
|
||||
if (!cameraConfig || !sourceIsLoaded) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
|
||||
@ -93,13 +97,31 @@ export default function Camera({ camera }) {
|
||||
|
||||
let player;
|
||||
if (viewMode === 'live') {
|
||||
if (viewSource == 'mse') {
|
||||
player = (
|
||||
<Fragment>
|
||||
<div>
|
||||
<JSMpegPlayer camera={camera} width={liveWidth} height={cameraConfig.live.height} />
|
||||
<div className="max-w-5xl">
|
||||
<MsePlayer camera={camera} />
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
} else if (viewSource == 'webrtc') {
|
||||
player = (
|
||||
<Fragment>
|
||||
<div className="max-w-5xl">
|
||||
<WebRtcPlayer camera={camera} />
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
player = (
|
||||
<Fragment>
|
||||
<div>
|
||||
<JSMpegPlayer camera={camera} width={jsmpegWidth} height={cameraConfig.restream.jsmpeg.height} />
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
} else if (viewMode === 'debug') {
|
||||
player = (
|
||||
<Fragment>
|
||||
@ -120,7 +142,23 @@ export default function Camera({ camera }) {
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-2 px-4">
|
||||
<Heading size="2xl">{camera.replaceAll('_', ' ')}</Heading>
|
||||
<div className="flex justify-between">
|
||||
<Heading className="p-2" size="2xl">
|
||||
{camera.replaceAll('_', ' ')}
|
||||
</Heading>
|
||||
<select
|
||||
className="basis-1/8 cursor-pointer rounded dark:bg-slate-800"
|
||||
value={viewSource}
|
||||
onChange={(e) => setViewSource(e.target.value)}
|
||||
>
|
||||
{sourceValues.map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<ButtonsTabbed viewModes={['live', 'debug']} setViewMode={setViewMode} />
|
||||
|
||||
{player}
|
||||
|
@ -11,7 +11,7 @@ describe('Camera Route', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
mockSetOptions = jest.fn();
|
||||
mockUsePersistence = jest.spyOn(Context, 'usePersistence').mockImplementation(() => [{}, mockSetOptions]);
|
||||
mockUsePersistence = jest.spyOn(Context, 'usePersistence').mockImplementation(() => [{}, mockSetOptions, true]);
|
||||
jest.spyOn(AutoUpdatingCameraImage, 'default').mockImplementation(({ searchParams }) => {
|
||||
return <div data-testid="mock-image">{searchParams.toString()}</div>;
|
||||
});
|
||||
@ -32,11 +32,12 @@ describe('Camera Route', () => {
|
||||
regions: false,
|
||||
},
|
||||
mockSetOptions,
|
||||
true,
|
||||
]);
|
||||
|
||||
render(<Camera camera="front" />);
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading…'));
|
||||
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading…'), { timeout: 10 });
|
||||
|
||||
fireEvent.click(screen.queryByText('Debug'));
|
||||
fireEvent.click(screen.queryByText('Show Options'));
|
||||
@ -47,15 +48,20 @@ describe('Camera Route', () => {
|
||||
|
||||
test('updates camera feed options to persistence', async () => {
|
||||
mockUsePersistence
|
||||
.mockReturnValueOnce([{}, mockSetOptions])
|
||||
.mockReturnValueOnce([{}, mockSetOptions])
|
||||
.mockReturnValueOnce([{}, mockSetOptions])
|
||||
.mockReturnValueOnce([{ bbox: true }, mockSetOptions])
|
||||
.mockReturnValueOnce([{ bbox: true, timestamp: true }, mockSetOptions]);
|
||||
.mockReturnValueOnce([{}, mockSetOptions, true])
|
||||
.mockReturnValueOnce([{}, mockSetOptions, true])
|
||||
.mockReturnValueOnce([{}, mockSetOptions, true])
|
||||
.mockReturnValueOnce([{}, mockSetOptions, true])
|
||||
.mockReturnValueOnce([{}, mockSetOptions, true])
|
||||
.mockReturnValueOnce([{}, mockSetOptions, true])
|
||||
.mockReturnValueOnce([{}, mockSetOptions, true])
|
||||
.mockReturnValueOnce([{}, mockSetOptions, true])
|
||||
.mockReturnValueOnce([{ bbox: true }, mockSetOptions, true])
|
||||
.mockReturnValueOnce([{ bbox: true, timestamp: true }, mockSetOptions, true]);
|
||||
|
||||
render(<Camera camera="front" />);
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading…'));
|
||||
await waitForElementToBeRemoved(() => screen.queryByLabelText('Loading…'), { timeout: 10 });
|
||||
|
||||
fireEvent.click(screen.queryByText('Debug'));
|
||||
fireEvent.click(screen.queryByText('Show Options'));
|
||||
@ -63,9 +69,8 @@ describe('Camera Route', () => {
|
||||
fireEvent.change(screen.queryByTestId('timestamp-input'), { target: { checked: true } });
|
||||
fireEvent.click(screen.queryByText('Hide Options'));
|
||||
|
||||
expect(mockUsePersistence).toHaveBeenCalledTimes(5);
|
||||
expect(mockUsePersistence).toHaveBeenCalledTimes(10);
|
||||
expect(mockSetOptions).toHaveBeenCalledTimes(2);
|
||||
expect(mockSetOptions).toHaveBeenCalledWith({ bbox: true, timestamp: true });
|
||||
expect(screen.queryByTestId('mock-image')).toHaveTextContent('bbox=1×tamp=1');
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user