1 """Provide functionality to record stream."""
3 from __future__
import annotations
5 from collections
import deque
6 from io
import DEFAULT_BUFFER_SIZE, BytesIO
9 from typing
import TYPE_CHECKING
17 RECORDER_CONTAINER_FORMAT,
19 SEGMENT_CONTAINER_FORMAT,
21 from .core
import PROVIDERS, IdleTimer, Segment, StreamOutput, StreamSettings
22 from .fmp4utils
import read_init, transform_init
27 _LOGGER = logging.getLogger(__name__)
32 """Only here so Provider Registry works."""
35 @PROVIDERS.register(RECORDER_PROVIDER)
37 """Represents the Recorder Output format."""
42 idle_timer: IdleTimer,
43 stream_settings: StreamSettings,
44 dynamic_stream_settings: DynamicStreamSettings,
46 """Initialize recorder output."""
47 super().
__init__(hass, idle_timer, stream_settings, dynamic_stream_settings)
52 """Return provider name."""
53 return RECORDER_PROVIDER
55 def prepend(self, segments: list[Segment]) ->
None:
56 """Prepend segments to existing list."""
57 self._segments.extendleft(reversed(segments))
65 """Handle saving stream."""
67 os.makedirs(os.path.dirname(self.video_path), exist_ok=
True)
69 pts_adjuster: dict[str, int |
None] = {
"video":
None,
"audio":
None}
70 output: av.container.OutputContainer |
None =
None
79 last_sequence =
float(
"-inf")
81 def write_segment(segment: Segment) ->
None:
82 """Write a segment to output."""
84 nonlocal output, output_v, output_a, last_stream_id, running_duration, last_sequence
88 if segment.sequence <= last_sequence:
90 last_sequence = segment.sequence
94 BytesIO(segment.init + segment.get_data()),
96 format=SEGMENT_CONTAINER_FORMAT,
99 if source.duration
is None:
102 source_v = source.streams.video[0]
104 source.streams.audio[0]
if len(source.streams.audio) > 0
else None
109 container_options: dict[str, str] = {
110 "video_track_timescale":
str(
int(1 / source_v.time_base)),
111 "movflags":
"frag_keyframe+empty_moov",
115 self.video_path +
".tmp",
117 format=RECORDER_CONTAINER_FORMAT,
118 container_options=container_options,
123 output_v = output.add_stream(template=source_v)
124 context = output_v.codec_context
125 context.global_header =
True
126 if source_a
and not output_a:
127 output_a = output.add_stream(template=source_a)
131 if last_stream_id != segment.stream_id:
132 last_stream_id = segment.stream_id
133 pts_adjuster[
"video"] =
int(
134 (running_duration - source.start_time)
135 / (av.time_base * source_v.time_base)
138 pts_adjuster[
"audio"] =
int(
139 (running_duration - source.start_time)
140 / (av.time_base * source_a.time_base)
144 for packet
in source.demux():
145 if packet.pts
is None:
147 packet.pts += pts_adjuster[packet.stream.type]
148 packet.dts += pts_adjuster[packet.stream.type]
149 stream = output_v
if packet.stream.type ==
"video" else output_a
151 packet.stream = stream
154 running_duration += source.duration - source.start_time
158 def write_transform_matrix_and_rename(video_path: str) ->
None:
159 """Update the transform matrix and write to the desired filename."""
161 open(video_path +
".tmp", mode=
"rb")
as in_file,
162 open(video_path, mode=
"wb")
as out_file,
168 in_file.seek(len(init))
169 while chunk := in_file.read(DEFAULT_BUFFER_SIZE):
170 out_file.write(chunk)
171 os.remove(video_path +
".tmp")
174 segments: deque[Segment],
175 output: av.container.OutputContainer |
None,
178 """Finish writing output."""
181 write_segment(segments.popleft())
183 _LOGGER.error(
"Recording failed to capture anything")
187 write_transform_matrix_and_rename(video_path)
188 except FileNotFoundError:
191 "Error writing to '%s'. There are likely multiple recordings"
192 " writing to the same file"
198 while len(self._segments) > 1:
199 await self.
_hass_hass.async_add_executor_job(
200 write_segment, self._segments.popleft()
203 if not self._segments:
204 await self.
recvrecv()
206 while not self.
idleidle:
207 await self.
recvrecv()
208 await self.
_hass_hass.async_add_executor_job(
209 write_segment, self._segments.popleft()
212 await self.
_hass_hass.async_add_executor_job(
213 finish_writing, self._segments, output, self.video_path
None prepend(self, list[Segment] segments)
None __init__(self, HomeAssistant hass, IdleTimer idle_timer, StreamSettings stream_settings, DynamicStreamSettings dynamic_stream_settings)
None open(self, **Any kwargs)
bytes transform_init(bytes init, Orientation orientation)
bytes read_init(BufferedIOBase bytes_io)
None async_setup_recorder(HomeAssistant hass)