Skip to content

Spotify Connect Basics

Warning:Commercial Hardware tools and the eSDK are available only for approved partners

Spotify Connect allows users to control hardware devices from the Spotify mobile app. For example, by selecting a wireless speaker in the Connect menu of the Spotify mobile app, playback will be transferred from the phone to the wireless speaker. The phone continues to work as a remote control, so the user can pause playback, skip tracks, or change the volume on the speaker by using the Spotify mobile app.

The application that runs on the wireless speaker receives notifications through the callbacks defined in the Spotify Embedded SDK. In these callbacks, the application updates the speaker's UI (if any) and plays the audio data that it receives.

For example, when the user transfers playback to the speaker, one of the callbacks that is invoked is SpCallbackPlaybackNotify() with the event kSpPlaybackNotifyBecameActive. When the user pauses playback using the Spotify mobile app, the application receives the event kSpPlaybackNotifyPause.

Note: In order to test Connect, log in to the Spotify mobile app while on the same WiFi as your Spotify Embedded application. Alternatively, if you don't have ZeroConf working, log in with the same user account on your Spotify Embedded application. When you tap the "Devices available" button in the mobile app or the speaker icon in the desktop app, the Spotify Embedded application appears in the list of available speakers under the name that you specified in SpConfig::display_name.

Reacting To Volume Changes

The application is responsible for applying the desired volume level to the audio data that is delivered in the audio data callback.

Whenever the user changes the playback volume of the speaker using Spotify Connect, the callback SpCallbackPlaybackApplyVolume() is invoked. The application can then either set the volume level in the audio driver or apply the volume to the samples when decoding them from the data that eSDK delivers.

In addition, the application is expected to inform the library if the output volume changes without having received a volume change event through Connect. For example, the speaker might have a volume knob that changes the master volume of the device. When this happens, the application should call the function SpPlaybackUpdateVolume() so that Connect-enabled remote control apps can update the volume of the speaker in their UIs.

Note: When the library is initialized, it assumes a volume level of 65535 (maximum volume). The application must invoke SpPlaybackUpdateVolume() at some point after calling SpInit() to inform the library of the actual volume level of the device's audio output.

The following example shows how to do this:


_30
...
_30
_30
void CallbackPlaybackApplyVolume(uint16_t volume, uint8_t remote, void *context) {
_30
LOG("Playback status: volume now %u\n", volume);
_30
audio_callbacks.audio_volume(volume);
_30
}
_30
_30
int main(int argc, char *argv[]) {
_30
...
_30
SpPlaybackCallbacks playback_cb;
_30
...
_30
_30
memset(&playback_cb, 0, sizeof(playback_cb));
_30
playback_cb.on_audio_data = audio_callbacks.audio_data;
_30
playback_cb.on_apply_volume = CallbackPlaybackApplyVolume;
_30
SpRegisterPlaybackCallbacks(&playback_callbacks, NULL);
_30
_30
while (error_occurred == kSpErrorOk) {
_30
SpPumpEvents();
_30
_30
/* Get the volume level of the audio driver using an application-defined
_30
function. If it doesn't match the volume level that was last reported
_30
to the library, report the new volume.
_30
*/
_30
if (SoundGetOutputVolume() != SpPlaybackGetVolume())
_30
SpPlaybackUpdateVolume(SoundGetOutputVolume());
_30
}
_30
_30
return 0;
_30
}

Displaying Track Metadata

If the device has a UI, it should display information about the currently playing track, such as the track name and artist name, the length of the track, and the current playback position within the track.

It will also want to display the playback status ("playing" or "paused") and whether "shuffle" and "repeat" are enabled.

The Embedded SDK invokes callbacks when any of this information changes. Here is an example that shows how to use two of them, SpCallbackPlaybackNotify() and SpCallbackPlaybackSeek():


