mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Add diagnostics to stream's Stream objects (#68020)
* Add diagnostics to stream's Stream objects Add diagnostics key/value pair to the Stream object. Diagnostics support in camera integration will be added in a follow up and will access the diagnostics on the Stream object, similar to the examples in the unit test. * Rename to audio/video codec * Fix test codec names * Update tests/components/stream/test_worker.py Co-authored-by: uvjustin <46082645+uvjustin@users.noreply.github.com> Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> Co-authored-by: uvjustin <46082645+uvjustin@users.noreply.github.com>
This commit is contained in:
parent
2686be921c
commit
41a032e3e3
@ -23,7 +23,7 @@ import secrets
|
||||
import threading
|
||||
import time
|
||||
from types import MappingProxyType
|
||||
from typing import cast
|
||||
from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -51,6 +51,7 @@ from .const import (
|
||||
TARGET_SEGMENT_DURATION_NON_LL_HLS,
|
||||
)
|
||||
from .core import PROVIDERS, IdleTimer, KeyFrameConverter, StreamOutput, StreamSettings
|
||||
from .diagnostics import Diagnostics
|
||||
from .hls import HlsStreamOutput, async_setup_hls
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -225,6 +226,7 @@ class Stream:
|
||||
if stream_label
|
||||
else _LOGGER
|
||||
)
|
||||
self._diagnostics = Diagnostics()
|
||||
|
||||
def endpoint_url(self, fmt: str) -> str:
|
||||
"""Start the stream and returns a url for the output format."""
|
||||
@ -259,6 +261,7 @@ class Stream:
|
||||
self.hass, IdleTimer(self.hass, timeout, idle_callback)
|
||||
)
|
||||
self._outputs[fmt] = provider
|
||||
|
||||
return provider
|
||||
|
||||
def remove_provider(self, provider: StreamOutput) -> None:
|
||||
@ -310,6 +313,7 @@ class Stream:
|
||||
|
||||
def update_source(self, new_source: str) -> None:
|
||||
"""Restart the stream with a new stream source."""
|
||||
self._diagnostics.increment("update_source")
|
||||
self._logger.debug(
|
||||
"Updating stream source %s", redact_credentials(str(new_source))
|
||||
)
|
||||
@ -323,11 +327,13 @@ class Stream:
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from .worker import StreamState, StreamWorkerError, stream_worker
|
||||
|
||||
stream_state = StreamState(self.hass, self.outputs)
|
||||
stream_state = StreamState(self.hass, self.outputs, self._diagnostics)
|
||||
wait_timeout = 0
|
||||
while not self._thread_quit.wait(timeout=wait_timeout):
|
||||
start_time = time.time()
|
||||
self.hass.add_job(self._async_update_state, True)
|
||||
self._diagnostics.set_value("keepalive", self.keepalive)
|
||||
self._diagnostics.increment("start_worker")
|
||||
try:
|
||||
stream_worker(
|
||||
self.source,
|
||||
@ -337,6 +343,7 @@ class Stream:
|
||||
self._thread_quit,
|
||||
)
|
||||
except StreamWorkerError as err:
|
||||
self._diagnostics.increment("worker_error")
|
||||
self._logger.error("Error from stream worker: %s", str(err))
|
||||
|
||||
stream_state.discontinuity()
|
||||
@ -358,6 +365,7 @@ class Stream:
|
||||
if time.time() - start_time > STREAM_RESTART_RESET_TIME:
|
||||
wait_timeout = 0
|
||||
wait_timeout += STREAM_RESTART_INCREMENT
|
||||
self._diagnostics.set_value("retry_timeout", wait_timeout)
|
||||
self._logger.debug(
|
||||
"Restarting stream worker in %d seconds: %s",
|
||||
wait_timeout,
|
||||
@ -447,6 +455,10 @@ class Stream:
|
||||
width=width, height=height
|
||||
)
|
||||
|
||||
def get_diagnostics(self) -> dict[str, Any]:
|
||||
"""Return diagnostics information for the stream."""
|
||||
return self._diagnostics.as_dict()
|
||||
|
||||
|
||||
def _should_retry() -> bool:
|
||||
"""Return true if worker failures should be retried, for disabling during tests."""
|
||||
|
33
homeassistant/components/stream/diagnostics.py
Normal file
33
homeassistant/components/stream/diagnostics.py
Normal file
@ -0,0 +1,33 @@
|
||||
"""Diagnostics for debugging.
|
||||
|
||||
The stream component does not have config entries itself, and all diagnostics
|
||||
information is managed by dependent components (e.g. camera)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
from typing import Any
|
||||
|
||||
|
||||
class Diagnostics:
|
||||
"""Holds diagnostics counters and key/values."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize Diagnostics."""
|
||||
self._counter: Counter = Counter()
|
||||
self._values: dict[str, Any] = {}
|
||||
|
||||
def increment(self, key: str) -> None:
|
||||
"""Increment a counter for the spcified key/event."""
|
||||
self._counter.update(Counter({key: 1}))
|
||||
|
||||
def set_value(self, key: str, value: Any) -> None:
|
||||
"""Update a key/value pair."""
|
||||
self._values[key] = value
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
"""Return diagnostics as a debug dictionary."""
|
||||
result = {k: self._counter[k] for k in self._counter}
|
||||
result.update(self._values)
|
||||
return result
|
@ -27,6 +27,7 @@ from .const import (
|
||||
SOURCE_TIMEOUT,
|
||||
)
|
||||
from .core import KeyFrameConverter, Part, Segment, StreamOutput, StreamSettings
|
||||
from .diagnostics import Diagnostics
|
||||
from .hls import HlsStreamOutput
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -52,6 +53,7 @@ class StreamState:
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
outputs_callback: Callable[[], Mapping[str, StreamOutput]],
|
||||
diagnostics: Diagnostics,
|
||||
) -> None:
|
||||
"""Initialize StreamState."""
|
||||
self._stream_id: int = 0
|
||||
@ -62,6 +64,7 @@ class StreamState:
|
||||
# sequence gets incremented before the first segment so the first segment
|
||||
# has a sequence number of 0.
|
||||
self._sequence = -1
|
||||
self._diagnostics = diagnostics
|
||||
|
||||
@property
|
||||
def sequence(self) -> int:
|
||||
@ -93,6 +96,11 @@ class StreamState:
|
||||
"""Return the active stream outputs."""
|
||||
return list(self._outputs_callback().values())
|
||||
|
||||
@property
|
||||
def diagnostics(self) -> Diagnostics:
|
||||
"""Return diagnostics object."""
|
||||
return self._diagnostics
|
||||
|
||||
|
||||
class StreamMuxer:
|
||||
"""StreamMuxer re-packages video/audio packets for output."""
|
||||
@ -468,6 +476,10 @@ def stream_worker(
|
||||
# Some audio streams do not have a profile and throw errors when remuxing
|
||||
if audio_stream and audio_stream.profile is None:
|
||||
audio_stream = None
|
||||
stream_state.diagnostics.set_value("container_format", container.format.name)
|
||||
stream_state.diagnostics.set_value("video_codec", video_stream.name)
|
||||
if audio_stream:
|
||||
stream_state.diagnostics.set_value("audio_codec", audio_stream.name)
|
||||
|
||||
dts_validator = TimestampValidator()
|
||||
container_packets = PeekIterator(
|
||||
|
@ -177,6 +177,14 @@ async def test_hls_stream(
|
||||
fail_response = await hls_client.get()
|
||||
assert fail_response.status == HTTPStatus.NOT_FOUND
|
||||
|
||||
assert stream.get_diagnostics() == {
|
||||
"container_format": "mov,mp4,m4a,3gp,3g2,mj2",
|
||||
"keepalive": False,
|
||||
"start_worker": 1,
|
||||
"video_codec": "h264",
|
||||
"worker_error": 1,
|
||||
}
|
||||
|
||||
|
||||
async def test_stream_timeout(
|
||||
hass, hass_client, setup_component, stream_worker_sync, h264_video
|
||||
|
@ -270,7 +270,7 @@ class MockPyAv:
|
||||
|
||||
def run_worker(hass, stream, stream_source):
|
||||
"""Run the stream worker under test."""
|
||||
stream_state = StreamState(hass, stream.outputs)
|
||||
stream_state = StreamState(hass, stream.outputs, stream._diagnostics)
|
||||
stream_worker(
|
||||
stream_source, {}, stream_state, KeyFrameConverter(hass), threading.Event()
|
||||
)
|
||||
@ -873,6 +873,14 @@ async def test_h265_video_is_hvc1(hass, record_worker_sync):
|
||||
|
||||
stream.stop()
|
||||
|
||||
assert stream.get_diagnostics() == {
|
||||
"container_format": "mov,mp4,m4a,3gp,3g2,mj2",
|
||||
"keepalive": False,
|
||||
"start_worker": 1,
|
||||
"video_codec": "hevc",
|
||||
"worker_error": 1,
|
||||
}
|
||||
|
||||
|
||||
async def test_get_image(hass, record_worker_sync):
|
||||
"""Test that the has_keyframe metadata matches the media."""
|
||||
|
Loading…
x
Reference in New Issue
Block a user