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
38 changes: 36 additions & 2 deletions pyatv/protocols/airplay/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from abc import ABC
import logging
from random import randrange
from typing import Any, List, NamedTuple, Optional, Tuple
from typing import Any, List, Mapping, NamedTuple, Optional, Tuple

from pyatv.auth.hap_channel import AbstractHAPChannel
from pyatv.protocols.airplay.utils import decode_plist_body, encode_plist_body
Expand Down Expand Up @@ -57,8 +57,23 @@ def parse_response(data: bytes) -> Tuple[Optional[HttpResponse], bytes, bytes]:
return response, data[: len(data) - len(rest)], rest


class EventChannelListener(ABC):
"""Listener interface for EventChannel."""

def handle_event(self, event: Mapping[str, Any]) -> None:
"""Handle an event pushed by the receiver."""


class EventChannel(BaseEventChannel):
"""Connection used to handle the event channel."""
"""Connection used to handle the event channel.

The receiver pushes playback state here: `playbackState` messages carrying
position, duration and whether it is playing, and `notification` messages
such as `currentItemChanged` and `itemPlayedToEnd`. None of them expect a
response beyond an acknowledgement.
"""

listener: Optional[EventChannelListener] = None

def handle_received(self) -> None:
"""Handle received data that was put in buffer."""
Expand All @@ -71,6 +86,7 @@ def handle_received(self) -> None:
break

_LOGGER.debug("Got message on event channel: %s", request)
self._dispatch_event(request)

# Send a positive response to satisfy the other end of the channel
headers = {
Expand All @@ -96,6 +112,24 @@ def handle_received(self) -> None:
except Exception:
_LOGGER.exception("Failed to handle message on event channel")

def _dispatch_event(self, request: HttpRequest) -> None:
"""Decode an event and hand it to the listener, if any."""
if self.listener is None:
return

body = request.body if isinstance(request.body, bytes) else b""
if not body.startswith(b"bplist00"):
return

# The payload is a plist whose "data" member is itself a plist.
outer = decode_plist_body(body)
if not isinstance(outer, dict):
return
data = outer.get("params", {}).get("data")
event = decode_plist_body(data) if isinstance(data, bytes) else outer
if isinstance(event, dict):
self.listener.handle_event(event)


class DataStreamListener(ABC):
"""Listener interface for DataStreamChannel."""
Expand Down
10 changes: 10 additions & 0 deletions pyatv/protocols/airplay/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ async def _wait_for_media_to_end(self) -> None:
attempts: int = WAIT_RETRIES
video_started: bool = False

# Receivers that report playback state themselves are not polled: the
# endpoint below is not part of such a session and answers with an error.
try:
if await self.stream_protocol.wait_for_media_end():
_LOGGER.debug("media playback ended")
return
except exceptions.ConnectionLostError:
_LOGGER.debug("Connection was lost, assuming video playback stopped")
return

while True:
# 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
Expand Down
8 changes: 8 additions & 0 deletions pyatv/protocols/raop/protocols/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ async def send_audio_packet(
async def play_url(self, timing_server_port: int, url: str, position: float = 0.0):
"""Play media from a URL."""

async def wait_for_media_end(self) -> bool:
"""Wait until the receiver reports that playback finished.

Returns False if this protocol cannot report it, in which case the
caller should fall back to polling GET /playback-info.
"""
return False


class TimingServer(asyncio.Protocol):
"""Basic timing server responding to timing requests."""
Expand Down
Loading