Skip to content

Commit

Permalink
Merge 8b72895 into a9f0ca9
Browse files Browse the repository at this point in the history
  • Loading branch information
postlund committed Dec 11, 2019
2 parents a9f0ca9 + 8b72895 commit 8830ad4
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 11 deletions.
41 changes: 30 additions & 11 deletions pyatv/airplay/player.py
Expand Up @@ -10,6 +10,8 @@

_LOGGER = logging.getLogger(__name__)

PLAY_RETRIES = 3
WAIT_RETRIES = 5
TIMEOUT = 10
HEADERS = {
'User-Agent': 'MediaControl/1.0',
Expand All @@ -34,21 +36,38 @@ async def play_url(self, url, position=0):
'X-Apple-Session-ID': str(uuid4()),
}

# pylint: disable=no-member
_, status = await self.http.post_data(
'play',
headers=HEADERS,
data=plistlib.dumps(body, fmt=plistlib.FMT_BINARY),
timeout=TIMEOUT)
# TODO: Should be more fine-grained
if 400 <= status < 600:
raise exceptions.AuthenticationError()
await self._wait_for_media_to_end()
retry = 0
while retry < PLAY_RETRIES:
# pylint: disable=no-member
_, status = await self.http.post_data(
'play',
headers=HEADERS,
data=plistlib.dumps(body, fmt=plistlib.FMT_BINARY),
timeout=TIMEOUT)

# Sometimes AirPlay fails with "Internal Server Error", we
# apply a "lets try again"-approach to that
if status == 500:
retry += 1
_LOGGER.debug('Failed to stream %s, retry %d of %d',
url, retry, PLAY_RETRIES)
await asyncio.sleep(1.0, loop=self.loop)
continue

# TODO: Should be more fine-grained
if 400 <= status < 600:
raise exceptions.AuthenticationError(
'Status code: ' + str(status))

await self._wait_for_media_to_end()
return

raise exceptions.PlaybackError('Max retries exceeded')

# Poll playback-info to find out if something is playing. It might take
# some time until the media starts playing, give it 5 seconds (attempts)
async def _wait_for_media_to_end(self):
attempts = 5
attempts = WAIT_RETRIES
video_started = False

while True:
Expand Down
4 changes: 4 additions & 0 deletions pyatv/exceptions.py
Expand Up @@ -55,3 +55,7 @@ class DeviceIdMissingError(Exception):

class BackOffError(Exception):
"""Thrown when device mandates a backoff period."""


class PlaybackError(Exception):
"""Thrown when media playback failed."""
11 changes: 11 additions & 0 deletions tests/airplay/fake_airplay_device.py
Expand Up @@ -49,6 +49,8 @@ def __init__(self, testcase):
self.last_airplay_url = None
self.last_airplay_start = None
self.last_airplay_uuid = None
self.play_count = 0
self.injected_play_fails = 0
self.tc = testcase
self.app = web.Application()

Expand Down Expand Up @@ -78,6 +80,11 @@ def _get_response(self, response_name, pop=True):

async def handle_airplay_play(self, request):
"""Handle AirPlay play requests."""
self.play_count += 1

if self.injected_play_fails > 0:
self.injected_play_fails -= 1
return web.Response(status=500)
if not self.has_authenticated:
return web.Response(status=503)

Expand Down Expand Up @@ -149,6 +156,10 @@ def __init__(self, fake_airplay_device):
"""Initialize a new AirPlayUseCases."""
self.device = fake_airplay_device

def airplay_play_failure(self, count):
"""Make play command fail a number of times."""
self.device.injected_play_fails = count

def airplay_playback_idle(self):
"""Make playback-info return idle info."""
plist = dict(readyToPlay=False, uuid=123)
Expand Down
20 changes: 20 additions & 0 deletions tests/airplay/test_airplay.py
Expand Up @@ -60,3 +60,23 @@ async def test_play_video_no_permission(self):

with self.assertRaises(exceptions.NoCredentialsError):
await self.player.play_url(STREAM, position=START_POSITION)

@unittest_run_loop
async def test_play_with_retries(self):
self.usecase.airplay_play_failure(2)
self.usecase.airplay_playback_playing()
self.usecase.airplay_playback_idle()

await self.player.play_url(STREAM, position=START_POSITION)

self.assertEqual(
self.fake_device.play_count, 3) # Two retries + success

@unittest_run_loop
async def test_play_with_too_many_retries(self):
self.usecase.airplay_play_failure(10)
self.usecase.airplay_playback_playing()
self.usecase.airplay_playback_idle()

with self.assertRaises(exceptions.PlaybackError):
await self.player.play_url(STREAM, position=START_POSITION)

0 comments on commit 8830ad4

Please sign in to comment.