Reversing Spotify Connect protocol
Spotify has been promising for months now to release "Spotify Connect" as a part of libspotify. Instead, they are steadily striking "exclusive deals" with one manufacturer after another (called "Spotify Partners") but knowingly ignore "Spotify Customers" like me, with slightly older hardware, but who might have been more loyal to Spotify then any new hardware buyers, loyal customers who are now desperately locked waiting for improbable firmware updates..
Onkyo also promised to add Spotify Connect to it's receivers line (specifically they mentioned NR-636 model). I have a sour feeling that my NR-626 model, bought just in October 2013, is not going to be supported (even though it is fully capable to).
Such marketing sucks ass. I already decided that I am not going to choose Onkyo for my next amp upgrade. And I might switch away from Spotify after paying them for 3 years too.
So yeah, fuck them - I'll do this stuff myself.
I was able to extract Spotify Connect implementation from one speakers manufacturer firmware, finally! (won't mention names for obvious reasons ;-).
So I am reverse-engineering Spotify Connect at the moment and it doesn't seem too hard. It is a relatively-simple protocol. Much simplier than what libspotify does.
Spotify Connect Protocol
The ARM library is rather small (161 kb) and is called libspotify_embedded.so.
It's relevant exports:
4: 00014c1c 152 FUNC GLOBAL DEFAULT 10 SpPlaybackSetBitrate 5: 00014f14 164 FUNC GLOBAL DEFAULT 10 SpConnectionLoginPassword 6: 00015354 152 FUNC GLOBAL DEFAULT 10 SpPlaybackIsPlaying 19: 00014cb4 152 FUNC GLOBAL DEFAULT 10 SpConnectionIsLoggedIn 21: 0001505c 152 FUNC GLOBAL DEFAULT 10 SpPlaybackEnableRepeat 23: 00014de4 152 FUNC GLOBAL DEFAULT 10 SpConnectionSetConnectivity 24: 00014ac0 184 FUNC GLOBAL DEFAULT 10 SpGetMetadataImageURL 25: 0001577c 152 FUNC GLOBAL DEFAULT 10 SpPlaybackPause 27: 000159f4 164 FUNC GLOBAL DEFAULT 10 SpRegisterConnectionCallbacks 28: 00015bc8 4 FUNC GLOBAL DEFAULT 10 SpInit 33: 000146bc 164 FUNC GLOBAL DEFAULT 10 SpPlayPreset 37: 000155b4 152 FUNC GLOBAL DEFAULT 10 SpPlaybackSeek 39: 00014620 156 FUNC GLOBAL DEFAULT 10 SpGetPreset 41: 00014fb8 164 FUNC GLOBAL DEFAULT 10 SpConnectionLoginBlob 42: 00015224 152 FUNC GLOBAL DEFAULT 10 SpPlaybackIsRepeated 43: 000153ec 152 FUNC GLOBAL DEFAULT 10 SpPlaybackGetVolume 45: 00015a98 152 FUNC GLOBAL DEFAULT 10 SpFree 46: 00015950 164 FUNC GLOBAL DEFAULT 10 SpRegisterPlaybackCallbacks 47: 00015814 152 FUNC GLOBAL DEFAULT 10 SpPlaybackPlay 53: 0001551c 152 FUNC GLOBAL DEFAULT 10 SpPlaybackGetPosition 54: 0001564c 152 FUNC GLOBAL DEFAULT 10 SpPlaybackSkipToPrev 55: 000156e4 152 FUNC GLOBAL DEFAULT 10 SpPlaybackSkipToNext 57: 00015b30 152 FUNC GLOBAL DEFAULT 10 SpGetLibraryVersion 64: 00014a14 172 FUNC GLOBAL DEFAULT 10 SpGetMetadataValidRange 68: 0001497c 152 FUNC GLOBAL DEFAULT 10 SpSetDisplayName 69: 00014d4c 152 FUNC GLOBAL DEFAULT 10 SpConnectionLogout 70: 00014760 176 FUNC GLOBAL DEFAULT 10 SpConnectionLoginZeroConf 72: 00014e7c 152 FUNC GLOBAL DEFAULT 10 SpConnectionLoginOauthToken 74: 000152bc 152 FUNC GLOBAL DEFAULT 10 SpPlaybackIsShuffled 78: 00015484 152 FUNC GLOBAL DEFAULT 10 SpPlaybackUpdateVolume 81: 000158ac 164 FUNC GLOBAL DEFAULT 10 SpRegisterDebugCallbacks 82: 00014b78 164 FUNC GLOBAL DEFAULT 10 SpGetMetadata 83: 000148a8 212 FUNC GLOBAL DEFAULT 10 SpPumpEvents 90: 0001518c 152 FUNC GLOBAL DEFAULT 10 SpPlaybackIsActiveDevice 94: 00014810 152 FUNC GLOBAL DEFAULT 10 SpZeroConfGetVars 97: 000150f4 152 FUNC GLOBAL DEFAULT 10 SpPlaybackEnableShuffle
That's pretty much it ;)
How it works in a nutshell
It advertises itself via Zeroconf (avahi-publish) as a local _spotify-connect._tcp service, which runs a simple HTTP service.
When iOS client is started, it tries to resolve this service and connects to it if available.
Client then served with a following HTTP response:
{"status": 101, "statusString": "OK", "spotifyError": 0, "version": "2.0.0", "deviceID": "00:00:00:00:00:00", "publicKey": "gjigiijiwjiwjrijkdaldklasdaldladkdkladlasdksldkladklalkdlakdlaskdlaskldkadkldklaskdlskdlaskdlakdldkasldkasldklsakdlaskdakdkadasd", "remoteName": "MY SPEAKERS", "activeUser": ""}
At this moment, "MY SPEAKERS" appear under "Spotify connect" menu as a device on the network.
After it's selected, client sends "addUser" command, where it passes userName, blob and a clientKey - everything needed for the sign-in to Spotify.
So the library then establish connection with one of Spotify Access Points via SpConnectionLoginBlob(username, blob).
"Spotify connect" icon goes green.
Now all communication happens through Spotify server in a sort-of IRC like protocol called Gaia(?) v2.0.
These are the commands it supports:
"load",0
"play",0
"pause",0
"seek",0
"skip-prev",0 "skip-next",0 "volume",0
"shuffle",0
"repeat",0
"replace",0
"fallback",0
"pull",0
"error",0
"goodbye",0
"logout",0
You see, it's pretty basic ;) Much simplier than doing it all via libspotify.
I haven't found any heavy encryption AES stuff in the library (yet?) and only some simplier encryption routines, so hopefully it just uses public key encryption on the stream.
Running it with player binaries under Qemu-arm, I was able to make my PC receive PCM sound stream, whilst controlling songs with iPhone Spotify client. Neat :)
But that's just half-work (or rather 15%). The library must be written from scratch to be usable and to support all platforms. Further analysing all the logic and converting it to a high-level language is the hardest part.
As much as I'd like to make and release this project as open-source, I straggle to find time at the moment to commit daily on this, so I am calling for collaborators. Drop me a line if you want to achieve this honourable task together!;)