Go2rtc bump and set ffmpeg logs to debug (#130371)

This commit is contained in:
Robert Resch 2024-11-12 11:53:14 +01:00 committed by GitHub
parent 7758d8ba48
commit cb9cc0f801
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 51 additions and 268 deletions

View file

@ -55,7 +55,7 @@ RUN \
"armv7") go2rtc_suffix='arm' ;; \ "armv7") go2rtc_suffix='arm' ;; \
*) go2rtc_suffix=${BUILD_ARCH} ;; \ *) go2rtc_suffix=${BUILD_ARCH} ;; \
esac \ esac \
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.6/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \ && curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.7/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
&& chmod +x /bin/go2rtc \ && chmod +x /bin/go2rtc \
# Verify go2rtc can be executed # Verify go2rtc can be executed
&& go2rtc --version && go2rtc --version

View file

@ -1,8 +1,5 @@
"""The go2rtc component.""" """The go2rtc component."""
from __future__ import annotations
from dataclasses import dataclass
import logging import logging
import shutil import shutil
@ -41,13 +38,7 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey from homeassistant.util.hass_dict import HassKey
from homeassistant.util.package import is_docker_env from homeassistant.util.package import is_docker_env
from .const import ( from .const import CONF_DEBUG_UI, DEBUG_UI_URL_MESSAGE, DOMAIN, HA_MANAGED_URL
CONF_DEBUG_UI,
DEBUG_UI_URL_MESSAGE,
DOMAIN,
HA_MANAGED_RTSP_PORT,
HA_MANAGED_URL,
)
from .server import Server from .server import Server
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -94,22 +85,13 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
_DATA_GO2RTC: HassKey[Go2RtcData] = HassKey(DOMAIN) _DATA_GO2RTC: HassKey[str] = HassKey(DOMAIN)
_RETRYABLE_ERRORS = (ClientConnectionError, ServerConnectionError) _RETRYABLE_ERRORS = (ClientConnectionError, ServerConnectionError)
@dataclass(frozen=True)
class Go2RtcData:
"""Data for go2rtc."""
url: str
managed: bool
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up WebRTC.""" """Set up WebRTC."""
url: str | None = None url: str | None = None
managed = False
if DOMAIN not in config and DEFAULT_CONFIG_DOMAIN not in config: if DOMAIN not in config and DEFAULT_CONFIG_DOMAIN not in config:
await _remove_go2rtc_entries(hass) await _remove_go2rtc_entries(hass)
return True return True
@ -144,9 +126,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop) hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop)
url = HA_MANAGED_URL url = HA_MANAGED_URL
managed = True
hass.data[_DATA_GO2RTC] = Go2RtcData(url, managed) hass.data[_DATA_GO2RTC] = url
discovery_flow.async_create_flow( discovery_flow.async_create_flow(
hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={} hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={}
) )
@ -161,32 +142,28 @@ async def _remove_go2rtc_entries(hass: HomeAssistant) -> None:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up go2rtc from a config entry.""" """Set up go2rtc from a config entry."""
data = hass.data[_DATA_GO2RTC] url = hass.data[_DATA_GO2RTC]
# Validate the server URL # Validate the server URL
try: try:
client = Go2RtcRestClient(async_get_clientsession(hass), data.url) client = Go2RtcRestClient(async_get_clientsession(hass), url)
await client.validate_server_version() await client.validate_server_version()
except Go2RtcClientError as err: except Go2RtcClientError as err:
if isinstance(err.__cause__, _RETRYABLE_ERRORS): if isinstance(err.__cause__, _RETRYABLE_ERRORS):
raise ConfigEntryNotReady( raise ConfigEntryNotReady(
f"Could not connect to go2rtc instance on {data.url}" f"Could not connect to go2rtc instance on {url}"
) from err ) from err
_LOGGER.warning( _LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err)
"Could not connect to go2rtc instance on %s (%s)", data.url, err
)
return False return False
except Go2RtcVersionError as err: except Go2RtcVersionError as err:
raise ConfigEntryNotReady( raise ConfigEntryNotReady(
f"The go2rtc server version is not supported, {err}" f"The go2rtc server version is not supported, {err}"
) from err ) from err
except Exception as err: # noqa: BLE001 except Exception as err: # noqa: BLE001
_LOGGER.warning( _LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err)
"Could not connect to go2rtc instance on %s (%s)", data.url, err
)
return False return False
provider = WebRTCProvider(hass, data) provider = WebRTCProvider(hass, url)
async_register_webrtc_provider(hass, provider) async_register_webrtc_provider(hass, provider)
return True return True
@ -204,12 +181,12 @@ async def _get_binary(hass: HomeAssistant) -> str | None:
class WebRTCProvider(CameraWebRTCProvider): class WebRTCProvider(CameraWebRTCProvider):
"""WebRTC provider.""" """WebRTC provider."""
def __init__(self, hass: HomeAssistant, data: Go2RtcData) -> None: def __init__(self, hass: HomeAssistant, url: str) -> None:
"""Initialize the WebRTC provider.""" """Initialize the WebRTC provider."""
self._hass = hass self._hass = hass
self._data = data self._url = url
self._session = async_get_clientsession(hass) self._session = async_get_clientsession(hass)
self._rest_client = Go2RtcRestClient(self._session, data.url) self._rest_client = Go2RtcRestClient(self._session, url)
self._sessions: dict[str, Go2RtcWsClient] = {} self._sessions: dict[str, Go2RtcWsClient] = {}
@property @property
@ -231,7 +208,7 @@ class WebRTCProvider(CameraWebRTCProvider):
) -> None: ) -> None:
"""Handle the WebRTC offer and return the answer via the provided callback.""" """Handle the WebRTC offer and return the answer via the provided callback."""
self._sessions[session_id] = ws_client = Go2RtcWsClient( self._sessions[session_id] = ws_client = Go2RtcWsClient(
self._session, self._data.url, source=camera.entity_id self._session, self._url, source=camera.entity_id
) )
if not (stream_source := await camera.stream_source()): if not (stream_source := await camera.stream_source()):
@ -242,34 +219,18 @@ class WebRTCProvider(CameraWebRTCProvider):
streams = await self._rest_client.streams.list() streams = await self._rest_client.streams.list()
if self._data.managed: if (stream := streams.get(camera.entity_id)) is None or not any(
# HA manages the go2rtc instance stream_source == producer.url for producer in stream.producers
stream_original_name = f"{camera.entity_id}_original"
stream_redirect_sources = [
f"rtsp://127.0.0.1:{HA_MANAGED_RTSP_PORT}/{stream_original_name}",
f"ffmpeg:{stream_original_name}#audio=opus",
]
if (
(stream_org := streams.get(stream_original_name)) is None
or not any(
stream_source == producer.url for producer in stream_org.producers
)
or (stream_redirect := streams.get(camera.entity_id)) is None
or stream_redirect_sources != [p.url for p in stream_redirect.producers]
):
await self._rest_client.streams.add(stream_original_name, stream_source)
await self._rest_client.streams.add(
camera.entity_id, stream_redirect_sources
)
# go2rtc instance is managed outside HA
elif (stream_org := streams.get(camera.entity_id)) is None or not any(
stream_source == producer.url for producer in stream_org.producers
): ):
await self._rest_client.streams.add( await self._rest_client.streams.add(
camera.entity_id, camera.entity_id,
[stream_source, f"ffmpeg:{camera.entity_id}#audio=opus"], [
stream_source,
# We are setting any ffmpeg rtsp related logs to debug
# Connection problems to the camera will be logged by the first stream
# Therefore setting it to debug will not hide any important logs
f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug",
],
) )
@callback @callback

