postlund / pyatv

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

Add support for audio streaming (RAOP) #1059

Closed postlund closed 3 years ago

postlund commented 3 years ago

TL;DR Audio streaming (AirPlay) is coming to pyatv. Keep on reading if you are interested (instructions at the end).

So... I'm pretty excited about this! Soon after opening this issue, I will push a PR (#1060) with an MVP implementation of Remote Audio Output Protocol (RAOP) support in pyatv. For those not familiar with RAOP, it's the audio streaming part of AirPlay (previously also called AirTunes). It's basically what makes it possible to stream audio to an AirPlay receiver, e.g. an AirPort Express or a HomePod. Something able to send a stream to a receiver is called a sender, so this makes pyatv an AirPlay sender.

There are several AirPlay senders out there today, both open source and proprietary. One example of a great open source sender is owntone (formerly known as forked-daapd). A lot of inspiration has been taken from owntone and also other open source senders when creating this, so a big thank you to all involved! A list of sources is available at the end.

Support for streaming audio to AirPort Expresses and HomePods have been requested for a while. As there doesn't seem to exist any working RAOP implementations in python, I have deferred that until later as there's a lot of work involved in writing that code. Python is maybe not the ideal language either, mainly for performance reasons. But I took on the challenge and I have written a tiny implemetation to get started. There are lots and lots of issues and lack in functionality, so don't expect to have anything useful in a while. But I decided that I want to merge something and put it out there for people to try out. So please, give it a spin and let me know if it works for you. It's interesting to know what kind of AirPlay receivers you stream to and what kind of device you stream from (performance reasons).

