2019-04-03 15:40:03 +00:00
|
|
|
"""Provide functionality to stream HLS."""
|
2019-03-12 02:57:10 +00:00
|
|
|
from aiohttp import web
|
|
|
|
|
|
|
|
from homeassistant.core import callback
|
|
|
|
from homeassistant.util.dt import utcnow
|
|
|
|
|
|
|
|
from .const import FORMAT_CONTENT_TYPE
|
2019-10-14 21:20:18 +00:00
|
|
|
from .core import PROVIDERS, StreamOutput, StreamView
|
2019-03-12 02:57:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_setup_hls(hass):
|
|
|
|
"""Set up api endpoints."""
|
|
|
|
hass.http.register_view(HlsPlaylistView())
|
|
|
|
hass.http.register_view(HlsSegmentView())
|
2019-07-31 19:25:30 +00:00
|
|
|
return "/api/hls/{}/playlist.m3u8"
|
2019-03-12 02:57:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
class HlsPlaylistView(StreamView):
|
|
|
|
"""Stream view to serve a M3U8 stream."""
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
url = r"/api/hls/{token:[a-f0-9]+}/playlist.m3u8"
|
|
|
|
name = "api:stream:hls:playlist"
|
2019-03-12 02:57:10 +00:00
|
|
|
cors_allowed = True
|
|
|
|
|
|
|
|
async def handle(self, request, stream, sequence):
|
|
|
|
"""Return m3u8 playlist."""
|
|
|
|
renderer = M3U8Renderer(stream)
|
2019-07-31 19:25:30 +00:00
|
|
|
track = stream.add_provider("hls")
|
2019-03-12 02:57:10 +00:00
|
|
|
stream.start()
|
|
|
|
# Wait for a segment to be ready
|
|
|
|
if not track.segments:
|
|
|
|
await track.recv()
|
2019-07-31 19:25:30 +00:00
|
|
|
headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]}
|
|
|
|
return web.Response(
|
|
|
|
body=renderer.render(track, utcnow()).encode("utf-8"), headers=headers
|
|
|
|
)
|
2019-03-12 02:57:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
class HlsSegmentView(StreamView):
|
|
|
|
"""Stream view to serve a MPEG2TS segment."""
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
url = r"/api/hls/{token:[a-f0-9]+}/segment/{sequence:\d+}.ts"
|
|
|
|
name = "api:stream:hls:segment"
|
2019-03-12 02:57:10 +00:00
|
|
|
cors_allowed = True
|
|
|
|
|
|
|
|
async def handle(self, request, stream, sequence):
|
|
|
|
"""Return mpegts segment."""
|
2019-07-31 19:25:30 +00:00
|
|
|
track = stream.add_provider("hls")
|
2019-03-12 02:57:10 +00:00
|
|
|
segment = track.get_segment(int(sequence))
|
|
|
|
if not segment:
|
|
|
|
return web.HTTPNotFound()
|
2019-07-31 19:25:30 +00:00
|
|
|
headers = {"Content-Type": "video/mp2t"}
|
2019-03-12 02:57:10 +00:00
|
|
|
return web.Response(body=segment.segment.getvalue(), headers=headers)
|
|
|
|
|
|
|
|
|
|
|
|
class M3U8Renderer:
|
|
|
|
"""M3U8 Render Helper."""
|
|
|
|
|
|
|
|
def __init__(self, stream):
|
|
|
|
"""Initialize renderer."""
|
|
|
|
self.stream = stream
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def render_preamble(track):
|
|
|
|
"""Render preamble."""
|
2019-09-03 19:14:39 +00:00
|
|
|
return ["#EXT-X-VERSION:3", f"#EXT-X-TARGETDURATION:{track.target_duration}"]
|
2019-03-12 02:57:10 +00:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def render_playlist(track, start_time):
|
|
|
|
"""Render playlist."""
|
|
|
|
segments = track.segments
|
|
|
|
|
|
|
|
if not segments:
|
|
|
|
return []
|
|
|
|
|
|
|
|
playlist = ["#EXT-X-MEDIA-SEQUENCE:{}".format(segments[0])]
|
|
|
|
|
|
|
|
for sequence in segments:
|
|
|
|
segment = track.get_segment(sequence)
|
2019-07-31 19:25:30 +00:00
|
|
|
playlist.extend(
|
|
|
|
[
|
|
|
|
"#EXTINF:{:.04f},".format(float(segment.duration)),
|
2019-09-03 19:14:39 +00:00
|
|
|
f"./segment/{segment.sequence}.ts",
|
2019-07-31 19:25:30 +00:00
|
|
|
]
|
|
|
|
)
|
2019-03-12 02:57:10 +00:00
|
|
|
|
|
|
|
return playlist
|
|
|
|
|
|
|
|
def render(self, track, start_time):
|
|
|
|
"""Render M3U8 file."""
|
|
|
|
lines = (
|
2019-07-31 19:25:30 +00:00
|
|
|
["#EXTM3U"]
|
|
|
|
+ self.render_preamble(track)
|
|
|
|
+ self.render_playlist(track, start_time)
|
2019-03-12 02:57:10 +00:00
|
|
|
)
|
|
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
@PROVIDERS.register("hls")
|
2019-03-12 02:57:10 +00:00
|
|
|
class HlsStreamOutput(StreamOutput):
|
|
|
|
"""Represents HLS Output formats."""
|
|
|
|
|
2019-03-28 04:47:07 +00:00
|
|
|
@property
|
|
|
|
def name(self) -> str:
|
|
|
|
"""Return provider name."""
|
2019-07-31 19:25:30 +00:00
|
|
|
return "hls"
|
2019-03-28 04:47:07 +00:00
|
|
|
|
2019-03-12 02:57:10 +00:00
|
|
|
@property
|
|
|
|
def format(self) -> str:
|
|
|
|
"""Return container format."""
|
2019-07-31 19:25:30 +00:00
|
|
|
return "mpegts"
|
2019-03-12 02:57:10 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def audio_codec(self) -> str:
|
|
|
|
"""Return desired audio codec."""
|
2019-07-31 19:25:30 +00:00
|
|
|
return "aac"
|
2019-03-12 02:57:10 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def video_codec(self) -> str:
|
|
|
|
"""Return desired video codec."""
|
2019-07-31 19:25:30 +00:00
|
|
|
return "h264"
|