Attempting to AirPlay from Android
November 8, 2025I honestly never expected to publish Apple AirPlay content on my blog.
It’s not like I flat out hate Apple,
but I’m not really a fan of buying into closed systems that end up becoming e-waste,
because I’m unable to use them the way I want to.
And yet, here we are, attempting to stream to an AirPlay receiver using an Android phone.
Last Christmas, I came across shairport-sync, a piece of open-source software
using which you can basically turn any Linux computer into an AirPlay receiver.
Whilst this is nice for a lot of things, I don’t actually own an iPhone or iPad,
which I could use to stream media to those devices.
As it turns out, AirPlay uses the Remote Audio Output Protocol,
which in turn is supported by PipeWire.
This allows you to use your Linux device to stream audio to AirPlay receivers,
including shairport-sync.
Unfortunately, we can not use PipeWire on Android by default,
nor can we easily forward the system sound.
Which is exactly where this guide comes in.
Instead of giving you a rundown of all the details now, I’ll explain the software as we go along.
System setup
The first thing you want to do is install Termux.
Termux is an Android terminal emulator and Linux environment, which allows you to run a minimal Linux system right on your phone.
It basically allows us to eliminate a secondary computer from this whole setup,
as we can run all the stuff we need right on the phone.
When you launch Termux, you are thrown into a simple Linux command line,
from which you can execute commands.
In our case, the first thing we want to do is install proot-distro,
which allows us to easily set up an Alpine Linux subsystem.
(Note: You could theoretically do this without the Alpine system; I’m just more used to the way it works.
It also makes it easy to reset the system by running proot-distro reset alpine)
pkg install proot-distro
Afterwards, we can install and launch Alpine Linux:
# Install Alpine Linux
proot-distro install alpine
# Enter the system
proot-distro login alpine
Audio setup
The first thing we are trying to get to work is the connection to your AirPlay receiver.
For the sake of this post, I’m going to assume that you are running shairport-sync on a secondary device in your home network.
The setup involves a couple of user-specific applications (apps that are not storing their data system-wide), which is why we have to set up a directory where all of the files, like sockets or pipes, can be stored:
mkdir runtime
export XDG_RUNTIME_DIR=$HOME/runtime
To make launching some of the programs we rely on easier,
we are going to make use of an init system called OpenRC.
Because we are not using it to boot (or initialize) a real system
but rather to manage local services, we have to enable its softlevel mode.
# Install OpenRC
apk add openrc
# Configure OpenRC
mkdir -p $XDG_RUNTIME_DIR/openrc
touch $XDG_RUNTIME_DIR/openrc/softlevel
As indicated at the start of this post, we’ll be using PipeWire as the sound system,
as it has native support for RAOP.
But before we can set up PipeWire, we have to install D-Bus,
a communication programm that allows processes on the same machine to exchange messages.
# Install D-Bus
apk add dbus
# Starts the dbus service, as a user-service (indicated by the -U)
rc-service -U dbus start
Besides PipeWire, we’ll also be installing WirePlumber a session manager for PipeWire.
# Install PipeWire & WirePlumber
apk add pipewire wireplumber
# Start PipeWire as a user-service
rc-service -U pipewire start
# Start WirePlumber as a user-service
rc-service -U wireplumber start
After starting them, you can make sure they are running by checking their status:
rc-service -U pipewire status
rc-service -U wireplumber status
Configuring a remote device
With PipeWire working, we can now configure the target speaker.
Normally, you would use the RAOP Discover module;
however, to be able to use it, we would have to launch some services that we can not run on Android without root.
So we have to manually configure the AirPlay Sink instead.
Start by creating a PipeWire configuration directory:
mkdir -p ~/.config/pipewire/pipewire.conf.d
You can create files in that directory, which will be loaded by pipewire and applied to your configuration.
For this example, we’ll create ~/.config/pipewire/pipewire.conf.d/my-device.conf, and edit it using a text editor.
Once you are done, your file should look something like this: (See the PipeWire docs for more options)
context.modules = [
{ name = libpipewire-module-raop-sink
args = {
# Remote Device IP
raop.ip = "192.168.178.20"
# Remote Device Port (5000 for shairport-sync)
raop.port = 5000
# Remote Device Names, change them as you like
raop.name = "my-device"
raop.hostname = "My Device"
raop.transport = "udp"
}
}
]
Before we can apply the configuration, we have to install pipewire-zeroconf,
which contains the required modules.
Afterwards we can restart PipeWire to load the config:
# Install Zeroconf modules
apk add pipewire-zeroconf
# Restart Wireplumber (automatically restarts Pipe"ire)
rc-service -U wireplumber restart
To validate that PipeWire loaded your config file,
you can use the pw-cli tool to list all available objects and check if your remote shows up:
# Install PipeWire tools
apk add pipewire-tools
# List all available objects
pw-cli ls
Testing Sound
To validate that sound is working, you can download a sample audio file, which you can play over the connection:
# Download example audio file
wget https://download.samplelib.com/wav/sample-3s.wav
# Playback audio file
pw-play sample-3s.wav
Sometimes there is no sound the first time,
probably because it is still trying to establish a connection.
But you should hear the sound the second time you run the command.
Audio Input
Because we don’t want the phone speakers to continue playing sound while transmitting,
we have to forward the audio to our PipeWire setup.
We’ll be using Scrcpy, a tool that is normally used to mirror your phone screen to your computer.
However, it can also be used to stream your phone camera or, in our case, the phone audio.
Scrcpy runs over ADB, the Android Debugging Bridge,
which allows your phone to be controlled by a connected computer.
In our case, we’ll connect Termux to the phone itself via ADB over WiFi.
To be able to, you have to enable ADB in your phone’s Developer Options.
Alpine Linux has ADB in the official package repositories,
alongside other tools in a package called android-tools,
which makes installing it very easy.
apk add android-tools
Sadly, enabling ADB to work over WiFi is not as easy.
I’m using /e/OS on my phone, which is essentially a modified version of Android.
And it allows me to enable ADB WiFi from the settings menu.
However, if your Android flavour doesn’t allow this (most preinstalled ones don’t),
you have to use a computer to enable it. (Note: You have to reenable it whenever you reboot your phone).
ADB over WiFi can be used to gain remote access to your device if not used carefully.
Only do this if you know what you are doing and trust the network you are connected to.
If you are using a computer to enable ADB over WiFi, you can follow these steps:
# Enable ADB WIFI on the phone on port 5555
adb tcpip 5555
After enabling it, you can connect the device in Termux by running adb connect 127.0.0.1:5555.
When prompted, you want to click Always allow access from this PC.
If your operating system allows you to enable ADB over WIFI without a computer,
you can open the settings app and Termux at the same time in split-screen mode.
After enabling ADB over WiFi, you can pair a new device using a pin code by running adb pair 127.0.0.1:<port>,
where <port> is the port shown in the pair dialog.
The pair program will ask you for the pin shown in the settings menu.
Once paired, you can connect the device in Termux by running adb connect 127.0.0.1:<port>,
where <port> is the port shown in the settings menu.
Note that this method is more secure than the first one, because pairing is not possible unless in pairing mode.
With all that setup out of the way, you are now ready to install scrcpy.
apk add scrcpy
Now all you need to do is run the following command to forward all audio from your phone to PipeWire:
SDL_AUDIODRIVER=pipewire scrcpy --no-video --require-audio --no-window
If you are prompted to select a device, you can append --tcpip=127.0.0.1:<port>,
where <port> is the ADB port you used to connect the device earlier.
Let the music play.
Conclusion
Wow, what a ride - but we have sound output working.
Entirely using open-source software; who would have thought.
Well, unfortunately the initial excitement sort of fades away after some time.
Besides the ADB Wifi security concerns I already pointed out,
you’ll probably notice the audio stuttering sometimes (depending on your phone load).
This can partially be fixed by letting Termux acquire a wake lock.
But scrcpy audio streaming over wifi isn’t perfect (it’s a lot better over USB).
The entire chain also has a significant amount of delay, which is fine if you only listen to music,
but it would stand out a lot when watching a video.
Either way, I think this is a pretty cool showcase, and I think that you could do a lot of other cool stuff using the PipeWire setup or scrcpy - just maybe not the combination of the two.
Script
You might be wondering whether or not you have to run all of the commands every time you want to launch a stream.
Well, yes and no.
A lot of the commands were just for installing packages and getting everything set up for first-time use.
But a lot of the services have to be started every time,
and the ADB connection will probably always involve some manual effort to select the correct device and port.
Below is a stripped-down version of the steps you have to follow to restart the stream after closing Termux:
# Enter the Alpine Subsystem
proot-distro login alpine
# Run inside the Alpine Subsystem:
export XDG_RUNTIME_DIR=$HOME/runtime
rc-service -U dbus start
rc-service -U wireplumber start
rc-service -U pipewire start
adb connect 127.0.0.1:<port>
SDL_AUDIODRIVER=pipewire scrcpy --no-video --require-audio --no-window
If you wanted to, you could combine all the commands into a shell script,
which reads the ADB connection details
and then configures scrcpy for you.
But I’ll leave that for you to experiment with.