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
84 changes: 45 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,41 @@ This package is not on PyPI (yet) so install from this repo:

Easy!

# discover airplay devices
$ airplay discover

# send a local photo
$ airplay photo ~/image.jpg
# send a internet photo and display for 5 seconds
$ airplay photo http://www.beihaiting.com/uploads/allimg/150401/10723-150401193I54I.jpg -t 5

# screen cast(without audio yet) for 120 seconds
# for linux like debian/ubuntu: sudo apt-get install scrot
$ airplay screen -t 120

# play a remote video file
$ airplay http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4

$ airplay video http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4
# play a remote video file, but start it half way through
$ airplay -p 0.5 http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4

$ airplay video -p 0.5 http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4
# play a local video file
$ airplay /path/to/some/local/file.mp4

$ airplay video /path/to/some/local/file.mp4
# or play to a specific device
$ airplay --device 192.0.2.23:7000 http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4

$ airplay video --device 192.0.2.23:7000 http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4
$ airplay --help
usage: airplay [-h] [--position POSITION] [--device DEVICE] path

Playback a local or remote video file via AirPlay. This does not do any on-
the-fly transcoding (yet), so the file must already be suitable for the
AirPlay device.

positional arguments:
path An absolute path or URL to a video file

optional arguments:
-h, --help show this help message and exit
--position POSITION, --pos POSITION, -p POSITION
Where to being playback [0.0-1.0]
--device DEVICE, --dev DEVICE, -d DEVICE
Playback video to a specific device
[<host/ip>:(<port>)]
Usage: airplay [OPTIONS] COMMAND [ARGS]...

Options:
--help Show this message and exit.

Commands:
discover Discover AirPlay devices in connected network
photo AirPlay a photo
screen AirPlay screen cast(without voice yet)
video AirPlay a video



Expand All @@ -52,55 +58,55 @@ Awesome! This package is compatible with Python >= 2.7 (including Python 3!)

# Import the AirPlay class
>>> from airplay import AirPlay

# If you have zeroconf installed, the find() classmethod will locate devices for you
>>> AirPlay.find(fast=True)
[<airplay.airplay.AirPlay object at 0x1005d9a90>]

# or you can manually specify a host/ip and optionally a port
>>> ap = AirPlay('192.0.2.23')
>>> ap = AirPlay('192.0.2.3', 7000)

# Query the device
>>> ap.server_info()
{'protovers': '1.0', 'deviceid': 'FF:FF:FF:FF:FF:FF', 'features': 955001077751, 'srcvers': '268.1', 'vv': 2, 'osBuildVersion': '13U717', 'model': 'AppleTV5,3', 'macAddress': 'FF:FF:FF:FF:FF:FF'}

# Play a video
>>> ap.play('http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4')
True

# Get detailed playback information
>>> ap.playback_info()
{'duration': 60.095, 'playbackLikelyToKeepUp': True, 'readyToPlayMs': 0, 'rate': 1.0, 'playbackBufferEmpty': True, 'playbackBufferFull': False, 'loadedTimeRanges': [{'start': 0.0, 'duration': 60.095}], 'seekableTimeRanges': [{'start': 0.0, 'duration': 60.095}], 'readyToPlay': 1, 'position': 4.144803403}

# Get just the playhead position
>>> ap.scrub()
{'duration': 60.095001, 'position': 12.465443}

# Seek to an absolute position
>>> ap.scrub(30)
{'duration': 60.095001, 'position': 30.0}

# Pause playback
>>> ap.rate(0.0)
True

# Resume playback
>>> ap.rate(1.0)
True

# Stop playback completely
>>> ap.stop()
True

# Start a webserver to stream a local file to an AirPlay device
>>> ap.serve('/tmp/home_movie.mp4')
'http://192.0.2.114:51058/home_movie.mp4'

# Playback the generated URL
>>> ap.play('http://192.0.2.114:51058/home_movie.mp4')
True

# Read events from a generator as the device emits them
>>> for event in ap.events():
... print(event)
Expand Down Expand Up @@ -144,7 +150,7 @@ Discover AirPlay devices using Zeroconf/Bonjour

#### Arguments
* **timeout (int):** The number of seconds to wait for responses. If fast is False, then this function will always block for this number of seconds.

