Spotify Connect Basics
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_30void 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_30int 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():
_93static struct SpMetadata current_metadata[3];_93..._93_93static 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(¤t_metadata, 0, sizeof(current_metadata));_93 SpGetMetadata(¤t_metadata[0], kSpMetadataTrackPrevious);_93 SpGetMetadata(¤t_metadata[2], kSpMetadataTrackNext);_93 err = SpGetMetadata(¤t_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_93default:_93 break;_93 }_93}_93_93static void CallbackPlaybackSeek(uint32_t position_ms, void *context)_93{_93 LOG("Playback status: seeked to %u\n", position_ms);_93}_93_93int 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_73struct SpMetadata previous_metadata = {0};_73uint8_t previous_playing = 0;_73uint8_t previous_shuffle = 0;_73uint8_t previous_repeat = 0;_73uint32_t previous_position_ms = 0;_73uint8_t previous_active = 0;_73_73int 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:
_20uint32_t disallow_skip_reasons;_20SpMetadata metadata;_20If (kSpErrorOk != SpGetMetadata(&metadata, kSpMetadaraTrackCurrent)) {_20 // An error occurred. Maybe there is no track currently playing?_20 return;_20}_20disallow_skip_reasons = metadata.playback_restrictions.disallow_skipping_next_reasons;_20if (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 toSP_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 repeat | Repeat | Repeat Track | |
---|---|---|---|
Changing | SpPlaybackEnableRepeat(0) | SpPlaybackEnableRepeat(1) | SpPlaybackEnableRepeat(2) |
Querying | SpPlaybackGetRepeatMode == 0 | SpPlaybackGetRepeatMode == 1 | SpPlaybackGetRepeatMode == 2 |
Restricted? | disallow_toggling_repeat_context_reasons != 0 | disallow_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.
_31void 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_61void 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...