mirror of
https://github.com/home-assistant/core.git
synced 2025-04-26 18:27:51 +00:00
Adjust segment duration calculation in stream (#51149)
* Calculate min segment duration internally * Rename segments to sequences in StreamOutput * Fix segment duration calculation in test_worker
This commit is contained in:
parent
e08de22737
commit
6ad29aec2c
@ -19,7 +19,11 @@ OUTPUT_IDLE_TIMEOUT = 300 # Idle timeout due to inactivity
|
|||||||
|
|
||||||
NUM_PLAYLIST_SEGMENTS = 3 # Number of segments to use in HLS playlist
|
NUM_PLAYLIST_SEGMENTS = 3 # Number of segments to use in HLS playlist
|
||||||
MAX_SEGMENTS = 4 # Max number of segments to keep around
|
MAX_SEGMENTS = 4 # Max number of segments to keep around
|
||||||
MIN_SEGMENT_DURATION = 1.5 # Each segment is at least this many seconds
|
TARGET_SEGMENT_DURATION = 2.0 # Each segment is about this many seconds
|
||||||
|
SEGMENT_DURATION_ADJUSTER = 0.1 # Used to avoid missing keyframe boundaries
|
||||||
|
MIN_SEGMENT_DURATION = (
|
||||||
|
TARGET_SEGMENT_DURATION - SEGMENT_DURATION_ADJUSTER
|
||||||
|
) # Each segment is at least this many seconds
|
||||||
|
|
||||||
PACKETS_TO_WAIT_FOR_AUDIO = 20 # Some streams have an audio stream with no audio
|
PACKETS_TO_WAIT_FOR_AUDIO = 20 # Some streams have an audio stream with no audio
|
||||||
MAX_TIMESTAMP_GAP = 10000 # seconds - anything from 10 to 50000 is probably reasonable
|
MAX_TIMESTAMP_GAP = 10000 # seconds - anything from 10 to 50000 is probably reasonable
|
||||||
|
@ -105,7 +105,7 @@ class StreamOutput:
|
|||||||
return -1
|
return -1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def segments(self) -> list[int]:
|
def sequences(self) -> list[int]:
|
||||||
"""Return current sequence from segments."""
|
"""Return current sequence from segments."""
|
||||||
return [s.sequence for s in self._segments]
|
return [s.sequence for s in self._segments]
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ class HlsMasterPlaylistView(StreamView):
|
|||||||
# Need to calculate max bandwidth as input_container.bit_rate doesn't seem to work
|
# 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
|
# Calculate file size / duration and use a small multiplier to account for variation
|
||||||
# hls spec already allows for 25% variation
|
# hls spec already allows for 25% variation
|
||||||
segment = track.get_segment(track.segments[-1])
|
segment = track.get_segment(track.sequences[-1])
|
||||||
bandwidth = round(
|
bandwidth = round(
|
||||||
(len(segment.init) + len(segment.moof_data)) * 8 / segment.duration * 1.2
|
(len(segment.init) + len(segment.moof_data)) * 8 / segment.duration * 1.2
|
||||||
)
|
)
|
||||||
@ -53,7 +53,7 @@ class HlsMasterPlaylistView(StreamView):
|
|||||||
track = stream.add_provider(HLS_PROVIDER)
|
track = stream.add_provider(HLS_PROVIDER)
|
||||||
stream.start()
|
stream.start()
|
||||||
# Wait for a segment to be ready
|
# Wait for a segment to be ready
|
||||||
if not track.segments and not await track.recv():
|
if not track.sequences and not await track.recv():
|
||||||
return web.HTTPNotFound()
|
return web.HTTPNotFound()
|
||||||
headers = {"Content-Type": FORMAT_CONTENT_TYPE[HLS_PROVIDER]}
|
headers = {"Content-Type": FORMAT_CONTENT_TYPE[HLS_PROVIDER]}
|
||||||
return web.Response(body=self.render(track).encode("utf-8"), headers=headers)
|
return web.Response(body=self.render(track).encode("utf-8"), headers=headers)
|
||||||
@ -112,7 +112,7 @@ class HlsPlaylistView(StreamView):
|
|||||||
track = stream.add_provider(HLS_PROVIDER)
|
track = stream.add_provider(HLS_PROVIDER)
|
||||||
stream.start()
|
stream.start()
|
||||||
# Wait for a segment to be ready
|
# Wait for a segment to be ready
|
||||||
if not track.segments and not await track.recv():
|
if not track.sequences and not await track.recv():
|
||||||
return web.HTTPNotFound()
|
return web.HTTPNotFound()
|
||||||
headers = {"Content-Type": FORMAT_CONTENT_TYPE[HLS_PROVIDER]}
|
headers = {"Content-Type": FORMAT_CONTENT_TYPE[HLS_PROVIDER]}
|
||||||
return web.Response(body=self.render(track).encode("utf-8"), headers=headers)
|
return web.Response(body=self.render(track).encode("utf-8"), headers=headers)
|
||||||
|
@ -25,8 +25,8 @@ from homeassistant.components.stream import Stream
|
|||||||
from homeassistant.components.stream.const import (
|
from homeassistant.components.stream.const import (
|
||||||
HLS_PROVIDER,
|
HLS_PROVIDER,
|
||||||
MAX_MISSING_DTS,
|
MAX_MISSING_DTS,
|
||||||
MIN_SEGMENT_DURATION,
|
|
||||||
PACKETS_TO_WAIT_FOR_AUDIO,
|
PACKETS_TO_WAIT_FOR_AUDIO,
|
||||||
|
TARGET_SEGMENT_DURATION,
|
||||||
)
|
)
|
||||||
from homeassistant.components.stream.worker import SegmentBuffer, stream_worker
|
from homeassistant.components.stream.worker import SegmentBuffer, stream_worker
|
||||||
|
|
||||||
@ -36,9 +36,10 @@ AUDIO_STREAM_FORMAT = "mp3"
|
|||||||
VIDEO_STREAM_FORMAT = "h264"
|
VIDEO_STREAM_FORMAT = "h264"
|
||||||
VIDEO_FRAME_RATE = 12
|
VIDEO_FRAME_RATE = 12
|
||||||
AUDIO_SAMPLE_RATE = 11025
|
AUDIO_SAMPLE_RATE = 11025
|
||||||
|
KEYFRAME_INTERVAL = 1 # in seconds
|
||||||
PACKET_DURATION = fractions.Fraction(1, VIDEO_FRAME_RATE) # in seconds
|
PACKET_DURATION = fractions.Fraction(1, VIDEO_FRAME_RATE) # in seconds
|
||||||
SEGMENT_DURATION = (
|
SEGMENT_DURATION = (
|
||||||
math.ceil(MIN_SEGMENT_DURATION / PACKET_DURATION) * PACKET_DURATION
|
math.ceil(TARGET_SEGMENT_DURATION / KEYFRAME_INTERVAL) * KEYFRAME_INTERVAL
|
||||||
) # in seconds
|
) # in seconds
|
||||||
TEST_SEQUENCE_LENGTH = 5 * VIDEO_FRAME_RATE
|
TEST_SEQUENCE_LENGTH = 5 * VIDEO_FRAME_RATE
|
||||||
LONGER_TEST_SEQUENCE_LENGTH = 20 * VIDEO_FRAME_RATE
|
LONGER_TEST_SEQUENCE_LENGTH = 20 * VIDEO_FRAME_RATE
|
||||||
@ -102,7 +103,8 @@ class PacketSequence:
|
|||||||
pts = self.packet * PACKET_DURATION / time_base
|
pts = self.packet * PACKET_DURATION / time_base
|
||||||
duration = PACKET_DURATION / time_base
|
duration = PACKET_DURATION / time_base
|
||||||
stream = VIDEO_STREAM
|
stream = VIDEO_STREAM
|
||||||
is_keyframe = True
|
# Pretend we get 1 keyframe every second
|
||||||
|
is_keyframe = not (self.packet - 1) % (VIDEO_FRAME_RATE * KEYFRAME_INTERVAL)
|
||||||
size = 3
|
size = 3
|
||||||
|
|
||||||
return FakePacket()
|
return FakePacket()
|
||||||
@ -247,8 +249,13 @@ async def test_stream_worker_success(hass):
|
|||||||
async def test_skip_out_of_order_packet(hass):
|
async def test_skip_out_of_order_packet(hass):
|
||||||
"""Skip a single out of order packet."""
|
"""Skip a single out of order packet."""
|
||||||
packets = list(PacketSequence(TEST_SEQUENCE_LENGTH))
|
packets = list(PacketSequence(TEST_SEQUENCE_LENGTH))
|
||||||
|
# for this test, make sure the out of order index doesn't happen on a keyframe
|
||||||
|
out_of_order_index = OUT_OF_ORDER_PACKET_INDEX
|
||||||
|
if packets[out_of_order_index].is_keyframe:
|
||||||
|
out_of_order_index += 1
|
||||||
# This packet is out of order
|
# This packet is out of order
|
||||||
packets[OUT_OF_ORDER_PACKET_INDEX].dts = -9090
|
assert not packets[out_of_order_index].is_keyframe
|
||||||
|
packets[out_of_order_index].dts = -9090
|
||||||
|
|
||||||
decoded_stream = await async_decode_stream(hass, iter(packets))
|
decoded_stream = await async_decode_stream(hass, iter(packets))
|
||||||
segments = decoded_stream.segments
|
segments = decoded_stream.segments
|
||||||
@ -257,11 +264,9 @@ async def test_skip_out_of_order_packet(hass):
|
|||||||
# If skipped packet would have been the first packet of a segment, the previous
|
# If skipped packet would have been the first packet of a segment, the previous
|
||||||
# segment will be longer by a packet duration
|
# segment will be longer by a packet duration
|
||||||
# We also may possibly lose a segment due to the shifting pts boundary
|
# We also may possibly lose a segment due to the shifting pts boundary
|
||||||
if OUT_OF_ORDER_PACKET_INDEX % PACKETS_PER_SEGMENT == 0:
|
if out_of_order_index % PACKETS_PER_SEGMENT == 0:
|
||||||
# Check duration of affected segment and remove it
|
# Check duration of affected segment and remove it
|
||||||
longer_segment_index = int(
|
longer_segment_index = int((out_of_order_index - 1) * SEGMENTS_PER_PACKET)
|
||||||
(OUT_OF_ORDER_PACKET_INDEX - 1) * SEGMENTS_PER_PACKET
|
|
||||||
)
|
|
||||||
assert (
|
assert (
|
||||||
segments[longer_segment_index].duration
|
segments[longer_segment_index].duration
|
||||||
== SEGMENT_DURATION + PACKET_DURATION
|
== SEGMENT_DURATION + PACKET_DURATION
|
||||||
@ -327,15 +332,21 @@ async def test_skip_initial_bad_packets(hass):
|
|||||||
|
|
||||||
decoded_stream = await async_decode_stream(hass, iter(packets))
|
decoded_stream = await async_decode_stream(hass, iter(packets))
|
||||||
segments = decoded_stream.segments
|
segments = decoded_stream.segments
|
||||||
# Check number of segments
|
|
||||||
assert len(segments) == int(
|
|
||||||
(num_packets - num_bad_packets - 1) * SEGMENTS_PER_PACKET
|
|
||||||
)
|
|
||||||
# Check sequence numbers
|
# Check sequence numbers
|
||||||
assert all(segments[i].sequence == i for i in range(len(segments)))
|
assert all(segments[i].sequence == i for i in range(len(segments)))
|
||||||
# Check segment durations
|
# Check segment durations
|
||||||
assert all(s.duration == SEGMENT_DURATION for s in segments)
|
assert all(s.duration == SEGMENT_DURATION for s in segments)
|
||||||
assert len(decoded_stream.video_packets) == num_packets - num_bad_packets
|
assert (
|
||||||
|
len(decoded_stream.video_packets)
|
||||||
|
== num_packets
|
||||||
|
- math.ceil(num_bad_packets / (VIDEO_FRAME_RATE * KEYFRAME_INTERVAL))
|
||||||
|
* VIDEO_FRAME_RATE
|
||||||
|
* KEYFRAME_INTERVAL
|
||||||
|
)
|
||||||
|
# Check number of segments
|
||||||
|
assert len(segments) == int(
|
||||||
|
(len(decoded_stream.video_packets) - 1) * SEGMENTS_PER_PACKET
|
||||||
|
)
|
||||||
assert len(decoded_stream.audio_packets) == 0
|
assert len(decoded_stream.audio_packets) == 0
|
||||||
|
|
||||||
|
|
||||||
@ -363,6 +374,9 @@ async def test_skip_missing_dts(hass):
|
|||||||
bad_packet_start = int(LONGER_TEST_SEQUENCE_LENGTH / 2)
|
bad_packet_start = int(LONGER_TEST_SEQUENCE_LENGTH / 2)
|
||||||
num_bad_packets = MAX_MISSING_DTS - 1
|
num_bad_packets = MAX_MISSING_DTS - 1
|
||||||
for i in range(bad_packet_start, bad_packet_start + num_bad_packets):
|
for i in range(bad_packet_start, bad_packet_start + num_bad_packets):
|
||||||
|
if packets[i].is_keyframe:
|
||||||
|
num_bad_packets -= 1
|
||||||
|
continue
|
||||||
packets[i].dts = None
|
packets[i].dts = None
|
||||||
|
|
||||||
decoded_stream = await async_decode_stream(hass, iter(packets))
|
decoded_stream = await async_decode_stream(hass, iter(packets))
|
||||||
@ -450,6 +464,7 @@ async def test_audio_is_first_packet(hass):
|
|||||||
packets[0].stream = AUDIO_STREAM
|
packets[0].stream = AUDIO_STREAM
|
||||||
packets[0].dts = packets[1].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
|
packets[0].dts = packets[1].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
|
||||||
packets[0].pts = packets[1].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
|
packets[0].pts = packets[1].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
|
||||||
|
packets[1].is_keyframe = True # Move the video keyframe from packet 0 to packet 1
|
||||||
packets[2].stream = AUDIO_STREAM
|
packets[2].stream = AUDIO_STREAM
|
||||||
packets[2].dts = packets[3].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
|
packets[2].dts = packets[3].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
|
||||||
packets[2].pts = packets[3].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
|
packets[2].pts = packets[3].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE
|
||||||
|
Loading…
x
Reference in New Issue
Block a user