View file

@ -6,4 +6,3 @@ CONF_DEBUG_UI = "debug_ui"
DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time." DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time."
HA_MANAGED_API_PORT = 11984 HA_MANAGED_API_PORT = 11984
HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/" HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/"
HA_MANAGED_RTSP_PORT = 18554

View file

@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import HA_MANAGED_API_PORT, HA_MANAGED_RTSP_PORT, HA_MANAGED_URL from .const import HA_MANAGED_API_PORT, HA_MANAGED_URL
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_TERMINATE_TIMEOUT = 5 _TERMINATE_TIMEOUT = 5
@ -33,7 +33,7 @@ api:
listen: "{api_ip}:{api_port}" listen: "{api_ip}:{api_port}"
rtsp: rtsp:
listen: "127.0.0.1:{rtsp_port}" listen: "127.0.0.1:18554"
webrtc: webrtc:
listen: ":18555/tcp" listen: ":18555/tcp"
@ -68,9 +68,7 @@ def _create_temp_file(api_ip: str) -> str:
with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file: with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file:
file.write( file.write(
_GO2RTC_CONFIG_FORMAT.format( _GO2RTC_CONFIG_FORMAT.format(
api_ip=api_ip, api_ip=api_ip, api_port=HA_MANAGED_API_PORT
api_port=HA_MANAGED_API_PORT,
rtsp_port=HA_MANAGED_RTSP_PORT,
).encode() ).encode()
) )
return file.name return file.name

