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:

...

void CallbackPlaybackApplyVolume(uint16_t volume, uint8_t remote, void *context) {
  LOG("Playback status: volume now %u\n", volume);
  audio_callbacks.audio_volume(volume);
}

int main(int argc, char *argv[]) {
...
  SpPlaybackCallbacks playback_cb;
...

  memset(&playback_cb, 0, sizeof(playback_cb));
  playback_cb.on_audio_data = audio_callbacks.audio_data;
  playback_cb.on_apply_volume = CallbackPlaybackApplyVolume;
  SpRegisterPlaybackCallbacks(&playback_callbacks, NULL);

  while (error_occurred == kSpErrorOk) {
    SpPumpEvents();

    /* Get the volume level of the audio driver using an application-defined
       function. If it doesn't match the volume level that was last reported
       to the library, report the new volume.
     */
    if (SoundGetOutputVolume() != SpPlaybackGetVolume())
      SpPlaybackUpdateVolume(SoundGetOutputVolume());
  }

  return 0;
}

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():

static struct SpMetadata current_metadata[3];
...

static void CallbackPlaybackNotify(enum SpPlaybackNotification event,
				   void *context)
{
  SpError err;
  switch (event) {
  case kSpPlaybackNotifyPlay:
    LOG("Playback status: playing\n");
    if (active && !playing)
      audio_callbacks.audio_pause(0);
      playing = 1;
    break;
  case kSpPlaybackNotifyPause:
    LOG("Playback status: paused\n");
    if (active && playing)
      audio_callbacks.audio_pause(1);
    playing = 0;
    break;
  case kSpPlaybackNotifyTrackChanged:
    memset(&current_metadata, 0, sizeof(current_metadata));
    SpGetMetadata(&current_metadata[0], kSpMetadataTrackPrevious);
    SpGetMetadata(&current_metadata[2], kSpMetadataTrackNext);
    err = SpGetMetadata(&current_metadata[1], kSpMetadataTrackCurrent);
    if (err == kSpErrorOk) {
      LOG("Track event: playing %s -- %s\n",
             current_metadata[1].artist, current_metadata[1].track);
      LOG("             prev: %s, next: %s\n",
             current_metadata[0].track[0] ? current_metadata[0].track : "N/A",
             current_metadata[2].track[0] ? current_metadata[2].track : "N/A");
    } else {
        LOG("Track event: start of unknown track\n");
    }
    break;
  case kSpPlaybackNotifyMetadataChanged: {
    struct SpMetadata new_metadata[3];
    SpGetMetadata(&new_metadata[0], kSpMetadataTrackPrevious);
    SpGetMetadata(&new_metadata[2], kSpMetadataTrackNext);
    err = SpGetMetadata(&new_metadata[1], kSpMetadataTrackCurrent);
    if (memcmp(current_metadata, new_metadata, sizeof(current_metadata))) {
      memcpy(current_metadata, new_metadata, sizeof(current_metadata));
      LOG("Metadata changed: playing %s -- %s\n",
             current_metadata[1].artist, current_metadata[1].track);
      LOG("             prev: %s, next: %s\n",
             current_metadata[0].track[0] ? current_metadata[0].track : "N/A",
             current_metadata[2].track[0] ? current_metadata[2].track : "N/A");
    } else {
      LOG("Metadata change: Nothing to update\n");
    }
    break;
  }
  case kSpPlaybackNotifyShuffleOn:
    LOG("Shuffle status: 1\n");
    break;
  case kSpPlaybackNotifyShuffleOff:
    LOG("Shuffle status: 0\n");
    break;
  case kSpPlaybackNotifyRepeatOn:
    LOG("Repeat status: 1\n");
    break;
  case kSpPlaybackNotifyRepeatOff:
    LOG("Repeat status: 0\n");
    break;
...

default:
    break;
  }
}

static void CallbackPlaybackSeek(uint32_t position_ms, void *context)
{
  LOG("Playback status: seeked to %u\n", position_ms);
}

int main(int argc, char *argv[]) {
...
  struct SpPlaybackCallbacks playback_cb;
...

  memset(&playback_callbacks, 0, sizeof(playback_callbacks));
  playback_callbacks.on_notify = CallbackPlaybackNotify;
  playback_callbacks.on_audio_data = audio_callbacks.audio_data;
  playback_callbacks.on_seek = CallbackPlaybackSeek;
  playback_callbacks.on_apply_volume = CallbackPlaybackApplyVolume;

...
  SpRegisterPlaybackCallbacks(&playback_callbacks, NULL);
...

  return 0;
}

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