_93
static struct SpMetadata current_metadata[3];
_93
...
_93
_93
static void CallbackPlaybackNotify(enum SpPlaybackNotification event,
_93
void *context)
_93
{
_93
SpError err;
_93
switch (event) {
_93
case kSpPlaybackNotifyPlay:
_93
LOG("Playback status: playing\n");
_93
if (active && !playing)
_93
audio_callbacks.audio_pause(0);
_93
playing = 1;
_93
break;
_93
case kSpPlaybackNotifyPause:
_93
LOG("Playback status: paused\n");
_93
if (active && playing)
_93
audio_callbacks.audio_pause(1);
_93
playing = 0;
_93
break;
_93
case kSpPlaybackNotifyTrackChanged:
_93
memset(&current_metadata, 0, sizeof(current_metadata));
_93
SpGetMetadata(&current_metadata[0], kSpMetadataTrackPrevious);
_93
SpGetMetadata(&current_metadata[2], kSpMetadataTrackNext);
_93
err = SpGetMetadata(&current_metadata[1], kSpMetadataTrackCurrent);
_93
if (err == kSpErrorOk) {
_93
LOG("Track event: playing %s -- %s\n",
_93
current_metadata[1].artist, current_metadata[1].track);
_93
LOG(" prev: %s, next: %s\n",
_93
current_metadata[0].track[0] ? current_metadata[0].track : "N/A",
_93
current_metadata[2].track[0] ? current_metadata[2].track : "N/A");
_93
} else {
_93
LOG("Track event: start of unknown track\n");
_93
}
_93
break;
_93
case kSpPlaybackNotifyMetadataChanged: {
_93
struct SpMetadata new_metadata[3];
_93
SpGetMetadata(&new_metadata[0], kSpMetadataTrackPrevious);
_93
SpGetMetadata(&new_metadata[2], kSpMetadataTrackNext);
_93
err = SpGetMetadata(&new_metadata[1], kSpMetadataTrackCurrent);
_93
if (memcmp(current_metadata, new_metadata, sizeof(current_metadata))) {
_93
memcpy(current_metadata, new_metadata, sizeof(current_metadata));
_93
LOG("Metadata changed: playing %s -- %s\n",
_93
current_metadata[1].artist, current_metadata[1].track);
_93
LOG(" prev: %s, next: %s\n",
_93
current_metadata[0].track[0] ? current_metadata[0].track : "N/A",
_93
current_metadata[2].track[0] ? current_metadata[2].track : "N/A");
_93
} else {
_93
LOG("Metadata change: Nothing to update\n");
_93
}
_93
break;
_93
}
_93
case kSpPlaybackNotifyShuffleOn:
_93
LOG("Shuffle status: 1\n");
_93
break;
_93
case kSpPlaybackNotifyShuffleOff:
_93
LOG("Shuffle status: 0\n");
_93
break;
_93
case kSpPlaybackNotifyRepeatOn:
_93
LOG("Repeat status: 1\n");
_93
break;
_93
case kSpPlaybackNotifyRepeatOff:
_93
LOG("Repeat status: 0\n");
_93
break;
_93
...
_93
_93
default:
_93
break;
_93
}
_93
}
_93
_93
static void CallbackPlaybackSeek(uint32_t position_ms, void *context)
_93
{
_93
LOG("Playback status: seeked to %u\n", position_ms);
_93
}
_93
_93
int main(int argc, char *argv[]) {
_93
...
_93
struct SpPlaybackCallbacks playback_cb;
_93
...
_93
_93
memset(&playback_callbacks, 0, sizeof(playback_callbacks));
_93
playback_callbacks.on_notify = CallbackPlaybackNotify;
_93
playback_callbacks.on_audio_data = audio_callbacks.audio_data;
_93
playback_callbacks.on_seek = CallbackPlaybackSeek;
_93
playback_callbacks.on_apply_volume = CallbackPlaybackApplyVolume;
_93
_93
...
_93
SpRegisterPlaybackCallbacks(&playback_callbacks, NULL);
_93
...
_93
_93
return 0;
_93
}

