postlund / pyatv

A client library for Apple TV and AirPlay devices
https://pyatv.dev
MIT License
888 stars 100 forks source link

play_url results in pyatv.exceptions.HttpError #2512

Closed knallle closed 1 month ago

knallle commented 1 month ago

Describe the bug

Not sure if it's a bug, but the behavior is strange. I have read the documentation and browsed through open and closed issues without finding a solution.

I want to play_url() on my AppleTV. I can connect to it using the Python API and atvremote. Both allow me to perform basic commands such as up/down, turn_off/turn_on, play/pause etc.

However, play_url() results in pyatv.exceptions.HttpError, both with the Python API and atvremote.

Error log

Kalle ~ % atvremote --id "<valid-id>" play_url=http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4
Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pyatv/scripts/atvremote.py", line 997, in _run_application
    return await cli_handler(loop)
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pyatv/scripts/atvremote.py", line 726, in cli_handler
    return await _handle_commands(args, config, storage, loop)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pyatv/scripts/atvremote.py", line 876, in _handle_commands
    ret = await _handle_device_command(args, cmd, atv, storage, loop)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pyatv/scripts/atvremote.py", line 932, in _handle_device_command
    return await _exec_command(atv.stream, cmd, True, *cmd_args)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pyatv/scripts/atvremote.py", line 964, in _exec_command
    value = await tmp(*args)
            ^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pyatv/core/facade.py", line 359, in play_url
    await self.relay("play_url")(url, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pyatv/protocols/airplay/__init__.py", line 136, in play_url
    return await self._play_task
           ^^^^^^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pyatv/protocols/airplay/player.py", line 68, in play_url
    await self._wait_for_media_to_end()
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pyatv/protocols/airplay/player.py", line 84, in _wait_for_media_to_end
    resp = await self.rtsp.connection.get("/playback-info")
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pyatv/support/http.py", line 423, in get
    return await self.send_and_receive("GET", path, allow_error=allow_error)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/pyatv/support/http.py", line 496, in send_and_receive
    raise exceptions.HttpError(
pyatv.exceptions.HttpError: HTTP/1.1 method GET failed with code 500: Internal Server Error

>>> An error occurred, full stack trace above

How to reproduce the bug?

Run atvremote --id "<valid-id>" play_url=http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4

(The basic example from https://pyatv.dev/documentation/atvremote/#streaming)

What is expected behavior?

The video playing on the Apple TV

Operating System

macOS

Python

3.11

pyatv

0.15.1

Device

Apple TV 4K, tvOS 18.0

Additional context

My host and Apple TV are on the same network and communication is verified by being able to run basic commands.

afairclo commented 1 month ago

Identical error here after upgrading to Home Assistant 2024.9.3

knallle commented 1 month ago

I did a clean install on another mac running Python 3.12.6. Got the same result. Tried with Python 3.10.11. Got the same result. Tried downgrading to pyatv 0.15.0. Got the same result.

A (perhaps) interesting observation is that when the play_url command is issued, the Apple TV briefly displays a "loading spinner" as if it initiates playback. This happens for ≈0.5s and the stack trace included above is displayed at the same time as the loading spinner is aborted.

knallle commented 1 month ago

Diving deeper in the stack trace, I see that the crash happens on line 84 in pyatv/protocols/airplay/player.py when we wait for playback information from the Apple TV:

# In some cases this call will fail if video was stopped by the sender,
# e.g. stopping video via remote control. For now, handle this gracefully
# by not spewing an exception.
try:
    resp = await self.rtsp.connection.get("/playback-info") # <---- CRASH
except (RuntimeError, exceptions.ConnectionLostError):
    _LOGGER.debug("Connection was lost, assuming video playback stopped")
    break

self.rtsp.connection.get() uses send_and_receive() in pyatv/support/http.py:

try:
    async with async_timeout.timeout(timeout):
        await pending_request.event.wait()

    if pending_request.connection_closed:
        raise exceptions.ConnectionLostError("connection was lost")

    response = pending_request.response

    # This should never happen, but raise an exception for the sake of it
    if response is None:
        raise RuntimeError("did not get a response")
except asyncio.TimeoutError as ex:
    raise TimeoutError(f"no response to {method} {uri} ({protocol})") from ex
finally:
    # If request failed and event is still in request queue, remove it
    if pending_request in self._requests:
        self._requests.remove(pending_request)

_LOGGER.debug("Got %s response: %s:", response.protocol, response)

if response.code == 403:
    raise exceptions.AuthenticationError("not authenticated")

# Password required
if response.code == 401:
    if allow_error:
        return response
    raise exceptions.AuthenticationError("not authenticated")

# Positive response
if 200 <= response.code < 300 or allow_error:
    return response

raise exceptions.HttpError( # <--- This is the error I get
    f"{protocol} method {method} failed with code "
    f"{response.code}: {response.message}",
    response.code,
)

So the error I see is simply a 500 on the GET request to the Apple TV. My guess, therefore, is that the API has changed on Apple's side in tvOS 18.0.

knallle commented 1 month ago

Realised this is already discussed in https://github.com/postlund/pyatv/issues/2403#issuecomment-2183329311