philippe44 / SpotConnect

Turn any UPnP or AirPlay player into a Spotify Connect device
MIT License
74 stars 3 forks source link
airplay bridge freebsd linux macos solaris spotify spotify-connect upnp windows

SpotConnect: Enable Spotify Connect for UPnP and AirPlay devices

Use these applications to add Spotify Connect capabilities to UPnP (like Sonos) or AirPlay players. Respectively spotupnp for UPnP and spotraop for AirPlay

SpotConnect can run on any machine that has access to your local network (Windows, MacOS x86 and arm64, Linux x86, x86_64, arm, aarch64, sparc, mips, powerpc, Solaris and FreeBSD). It does not need to be on your main computer. (For example, a Raspberry Pi works well). It will detect UPnP/Sonos or AirPlay players, create as many virtual Spotify Connect devices as needed, and act as a bridge/proxy between Spotify controller (iPhone, iPad, PC, Mac ...) and the real UPnP/Sonos or AirPlay players.

For UPnP, the audio, after being decoded from vorbis, can be sent in plain, or re-encoded using mp3, aac, vorbis, opus or flac. The tracks can be sent one-by one and use the capability of UPnP players to do gapless playback by sending the next track ahead of the current one, but not all players support that or might simply be faulty. There is also a 'flow' mode where all tracks are sent in a continuous stream, similar to a webradio. Note that this mode can be brittle with regard to track position. In 'flow' mode, metadata are likely not to be sent, unlesss player supports 'icy' protocol.

For AirPlay, the audio can be re-encoded using ALAC or left as raw PCM. Note that bridging also works with AppleTV, but you need to create a pairing key. This is done by launching the application with the -l option and following instructions. A config file with the required <raop_credentials> tag is automatically written to the directory from which the application was launched and will be required for further use. For software-based AirPlay emulators like most cheap knock-off, encryption is required (see below)

Please read carefully the credentials paragraph to understand how to handle Spotify credentials

Installing

  1. Pre-built binaries are in the SpotConnect-X.Y.Z.zip file (both spotupnp or spotraop), use the version that matches your OS. You can also look at releases

    • For AirPlay, the file is spotraop-<os>-<platform> (so spotraop-linux-aarch64 for AirPlay on Linux + arm64 CPU)
    • For UPnP/Sonos, the file is spotupnp-<os>-<platform> (so spotupnp-macos-arm64 for UPnP/Sonos on macOS + arm CPU)
  2. Store the \<executable> (e.g. spotupnp-linux-aarch64multi) in any directory.

  3. OS-specific steps:

    • macOS: Install openSSL and do the following steps to use the dynamic load library version:

      • install openssl: brew install openssl. This creates libraries (or at least links) into /usr/local/opt/openssl[/x.y.z]/lib where optional 'x.y.z' is a version number
      • create links to these libraries:
        ln -s /usr/local/opt/openssl[/x.y.z]/lib/libcrypto.dylib /usr/local/lib/libcrypto.dylib 
        ln -s /usr/local/opt/openssl[/x.y.z]/lib/libssl.dylib /usr/local/lib/libssl.dylib 
    • Non-Windows machines (including macOS), open a terminal and change directories to where the executable is stored and run chmod +x <executable>. (Example: chmod +x spotupnp-osx-multi). Note that if you choose to download the whole repository (instead of individual files) from you web browser and then unzip it, then in the bin/ sub-directory, file permissions should be already set.

    • Windows: Copy all the .dll as well if you want to use the non-static version or use the Windows MSVC package

  4. Don't use firewall or set ports using options below and open them.

    • Each device uses 1 port for HTTP (use -a parameter, default is random)
    • UPnP adds one extra port for discovery (use -b or \<upnp_socket> parameter, default is 49152 and user value must be above this)
  5. In Docker, you must use 'host' mode to enable audio webserver. Note that you can't have a NAT between your devices and the machine where AirConnect runs.

Running

Double click the \<executable> or launch it by typing ./<executable> in the same command line window.

You should start to see lots of log messages on screen. Using your iOS/Mac/iTunes/Airfoil/other client, you should now see new AirPlay devices and can try to play audio to them.

If it works, type exit, which terminates the executable, and then, on non-Windows/MacOS machines, relaunch it with -z so that it can run in the background and you can close the command line window. You can also start it automatically using any startup script or a Linux service as explained below. Nothing else should be required, no library or anything to install.

