Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pyatv/protocols/raop/audio_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,8 @@ def read(self, num_bytes: int) -> bytes:

# TODO: Should not be based on polling
while len(self._buffer) < num_bytes and not self._stop_stream:
if not self._buffer.fits(1):
break
if time.monotonic() - start_time > DEFAULT_TIMEOUT:
raise OperationTimeoutError("timed out reading from stream")

Expand Down
2 changes: 1 addition & 1 deletion requirements/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ cryptography==46.0.3
chacha20poly1305-reuseable==0.13.2
ifaddr==0.2.0
ifaddr==0.2.0
miniaudio==1.61
miniaudio==1.71
protobuf==6.33.2
pydantic==2.12.5
requests==2.32.5
Expand Down
Binary file added tests/data/audio_long.mp3
Binary file not shown.
50 changes: 50 additions & 0 deletions tests/protocols/raop/test_audio_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Tests for pyatv.protocols.raop.audio_source.

Includes a regression test for #2849: streaming a media file larger than
PatchedIceCastClient's internal BUFFER_SIZE must not deadlock during
miniaudio's decoder init.
"""

import pytest
from pytest_httpserver import HTTPServer

from pyatv.protocols.raop.audio_source import (
BUFFER_SIZE,
InternetSource,
)

from tests.utils import data_path

pytestmark = pytest.mark.asyncio

SHORT_FIXTURE = "static_3sec.ogg" # existing, ~4 KiB
LONG_FIXTURE = "audio_long.mp3" # new, just over BUFFER_SIZE


async def _serve_and_open(httpserver: HTTPServer, body: bytes, name: str) -> None:
httpserver.expect_request("/" + name).respond_with_data(
body, content_type="application/octet-stream"
)
url = httpserver.url_for("/" + name)
src = await InternetSource.open(url, sample_rate=44100, channels=2, sample_size=2)
try:
frames = await src.readframes(352)
assert frames, "InternetSource returned no audio frames after init"
finally:
await src.close()


async def test_internet_source_short_stream(httpserver: HTTPServer):
"""Streaming a small file over HTTP works (fast path, no deadlock window)."""
with open(data_path(SHORT_FIXTURE), "rb") as fh:
body = fh.read()
assert len(body) < BUFFER_SIZE
await _serve_and_open(httpserver, body, SHORT_FIXTURE)


async def test_internet_source_long_stream_no_deadlock(httpserver: HTTPServer):
"""Regression test for #2849: streaming a file > BUFFER_SIZE must not deadlock."""
with open(data_path(LONG_FIXTURE), "rb") as fh:
body = fh.read()
assert len(body) > BUFFER_SIZE
await _serve_and_open(httpserver, body, LONG_FIXTURE)