Refactor stream to create partial segments (#51282)

This commit is contained in:
uvjustin 2021-06-14 00:41:21 +08:00 committed by GitHub
parent 1adeb82930
commit 123e8f01a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 499 additions and 305 deletions

View file

@ -37,9 +37,12 @@ class HlsMasterPlaylistView(StreamView):
# Need to calculate max bandwidth as input_container.bit_rate doesn't seem to work
# Calculate file size / duration and use a small multiplier to account for variation
# hls spec already allows for 25% variation
segment = track.get_segment(track.sequences[-1])
segment = track.get_segment(track.sequences[-2])
bandwidth = round(
(len(segment.init) + len(segment.moof_data)) * 8 / segment.duration * 1.2
(len(segment.init) + sum(len(part.data) for part in segment.parts))
* 8
/ segment.duration
* 1.2
)
codecs = get_codec_string(segment.init)
lines = [
@ -53,9 +56,11 @@ class HlsMasterPlaylistView(StreamView):
"""Return m3u8 playlist."""
track = stream.add_provider(HLS_PROVIDER)
stream.start()
# Wait for a segment to be ready
# Make sure at least two segments are ready (last one may not be complete)
if not track.sequences and not await track.recv():
return web.HTTPNotFound()
if len(track.sequences) == 1 and not await track.recv():
return web.HTTPNotFound()
headers = {"Content-Type": FORMAT_CONTENT_TYPE[HLS_PROVIDER]}
return web.Response(body=self.render(track).encode("utf-8"), headers=headers)
@ -68,69 +73,72 @@ class HlsPlaylistView(StreamView):
cors_allowed = True
@staticmethod
def render_preamble(track):
"""Render preamble."""
return [
"#EXT-X-VERSION:6",
f"#EXT-X-TARGETDURATION:{track.target_duration}",
'#EXT-X-MAP:URI="init.mp4"',
]
@staticmethod
def render_playlist(track):
def render(track):
"""Render playlist."""
segments = list(track.get_segments())[-NUM_PLAYLIST_SEGMENTS:]
# NUM_PLAYLIST_SEGMENTS+1 because most recent is probably not yet complete
segments = list(track.get_segments())[-(NUM_PLAYLIST_SEGMENTS + 1) :]
if not segments:
return []
# To cap the number of complete segments at NUM_PLAYLIST_SEGMENTS,
# remove the first segment if the last segment is actually complete
if segments[-1].complete:
segments = segments[-NUM_PLAYLIST_SEGMENTS:]
first_segment = segments[0]
playlist = [
"#EXTM3U",
"#EXT-X-VERSION:6",
"#EXT-X-INDEPENDENT-SEGMENTS",
'#EXT-X-MAP:URI="init.mp4"',
f"#EXT-X-TARGETDURATION:{track.target_duration:.0f}",
f"#EXT-X-MEDIA-SEQUENCE:{first_segment.sequence}",
f"#EXT-X-DISCONTINUITY-SEQUENCE:{first_segment.stream_id}",
"#EXT-X-PROGRAM-DATE-TIME:"
+ first_segment.start_time.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
+ "Z",
# Since our window doesn't have many segments, we don't want to start
# at the beginning or we risk a behind live window exception in exoplayer.
# at the beginning or we risk a behind live window exception in Exoplayer.
# EXT-X-START is not supposed to be within 3 target durations of the end,
# but this seems ok
f"#EXT-X-START:TIME-OFFSET=-{EXT_X_START * track.target_duration:.3f},PRECISE=YES",
# but a value as low as 1.5 doesn't seem to hurt.
# A value below 3 may not be as useful for hls.js as many hls.js clients
# don't autoplay. Also, hls.js uses the player parameter liveSyncDuration
# which seems to take precedence for setting target delay. Yet it also
# doesn't seem to hurt, so we can stick with it for now.
f"#EXT-X-START:TIME-OFFSET=-{EXT_X_START * track.target_duration:.3f}",
]
last_stream_id = first_segment.stream_id
# Add playlist sections
for segment in segments:
if last_stream_id != segment.stream_id:
# Skip last segment if it is not complete
if segment.complete:
if last_stream_id != segment.stream_id:
playlist.extend(
[
"#EXT-X-DISCONTINUITY",
"#EXT-X-PROGRAM-DATE-TIME:"
+ segment.start_time.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
+ "Z",
]
)
playlist.extend(
[
"#EXT-X-DISCONTINUITY",
"#EXT-X-PROGRAM-DATE-TIME:"
+ segment.start_time.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
+ "Z",
f"#EXTINF:{segment.duration:.3f},",
f"./segment/{segment.sequence}.m4s",
]
)
playlist.extend(
[
f"#EXTINF:{float(segment.duration):.04f},",
f"./segment/{segment.sequence}.m4s",
]
)
last_stream_id = segment.stream_id
last_stream_id = segment.stream_id
return playlist
def render(self, track):
"""Render M3U8 file."""
lines = ["#EXTM3U"] + self.render_preamble(track) + self.render_playlist(track)
return "\n".join(lines) + "\n"
return "\n".join(playlist) + "\n"
async def handle(self, request, stream, sequence):
"""Return m3u8 playlist."""
track = stream.add_provider(HLS_PROVIDER)
stream.start()
# Wait for a segment to be ready
# Make sure at least two segments are ready (last one may not be complete)
if not track.sequences and not await track.recv():
return web.HTTPNotFound()
if len(track.sequences) == 1 and not await track.recv():
return web.HTTPNotFound()
headers = {"Content-Type": FORMAT_CONTENT_TYPE[HLS_PROVIDER]}
response = web.Response(
body=self.render(track).encode("utf-8"), headers=headers
@ -170,7 +178,7 @@ class HlsSegmentView(StreamView):
return web.HTTPNotFound()
headers = {"Content-Type": "video/iso.segment"}
return web.Response(
body=segment.moof_data,
body=segment.get_bytes_without_init(),
headers=headers,
)