In addition to these callbacks, the library contains functions to retrieve the current status of playback. Using these functions, the previous example can be rewritten without callbacks as follows.

Notes

  • The notifications kSpPlaybackNotifyNext and kSpPlaybackNotifyPrev cannot be replaced.

  • These functions reflect the state of the playback among all Connect-enabled devices, even if the device is not the active playback device.


_73
...
_73
_73
struct SpMetadata previous_metadata = {0};
_73
uint8_t previous_playing = 0;
_73
uint8_t previous_shuffle = 0;
_73
uint8_t previous_repeat = 0;
_73
uint32_t previous_position_ms = 0;
_73
uint8_t previous_active = 0;
_73
_73
int main(int argc, char *argv[]) {
_73
...
_73
struct SpMetadata metadata;
_73
uint8_t playing, shuffle, repeat, active;
_73
uint32_t position_ms;
_73
...
_73
_73
while (error_occurred == kSpErrorOk) {
_73
SpPumpEvents();
_73
_73
active = SpPlaybackIsActiveDevice();
_73
if (active != previous_active) {
_73
if (active) {
_73
LOG("This device is the active speaker.\n");
_73
} else {
_73
LOG("This device is not the active speaker.\n");
_73
LOG("The following state reflects what is playing on another\n");
_73
LOG("Connect-enabled device.\n");
_73
}
_73
previous_active = active;
_73
}
_73
_73
/* Retrieve the metadata of the current track and compare against the
_73
previous metadata. If the metadata changed, update the display.
_73
*/
_73
err = SpGetMetadata(&metadata, kSpMetadataTrackCurrent);
_73
if (err == kSpErrorFailed)
_73
memset(&metadata, 0, sizeof(metadata));
_73
if (memcmp(&metadata, &previous_metadata, sizeof(metadata))) {
_73
if (err == kSpErrorFailed)
_73
LOG("Nothing playing\n");
_73
else
_73
LOG("Playing track: \"%s\" - %s (%d:%02d)\n", metadata.track, metadata.artist,
_73
(metadata.duration_ms / 1000) / 60, (metadata.duration_ms / 1000) % 60);
_73
memcpy(&previous_metadata, &metadata, sizeof(previous_metadata));
_73
}
_73
_73
playing = SpPlaybackIsPlaying();
_73
if (playing != previous_playing) {
_73
LOG(playing ? "Playing\n" : "Paused\n");
_73
previous_playing = playing;
_73
}
_73
_73
shuffle = SpPlaybackIsShuffled();
_73
if (shuffle != previous_shuffle) {
_73
LOG(shuffle ? "Shuffle ON\n" : "Shuffle OFF\n");
_73
previous_shuffle = shuffle;
_73
}
_73
_73
repeat = SpPlaybackIsRepeated();
_73
if (repeat != previous_repeat) {
_73
LOG(repeat ? "Repeat ON\n" : "Repeat OFF\n");
_73
previous_repeat = repeat;
_73
}
_73
_73
position_sec = SpPlaybackGetPosition() / 1000;
_73
if (position_sec != previous_position_sec) {
_73
LOG("Playback position %d:%02d\n", position_sec / 60, position_sec % 60);
_73
previous_position_sec = position_sec;
_73
}
_73
}
_73
_73
return 0;
_73
}

Reacting to playback restrictions

The playback restrictions are part of the metadata for the current track. Whenever the restrictions change, the application will receive the notification (kSpPlaybackNotifyMetadataChanged). To retrieve the restrictions, call SpGetMetadata() with relative_index set to (kSpMetadataTrackCurrent), and look at the fields in SpMetadata::playback_restrictions:

  • A value of 0 means the action is allowed.
  • A value other than 0 means the action is not allowed. (See SP_PLAYBACK_RESTRICTION_UNKNOWN for a special case).

The corresponding playback API will most likely fail. For example, when the field "disallow_skipping_next_reasons" is not 0, and the application invokes SpPlaybackSkipToNext(), the API call will either fail with an error or have no effect.

