mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 18:57:06 +00:00
Change stream sequence number to start from 0 (#51101)
* Use constants for provider strings * Add last_sequence property
This commit is contained in:
parent
f45bc3abc7
commit
38e0cbe964
@ -31,8 +31,10 @@ from .const import (
|
|||||||
ATTR_ENDPOINTS,
|
ATTR_ENDPOINTS,
|
||||||
ATTR_STREAMS,
|
ATTR_STREAMS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
HLS_PROVIDER,
|
||||||
MAX_SEGMENTS,
|
MAX_SEGMENTS,
|
||||||
OUTPUT_IDLE_TIMEOUT,
|
OUTPUT_IDLE_TIMEOUT,
|
||||||
|
RECORDER_PROVIDER,
|
||||||
STREAM_RESTART_INCREMENT,
|
STREAM_RESTART_INCREMENT,
|
||||||
STREAM_RESTART_RESET_TIME,
|
STREAM_RESTART_RESET_TIME,
|
||||||
)
|
)
|
||||||
@ -90,7 +92,7 @@ async def async_setup(hass, config):
|
|||||||
|
|
||||||
# Setup HLS
|
# Setup HLS
|
||||||
hls_endpoint = async_setup_hls(hass)
|
hls_endpoint = async_setup_hls(hass)
|
||||||
hass.data[DOMAIN][ATTR_ENDPOINTS]["hls"] = hls_endpoint
|
hass.data[DOMAIN][ATTR_ENDPOINTS][HLS_PROVIDER] = hls_endpoint
|
||||||
|
|
||||||
# Setup Recorder
|
# Setup Recorder
|
||||||
async_setup_recorder(hass)
|
async_setup_recorder(hass)
|
||||||
@ -146,7 +148,9 @@ class Stream:
|
|||||||
|
|
||||||
@callback
|
@callback
|
||||||
def idle_callback():
|
def idle_callback():
|
||||||
if (not self.keepalive or fmt == "recorder") and fmt in self._outputs:
|
if (
|
||||||
|
not self.keepalive or fmt == RECORDER_PROVIDER
|
||||||
|
) and fmt in self._outputs:
|
||||||
self.remove_provider(self._outputs[fmt])
|
self.remove_provider(self._outputs[fmt])
|
||||||
self.check_idle()
|
self.check_idle()
|
||||||
|
|
||||||
@ -259,19 +263,19 @@ class Stream:
|
|||||||
raise HomeAssistantError(f"Can't write {video_path}, no access to path!")
|
raise HomeAssistantError(f"Can't write {video_path}, no access to path!")
|
||||||
|
|
||||||
# Add recorder
|
# Add recorder
|
||||||
recorder = self.outputs().get("recorder")
|
recorder = self.outputs().get(RECORDER_PROVIDER)
|
||||||
if recorder:
|
if recorder:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
f"Stream already recording to {recorder.video_path}!"
|
f"Stream already recording to {recorder.video_path}!"
|
||||||
)
|
)
|
||||||
recorder = self.add_provider("recorder", timeout=duration)
|
recorder = self.add_provider(RECORDER_PROVIDER, timeout=duration)
|
||||||
recorder.video_path = video_path
|
recorder.video_path = video_path
|
||||||
|
|
||||||
self.start()
|
self.start()
|
||||||
_LOGGER.debug("Started a stream recording of %s seconds", duration)
|
_LOGGER.debug("Started a stream recording of %s seconds", duration)
|
||||||
|
|
||||||
# Take advantage of lookback
|
# Take advantage of lookback
|
||||||
hls = self.outputs().get("hls")
|
hls = self.outputs().get(HLS_PROVIDER)
|
||||||
if lookback > 0 and hls:
|
if lookback > 0 and hls:
|
||||||
num_segments = min(int(lookback // hls.target_duration), MAX_SEGMENTS)
|
num_segments = min(int(lookback // hls.target_duration), MAX_SEGMENTS)
|
||||||
# Wait for latest segment, then add the lookback
|
# Wait for latest segment, then add the lookback
|
||||||
|
@ -4,13 +4,16 @@ DOMAIN = "stream"
|
|||||||
ATTR_ENDPOINTS = "endpoints"
|
ATTR_ENDPOINTS = "endpoints"
|
||||||
ATTR_STREAMS = "streams"
|
ATTR_STREAMS = "streams"
|
||||||
|
|
||||||
OUTPUT_FORMATS = ["hls"]
|
HLS_PROVIDER = "hls"
|
||||||
|
RECORDER_PROVIDER = "recorder"
|
||||||
|
|
||||||
|
OUTPUT_FORMATS = [HLS_PROVIDER]
|
||||||
|
|
||||||
SEGMENT_CONTAINER_FORMAT = "mp4" # format for segments
|
SEGMENT_CONTAINER_FORMAT = "mp4" # format for segments
|
||||||
RECORDER_CONTAINER_FORMAT = "mp4" # format for recorder output
|
RECORDER_CONTAINER_FORMAT = "mp4" # format for recorder output
|
||||||
AUDIO_CODECS = {"aac", "mp3"}
|
AUDIO_CODECS = {"aac", "mp3"}
|
||||||
|
|
||||||
FORMAT_CONTENT_TYPE = {"hls": "application/vnd.apple.mpegurl"}
|
FORMAT_CONTENT_TYPE = {HLS_PROVIDER: "application/vnd.apple.mpegurl"}
|
||||||
|
|
||||||
OUTPUT_IDLE_TIMEOUT = 300 # Idle timeout due to inactivity
|
OUTPUT_IDLE_TIMEOUT = 300 # Idle timeout due to inactivity
|
||||||
|
|
||||||
|
@ -97,6 +97,13 @@ class StreamOutput:
|
|||||||
"""Return True if the output is idle."""
|
"""Return True if the output is idle."""
|
||||||
return self._idle_timer.idle
|
return self._idle_timer.idle
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_sequence(self) -> int:
|
||||||
|
"""Return the last sequence number without iterating."""
|
||||||
|
if self._segments:
|
||||||
|
return self._segments[-1].sequence
|
||||||
|
return -1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def segments(self) -> list[int]:
|
def segments(self) -> list[int]:
|
||||||
"""Return current sequence from segments."""
|
"""Return current sequence from segments."""
|
||||||
@ -127,8 +134,7 @@ class StreamOutput:
|
|||||||
|
|
||||||
async def recv(self) -> Segment | None:
|
async def recv(self) -> Segment | None:
|
||||||
"""Wait for and retrieve the latest segment."""
|
"""Wait for and retrieve the latest segment."""
|
||||||
last_segment = max(self.segments, default=0)
|
if self._cursor is None or self._cursor <= self.last_sequence:
|
||||||
if self._cursor is None or self._cursor <= last_segment:
|
|
||||||
await self._event.wait()
|
await self._event.wait()
|
||||||
|
|
||||||
if not self._segments:
|
if not self._segments:
|
||||||
|
@ -3,7 +3,12 @@ from aiohttp import web
|
|||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
|
||||||
from .const import FORMAT_CONTENT_TYPE, MAX_SEGMENTS, NUM_PLAYLIST_SEGMENTS
|
from .const import (
|
||||||
|
FORMAT_CONTENT_TYPE,
|
||||||
|
HLS_PROVIDER,
|
||||||
|
MAX_SEGMENTS,
|
||||||
|
NUM_PLAYLIST_SEGMENTS,
|
||||||
|
)
|
||||||
from .core import PROVIDERS, HomeAssistant, IdleTimer, StreamOutput, StreamView
|
from .core import PROVIDERS, HomeAssistant, IdleTimer, StreamOutput, StreamView
|
||||||
from .fmp4utils import get_codec_string
|
from .fmp4utils import get_codec_string
|
||||||
|
|
||||||
@ -45,12 +50,12 @@ class HlsMasterPlaylistView(StreamView):
|
|||||||
|
|
||||||
async def handle(self, request, stream, sequence):
|
async def handle(self, request, stream, sequence):
|
||||||
"""Return m3u8 playlist."""
|
"""Return m3u8 playlist."""
|
||||||
track = stream.add_provider("hls")
|
track = stream.add_provider(HLS_PROVIDER)
|
||||||
stream.start()
|
stream.start()
|
||||||
# Wait for a segment to be ready
|
# Wait for a segment to be ready
|
||||||
if not track.segments and not await track.recv():
|
if not track.segments and not await track.recv():
|
||||||
return web.HTTPNotFound()
|
return web.HTTPNotFound()
|
||||||
headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]}
|
headers = {"Content-Type": FORMAT_CONTENT_TYPE[HLS_PROVIDER]}
|
||||||
return web.Response(body=self.render(track).encode("utf-8"), headers=headers)
|
return web.Response(body=self.render(track).encode("utf-8"), headers=headers)
|
||||||
|
|
||||||
|
|
||||||
@ -104,12 +109,12 @@ class HlsPlaylistView(StreamView):
|
|||||||
|
|
||||||
async def handle(self, request, stream, sequence):
|
async def handle(self, request, stream, sequence):
|
||||||
"""Return m3u8 playlist."""
|
"""Return m3u8 playlist."""
|
||||||
track = stream.add_provider("hls")
|
track = stream.add_provider(HLS_PROVIDER)
|
||||||
stream.start()
|
stream.start()
|
||||||
# Wait for a segment to be ready
|
# Wait for a segment to be ready
|
||||||
if not track.segments and not await track.recv():
|
if not track.segments and not await track.recv():
|
||||||
return web.HTTPNotFound()
|
return web.HTTPNotFound()
|
||||||
headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]}
|
headers = {"Content-Type": FORMAT_CONTENT_TYPE[HLS_PROVIDER]}
|
||||||
return web.Response(body=self.render(track).encode("utf-8"), headers=headers)
|
return web.Response(body=self.render(track).encode("utf-8"), headers=headers)
|
||||||
|
|
||||||
|
|
||||||
@ -122,7 +127,7 @@ class HlsInitView(StreamView):
|
|||||||
|
|
||||||
async def handle(self, request, stream, sequence):
|
async def handle(self, request, stream, sequence):
|
||||||
"""Return init.mp4."""
|
"""Return init.mp4."""
|
||||||
track = stream.add_provider("hls")
|
track = stream.add_provider(HLS_PROVIDER)
|
||||||
segments = track.get_segments()
|
segments = track.get_segments()
|
||||||
if not segments:
|
if not segments:
|
||||||
return web.HTTPNotFound()
|
return web.HTTPNotFound()
|
||||||
@ -139,7 +144,7 @@ class HlsSegmentView(StreamView):
|
|||||||
|
|
||||||
async def handle(self, request, stream, sequence):
|
async def handle(self, request, stream, sequence):
|
||||||
"""Return fmp4 segment."""
|
"""Return fmp4 segment."""
|
||||||
track = stream.add_provider("hls")
|
track = stream.add_provider(HLS_PROVIDER)
|
||||||
segment = track.get_segment(int(sequence))
|
segment = track.get_segment(int(sequence))
|
||||||
if not segment:
|
if not segment:
|
||||||
return web.HTTPNotFound()
|
return web.HTTPNotFound()
|
||||||
@ -150,7 +155,7 @@ class HlsSegmentView(StreamView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@PROVIDERS.register("hls")
|
@PROVIDERS.register(HLS_PROVIDER)
|
||||||
class HlsStreamOutput(StreamOutput):
|
class HlsStreamOutput(StreamOutput):
|
||||||
"""Represents HLS Output formats."""
|
"""Represents HLS Output formats."""
|
||||||
|
|
||||||
@ -161,4 +166,4 @@ class HlsStreamOutput(StreamOutput):
|
|||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""Return provider name."""
|
"""Return provider name."""
|
||||||
return "hls"
|
return HLS_PROVIDER
|
||||||
|
@ -12,7 +12,11 @@ from av.container import OutputContainer
|
|||||||
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
from .const import RECORDER_CONTAINER_FORMAT, SEGMENT_CONTAINER_FORMAT
|
from .const import (
|
||||||
|
RECORDER_CONTAINER_FORMAT,
|
||||||
|
RECORDER_PROVIDER,
|
||||||
|
SEGMENT_CONTAINER_FORMAT,
|
||||||
|
)
|
||||||
from .core import PROVIDERS, IdleTimer, Segment, StreamOutput
|
from .core import PROVIDERS, IdleTimer, Segment, StreamOutput
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -110,7 +114,7 @@ def recorder_save_worker(file_out: str, segments: deque[Segment]):
|
|||||||
output.close()
|
output.close()
|
||||||
|
|
||||||
|
|
||||||
@PROVIDERS.register("recorder")
|
@PROVIDERS.register(RECORDER_PROVIDER)
|
||||||
class RecorderOutput(StreamOutput):
|
class RecorderOutput(StreamOutput):
|
||||||
"""Represents HLS Output formats."""
|
"""Represents HLS Output formats."""
|
||||||
|
|
||||||
@ -122,7 +126,7 @@ class RecorderOutput(StreamOutput):
|
|||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""Return provider name."""
|
"""Return provider name."""
|
||||||
return "recorder"
|
return RECORDER_PROVIDER
|
||||||
|
|
||||||
def prepend(self, segments: list[Segment]) -> None:
|
def prepend(self, segments: list[Segment]) -> None:
|
||||||
"""Prepend segments to existing list."""
|
"""Prepend segments to existing list."""
|
||||||
|
@ -32,7 +32,9 @@ class SegmentBuffer:
|
|||||||
self._stream_id = 0
|
self._stream_id = 0
|
||||||
self._outputs_callback = outputs_callback
|
self._outputs_callback = outputs_callback
|
||||||
self._outputs: list[StreamOutput] = []
|
self._outputs: list[StreamOutput] = []
|
||||||
self._sequence = 0
|
# sequence gets incremented before the first segment so the first segment
|
||||||
|
# has a sequence number of 0.
|
||||||
|
self._sequence = -1
|
||||||
self._segment_start_pts = None
|
self._segment_start_pts = None
|
||||||
self._memory_file: BytesIO = cast(BytesIO, None)
|
self._memory_file: BytesIO = cast(BytesIO, None)
|
||||||
self._av_output: av.container.OutputContainer = None
|
self._av_output: av.container.OutputContainer = None
|
||||||
|
@ -7,7 +7,11 @@ import av
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.stream import create_stream
|
from homeassistant.components.stream import create_stream
|
||||||
from homeassistant.components.stream.const import MAX_SEGMENTS, NUM_PLAYLIST_SEGMENTS
|
from homeassistant.components.stream.const import (
|
||||||
|
HLS_PROVIDER,
|
||||||
|
MAX_SEGMENTS,
|
||||||
|
NUM_PLAYLIST_SEGMENTS,
|
||||||
|
)
|
||||||
from homeassistant.components.stream.core import Segment
|
from homeassistant.components.stream.core import Segment
|
||||||
from homeassistant.const import HTTP_NOT_FOUND
|
from homeassistant.const import HTTP_NOT_FOUND
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
@ -47,7 +51,7 @@ def hls_stream(hass, hass_client):
|
|||||||
|
|
||||||
async def create_client_for_stream(stream):
|
async def create_client_for_stream(stream):
|
||||||
http_client = await hass_client()
|
http_client = await hass_client()
|
||||||
parsed_url = urlparse(stream.endpoint_url("hls"))
|
parsed_url = urlparse(stream.endpoint_url(HLS_PROVIDER))
|
||||||
return HlsClient(http_client, parsed_url)
|
return HlsClient(http_client, parsed_url)
|
||||||
|
|
||||||
return create_client_for_stream
|
return create_client_for_stream
|
||||||
@ -93,7 +97,7 @@ async def test_hls_stream(hass, hls_stream, stream_worker_sync):
|
|||||||
stream = create_stream(hass, source)
|
stream = create_stream(hass, source)
|
||||||
|
|
||||||
# Request stream
|
# Request stream
|
||||||
stream.add_provider("hls")
|
stream.add_provider(HLS_PROVIDER)
|
||||||
stream.start()
|
stream.start()
|
||||||
|
|
||||||
hls_client = await hls_stream(stream)
|
hls_client = await hls_stream(stream)
|
||||||
@ -134,9 +138,9 @@ async def test_stream_timeout(hass, hass_client, stream_worker_sync):
|
|||||||
stream = create_stream(hass, source)
|
stream = create_stream(hass, source)
|
||||||
|
|
||||||
# Request stream
|
# Request stream
|
||||||
stream.add_provider("hls")
|
stream.add_provider(HLS_PROVIDER)
|
||||||
stream.start()
|
stream.start()
|
||||||
url = stream.endpoint_url("hls")
|
url = stream.endpoint_url(HLS_PROVIDER)
|
||||||
|
|
||||||
http_client = await hass_client()
|
http_client = await hass_client()
|
||||||
|
|
||||||
@ -176,7 +180,7 @@ async def test_stream_timeout_after_stop(hass, hass_client, stream_worker_sync):
|
|||||||
stream = create_stream(hass, source)
|
stream = create_stream(hass, source)
|
||||||
|
|
||||||
# Request stream
|
# Request stream
|
||||||
stream.add_provider("hls")
|
stream.add_provider(HLS_PROVIDER)
|
||||||
stream.start()
|
stream.start()
|
||||||
|
|
||||||
stream_worker_sync.resume()
|
stream_worker_sync.resume()
|
||||||
@ -196,7 +200,7 @@ async def test_stream_keepalive(hass):
|
|||||||
# Setup demo HLS track
|
# Setup demo HLS track
|
||||||
source = "test_stream_keepalive_source"
|
source = "test_stream_keepalive_source"
|
||||||
stream = create_stream(hass, source)
|
stream = create_stream(hass, source)
|
||||||
track = stream.add_provider("hls")
|
track = stream.add_provider(HLS_PROVIDER)
|
||||||
track.num_segments = 2
|
track.num_segments = 2
|
||||||
|
|
||||||
cur_time = 0
|
cur_time = 0
|
||||||
@ -231,7 +235,7 @@ async def test_hls_playlist_view_no_output(hass, hass_client, hls_stream):
|
|||||||
await async_setup_component(hass, "stream", {"stream": {}})
|
await async_setup_component(hass, "stream", {"stream": {}})
|
||||||
|
|
||||||
stream = create_stream(hass, STREAM_SOURCE)
|
stream = create_stream(hass, STREAM_SOURCE)
|
||||||
stream.add_provider("hls")
|
stream.add_provider(HLS_PROVIDER)
|
||||||
|
|
||||||
hls_client = await hls_stream(stream)
|
hls_client = await hls_stream(stream)
|
||||||
|
|
||||||
@ -246,7 +250,7 @@ async def test_hls_playlist_view(hass, hls_stream, stream_worker_sync):
|
|||||||
|
|
||||||
stream = create_stream(hass, STREAM_SOURCE)
|
stream = create_stream(hass, STREAM_SOURCE)
|
||||||
stream_worker_sync.pause()
|
stream_worker_sync.pause()
|
||||||
hls = stream.add_provider("hls")
|
hls = stream.add_provider(HLS_PROVIDER)
|
||||||
|
|
||||||
hls.put(Segment(1, INIT_BYTES, MOOF_BYTES, DURATION))
|
hls.put(Segment(1, INIT_BYTES, MOOF_BYTES, DURATION))
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -275,7 +279,7 @@ async def test_hls_max_segments(hass, hls_stream, stream_worker_sync):
|
|||||||
|
|
||||||
stream = create_stream(hass, STREAM_SOURCE)
|
stream = create_stream(hass, STREAM_SOURCE)
|
||||||
stream_worker_sync.pause()
|
stream_worker_sync.pause()
|
||||||
hls = stream.add_provider("hls")
|
hls = stream.add_provider(HLS_PROVIDER)
|
||||||
|
|
||||||
hls_client = await hls_stream(stream)
|
hls_client = await hls_stream(stream)
|
||||||
|
|
||||||
@ -316,7 +320,7 @@ async def test_hls_playlist_view_discontinuity(hass, hls_stream, stream_worker_s
|
|||||||
|
|
||||||
stream = create_stream(hass, STREAM_SOURCE)
|
stream = create_stream(hass, STREAM_SOURCE)
|
||||||
stream_worker_sync.pause()
|
stream_worker_sync.pause()
|
||||||
hls = stream.add_provider("hls")
|
hls = stream.add_provider(HLS_PROVIDER)
|
||||||
|
|
||||||
hls.put(Segment(1, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=0))
|
hls.put(Segment(1, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=0))
|
||||||
hls.put(Segment(2, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=0))
|
hls.put(Segment(2, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=0))
|
||||||
@ -346,7 +350,7 @@ async def test_hls_max_segments_discontinuity(hass, hls_stream, stream_worker_sy
|
|||||||
|
|
||||||
stream = create_stream(hass, STREAM_SOURCE)
|
stream = create_stream(hass, STREAM_SOURCE)
|
||||||
stream_worker_sync.pause()
|
stream_worker_sync.pause()
|
||||||
hls = stream.add_provider("hls")
|
hls = stream.add_provider(HLS_PROVIDER)
|
||||||
|
|
||||||
hls_client = await hls_stream(stream)
|
hls_client = await hls_stream(stream)
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ import av
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.stream import create_stream
|
from homeassistant.components.stream import create_stream
|
||||||
|
from homeassistant.components.stream.const import HLS_PROVIDER, RECORDER_PROVIDER
|
||||||
from homeassistant.components.stream.core import Segment
|
from homeassistant.components.stream.core import Segment
|
||||||
from homeassistant.components.stream.fmp4utils import get_init_and_moof_data
|
from homeassistant.components.stream.fmp4utils import get_init_and_moof_data
|
||||||
from homeassistant.components.stream.recorder import recorder_save_worker
|
from homeassistant.components.stream.recorder import recorder_save_worker
|
||||||
@ -119,7 +120,7 @@ async def test_record_lookback(
|
|||||||
stream = create_stream(hass, source)
|
stream = create_stream(hass, source)
|
||||||
|
|
||||||
# Start an HLS feed to enable lookback
|
# Start an HLS feed to enable lookback
|
||||||
stream.add_provider("hls")
|
stream.add_provider(HLS_PROVIDER)
|
||||||
stream.start()
|
stream.start()
|
||||||
|
|
||||||
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
||||||
@ -148,7 +149,7 @@ async def test_recorder_timeout(hass, hass_client, stream_worker_sync):
|
|||||||
stream = create_stream(hass, source)
|
stream = create_stream(hass, source)
|
||||||
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
||||||
await stream.async_record("/example/path")
|
await stream.async_record("/example/path")
|
||||||
recorder = stream.add_provider("recorder")
|
recorder = stream.add_provider(RECORDER_PROVIDER)
|
||||||
|
|
||||||
await recorder.recv()
|
await recorder.recv()
|
||||||
|
|
||||||
@ -252,7 +253,7 @@ async def test_record_stream_audio(
|
|||||||
stream = create_stream(hass, source)
|
stream = create_stream(hass, source)
|
||||||
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
with patch.object(hass.config, "is_allowed_path", return_value=True):
|
||||||
await stream.async_record("/example/path")
|
await stream.async_record("/example/path")
|
||||||
recorder = stream.add_provider("recorder")
|
recorder = stream.add_provider(RECORDER_PROVIDER)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
segment = await recorder.recv()
|
segment = await recorder.recv()
|
||||||
|
@ -23,6 +23,7 @@ import av
|
|||||||
|
|
||||||
from homeassistant.components.stream import Stream
|
from homeassistant.components.stream import Stream
|
||||||
from homeassistant.components.stream.const import (
|
from homeassistant.components.stream.const import (
|
||||||
|
HLS_PROVIDER,
|
||||||
MAX_MISSING_DTS,
|
MAX_MISSING_DTS,
|
||||||
MIN_SEGMENT_DURATION,
|
MIN_SEGMENT_DURATION,
|
||||||
PACKETS_TO_WAIT_FOR_AUDIO,
|
PACKETS_TO_WAIT_FOR_AUDIO,
|
||||||
@ -31,7 +32,6 @@ from homeassistant.components.stream.worker import SegmentBuffer, stream_worker
|
|||||||
|
|
||||||
STREAM_SOURCE = "some-stream-source"
|
STREAM_SOURCE = "some-stream-source"
|
||||||
# Formats here are arbitrary, not exercised by tests
|
# Formats here are arbitrary, not exercised by tests
|
||||||
STREAM_OUTPUT_FORMAT = "hls"
|
|
||||||
AUDIO_STREAM_FORMAT = "mp3"
|
AUDIO_STREAM_FORMAT = "mp3"
|
||||||
VIDEO_STREAM_FORMAT = "h264"
|
VIDEO_STREAM_FORMAT = "h264"
|
||||||
VIDEO_FRAME_RATE = 12
|
VIDEO_FRAME_RATE = 12
|
||||||
@ -198,7 +198,7 @@ class MockPyAv:
|
|||||||
async def async_decode_stream(hass, packets, py_av=None):
|
async def async_decode_stream(hass, packets, py_av=None):
|
||||||
"""Start a stream worker that decodes incoming stream packets into output segments."""
|
"""Start a stream worker that decodes incoming stream packets into output segments."""
|
||||||
stream = Stream(hass, STREAM_SOURCE)
|
stream = Stream(hass, STREAM_SOURCE)
|
||||||
stream.add_provider(STREAM_OUTPUT_FORMAT)
|
stream.add_provider(HLS_PROVIDER)
|
||||||
|
|
||||||
if not py_av:
|
if not py_av:
|
||||||
py_av = MockPyAv()
|
py_av = MockPyAv()
|
||||||
@ -218,7 +218,7 @@ async def async_decode_stream(hass, packets, py_av=None):
|
|||||||
async def test_stream_open_fails(hass):
|
async def test_stream_open_fails(hass):
|
||||||
"""Test failure on stream open."""
|
"""Test failure on stream open."""
|
||||||
stream = Stream(hass, STREAM_SOURCE)
|
stream = Stream(hass, STREAM_SOURCE)
|
||||||
stream.add_provider(STREAM_OUTPUT_FORMAT)
|
stream.add_provider(HLS_PROVIDER)
|
||||||
with patch("av.open") as av_open:
|
with patch("av.open") as av_open:
|
||||||
av_open.side_effect = av.error.InvalidDataError(-2, "error")
|
av_open.side_effect = av.error.InvalidDataError(-2, "error")
|
||||||
segment_buffer = SegmentBuffer(stream.outputs)
|
segment_buffer = SegmentBuffer(stream.outputs)
|
||||||
@ -237,9 +237,9 @@ async def test_stream_worker_success(hass):
|
|||||||
# segment arrives, hence the subtraction of one from the sequence length.
|
# segment arrives, hence the subtraction of one from the sequence length.
|
||||||
assert len(segments) == int((TEST_SEQUENCE_LENGTH - 1) * SEGMENTS_PER_PACKET)
|
assert len(segments) == int((TEST_SEQUENCE_LENGTH - 1) * SEGMENTS_PER_PACKET)
|
||||||
# Check sequence numbers
|
# Check sequence numbers
|
||||||
assert all([segments[i].sequence == i + 1 for i in range(len(segments))])
|
assert all(segments[i].sequence == i for i in range(len(segments)))
|
||||||
# Check segment durations
|
# Check segment durations
|
||||||
assert all([s.duration == SEGMENT_DURATION for s in segments])
|
assert all(s.duration == SEGMENT_DURATION for s in segments)
|
||||||
assert len(decoded_stream.video_packets) == TEST_SEQUENCE_LENGTH
|
assert len(decoded_stream.video_packets) == TEST_SEQUENCE_LENGTH
|
||||||
assert len(decoded_stream.audio_packets) == 0
|
assert len(decoded_stream.audio_packets) == 0
|
||||||
|
|
||||||
@ -253,7 +253,7 @@ async def test_skip_out_of_order_packet(hass):
|
|||||||
decoded_stream = await async_decode_stream(hass, iter(packets))
|
decoded_stream = await async_decode_stream(hass, iter(packets))
|
||||||
segments = decoded_stream.segments
|
segments = decoded_stream.segments
|
||||||
# Check sequence numbers
|
# Check sequence numbers
|
||||||
assert all([segments[i].sequence == i + 1 for i in range(len(segments))])
|
assert all(segments[i].sequence == i for i in range(len(segments)))
|
||||||
# If skipped packet would have been the first packet of a segment, the previous
|
# If skipped packet would have been the first packet of a segment, the previous
|
||||||
# segment will be longer by a packet duration
|
# segment will be longer by a packet duration
|
||||||
# We also may possibly lose a segment due to the shifting pts boundary
|
# We also may possibly lose a segment due to the shifting pts boundary
|
||||||
@ -273,7 +273,7 @@ async def test_skip_out_of_order_packet(hass):
|
|||||||
# Check number of segments
|
# Check number of segments
|
||||||
assert len(segments) == int((len(packets) - 1) * SEGMENTS_PER_PACKET)
|
assert len(segments) == int((len(packets) - 1) * SEGMENTS_PER_PACKET)
|
||||||
# Check remaining segment durations
|
# Check remaining segment durations
|
||||||
assert all([s.duration == SEGMENT_DURATION for s in segments])
|
assert all(s.duration == SEGMENT_DURATION for s in segments)
|
||||||
assert len(decoded_stream.video_packets) == len(packets) - 1
|
assert len(decoded_stream.video_packets) == len(packets) - 1
|
||||||
assert len(decoded_stream.audio_packets) == 0
|
assert len(decoded_stream.audio_packets) == 0
|
||||||
|
|
||||||
@ -290,9 +290,9 @@ async def test_discard_old_packets(hass):
|
|||||||
# Check number of segments
|
# Check number of segments
|
||||||
assert len(segments) == int((OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET)
|
assert len(segments) == int((OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET)
|
||||||
# Check sequence numbers
|
# Check sequence numbers
|
||||||
assert all([segments[i].sequence == i + 1 for i in range(len(segments))])
|
assert all(segments[i].sequence == i for i in range(len(segments)))
|
||||||
# Check segment durations
|
# Check segment durations
|
||||||
assert all([s.duration == SEGMENT_DURATION for s in segments])
|
assert all(s.duration == SEGMENT_DURATION for s in segments)
|
||||||
assert len(decoded_stream.video_packets) == OUT_OF_ORDER_PACKET_INDEX
|
assert len(decoded_stream.video_packets) == OUT_OF_ORDER_PACKET_INDEX
|
||||||
assert len(decoded_stream.audio_packets) == 0
|
assert len(decoded_stream.audio_packets) == 0
|
||||||
|
|
||||||
@ -309,9 +309,9 @@ async def test_packet_overflow(hass):
|
|||||||
# Check number of segments
|
# Check number of segments
|
||||||
assert len(segments) == int((OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET)
|
assert len(segments) == int((OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET)
|
||||||
# Check sequence numbers
|
# Check sequence numbers
|
||||||
assert all([segments[i].sequence == i + 1 for i in range(len(segments))])
|
assert all(segments[i].sequence == i for i in range(len(segments)))
|
||||||
# Check segment durations
|
# Check segment durations
|
||||||
assert all([s.duration == SEGMENT_DURATION for s in segments])
|
assert all(s.duration == SEGMENT_DURATION for s in segments)
|
||||||
assert len(decoded_stream.video_packets) == OUT_OF_ORDER_PACKET_INDEX
|
assert len(decoded_stream.video_packets) == OUT_OF_ORDER_PACKET_INDEX
|
||||||
assert len(decoded_stream.audio_packets) == 0
|
assert len(decoded_stream.audio_packets) == 0
|
||||||
|
|
||||||
@ -332,9 +332,9 @@ async def test_skip_initial_bad_packets(hass):
|
|||||||
(num_packets - num_bad_packets - 1) * SEGMENTS_PER_PACKET
|
(num_packets - num_bad_packets - 1) * SEGMENTS_PER_PACKET
|
||||||
)
|
)
|
||||||
# Check sequence numbers
|
# Check sequence numbers
|
||||||
assert all([segments[i].sequence == i + 1 for i in range(len(segments))])
|
assert all(segments[i].sequence == i for i in range(len(segments)))
|
||||||
# Check segment durations
|
# Check segment durations
|
||||||
assert all([s.duration == SEGMENT_DURATION for s in segments])
|
assert all(s.duration == SEGMENT_DURATION for s in segments)
|
||||||
assert len(decoded_stream.video_packets) == num_packets - num_bad_packets
|
assert len(decoded_stream.video_packets) == num_packets - num_bad_packets
|
||||||
assert len(decoded_stream.audio_packets) == 0
|
assert len(decoded_stream.audio_packets) == 0
|
||||||
|
|
||||||
@ -368,8 +368,8 @@ async def test_skip_missing_dts(hass):
|
|||||||
decoded_stream = await async_decode_stream(hass, iter(packets))
|
decoded_stream = await async_decode_stream(hass, iter(packets))
|
||||||
segments = decoded_stream.segments
|
segments = decoded_stream.segments
|
||||||
# Check sequence numbers
|
# Check sequence numbers
|
||||||
assert all([segments[i].sequence == i + 1 for i in range(len(segments))])
|
assert all(segments[i].sequence == i for i in range(len(segments)))
|
||||||
# Check segment durations (not counting the elongated segment)
|
# Check segment durations (not counting the last segment)
|
||||||
assert (
|
assert (
|
||||||
sum([segments[i].duration == SEGMENT_DURATION for i in range(len(segments))])
|
sum([segments[i].duration == SEGMENT_DURATION for i in range(len(segments))])
|
||||||
>= len(segments) - 1
|
>= len(segments) - 1
|
||||||
@ -495,9 +495,9 @@ async def test_pts_out_of_order(hass):
|
|||||||
# Check number of segments
|
# Check number of segments
|
||||||
assert len(segments) == int((TEST_SEQUENCE_LENGTH - 1) * SEGMENTS_PER_PACKET)
|
assert len(segments) == int((TEST_SEQUENCE_LENGTH - 1) * SEGMENTS_PER_PACKET)
|
||||||
# Check sequence numbers
|
# Check sequence numbers
|
||||||
assert all([segments[i].sequence == i + 1 for i in range(len(segments))])
|
assert all(segments[i].sequence == i for i in range(len(segments)))
|
||||||
# Check segment durations
|
# Check segment durations
|
||||||
assert all([s.duration == SEGMENT_DURATION for s in segments])
|
assert all(s.duration == SEGMENT_DURATION for s in segments)
|
||||||
assert len(decoded_stream.video_packets) == len(packets)
|
assert len(decoded_stream.video_packets) == len(packets)
|
||||||
assert len(decoded_stream.audio_packets) == 0
|
assert len(decoded_stream.audio_packets) == 0
|
||||||
|
|
||||||
@ -512,7 +512,7 @@ async def test_stream_stopped_while_decoding(hass):
|
|||||||
worker_wake = threading.Event()
|
worker_wake = threading.Event()
|
||||||
|
|
||||||
stream = Stream(hass, STREAM_SOURCE)
|
stream = Stream(hass, STREAM_SOURCE)
|
||||||
stream.add_provider(STREAM_OUTPUT_FORMAT)
|
stream.add_provider(HLS_PROVIDER)
|
||||||
|
|
||||||
py_av = MockPyAv()
|
py_av = MockPyAv()
|
||||||
py_av.container.packets = PacketSequence(TEST_SEQUENCE_LENGTH)
|
py_av.container.packets = PacketSequence(TEST_SEQUENCE_LENGTH)
|
||||||
@ -539,7 +539,7 @@ async def test_update_stream_source(hass):
|
|||||||
worker_wake = threading.Event()
|
worker_wake = threading.Event()
|
||||||
|
|
||||||
stream = Stream(hass, STREAM_SOURCE)
|
stream = Stream(hass, STREAM_SOURCE)
|
||||||
stream.add_provider(STREAM_OUTPUT_FORMAT)
|
stream.add_provider(HLS_PROVIDER)
|
||||||
# Note that keepalive is not set here. The stream is "restarted" even though
|
# Note that keepalive is not set here. The stream is "restarted" even though
|
||||||
# it is not stopping due to failure.
|
# it is not stopping due to failure.
|
||||||
|
|
||||||
@ -572,14 +572,14 @@ async def test_update_stream_source(hass):
|
|||||||
assert last_stream_source == STREAM_SOURCE + "-updated-source"
|
assert last_stream_source == STREAM_SOURCE + "-updated-source"
|
||||||
worker_wake.set()
|
worker_wake.set()
|
||||||
|
|
||||||
# Ccleanup
|
# Cleanup
|
||||||
stream.stop()
|
stream.stop()
|
||||||
|
|
||||||
|
|
||||||
async def test_worker_log(hass, caplog):
|
async def test_worker_log(hass, caplog):
|
||||||
"""Test that the worker logs the url without username and password."""
|
"""Test that the worker logs the url without username and password."""
|
||||||
stream = Stream(hass, "https://abcd:efgh@foo.bar")
|
stream = Stream(hass, "https://abcd:efgh@foo.bar")
|
||||||
stream.add_provider(STREAM_OUTPUT_FORMAT)
|
stream.add_provider(HLS_PROVIDER)
|
||||||
with patch("av.open") as av_open:
|
with patch("av.open") as av_open:
|
||||||
av_open.side_effect = av.error.InvalidDataError(-2, "error")
|
av_open.side_effect = av.error.InvalidDataError(-2, "error")
|
||||||
segment_buffer = SegmentBuffer(stream.outputs)
|
segment_buffer = SegmentBuffer(stream.outputs)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user