forked from Github/frigate
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:
@@ -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": {
|
||||
"quality": 7,
|
||||
"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)
|
||||
Reference in New Issue
Block a user