The UI of the application should update according to the restrictions to grey-out/disable any action that is currently not allowed. This is done in order to make clear to the user what is clickable and what is not to avoid confusion.

Restrictions are useful only for the current track. You should not look at the restrictions of the previous or upcoming tracks.

You must not build any solution that assumes some behaviour, because any current behaviour can change in the future if our licences change. For example advertisements might be skippable one day.

If the application wants to check the particular reason why an action is not allowed, it can check which bits of the field are set. For example:


_20
uint32_t disallow_skip_reasons;
_20
SpMetadata metadata;
_20
If (kSpErrorOk != SpGetMetadata(&metadata, kSpMetadaraTrackCurrent)) {
_20
// An error occurred. Maybe there is no track currently playing?
_20
return;
_20
}
_20
disallow_skip_reasons = metadata.playback_restrictions.disallow_skipping_next_reasons;
_20
if (0 == disallow_skip_reasons) {
_20
// Skipping to the next track is allowed. Can enable Skip button in UI.
_20
} else {
_20
// Skipping to the next track is not allowed. Can disable Skip button in UI.
_20
if (disallow_skip_reasons & SP_PLAYBACK_RESTRICTION_NO_NEXT_TRACK) {
_20
// Skipping is not allowed because there is no next track to skip to.
_20
}
_20
if (disallow_skip_reasons & SP_PLAYBACK_RESTRICTION_AD_DISALLOW) {
_20
// Skipping is not allowed because an ad is playing that can’t be interrupted.
_20
}
_20
// Any number of reasons can apply at the same time.
_20
// ...
_20
}

SP_PLAYBACK_RESTRICTION_UNKNOWN

Sometimes the "disallow_XXX_reasons" field for an action can be set to SP_PLAYBACK_RESTRICTION_UNKNOWN. This means that eSDK has not retrieved the restrictions from the backend yet. As soon as eSDK retrieves the information, the notification kSpPlaybackNotifyMetadataChanged will be sent again, and the application can check the field again. Until then treat the action as disallowed.

For example, here is a sequence of events:

  • When a new track has just started playing, kSpPlaybackNotifyMetadataChanged is sent.
  • When looking at the metadata, the track and artist names might already have changed, but the field disallow_skipping_next_reasons might be set to SP_PLAYBACK_RESTRICTION_UNKNOWN.

This means the eSDK has not received the information about whether there is another track after the one that just started playing. After a short while, the backend sends the information and kSpPlaybackNotifyMetadataChanged is sent again. This time, disallow_skipping_next_reasons might be set to 0.

Restrictions on "Repeat" and "Repeat Track"

In the Spotify UI, the "Repeat" button usually cycles through three states: "Don’t repeat", "Repeat", and "Repeat Track". These states are reflected in the eSDK API as follows:

 Don't repeatRepeatRepeat Track
ChangingSpPlaybackEnableRepeat(0)SpPlaybackEnableRepeat(1)SpPlaybackEnableRepeat(2)
QueryingSpPlaybackGetRepeatMode == 0SpPlaybackGetRepeatMode == 1SpPlaybackGetRepeatMode == 2
Restricted?disallow_toggling_repeat_context_reasons != 0disallow_toggling_repeat_track_reasons != 0

When implementing the UI in your application, make sure users don’t get stuck in a state when attempting to cycle through the states. For example, if "Repeat Track" is not allowed, the application can’t go from "Repeat" to "Repeat Track", and it must go to "Don’t repeat" directly.