View file

@ -112,7 +112,7 @@ LABEL "com.github.actions.icon"="terminal"
LABEL "com.github.actions.color"="gray-dark" LABEL "com.github.actions.color"="gray-dark"
""" """
_GO2RTC_VERSION = "1.9.6" _GO2RTC_VERSION = "1.9.7"
def _get_package_versions(file: Path, packages: set[str]) -> dict[str, str]: def _get_package_versions(file: Path, packages: set[str]) -> dict[str, str]:

View file

@ -3,7 +3,7 @@
from collections.abc import Callable, Generator from collections.abc import Callable, Generator
import logging import logging
from typing import NamedTuple from typing import NamedTuple
from unittest.mock import AsyncMock, Mock, call, patch from unittest.mock import AsyncMock, Mock, patch
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
from go2rtc_client import Stream from go2rtc_client import Stream
@ -238,7 +238,11 @@ async def _test_setup_and_signaling(
await test() await test()
rest_client.streams.add.assert_called_once_with( rest_client.streams.add.assert_called_once_with(
entity_id, ["rtsp://stream", f"ffmpeg:{camera.entity_id}#audio=opus"] entity_id,
[
"rtsp://stream",
f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug",
],
) )
# Stream exists but the source is different # Stream exists but the source is different
@ -252,7 +256,11 @@ async def _test_setup_and_signaling(
await test() await test()
rest_client.streams.add.assert_called_once_with( rest_client.streams.add.assert_called_once_with(
entity_id, ["rtsp://stream", f"ffmpeg:{camera.entity_id}#audio=opus"] entity_id,
[
"rtsp://stream",
f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug",
],
) )
# If the stream is already added, the stream should not be added again. # If the stream is already added, the stream should not be added again.
@ -296,7 +304,7 @@ async def _test_setup_and_signaling(
], ],
) )
@pytest.mark.parametrize("has_go2rtc_entry", [True, False]) @pytest.mark.parametrize("has_go2rtc_entry", [True, False])
async def test_setup_managed( async def test_setup_go_binary(
hass: HomeAssistant, hass: HomeAssistant,
rest_client: AsyncMock, rest_client: AsyncMock,
ws_client: Mock, ws_client: Mock,
@ -308,131 +316,15 @@ async def test_setup_managed(
config: ConfigType, config: ConfigType,
ui_enabled: bool, ui_enabled: bool,
) -> None: ) -> None:
"""Test the go2rtc setup with managed go2rtc instance.""" """Test the go2rtc config entry with binary."""
assert (len(hass.config_entries.async_entries(DOMAIN)) == 1) == has_go2rtc_entry assert (len(hass.config_entries.async_entries(DOMAIN)) == 1) == has_go2rtc_entry
camera = init_test_integration
entity_id = camera.entity_id def after_setup() -> None:
stream_name_original = f"{camera.entity_id}_original" server.assert_called_once_with(hass, "/usr/bin/go2rtc", enable_ui=ui_enabled)
assert camera.frontend_stream_type == StreamType.HLS server_start.assert_called_once()
assert await async_setup_component(hass, DOMAIN, config) await _test_setup_and_signaling(
await hass.async_block_till_done(wait_background_tasks=True) hass, rest_client, ws_client, config, after_setup, init_test_integration
config_entries = hass.config_entries.async_entries(DOMAIN)
assert len(config_entries) == 1
assert config_entries[0].state == ConfigEntryState.LOADED
server.assert_called_once_with(hass, "/usr/bin/go2rtc", enable_ui=ui_enabled)
server_start.assert_called_once()
receive_message_callback = Mock(spec_set=WebRTCSendMessage)
async def test() -> None:
await camera.async_handle_async_webrtc_offer(
OFFER_SDP, "session_id", receive_message_callback
)
ws_client.send.assert_called_once_with(
WebRTCOffer(
OFFER_SDP,
camera.async_get_webrtc_client_configuration().configuration.ice_servers,
)
)
ws_client.subscribe.assert_called_once()
# Simulate the answer from the go2rtc server
callback = ws_client.subscribe.call_args[0][0]
callback(WebRTCAnswer(ANSWER_SDP))
receive_message_callback.assert_called_once_with(HAWebRTCAnswer(ANSWER_SDP))
await test()
stream_added_calls = [
call(stream_name_original, "rtsp://stream"),
call(
entity_id,
[
f"rtsp://127.0.0.1:18554/{stream_name_original}",
f"ffmpeg:{stream_name_original}#audio=opus",
],
),
]
assert rest_client.streams.add.call_args_list == stream_added_calls
# Stream original missing
rest_client.streams.add.reset_mock()
rest_client.streams.list.return_value = {
entity_id: Stream(
[
Producer(f"rtsp://127.0.0.1:18554/{stream_name_original}"),
Producer(f"ffmpeg:{stream_name_original}#audio=opus"),
]
)
}
receive_message_callback.reset_mock()
ws_client.reset_mock()
await test()
assert rest_client.streams.add.call_args_list == stream_added_calls
# Stream original source different
rest_client.streams.add.reset_mock()
rest_client.streams.list.return_value = {
stream_name_original: Stream([Producer("rtsp://different")]),
entity_id: Stream(
[
Producer(f"rtsp://127.0.0.1:18554/{stream_name_original}"),
Producer(f"ffmpeg:{stream_name_original}#audio=opus"),
]
),
}
receive_message_callback.reset_mock()
ws_client.reset_mock()
await test()
assert rest_client.streams.add.call_args_list == stream_added_calls
# Stream source different
rest_client.streams.add.reset_mock()
rest_client.streams.list.return_value = {
stream_name_original: Stream([Producer("rtsp://stream")]),
entity_id: Stream([Producer("rtsp://different")]),
}
receive_message_callback.reset_mock()
ws_client.reset_mock()
await test()
assert rest_client.streams.add.call_args_list == stream_added_calls
# If the stream is already added, the stream should not be added again.
rest_client.streams.add.reset_mock()
rest_client.streams.list.return_value = {
stream_name_original: Stream([Producer("rtsp://stream")]),
entity_id: Stream(
[
Producer(f"rtsp://127.0.0.1:18554/{stream_name_original}"),
Producer(f"ffmpeg:{stream_name_original}#audio=opus"),
]
),
}
receive_message_callback.reset_mock()
ws_client.reset_mock()
await test()
rest_client.streams.add.assert_not_called()
assert isinstance(camera._webrtc_provider, WebRTCProvider)
# Set stream source to None and provider should be skipped
rest_client.streams.list.return_value = {}
receive_message_callback.reset_mock()
camera.set_stream_source(None)
await camera.async_handle_async_webrtc_offer(
OFFER_SDP, "session_id", receive_message_callback
)
receive_message_callback.assert_called_once_with(
WebRTCError("go2rtc_webrtc_offer_failed", "Camera has no stream source")
) )
await hass.async_stop() await hass.async_stop()
@ -448,7 +340,7 @@ async def test_setup_managed(
], ],
) )
@pytest.mark.parametrize("has_go2rtc_entry", [True, False]) @pytest.mark.parametrize("has_go2rtc_entry", [True, False])
async def test_setup_self_hosted( async def test_setup_go(
hass: HomeAssistant, hass: HomeAssistant,
rest_client: AsyncMock, rest_client: AsyncMock,
ws_client: Mock, ws_client: Mock,
@ -458,83 +350,16 @@ async def test_setup_self_hosted(
mock_is_docker_env: Mock, mock_is_docker_env: Mock,
has_go2rtc_entry: bool, has_go2rtc_entry: bool,
) -> None: ) -> None:
"""Test the go2rtc with selfhosted go2rtc instance.""" """Test the go2rtc config entry without binary."""
assert (len(hass.config_entries.async_entries(DOMAIN)) == 1) == has_go2rtc_entry assert (len(hass.config_entries.async_entries(DOMAIN)) == 1) == has_go2rtc_entry
config = {DOMAIN: {CONF_URL: "http://localhost:1984/"}} config = {DOMAIN: {CONF_URL: "http://localhost:1984/"}}
camera = init_test_integration
entity_id = camera.entity_id def after_setup() -> None:
assert camera.frontend_stream_type == StreamType.HLS server.assert_not_called()
assert await async_setup_component(hass, DOMAIN, config) await _test_setup_and_signaling(
await hass.async_block_till_done(wait_background_tasks=True) hass, rest_client, ws_client, config, after_setup, init_test_integration
config_entries = hass.config_entries.async_entries(DOMAIN)
assert len(config_entries) == 1
assert config_entries[0].state == ConfigEntryState.LOADED
server.assert_not_called()
receive_message_callback = Mock(spec_set=WebRTCSendMessage)
async def test() -> None:
await camera.async_handle_async_webrtc_offer(
OFFER_SDP, "session_id", receive_message_callback
)
ws_client.send.assert_called_once_with(
WebRTCOffer(
OFFER_SDP,
camera.async_get_webrtc_client_configuration().configuration.ice_servers,
)
)
ws_client.subscribe.assert_called_once()
# Simulate the answer from the go2rtc server
callback = ws_client.subscribe.call_args[0][0]
callback(WebRTCAnswer(ANSWER_SDP))
receive_message_callback.assert_called_once_with(HAWebRTCAnswer(ANSWER_SDP))
await test()
rest_client.streams.add.assert_called_once_with(
entity_id, ["rtsp://stream", f"ffmpeg:{camera.entity_id}#audio=opus"]
)
# Stream exists but the source is different
rest_client.streams.add.reset_mock()
rest_client.streams.list.return_value = {
entity_id: Stream([Producer("rtsp://different")])
}
receive_message_callback.reset_mock()
ws_client.reset_mock()
await test()
rest_client.streams.add.assert_called_once_with(
entity_id, ["rtsp://stream", f"ffmpeg:{camera.entity_id}#audio=opus"]
)
# If the stream is already added, the stream should not be added again.
rest_client.streams.add.reset_mock()
rest_client.streams.list.return_value = {
entity_id: Stream([Producer("rtsp://stream")])
}
receive_message_callback.reset_mock()
ws_client.reset_mock()
await test()
rest_client.streams.add.assert_not_called()
assert isinstance(camera._webrtc_provider, WebRTCProvider)
# Set stream source to None and provider should be skipped
rest_client.streams.list.return_value = {}
receive_message_callback.reset_mock()
camera.set_stream_source(None)
await camera.async_handle_async_webrtc_offer(
OFFER_SDP, "session_id", receive_message_callback
)
receive_message_callback.assert_called_once_with(
WebRTCError("go2rtc_webrtc_offer_failed", "Camera has no stream source")
) )
mock_get_binary.assert_not_called() mock_get_binary.assert_not_called()