1 """Provide functionality to stream HLS."""
3 from __future__
import annotations
5 from http
import HTTPStatus
6 from typing
import TYPE_CHECKING, cast
8 from aiohttp
import web
14 EXT_X_START_NON_LL_HLS,
18 NUM_PLAYLIST_SEGMENTS,
28 from .fmp4utils
import get_codec_string, transform_init
38 """Set up api endpoints."""
44 return "/api/hls/{}/master_playlist.m3u8"
47 @PROVIDERS.register(HLS_PROVIDER)
49 """Represents HLS Output formats."""
54 idle_timer: IdleTimer,
55 stream_settings: StreamSettings,
56 dynamic_stream_settings: DynamicStreamSettings,
58 """Initialize HLS output."""
63 dynamic_stream_settings,
64 deque_maxlen=MAX_SEGMENTS,
70 """Return provider name."""
76 self._segments.clear()
80 """Return the target duration."""
85 """Async put and also update the target duration.
87 The target duration is calculated as the max duration of any given segment.
88 Technically it should not change per the hls spec, but some cameras adjust
89 their GOPs periodically so we need to account for this change.
93 max((s.duration
for s
in self._segments), default=segment.duration)
98 """Fix incomplete segment at end of deque."""
103 """Fix incomplete segment at end of deque in event loop."""
106 if (last_segment := self._segments[-1]).parts:
107 last_segment.duration = sum(
108 part.duration
for part
in last_segment.parts
115 """Stream view used only for Chromecast compatibility."""
117 url =
r"/api/hls/{token:[a-f0-9]+}/master_playlist.m3u8"
118 name =
"api:stream:hls:master_playlist"
123 """Render M3U8 file."""
127 if not (segment := track.get_segment(track.sequences[-2])):
129 bandwidth = round(segment.data_size_with_init * 8 / segment.duration * 1.2)
133 f
'#EXT-X-STREAM-INF:BANDWIDTH={bandwidth},CODECS="{codecs}"',
136 return "\n".join(lines) +
"\n"
139 self, request: web.Request, stream: Stream, sequence: str, part_num: str
141 """Return m3u8 playlist."""
142 track = stream.add_provider(HLS_PROVIDER)
145 if not track.sequences
and not await track.recv():
146 return web.HTTPNotFound()
147 if len(track.sequences) == 1
and not await track.recv():
148 return web.HTTPNotFound()
149 response = web.Response(
150 body=self.
renderrender(track).encode(
"utf-8"),
152 "Content-Type": FORMAT_CONTENT_TYPE[HLS_PROVIDER],
155 response.enable_compression(web.ContentCoding.gzip)
160 """Stream view to serve a M3U8 stream."""
162 url =
r"/api/hls/{token:[a-f0-9]+}/playlist.m3u8"
163 name =
"api:stream:hls:playlist"
167 def render(cls, track: HlsStreamOutput) -> str:
168 """Render HLS playlist file."""
170 segments =
list(track.get_segments())[-(NUM_PLAYLIST_SEGMENTS + 1) :]
174 if segments[-1].complete:
175 segments = segments[-NUM_PLAYLIST_SEGMENTS:]
177 first_segment = segments[0]
181 "#EXT-X-INDEPENDENT-SEGMENTS",
182 '#EXT-X-MAP:URI="init.mp4"',
183 f
"#EXT-X-TARGETDURATION:{track.target_duration:.0f}",
184 f
"#EXT-X-MEDIA-SEQUENCE:{first_segment.sequence}",
185 f
"#EXT-X-DISCONTINUITY-SEQUENCE:{first_segment.stream_id}",
188 if track.stream_settings.ll_hls:
191 f
"#EXT-X-PART-INF:PART-TARGET={track.stream_settings.part_target_duration:.3f}",
192 f
"#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK={2*track.stream_settings.part_target_duration:.3f}",
193 f
"#EXT-X-START:TIME-OFFSET=-{EXT_X_START_LL_HLS*track.stream_settings.part_target_duration:.3f},PRECISE=YES",
206 f
"#EXT-X-START:TIME-OFFSET=-{EXT_X_START_NON_LL_HLS*track.target_duration:.3f},PRECISE=YES"
209 last_stream_id = first_segment.stream_id
215 for i, segment
in enumerate(segments[:-1], 3 - len(segments)):
218 last_stream_id=last_stream_id,
219 render_parts=i >= 0
and track.stream_settings.ll_hls,
223 last_stream_id = segment.stream_id
226 segments[-1].render_hls(
227 last_stream_id=last_stream_id,
228 render_parts=track.stream_settings.ll_hls,
229 add_hint=track.stream_settings.ll_hls,
233 return "\n".join(playlist) +
"\n"
236 def bad_request(blocking: bool, target_duration: float) -> web.Response:
237 """Return a HTTP Bad Request response."""
240 status=HTTPStatus.BAD_REQUEST,
244 def not_found(blocking: bool, target_duration: float) -> web.Response:
245 """Return a HTTP Not Found response."""
248 status=HTTPStatus.NOT_FOUND,
252 self, request: web.Request, stream: Stream, sequence: str, part_num: str
254 """Return m3u8 playlist."""
255 track: HlsStreamOutput = cast(
256 HlsStreamOutput, stream.add_provider(HLS_PROVIDER)
260 hls_msn: str | int |
None = request.query.get(
"_HLS_msn")
261 hls_part: str | int |
None = request.query.get(
"_HLS_part")
262 blocking_request = bool(hls_msn
or hls_part)
266 if hls_msn
is None and hls_part:
267 return web.HTTPBadRequest()
269 hls_msn =
int(hls_msn
or 0)
276 if hls_msn > track.last_sequence + 2:
277 return self.
bad_requestbad_request(blocking_request, track.target_duration)
284 hls_part =
int(hls_part)
286 while hls_msn > track.last_sequence:
287 if not await track.recv():
288 return self.
not_foundnot_found(blocking_request, track.target_duration)
289 if track.last_segment
is None:
290 return self.
not_foundnot_found(blocking_request, 0)
292 (last_segment := track.last_segment)
293 and hls_msn == last_segment.sequence
295 >= len(last_segment.parts)
297 + track.stream_settings.hls_advance_part_limit
299 return self.
bad_requestbad_request(blocking_request, track.target_duration)
303 (last_segment := track.last_segment)
304 and hls_msn == last_segment.sequence
305 and hls_part >= len(last_segment.parts)
307 if not await track.part_recv(
308 timeout=track.stream_settings.hls_part_timeout
310 return self.
not_foundnot_found(blocking_request, track.target_duration)
317 if hls_msn + 1 == last_segment.sequence:
318 if not (previous_segment := track.get_segment(hls_msn))
or (
319 hls_part >= len(previous_segment.parts)
320 and not last_segment.parts
321 and not await track.part_recv(
322 timeout=track.stream_settings.hls_part_timeout
325 return self.
not_foundnot_found(blocking_request, track.target_duration)
327 response = web.Response(
328 body=self.
renderrender(track).encode(
"utf-8"),
330 "Content-Type": FORMAT_CONTENT_TYPE[HLS_PROVIDER],
333 response.enable_compression(web.ContentCoding.gzip)
338 """Stream view to serve HLS init.mp4."""
340 url =
r"/api/hls/{token:[a-f0-9]+}/init.mp4"
341 name =
"api:stream:hls:init"
345 self, request: web.Request, stream: Stream, sequence: str, part_num: str
347 """Return init.mp4."""
348 track = stream.add_provider(HLS_PROVIDER)
349 if not (segments := track.get_segments())
or not (body := segments[0].init):
350 return web.HTTPNotFound()
352 body=
transform_init(body, stream.dynamic_stream_settings.orientation),
353 headers={
"Content-Type":
"video/mp4"},
358 """Stream view to serve a HLS fmp4 segment."""
360 url =
r"/api/hls/{token:[a-f0-9]+}/segment/{sequence:\d+}.{part_num:\d+}.m4s"
361 name =
"api:stream:hls:part"
365 self, request: web.Request, stream: Stream, sequence: str, part_num: str
368 track: HlsStreamOutput = cast(
369 HlsStreamOutput, stream.add_provider(HLS_PROVIDER)
371 track.idle_timer.awake()
376 (segment := track.get_segment(
int(sequence)))
378 await track.part_recv(timeout=track.stream_settings.hls_part_timeout)
379 and (segment := track.get_segment(
int(sequence)))
384 status=HTTPStatus.NOT_FOUND,
387 if int(part_num) == len(segment.parts):
388 await track.part_recv(timeout=track.stream_settings.hls_part_timeout)
389 if int(part_num) >= len(segment.parts):
390 return web.HTTPRequestRangeNotSatisfiable()
392 body=segment.parts[
int(part_num)].data,
394 "Content-Type":
"video/iso.segment",
400 """Stream view to serve a HLS fmp4 segment."""
402 url =
r"/api/hls/{token:[a-f0-9]+}/segment/{sequence:\d+}.m4s"
403 name =
"api:stream:hls:segment"
407 self, request: web.Request, stream: Stream, sequence: str, part_num: str
408 ) -> web.StreamResponse:
409 """Handle segments."""
410 track: HlsStreamOutput = cast(
411 HlsStreamOutput, stream.add_provider(HLS_PROVIDER)
413 track.idle_timer.awake()
418 (segment := track.get_segment(
int(sequence)))
420 await track.part_recv(timeout=track.stream_settings.hls_part_timeout)
421 and (segment := track.get_segment(
int(sequence)))
426 status=HTTPStatus.NOT_FOUND,
429 body=segment.get_data(),
431 "Content-Type":
"video/iso.segment",
web.Response handle(self, web.Request request, Stream stream, str sequence, str part_num)
web.Response handle(self, web.Request request, Stream stream, str sequence, str part_num)
str render(StreamOutput track)
web.Response handle(self, web.Request request, Stream stream, str sequence, str part_num)
str render(cls, HlsStreamOutput track)
web.Response not_found(bool blocking, float target_duration)
web.Response handle(self, web.Request request, Stream stream, str sequence, str part_num)
web.Response bad_request(bool blocking, float target_duration)
web.StreamResponse handle(self, web.Request request, Stream stream, str sequence, str part_num)
None __init__(self, HomeAssistant hass, IdleTimer idle_timer, StreamSettings stream_settings, DynamicStreamSettings dynamic_stream_settings)
None _async_discontinuity(self)
float target_duration(self)
None _async_put(self, Segment segment)
str get_codec_string(bytes mp4_bytes)
bytes transform_init(bytes init, Orientation orientation)
str async_setup_hls(HomeAssistant hass)