mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
Combine StreamBuffer into SegmentBuffer in stream (#51041)
* Combine StreamBuffer into SegmentBuffer in stream * Use new style type hint in comment Remove unused member self._segment * Change reset_av to static helper function * Change make_new_av to only return OutputContainer
This commit is contained in:
parent
0fb2504e0c
commit
2eb87b8806
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections import deque
|
from collections import deque
|
||||||
import io
|
import io
|
||||||
from typing import TYPE_CHECKING, Callable
|
from typing import Callable
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import attr
|
import attr
|
||||||
@ -16,23 +16,9 @@ from homeassistant.util.decorator import Registry
|
|||||||
|
|
||||||
from .const import ATTR_STREAMS, DOMAIN
|
from .const import ATTR_STREAMS, DOMAIN
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
import av.container
|
|
||||||
import av.video
|
|
||||||
|
|
||||||
PROVIDERS = Registry()
|
PROVIDERS = Registry()
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class StreamBuffer:
|
|
||||||
"""Represent a segment."""
|
|
||||||
|
|
||||||
segment: io.BytesIO = attr.ib()
|
|
||||||
output: av.container.OutputContainer = attr.ib()
|
|
||||||
vstream: av.video.VideoStream = attr.ib()
|
|
||||||
astream = attr.ib(default=None) # type=Optional[av.audio.AudioStream]
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
class Segment:
|
class Segment:
|
||||||
"""Represent a segment."""
|
"""Represent a segment."""
|
||||||
|
@ -2,8 +2,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import deque
|
from collections import deque
|
||||||
import io
|
from io import BytesIO
|
||||||
import logging
|
import logging
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
import av
|
import av
|
||||||
|
|
||||||
@ -17,38 +18,11 @@ from .const import (
|
|||||||
SEGMENT_CONTAINER_FORMAT,
|
SEGMENT_CONTAINER_FORMAT,
|
||||||
STREAM_TIMEOUT,
|
STREAM_TIMEOUT,
|
||||||
)
|
)
|
||||||
from .core import Segment, StreamBuffer, StreamOutput
|
from .core import Segment, StreamOutput
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def create_stream_buffer(video_stream, audio_stream, sequence):
|
|
||||||
"""Create a new StreamBuffer."""
|
|
||||||
|
|
||||||
segment = io.BytesIO()
|
|
||||||
container_options = {
|
|
||||||
# Removed skip_sidx - see https://github.com/home-assistant/core/pull/39970
|
|
||||||
"movflags": "frag_custom+empty_moov+default_base_moof+frag_discont+negative_cts_offsets",
|
|
||||||
"avoid_negative_ts": "disabled",
|
|
||||||
"fragment_index": str(sequence),
|
|
||||||
}
|
|
||||||
output = av.open(
|
|
||||||
segment,
|
|
||||||
mode="w",
|
|
||||||
format=SEGMENT_CONTAINER_FORMAT,
|
|
||||||
container_options={
|
|
||||||
"video_track_timescale": str(int(1 / video_stream.time_base)),
|
|
||||||
**container_options,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
vstream = output.add_stream(template=video_stream)
|
|
||||||
# Check if audio is requested
|
|
||||||
astream = None
|
|
||||||
if audio_stream and audio_stream.name in AUDIO_CODECS:
|
|
||||||
astream = output.add_stream(template=audio_stream)
|
|
||||||
return StreamBuffer(segment, output, vstream, astream)
|
|
||||||
|
|
||||||
|
|
||||||
class SegmentBuffer:
|
class SegmentBuffer:
|
||||||
"""Buffer for writing a sequence of packets to the output as a segment."""
|
"""Buffer for writing a sequence of packets to the output as a segment."""
|
||||||
|
|
||||||
@ -61,12 +35,41 @@ class SegmentBuffer:
|
|||||||
self._outputs: list[StreamOutput] = []
|
self._outputs: list[StreamOutput] = []
|
||||||
self._sequence = 0
|
self._sequence = 0
|
||||||
self._segment_start_pts = None
|
self._segment_start_pts = None
|
||||||
self._stream_buffer = None
|
self._memory_file: BytesIO = cast(BytesIO, None)
|
||||||
|
self._av_output: av.container.OutputContainer = None
|
||||||
|
self._input_video_stream: av.video.VideoStream = None
|
||||||
|
self._input_audio_stream = None # av.audio.AudioStream | None
|
||||||
|
self._output_video_stream: av.video.VideoStream = None
|
||||||
|
self._output_audio_stream = None # av.audio.AudioStream | None
|
||||||
|
|
||||||
def set_streams(self, video_stream, audio_stream):
|
@staticmethod
|
||||||
|
def make_new_av(
|
||||||
|
memory_file, sequence: int, input_vstream: av.video.VideoStream
|
||||||
|
) -> av.container.OutputContainer:
|
||||||
|
"""Make a new av OutputContainer."""
|
||||||
|
return av.open(
|
||||||
|
memory_file,
|
||||||
|
mode="w",
|
||||||
|
format=SEGMENT_CONTAINER_FORMAT,
|
||||||
|
container_options={
|
||||||
|
# Removed skip_sidx - see https://github.com/home-assistant/core/pull/39970
|
||||||
|
# "cmaf" flag replaces several of the movflags used, but too recent to use for now
|
||||||
|
"movflags": "frag_custom+empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer",
|
||||||
|
"avoid_negative_ts": "disabled",
|
||||||
|
"fragment_index": str(sequence + 1),
|
||||||
|
"video_track_timescale": str(int(1 / input_vstream.time_base)),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_streams(
|
||||||
|
self,
|
||||||
|
video_stream: av.video.VideoStream,
|
||||||
|
audio_stream,
|
||||||
|
# no type hint for audio_stream until https://github.com/PyAV-Org/PyAV/pull/775 is merged
|
||||||
|
) -> None:
|
||||||
"""Initialize output buffer with streams from container."""
|
"""Initialize output buffer with streams from container."""
|
||||||
self._video_stream = video_stream
|
self._input_video_stream = video_stream
|
||||||
self._audio_stream = audio_stream
|
self._input_audio_stream = audio_stream
|
||||||
|
|
||||||
def reset(self, video_pts):
|
def reset(self, video_pts):
|
||||||
"""Initialize a new stream segment."""
|
"""Initialize a new stream segment."""
|
||||||
@ -77,15 +80,27 @@ class SegmentBuffer:
|
|||||||
# Fetch the latest StreamOutputs, which may have changed since the
|
# Fetch the latest StreamOutputs, which may have changed since the
|
||||||
# worker started.
|
# worker started.
|
||||||
self._outputs = self._outputs_callback().values()
|
self._outputs = self._outputs_callback().values()
|
||||||
self._stream_buffer = create_stream_buffer(
|
self._memory_file = BytesIO()
|
||||||
self._video_stream, self._audio_stream, self._sequence
|
self._av_output = self.make_new_av(
|
||||||
|
memory_file=self._memory_file,
|
||||||
|
sequence=self._sequence,
|
||||||
|
input_vstream=self._input_video_stream,
|
||||||
)
|
)
|
||||||
|
self._output_video_stream = self._av_output.add_stream(
|
||||||
|
template=self._input_video_stream
|
||||||
|
)
|
||||||
|
# Check if audio is requested
|
||||||
|
self._output_audio_stream = None
|
||||||
|
if self._input_audio_stream and self._input_audio_stream.name in AUDIO_CODECS:
|
||||||
|
self._output_audio_stream = self._av_output.add_stream(
|
||||||
|
template=self._input_audio_stream
|
||||||
|
)
|
||||||
|
|
||||||
def mux_packet(self, packet):
|
def mux_packet(self, packet):
|
||||||
"""Mux a packet to the appropriate StreamBuffers."""
|
"""Mux a packet to the appropriate output stream."""
|
||||||
|
|
||||||
# Check for end of segment
|
# Check for end of segment
|
||||||
if packet.stream == self._video_stream and packet.is_keyframe:
|
if packet.stream == self._input_video_stream and packet.is_keyframe:
|
||||||
duration = (packet.pts - self._segment_start_pts) * packet.time_base
|
duration = (packet.pts - self._segment_start_pts) * packet.time_base
|
||||||
if duration >= MIN_SEGMENT_DURATION:
|
if duration >= MIN_SEGMENT_DURATION:
|
||||||
# Save segment to outputs
|
# Save segment to outputs
|
||||||
@ -95,19 +110,17 @@ class SegmentBuffer:
|
|||||||
self.reset(packet.pts)
|
self.reset(packet.pts)
|
||||||
|
|
||||||
# Mux the packet
|
# Mux the packet
|
||||||
if packet.stream == self._video_stream:
|
if packet.stream == self._input_video_stream:
|
||||||
packet.stream = self._stream_buffer.vstream
|
packet.stream = self._output_video_stream
|
||||||
self._stream_buffer.output.mux(packet)
|
self._av_output.mux(packet)
|
||||||
elif packet.stream == self._audio_stream:
|
elif packet.stream == self._input_audio_stream:
|
||||||
packet.stream = self._stream_buffer.astream
|
packet.stream = self._output_audio_stream
|
||||||
self._stream_buffer.output.mux(packet)
|
self._av_output.mux(packet)
|
||||||
|
|
||||||
def flush(self, duration):
|
def flush(self, duration):
|
||||||
"""Create a segment from the buffered packets and write to output."""
|
"""Create a segment from the buffered packets and write to output."""
|
||||||
self._stream_buffer.output.close()
|
self._av_output.close()
|
||||||
segment = Segment(
|
segment = Segment(self._sequence, self._memory_file, duration, self._stream_id)
|
||||||
self._sequence, self._stream_buffer.segment, duration, self._stream_id
|
|
||||||
)
|
|
||||||
for stream_output in self._outputs:
|
for stream_output in self._outputs:
|
||||||
stream_output.put(segment)
|
stream_output.put(segment)
|
||||||
|
|
||||||
@ -120,7 +133,7 @@ class SegmentBuffer:
|
|||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Close stream buffer."""
|
"""Close stream buffer."""
|
||||||
self._stream_buffer.output.close()
|
self._av_output.close()
|
||||||
|
|
||||||
|
|
||||||
def stream_worker(source, options, segment_buffer, quit_event): # noqa: C901
|
def stream_worker(source, options, segment_buffer, quit_event): # noqa: C901
|
||||||
|
Loading…
x
Reference in New Issue
Block a user