My main driver (and what I'm prioritizing) is support for the HomePod mini. Basically, getting good enough support in pyatv so that it can be used for TTS in Home Assistant. I was generously donated a HomePod mini (thank you!) for this particular usecase, so I'm gonna honor that and do what I can to make it happen! This doesn't mean that it won't work with other receivers (it should), but it's not something I will give additional time investigating for now. I have tried my current implementation with success on my HomePod mini and Yamaha RX-V773 receiver.

As I mentioned, the support is currently very limited, the design isn't very good and there are lots of issues beneath the surface. But better with something than nothing. I'm gonna use this issue as sort of an "epic" for work to be done. Below is a list of limitations and other tasks that needs to be dealt with. The main idea is to create a new issue with a more detailed description for each task as work progresses. I'm very open to external help! If you would like to help out, feel free to pick something below, open and issue and send a PR. Any questions can be asked here, I'll try to answer as good as I can. Streaming and working with audio (i.e. signal processing) is completely new to me, so I'm learning as I go along. Likely lots of beginner mistakes. So please challenge me!

Current state

Things to do

Limitations and out of scope

Try it out

Works more or less the same as play_url:

$ atvremote -s 10.0.0.4 stream_file=some_file.mp3

Easy as that.

References

Some references I've used, but probably missed some:

https://github.com/philippe44/RAOP-Player https://github.com/owntone/owntone-server https://openairplay.github.io/airplay-spec/introduction.html https://emanuelecozzi.net/docs/airplay2 https://stackoverflow.com/questions/34584522/airplay-protocol-how-to-use-raw-pcm-instead-of-alac https://nto.github.io/AirPlay.html

postlund commented 3 years ago

I have potential fixes for #1061 and #1062. With those applied, I've managed to play a six minute long file with no problems what so ever. So they seem solid. Will prepare and commit them later tonight.

nehalvpatel commented 3 years ago

I'm getting a miniaudio installation error when running pip3 install --upgrade git+https://github.com/postlund/pyatv.git in my venv.

Some searches reveal it to be an issue with M1 Macs. I'm running an M1 Mac mini. I'll try to install it on my Raspberry Pi.

Output:

...
Installing collected packages: miniaudio, bitarray
    Running setup.py install for miniaudio ... error
    ERROR: Command errored out with exit status 1:
     command: /Users/nehal/Documents/Projects/AppleTVMITM/pyatv_venv/bin/python3.9 -u -c 'import io, os, sys, setuptools, tokenize; sys.argv[0] = '"'"'/private/var/folders/8h/vtzgh1z92lvcxv52vk974k400000gn/T/pip-install-225s2xt4/miniaudio_e86f1908e234422c88c0e0217c4fb1de/setup.py'"'"'; __file__='"'"'/private/var/folders/8h/vtzgh1z92lvcxv52vk974k400000gn/T/pip-install-225s2xt4/miniaudio_e86f1908e234422c88c0e0217c4fb1de/setup.py'"'"';f = getattr(tokenize, '"'"'open'"'"', open)(__file__) if os.path.exists(__file__) else io.StringIO('"'"'from setuptools import setup; setup()'"'"');code = f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' install --record /private/var/folders/8h/vtzgh1z92lvcxv52vk974k400000gn/T/pip-record-tk0tk3f5/install-record.txt --single-version-externally-managed --compile --install-headers /Users/nehal/Documents/Projects/AppleTVMITM/pyatv_venv/include/site/python3.9/miniaudio
         cwd: /private/var/folders/8h/vtzgh1z92lvcxv52vk974k400000gn/T/pip-install-225s2xt4/miniaudio_e86f1908e234422c88c0e0217c4fb1de/
    Complete output (15 lines):
    running install
    running build
    running build_py
    creating build
    creating build/lib.macosx-11-arm64-3.9
    copying miniaudio.py -> build/lib.macosx-11-arm64-3.9
    running build_ext
    generating cffi module 'build/temp.macosx-11-arm64-3.9/_miniaudio.c'
    creating build/temp.macosx-11-arm64-3.9
    building '_miniaudio' extension
    creating build/temp.macosx-11-arm64-3.9/build
    creating build/temp.macosx-11-arm64-3.9/build/temp.macosx-11-arm64-3.9
    clang -Wno-unused-result -Wsign-compare -Wunreachable-code -fno-common -dynamic -DNDEBUG -g -fwrapv -O3 -Wall -isysroot /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk -DMA_NO_GENERATION=1 -I/private/var/folders/8h/vtzgh1z92lvcxv52vk974k400000gn/T/pip-install-225s2xt4/miniaudio_e86f1908e234422c88c0e0217c4fb1de -I/opt/homebrew/include -I/opt/homebrew/opt/openssl@1.1/include -I/opt/homebrew/opt/sqlite/include -I/Users/nehal/Documents/Projects/AppleTVMITM/pyatv_venv/include -I/opt/homebrew/opt/python@3.9/Frameworks/Python.framework/Versions/3.9/include/python3.9 -c build/temp.macosx-11-arm64-3.9/_miniaudio.c -o build/temp.macosx-11-arm64-3.9/build/temp.macosx-11-arm64-3.9/_miniaudio.o -g1 -O3 -ffast-math -mtune=native -march=native
    clang: error: the clang compiler does not support '-march=native'
    error: command '/usr/bin/clang' failed with exit code 1
    ----------------------------------------
ERROR: Command errored out with exit status 1: /Users/nehal/Documents/Projects/AppleTVMITM/pyatv_venv/bin/python3.9 -u -c 'import io, os, sys, setuptools, tokenize; sys.argv[0] = '"'"'/private/var/folders/8h/vtzgh1z92lvcxv52vk974k400000gn/T/pip-install-225s2xt4/miniaudio_e86f1908e234422c88c0e0217c4fb1de/setup.py'"'"'; __file__='"'"'/private/var/folders/8h/vtzgh1z92lvcxv52vk974k400000gn/T/pip-install-225s2xt4/miniaudio_e86f1908e234422c88c0e0217c4fb1de/setup.py'"'"';f = getattr(tokenize, '"'"'open'"'"', open)(__file__) if os.path.exists(__file__) else io.StringIO('"'"'from setuptools import setup; setup()'"'"');code = f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' install --record /private/var/folders/8h/vtzgh1z92lvcxv52vk974k400000gn/T/pip-record-tk0tk3f5/install-record.txt --single-version-externally-managed --compile --install-headers /Users/nehal/Documents/Projects/AppleTVMITM/pyatv_venv/include/site/python3.9/miniaudio Check the logs for full command output.
nehalvpatel commented 3 years ago

Just tested it on my Raspberry Pi with this file, and it worked!

postlund commented 3 years ago

That is great news, thanks for testing! 👍 It's good for me to know the limitations regarding miniaudio, I suspected it would backfire somewhere. Not sure yet if I'm gonna make it optional, i.e. use it only if it's installed. I'll think about that.

nehalvpatel commented 3 years ago

Maybe it could be raised as an issue with pyminiaudio? It seems like they would want to fix this installation error.

postlund commented 3 years ago

Yeah, sounds like a good idea to me. Definitely worth opening an issue to see if they are aware of the problem.

Schlaubischlump commented 3 years ago

Hi I love the idea of adding native RAOP support to pyatv. I already tried to implement RAOP /AirTunes v2 (not AirPlay2) as a standalone project in python a couple of years ago. It was somehow working. The pause function was still buggy and a lot of other things were broken. Last time I check (about a month ago) sending audio to multiple receivers, including metadata information was working. ATV pin pairing, password protected Apple TVs and remote controlling the sender are working as well (there seems to be a problem with iPhone and ATV 3 controls, shairport-sync controls are working fine... strange). At some point I decided, it is too much work to finish and cleanup the code all by myself and I decided to abandon the project. Instead I put my effort into providing python bindings for RAOP-Player. The fork includes a couple of new features. Better DMAP support, password protected Airplay devices, ATV device verification. You can find them here: https://github.com/Schlaubischlump/RAOP-Player-bindings

You can find the code of my original python implementation here: https://github.com/Schlaubischlump/raopy_old I made the repo public so that you can access it. It was private before. Feel free to use whatever code you need. If I can somehow help to make this pyatv implementation a reality let me know !

I got a couple of test devices: Apple TV 3, Apple TV 4K, original Homepod, as well if I should test something.

postlund commented 3 years ago

Hi and thanks for the feedback! 😄 Yeah, it's quite the task to take on and requires quite a bit of time. That's basically why I've set a pretty low bar for what I'm implementing so far. With the goal of adding more features over time. Supporting multiple receivers would be great for instance, but I haven't made any research into that so I have no idea how that works. Would also require a bit of fiddling to figure out a decent way to add group devices as pyatv is very "single device"-designed.

I will have a look at the code and see if there's anything that can help me 😄 If you feel like collaborating that would be very appreciated as well, not sure how much time I will have now when summer is coming up. But I'm gonna try to put a few hours into the project every now and then.

I saw you posted a few other issues, I'll have a look at those as well!

postlund commented 3 years ago

I've implemented most of the things here and even more than I expected, so I feel this issue can be closed. Will open additional ones for other features and perhaps also AirPlay v2 in the future.