...

struct SpMetadata previous_metadata = {0};
uint8_t previous_playing = 0;
uint8_t previous_shuffle = 0;
uint8_t previous_repeat = 0;
uint32_t previous_position_ms = 0;
uint8_t previous_active = 0;

int main(int argc, char *argv[]) {
...
  struct SpMetadata metadata;
  uint8_t playing, shuffle, repeat, active;
  uint32_t position_ms;
...

  while (error_occurred == kSpErrorOk) {
    SpPumpEvents();

    active = SpPlaybackIsActiveDevice();
    if (active != previous_active) {
      if (active) {
        LOG("This device is the active speaker.\n");
      } else {
        LOG("This device is not the active speaker.\n");
        LOG("The following state reflects what is playing on another\n");
        LOG("Connect-enabled device.\n");
      }
      previous_active = active;
    }

    /* Retrieve the metadata of the current track and compare against the
       previous metadata. If the metadata changed, update the display.
     */
    err = SpGetMetadata(&metadata, kSpMetadataTrackCurrent);
    if (err == kSpErrorFailed)
      memset(&metadata, 0, sizeof(metadata));
    if (memcmp(&metadata, &previous_metadata, sizeof(metadata))) {
      if (err == kSpErrorFailed)
        LOG("Nothing playing\n");
      else
        LOG("Playing track: \"%s\" - %s (%d:%02d)\n", metadata.track, metadata.artist,
               (metadata.duration_ms / 1000) / 60, (metadata.duration_ms / 1000) % 60);
      memcpy(&previous_metadata, &metadata, sizeof(previous_metadata));
    }

    playing = SpPlaybackIsPlaying();
    if (playing != previous_playing) {
      LOG(playing ? "Playing\n" : "Paused\n");
      previous_playing = playing;
    }

    shuffle = SpPlaybackIsShuffled();
    if (shuffle != previous_shuffle) {
      LOG(shuffle ? "Shuffle ON\n" : "Shuffle OFF\n");
      previous_shuffle = shuffle;
    }

    repeat = SpPlaybackIsRepeated();
    if (repeat != previous_repeat) {
      LOG(repeat ? "Repeat ON\n" : "Repeat OFF\n");
      previous_repeat = repeat;
    }

    position_sec = SpPlaybackGetPosition() / 1000;
    if (position_sec != previous_position_sec) {
      LOG("Playback position %d:%02d\n", position_sec / 60, position_sec % 60);
      previous_position_sec = position_sec;
    }
  }

  return 0;
}

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:

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

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 == 0 SpPlaybackGetRepeatMode == 1 SpPlaybackGetRepeatMode == 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.

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

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.


void CallbackPlaybackNotify(enum SpPlaybackNotification event, void *context) {
  SpError err;

  switch (event) {
	case kSpPlaybackNotifyPlay:
		LOG("Playback status: playing\n");
		if (active && !playing)
			audio_callbacks.audio_pause(0);
		playing = 1;
		break;
	case kSpPlaybackNotifyPause:
		LOG("Playback status: paused\n");
		if (active && playing)
			audio_callbacks.audio_pause(1);
		playing = 0;
		break;
	}
...

	case kSpPlaybackNotifyNext:
		LOG("Playing skipped to next track\n");
		break;
	case kSpPlaybackNotifyPrev:
		LOG("Playing jumped to previous track\n");
		break;
	case kSpPlaybackNotifyBecameActive:
		active = 1;
		if (playing)
			audio_callbacks.audio_pause(0);
		LOG("Became active\n");
		break;
	case kSpPlaybackNotifyBecameInactive:
		active = 0;
		LOG("Became inactive\n");
		if (playing) {
			audio_callbacks.audio_pause(1);
			playing = 0;
		}
		break;
	case kSpPlaybackNotifyLostPermission:
		LOG("Lost permission\n");
		if (playing)
			audio_callbacks.audio_pause(1);
		break;
	case kSpPlaybackEventAudioFlush:
		LOG("Audio flush\n");
		audio_callbacks.audio_flush();
		break;
	case kSpPlaybackNotifyAudioDeliveryDone:
		LOG("Audio delivery done\n");
		break;
	case kSpPlaybackNotifyTrackDelivered:
		LOG("Track delivered\n");
		break;
	default:
		break;
	}
}

...