For each platform, there is a normal and a '-static' version. This one includes all libraries directly inside the application, so normally there is no dependence to 3rd party shared libraries, including SSL. You can try it if the normal fails to load (especially on old systems), but static linkage is a blessing a curse (exact reasons out of scope of this README). Now, if the static version still does not work, there are other solutons that are pretty technical, see here. Best is that you open an issue if you want help with tha

Common information:

Use -h for command line details

Config file parameters

The default configuration file is config.xml, stored in the same directory as the \<executable>. Each of "Common" parameters below can be set in the <common> section to apply to all devices. It can also be set in any <device> section to apply only to a specific device and overload the value set in <common>. Use the -x <config> command line option to use a config file of your choice.

Common

UPnP

AirPlay

Apple TV

Global

These are set in the main <spotraop> section:

There are many other parameters, to list all of them, use -i <config> to create a default config file.

Credentials

A player can be discovered using the ZeroConf protocol or can spontaneuously register to Spotify servers. When using ZeroConf, the player is by default not registered to Spotify servers and if you use a WebAPI application (e.g. an HomeAutomation service), it will not list it. Here is the reason why:

In ZeroConf, the player simply broadcasts (using mDNS) information to your local network so that a local Spotify listener can discover it, query Spotify servers to get credentials and pass these back to the player which then will register to the servers. This is why you don't need to enter any credentials for the player. It's convenient because only the listener stores the sensitive credentials but again, this means that the player will not be "standby" on Spotify servers.

There is also a way for players to store credentials and immediately register to Spotify servers upon startup. You can either pass your username (-U) and password (-P) on the command line so that SpotConnect has the full information to register, but it's not ideal in term of security and you have to make sure the script that launches it is not readable by unauthorized users. Luckily, Spotify provides a mechansim by which their servers provide device-specific reusable credentials in a form of a token which is less sensitive. SpotConnect can query and store these, using two different methods:

1- Store credentials in the config file: using the -j command line option, SpotConnect reads the .xml config file for <credentials> JSON tag and uses it to register. When SpotConnect receives such reusable credentials from Spotify, it will also store them into the config file, of course assuming that the -I option is set to authorize its update. Note that the '-j' on command line is equivalent to the root tag <credentials> of the config file. It's a global enablement parameter and cannot be set per player.