* **fast (bool):** If True, do not wait for timeout to expire return as soon as we've found at least one AirPlay device.

#### Returns
Expand Down Expand Up @@ -218,7 +224,7 @@ Return the current playback position, optionally seek to a specific position

>>> ap.scrub()
{'duration': 60.095001, 'position': 12.465443}

>>> ap.scrub(30)
{'duration': 60.095001, 'position': 30.0}

Expand Down
51 changes: 44 additions & 7 deletions airplay/airplay.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import socket
import time
import warnings
import traceback

from multiprocessing import Process, Queue

Expand Down Expand Up @@ -84,7 +85,13 @@ def do_POST(self):
raise RuntimeError('Received an event with a zero length body.')

# parse XML plist
self.event = plist_loads(self.rfile.read(content_length))
xml = self.rfile.read(content_length)
if len(xml) < content_length:
# sometimes the content is not complete...
self.event = plist_loads(xml+"</dict></plist>")
else:
self.event = plist_loads(xml)



class AirPlay(object):
Expand Down Expand Up @@ -180,7 +187,7 @@ def _monitor_events(self, event_queue, control_queue): # pragma: no cover
# parse it
try:
req = AirPlayEvent(FakeSocket(raw_request), event_socket.getpeername(), None)
except RuntimeError as exc:
except Exception as exc:
raise RuntimeError(
"Unexpected request from AirPlay while processing events\n"
"Error: {0}\nReceived:\n{1}".format(exc, raw_request)
Expand All @@ -190,7 +197,8 @@ def _monitor_events(self, event_queue, control_queue): # pragma: no cover
event_socket.send(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")

# skip non-video events
if req.event.get('category', None) != 'video':
if not hasattr(req, 'event') or req.event is None or \
req.event.get('category', None) != 'video':
continue

# send the event back to the parent process
Expand All @@ -199,6 +207,7 @@ def _monitor_events(self, event_queue, control_queue): # pragma: no cover
except KeyboardInterrupt:
return
except Exception as exc:
traceback.print_exc()
event_queue.put(exc)
return

Expand Down Expand Up @@ -245,12 +254,13 @@ def events(self, block=True):
except Empty:
return

def _command(self, uri, method='GET', body='', **kwargs):
def _command(self, uri, method='GET', header={}, body='', **kwargs):
"""Makes an HTTP request through to an AirPlay server

Args:
uri(string): The URI to request
method(string): The HTTP verb to use when requesting `uri`, defaults to GET
header(dict): The header to be send with the request
body(string): If provided, will be sent witout alteration as the request body.
Content-Length header will be set to len(`body`)
**kwargs: If provided, Will be converted to a query string and appended to `uri`
Expand All @@ -265,8 +275,12 @@ def _command(self, uri, method='GET', body='', **kwargs):
# generate the request
if len(kwargs):
uri = uri + '?' + urlencode(kwargs)

request = method + " " + uri + " HTTP/1.1\r\nContent-Length: " + str(len(body)) + "\r\n\r\n" + body
# request = method + " " + uri + " HTTP/1.1\r\nContent-Length: " + str(len(body)) + "\r\n\r\n" + body
header['Content-Length'] = str(len(body))
sheader = ""
for key, value in header.items():
sheader += key + ": " + value + "\r\n"
request = method + " " + uri + " HTTP/1.1\r\n" + sheader + "\r\n" + body

try:
request = bytes(request, 'UTF-8')
Expand Down Expand Up @@ -342,7 +356,30 @@ def play(self, url, position=0.0):
return self._command(
'/play',
'POST',
"Content-Location: {0}\nStart-Position: {1}\n\n".format(url, float(position))
body="Content-Location: {0}\nStart-Position: {1}\n\n".format(url, float(position))
)

def photo(self, img):
"""Start a photo display

Args:
img(bytes): image data

Returns:
bool: The request was accepted.

"""
header = {
"Cache-Control": "no-cache",
"Pragma": "no-cache",
"Connection": "keep-alive",
"Content-Type": "application/octet-stream"
}
return self._command(
'/photo',
'PUT',
header,
img
)

def rate(self, rate):
Expand Down
Loading