_31
void on_cycle_repeat_button_pressed() {
_31
uint32_t disallow_repeat, disallow_repeat_track;
_31
SpMetadata metadata;
_31
If (SpGetMetadata(&metadata, kSpMetadataTrackCurrent)!= kSpErrorOk) {
_31
// An error occurred. Maybe there is no track currently playing?
_31
return;
_31
}
_31
disallow_repeat = metadata.playback_restrictions.disallow_toggling_repeat_context_reasons;
_31
disallow_repeat_track = metadata.playback_restrictions.disallow_toggling_repeat_track_reasons;
_31
// Currently in “Don’t repeat”, want to go to “Repeat” (if allowed),
_31
// otherwise to “Repeat Track” (if allowed), otherwise no change.
_31
if (SpPlaybackGetRepeatMode() == 0) {
_31
if (disallow_repeat == 0) {
_31
SpPlaybackEnableRepeat(1);
_31
} else if (0 == disallow_repeat_track) {
_31
SpPlaybackEnableRepeat(2);
_31
}
_31
// Currently in “Repeat”, want to go to “Repeat Track” (if allowed),
_31
// otherwise to “Don’t repeat”.
_31
} else if (SpPlaybackGetRepeatMode() == 1) {
_31
if (disallow_repeat_track == 0) {
_31
SpPlaybackEnableRepeat(2);
_31
} else {
_31
SpPlaybackEnableRepeat(0);
_31
}
_31
// Currently in “Repeat Track”, want to go to “Don’t repeat”.
_31
} else {
_31
//
_31
SpPlaybackEnableRepeat(0);
_31
}
_31
}

Other playback events

There are additional playback events that the application can react to. Please see the enum SpPlaybackNotification for a complete list.

Important here is to actively listen for the audio playback state. You will have to pause (kSpPlaybackNotifyPause) and unpause (kSpPlaybackNotifyPlay) independently of the audio data arriving from the audio callback.

Audio data may be delivered while you are paused, and you might have to start playback even though no samples have arrived since the last pause - these samples are then already delivered to you and must be kept in your audio pipeline.

Similarly, you should keep track of if you are the active device or not. Only the active device plays audio. You must receive kSpPlaybackNotifyBecameActive in order to be allowed to play audio. It can be received while paused, so that once kSpPlaybackNotifyPlay is then received, you should start playing.

The following code shows how these events can be used.


_61
_61
void CallbackPlaybackNotify(enum SpPlaybackNotification event, void *context) {
_61
SpError err;
_61
_61
switch (event) {
_61
case kSpPlaybackNotifyPlay:
_61
LOG("Playback status: playing\n");
_61
if (active && !playing)
_61
audio_callbacks.audio_pause(0);
_61
playing = 1;
_61
break;
_61
case kSpPlaybackNotifyPause:
_61
LOG("Playback status: paused\n");
_61
if (active && playing)
_61
audio_callbacks.audio_pause(1);
_61
playing = 0;
_61
break;
_61
}
_61
...
_61
_61
case kSpPlaybackNotifyNext:
_61
LOG("Playing skipped to next track\n");
_61
break;
_61
case kSpPlaybackNotifyPrev:
_61
LOG("Playing jumped to previous track\n");
_61
break;
_61
case kSpPlaybackNotifyBecameActive:
_61
active = 1;
_61
if (playing)
_61
audio_callbacks.audio_pause(0);
_61
LOG("Became active\n");
_61
break;
_61
case kSpPlaybackNotifyBecameInactive:
_61
active = 0;
_61
LOG("Became inactive\n");
_61
if (playing) {
_61
audio_callbacks.audio_pause(1);
_61
playing = 0;
_61
}
_61
break;
_61
case kSpPlaybackNotifyLostPermission:
_61
LOG("Lost permission\n");
_61
if (playing)
_61
audio_callbacks.audio_pause(1);
_61
break;
_61
case kSpPlaybackEventAudioFlush:
_61
LOG("Audio flush\n");
_61
audio_callbacks.audio_flush();
_61
break;
_61
case kSpPlaybackNotifyAudioDeliveryDone:
_61
LOG("Audio delivery done\n");
_61
break;
_61
case kSpPlaybackNotifyTrackDelivered:
_61
LOG("Track delivered\n");
_61
break;
_61
default:
_61
break;
_61
}
_61
}
_61
_61
...