2- Store credentials in separated files: using the -J <path> command line option, SpotConnect writes and read such reusable credentials in files named spotraop-XXXXXXXX.json (respectively spotupnp...). The root tag <credentials_path> in config file is equivalent to the -J command line to set base path for these files. This can be a slightly better option to secure such credentials (which again are less sensitive, they won't allow access to your account).

SpotConnect can only write reusable credentials when it obtains it. This can done either by running it once with -U and -P and wait for all UPnP/AirPlay players to be discovered, or by playing something onto each player at least once using (e.g.) Spotify desktop application. Of course, at least one of -J or -j' and -I options must be used to allow storage of these credentials.

So for example, if you set -J, and a credentials files is found, it will be used to register directly onto servers. If there is no file, this SpotConnect player will remain in ZeroConf mode until it is discovered by Spotify desktop application at which time credentials are received and stored and so next time it will register right away.

I don't know for how long these reusable credentials are valid. In case they become unusable, you can just use -U and -P on command line, it will force re-acquisition, or you can delete the credential files and erase the <credentials> tags in the config file. Note that if you use -J and -j at the same time, per-player files and config file will be updated but the credentials found in dedicated files have precedence.

Start automatically in Linux

  1. Create a file in /etc/systemd/system, e.g. airupnp.service with the following content (assuming the airupnp binary is in /var/lib/airconnect)
[Unit]  
Description=Spotify bridge  
After=network-online.target  
Wants=network-online.target  

[Service]  
ExecStart=/var/lib/spotconnect/spotupnp-linux-arm -Z -x /var/lib/airconnect/spotupnp.xml   
Restart=on-failure  
RestartSec=30  

[Install]  
WantedBy=multi-user.target   
  1. Enable the service sudo systemctl enable spotupnp.service

  2. Start the service sudo service spotupnp start

To start or stop manually the service, type sudo service spotupnp start|stop in a command line window

To disable the service, type sudo systemctl disable spotupnp.service

To view the log, journalctl -u spotupnp.service

On rPi lite, add the following to the /boot/cmdline.txt: init=/bin/systemd

Start automatically in macOS (credits @aiwipro)

Create the file com.spotupnp.bridge.plist in ~/Library/LaunchAgents/

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.spotupnp.bridge</string>
    <key>ProgramArguments</key>
    <array>
        <string>/[path]/spotupnp-macos</string>
    <string>-Z</string>
        <string>-x</string>
        <string>/[path]/spotupnp.xml</string>
        <string>-f</string>
        <string>/[path]/spotupnp.log</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>LaunchOnlyOnce</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
</dict>
</plist>

Where [path] is the path where you've stored the spotupnpexecutable (without the []). It can be for example Users/xxx/spotconnect where xxx is your user name

Start automatically under Windows

There are many tools that allow an application to be run as a service. You can try this one

Synology installation

N/A

Player specific hints and tips

Sonos

The upnp version is often used with Sonos players. When a Sonos group is created, only the master of that group will appear as a Spotify Connect player and others will be removed if they were already detected. If the group is later split, then individual players will re-appear.

When changing volume of a group, each player's volume is changed trying to respect the relative values. It's not perfect and stil under test now. To reset all volumes to the same value, simply move the cursor to 0 and then to the new value. All players will have the same volume then. You need to use the Sonos application to change individual volumes.

To identify your Sonos players, pick an identified IP address, and visit the Sonos status page in your browser, like http://192.168.1.126:1400/support/review. Click Zone Players and you will see the identifiers for your players in the UUID column.

Bose SoundTouch

@chpusch has found that Bose SoundTouch work well including synchonisation (as for Sonos, you need to use Bose's native application for grouping / ungrouping). I don't have a SoundTouch system so I cannot do the level of slave/master detection I did for Sonos

Pioneer/Phorus/Play-Fi

Some of these speakers only support mp3

Misc tips

HTTP & UPnP specificities

HTTP content-length and transfer modes

Lots of UPnP player have very poor quality HTTP and UPnP stacks, in addition of UPnP itself being a poorly defined/certified standard. One of the main difficulty comes from the fact that AirConnect cannot provide the length of the file being streamed as Spotify does not provide it.

The HTTP standard is clear that the "content-length" header is optional and can be omitted when server does not know the size of the source. If the client is HTTP 1.1 there is another possibility which is to use "chunked" mode where the body of the message is divided into chunks of variable length. This is explicitely made for case of unknown source length and an HTTP client that claims to support 1.1 must support chunked-encoding.

The default mode of SpotUPnP is "chunked-encoding" (\<http_content_length> = -3) but unfortunately some players who claim to be HTTP 1.1 do not support it. You can then try "no length" (\ = -1). Another option is add a fake content-length (\<http_content_length> = 0). It is estimating the duration with a comfortable margin... When using pcm or wav, the length can be deduced from duration, so a real value is sent. The last option is -2 where a "content-length" is sent only if it can be properly calculated (wav and pcm codecs). Note that if player is HTTP 1.0 and http_header is set to -3, SpotUPnP will fallback no content-length. The command line option -g has the same effect that \<http_content_length> in the \<common> section of a config file.

All this might still not work as some players do not understand that the source is not a randomly accessible (searchable) file and want to get the first(e.g.) 128kB to try to do some smart guess on the length, close the connection, re-open it from the beginning and expect to have the same content. I'm trying to keep a buffer of last recently sent bytes to be able to resend-it, but that does not always works. Normally, players should understand that when they ask for a range and the response is 200 (full content), it means the source does not support range request but some don't.

To add insult to injury, when pausing some players close the connection and re-open it upon resume, but want the whole resource again, they can't even bother do a range-request starting at the last byte they received. That happens regardless of how you've instructed them that they should NOT do that. The only option is then to cache the whole track, which I can't do in memory, so in that case use the option use_filecache (or -C on command line) to have the whole track buffered on disk (in system tmp's). Now, even that might not suffice in chunked-encoding mode, these players WANT a track size to be able to pause. So in that case you need use HTTP mode 0 as well.

UPnP is a boatload of crap, unfortunately...

Compiling from source

It's a CMake-oriented build, and there is a bash script (built.sh) and Windows one (build.cmd). The bash script is intended for cross-platform build and you might be able to call directly your native compiler, but have a look at the command line in the build.sh to make sure it can work.

Please see here to know how to rebuild my apps in general

More precisely, here are the steps you should take:

Credits