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:
Nicolas Mowen
2022-11-02 05:36:09 -06:00
committed by GitHub
parent b4d4adb75b
commit d8123d2497
26 changed files with 614 additions and 86 deletions

View File

@@ -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()

View File

@@ -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(

View File

@@ -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
View 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)

View File

@@ -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):

View 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)