Browse Source

add missing files, removing un-nedded ones

philippe44 1 năm trước cách đây
mục cha
commit
806cb054ba

+ 30 - 35
components/spotify/cspot/include/CDNTrackStream.h → components/spotify/cspot/include/CDNAudioFile.h

@@ -1,10 +1,10 @@
 #pragma once
 
-#include <cstddef>       // for size_t
-#include <cstdint>       // for uint8_t
-#include <memory>        // for shared_ptr, unique_ptr
-#include <string>        // for string
-#include <vector>        // for vector
+#include <cstddef>  // for size_t
+#include <cstdint>  // for uint8_t
+#include <memory>   // for shared_ptr, unique_ptr
+#include <string>   // for string
+#include <vector>   // for vector
 
 #include "Crypto.h"      // for Crypto
 #include "HTTPClient.h"  // for HTTPClient
@@ -16,46 +16,45 @@ class WrappedSemaphore;
 namespace cspot {
 class AccessKeyFetcher;
 
-class CDNTrackStream {
+class CDNAudioFile {
 
  public:
-  CDNTrackStream(std::shared_ptr<cspot::AccessKeyFetcher>);
-  ~CDNTrackStream();
-
-  enum class Status { INITIALIZING, HAS_DATA, HAS_URL, FAILED };
-
-  struct TrackInfo {
-    std::string trackId;
-    std::string name;
-    std::string album;
-    std::string artist;
-    std::string imageUrl;
-    int duration;
-  };
-
-  TrackInfo trackInfo;
-
-  Status status;
-  std::unique_ptr<bell::WrappedSemaphore> trackReady;
-
-  void fetchFile(const std::vector<uint8_t>& trackId,
-                 const std::vector<uint8_t>& audioKey);
-
-  void fail();
+  CDNAudioFile(const std::string& cdnUrl, const std::vector<uint8_t>& audioKey);
 
+  /**
+  * @brief Opens connection to the provided cdn url, and fetches track metadata.
+  */
   void openStream();
 
+  /**
+  * @brief Read and decrypt part of the cdn stream
+  *
+  * @param dst buffer where to read received data to
+  * @param amount of bytes to read
+  *
+  * @returns amount of bytes read
+  */
   size_t readBytes(uint8_t* dst, size_t bytes);
 
+  /**
+  * @brief Returns current position in CDN stream
+  */
   size_t getPosition();
 
+  /**
+  * @brief returns total size of the audio file in bytes
+  */
   size_t getSize();
 
+  /**
+  * @brief Seeks the track to provided position
+  * @param position position where to seek the track
+  */
   void seek(size_t position);
 
  private:
   const int OPUS_HEADER_SIZE = 8 * 1024;
-  const int OPUS_FOOTER_PREFFERED = 1024 * 12; // 12K should be safe
+  const int OPUS_FOOTER_PREFFERED = 1024 * 12;  // 12K should be safe
   const int SEEK_MARGIN_SIZE = 1024 * 4;
 
   const int HTTP_BUFFER_SIZE = 1024 * 14;
@@ -74,12 +73,9 @@ class CDNTrackStream {
                                            0x3f, 0x63, 0x0d, 0x93};
   std::unique_ptr<Crypto> crypto;
 
-  std::shared_ptr<cspot::AccessKeyFetcher> accessKeyFetcher;
-
   std::unique_ptr<bell::HTTPClient::Response> httpConnection;
-  bool isConnected = false;
 
-  size_t position = 0; // Spotify header size
+  size_t position = 0;
   size_t totalFileSize = 0;
   size_t lastRequestPosition = 0;
   size_t lastRequestCapacity = 0;
@@ -87,7 +83,6 @@ class CDNTrackStream {
   bool enableRequestMargin = false;
 
   std::string cdnUrl;
-  std::vector<uint8_t> trackId;
   std::vector<uint8_t> audioKey;
 
   void decrypt(uint8_t* dst, size_t nbytes, size_t pos);

+ 0 - 40
components/spotify/cspot/include/TrackProvider.h

@@ -1,40 +0,0 @@
-#pragma once
-
-#include <stdint.h>                // for uint8_t
-#include <memory>                  // for shared_ptr, unique_ptr, weak_ptr
-#include <vector>                  // for vector
-
-#include "MercurySession.h"        // for MercurySession
-#include "TrackReference.h"        // for TrackReference
-#include "protobuf/metadata.pb.h"  // for Episode, Restriction, Track
-
-namespace cspot {
-class AccessKeyFetcher;
-class CDNTrackStream;
-struct Context;
-
-class TrackProvider {
- public:
-  TrackProvider(std::shared_ptr<cspot::Context> ctx);
-  ~TrackProvider();
-
-  std::shared_ptr<CDNTrackStream> loadFromTrackRef(TrackReference& trackRef);
-
- private:
-  std::shared_ptr<AccessKeyFetcher> accessKeyFetcher;
-  std::shared_ptr<cspot::Context> ctx;
-  std::unique_ptr<cspot::CDNTrackStream> cdnStream;
-
-  Track trackInfo;
-  Episode episodeInfo;
-  std::weak_ptr<CDNTrackStream> currentTrackReference;
-  TrackReference trackIdInfo;
-
-  void queryMetadata();
-  void onMetadataResponse(MercurySession::Response& res);
-  bool doRestrictionsApply(Restriction* restrictions, int count);
-  void fetchFile(const std::vector<uint8_t>& fileId,
-                 const std::vector<uint8_t>& trackId);
-  bool canPlayTrack(int index);
-};
-}  // namespace cspot

+ 134 - 0
components/spotify/cspot/include/TrackQueue.h

@@ -0,0 +1,134 @@
+#pragma once
+
+#include <stddef.h>  // for size_t
+#include <atomic>
+#include <deque>
+#include <mutex>
+#include <functional>
+
+#include "BellTask.h"
+#include "PlaybackState.h"
+#include "TrackReference.h"
+
+#include "protobuf/metadata.pb.h"  // for Track, _Track, AudioFile, Episode
+
+namespace bell {
+class WrappedSemaphore;
+};
+
+namespace cspot {
+struct Context;
+class AccessKeyFetcher;
+class CDNAudioFile;
+
+// Used in got track info event
+struct TrackInfo {
+  std::string name, album, artist, imageUrl, trackId;
+  uint32_t duration;
+
+  void loadPbTrack(Track* pbTrack, const std::vector<uint8_t>& gid);
+  void loadPbEpisode(Episode* pbEpisode, const std::vector<uint8_t>& gid);
+};
+
+class QueuedTrack {
+ public:
+  QueuedTrack(TrackReference& ref, std::shared_ptr<cspot::Context> ctx,
+              uint32_t requestedPosition = 0);
+  ~QueuedTrack();
+
+  enum class State {
+    QUEUED,
+    PENDING_META,
+    KEY_REQUIRED,
+    PENDING_KEY,
+    CDN_REQUIRED,
+    READY,
+    FAILED
+  };
+
+  std::shared_ptr<bell::WrappedSemaphore> loadedSemaphore;
+
+  State state = State::QUEUED;  // Current state of the track
+  TrackReference ref;           // Holds GID, URI and Context
+  TrackInfo trackInfo;  // Full track information fetched from spotify, name etc
+
+  uint32_t requestedPosition;
+  std::string identifier;
+
+  // Will return nullptr if the track is not ready
+  std::shared_ptr<cspot::CDNAudioFile> getAudioFile();
+
+  // --- Steps ---
+  void stepLoadMetadata(
+      Track* pbTrack, Episode* pbEpisode, std::mutex& trackListMutex,
+      std::shared_ptr<bell::WrappedSemaphore> updateSemaphore);
+
+  void stepParseMetadata(Track* pbTrack, Episode* pbEpisode);
+
+  void stepLoadAudioFile(
+      std::mutex& trackListMutex,
+      std::shared_ptr<bell::WrappedSemaphore> updateSemaphore);
+
+  void stepLoadCDNUrl(const std::string& accessKey);
+
+  void expire();
+
+ private:
+  std::shared_ptr<cspot::Context> ctx;
+
+  uint64_t pendingMercuryRequest = 0;
+  uint32_t pendingAudioKeyRequest = 0;
+
+  std::vector<uint8_t> trackId, fileId, audioKey;
+  std::string cdnUrl;
+};
+
+class TrackQueue : public bell::Task {
+ public:
+  TrackQueue(std::shared_ptr<cspot::Context> ctx,
+             std::shared_ptr<cspot::PlaybackState> playbackState);
+  ~TrackQueue();
+
+  enum class SkipDirection { NEXT, PREV };
+
+  std::shared_ptr<bell::WrappedSemaphore> playableSemaphore;
+  std::atomic<bool> notifyPending = false;
+
+
+  void runTask() override;
+  void stopTask();
+
+  bool hasTracks();
+  bool isFinished();
+  bool skipTrack(SkipDirection dir, bool expectNotify = true);
+  void updateTracks(uint32_t requestedPosition = 0, bool initial = false);
+  TrackInfo getTrackInfo(std::string_view identifier);
+  std::shared_ptr<QueuedTrack> consumeTrack(
+      std::shared_ptr<QueuedTrack> prevSong, int& offset);
+
+ private:
+  static const int MAX_TRACKS_PRELOAD = 3;
+
+  std::shared_ptr<cspot::AccessKeyFetcher> accessKeyFetcher;
+  std::shared_ptr<PlaybackState> playbackState;
+  std::shared_ptr<cspot::Context> ctx;
+  std::shared_ptr<bell::WrappedSemaphore> processSemaphore;
+
+  std::deque<std::shared_ptr<QueuedTrack>> preloadedTracks;
+  std::vector<TrackReference> currentTracks;
+  std::mutex tracksMutex, runningMutex;
+
+  // PB data
+  Track pbTrack;
+  Episode pbEpisode;
+
+  std::string accessKey;
+
+  int16_t currentTracksIndex = -1;
+
+  bool isRunning = false;
+
+  void processTrack(std::shared_ptr<QueuedTrack> track);
+  bool queueNextTrack(int offset = 0, uint32_t positionMs = 0);
+};
+}  // namespace cspot

+ 14 - 65
components/spotify/cspot/src/CDNTrackStream.cpp → components/spotify/cspot/src/CDNAudioFile.cpp

@@ -1,4 +1,4 @@
-#include "CDNTrackStream.h"
+#include "CDNAudioFile.h"
 
 #include <string.h>          // for memcpy
 #include <functional>        // for __base
@@ -7,15 +7,16 @@
 #include <string_view>       // for string_view
 #include <type_traits>       // for remove_extent_t
 
-#include "AccessKeyFetcher.h"     // for AccessKeyFetcher
-#include "BellLogger.h"           // for AbstractLogger
+#include "AccessKeyFetcher.h"  // for AccessKeyFetcher
+#include "BellLogger.h"        // for AbstractLogger
+#include "Crypto.h"
 #include "Logger.h"               // for CSPOT_LOG
 #include "Packet.h"               // for cspot
 #include "SocketStream.h"         // for SocketStream
 #include "Utils.h"                // for bigNumAdd, bytesToHexString, string...
 #include "WrappedSemaphore.h"     // for WrappedSemaphore
 #ifdef BELL_ONLY_CJSON
-#include "cJSON.h"
+#include "cJSON.h "
 #else
 #include "nlohmann/json.hpp"      // for basic_json<>::object_t, basic_json
 #include "nlohmann/json_fwd.hpp"  // for json
@@ -23,73 +24,22 @@
 
 using namespace cspot;
 
-CDNTrackStream::CDNTrackStream(
-    std::shared_ptr<cspot::AccessKeyFetcher> accessKeyFetcher) {
-  this->accessKeyFetcher = accessKeyFetcher;
-  this->status = Status::INITIALIZING;
-  this->trackReady = std::make_unique<bell::WrappedSemaphore>(5);
+CDNAudioFile::CDNAudioFile(const std::string& cdnUrl,
+                           const std::vector<uint8_t>& audioKey)
+    : cdnUrl(cdnUrl), audioKey(audioKey) {
   this->crypto = std::make_unique<Crypto>();
 }
 
-CDNTrackStream::~CDNTrackStream() {}
-
-void CDNTrackStream::fail() {
-  this->status = Status::FAILED;
-  this->trackReady->give();
-}
-
-void CDNTrackStream::fetchFile(const std::vector<uint8_t>& trackId,
-                               const std::vector<uint8_t>& audioKey) {
-  this->status = Status::HAS_DATA;
-  this->trackId = trackId;
-  this->audioKey = std::vector<uint8_t>(audioKey.begin() + 4, audioKey.end());
-
-  accessKeyFetcher->getAccessKey([this, trackId, audioKey](std::string key) {
-    CSPOT_LOG(info, "Received access key, fetching CDN URL...");
-
-    std::string requestUrl = string_format(
-        "https://api.spotify.com/v1/storage-resolve/files/audio/interactive/"
-        "%s?alt=json&product=9",
-        bytesToHexString(trackId).c_str());
-
-    auto req = bell::HTTPClient::get(
-        requestUrl,
-        {bell::HTTPClient::ValueHeader({"Authorization", "Bearer " + key})});
-
-    std::string_view result = req->body();
-
-#ifdef BELL_ONLY_CJSON
-    cJSON* jsonResult = cJSON_Parse(result.data());
-    std::string cdnUrl =
-        cJSON_GetArrayItem(cJSON_GetObjectItem(jsonResult, "cdnurl"), 0)
-            ->valuestring;
-    cJSON_Delete(jsonResult);
-#else
-    auto jsonResult = nlohmann::json::parse(result);
-    std::string cdnUrl = jsonResult["cdnurl"][0];
-#endif
-    if (this->status != Status::FAILED) {
-
-      this->cdnUrl = cdnUrl;
-      this->status = Status::HAS_URL;
-      CSPOT_LOG(info, "Received CDN URL, %s", cdnUrl.c_str());
-
-      this->openStream();
-      this->trackReady->give();
-    }
-  });
-}
-
-size_t CDNTrackStream::getPosition() {
+size_t CDNAudioFile::getPosition() {
   return this->position;
 }
 
-void CDNTrackStream::seek(size_t newPos) {
+void CDNAudioFile::seek(size_t newPos) {
   this->enableRequestMargin = true;
   this->position = newPos;
 }
 
-void CDNTrackStream::openStream() {
+void CDNAudioFile::openStream() {
   CSPOT_LOG(info, "Opening HTTP stream to %s", this->cdnUrl.c_str());
 
   // Open connection, read first 128 bytes
@@ -121,10 +71,9 @@ void CDNTrackStream::openStream() {
   this->position = 0;
   this->lastRequestPosition = 0;
   this->lastRequestCapacity = 0;
-  this->isConnected = true;
 }
 
-size_t CDNTrackStream::readBytes(uint8_t* dst, size_t bytes) {
+size_t CDNAudioFile::readBytes(uint8_t* dst, size_t bytes) {
   size_t offsetPosition = position + SPOTIFY_OPUS_HEADER;
   size_t actualFileSize = this->totalFileSize + SPOTIFY_OPUS_HEADER;
 
@@ -199,11 +148,11 @@ size_t CDNTrackStream::readBytes(uint8_t* dst, size_t bytes) {
   return bytes;
 }
 
-size_t CDNTrackStream::getSize() {
+size_t CDNAudioFile::getSize() {
   return this->totalFileSize;
 }
 
-void CDNTrackStream::decrypt(uint8_t* dst, size_t nbytes, size_t pos) {
+void CDNAudioFile::decrypt(uint8_t* dst, size_t nbytes, size_t pos) {
   auto calculatedIV = bigNumAdd(audioAESIV, pos / 16);
 
   this->crypto->aesCTRXcrypt(this->audioKey, calculatedIV, dst, nbytes);

+ 0 - 245
components/spotify/cspot/src/TrackProvider.cpp

@@ -1,245 +0,0 @@
-#include "TrackProvider.h"
-
-#include <assert.h>                // for assert
-#include <string.h>                // for strlen
-#include <cstdint>                 // for uint8_t
-#include <functional>              // for __base
-#include <memory>                  // for shared_ptr, weak_ptr, make_shared
-#include <string>                  // for string, operator+
-#include <type_traits>             // for remove_extent_t
-
-#include "AccessKeyFetcher.h"      // for AccessKeyFetcher
-#include "BellLogger.h"            // for AbstractLogger
-#include "CDNTrackStream.h"        // for CDNTrackStream, CDNTrackStream::Tr...
-#include "CSpotContext.h"          // for Context::ConfigState, Context (ptr...
-#include "Logger.h"                // for CSPOT_LOG
-#include "MercurySession.h"        // for MercurySession, MercurySession::Da...
-#include "NanoPBHelper.h"          // for pbArrayToVector, pbDecode
-#include "Packet.h"                // for cspot
-#include "TrackReference.h"        // for TrackReference, TrackReference::Type
-#include "Utils.h"                 // for bytesToHexString, string_format
-#include "WrappedSemaphore.h"      // for WrappedSemaphore
-#include "pb_decode.h"             // for pb_release
-#include "protobuf/metadata.pb.h"  // for Track, _Track, AudioFile, Episode
-
-using namespace cspot;
-
-TrackProvider::TrackProvider(std::shared_ptr<cspot::Context> ctx) {
-  this->accessKeyFetcher = std::make_shared<cspot::AccessKeyFetcher>(ctx);
-  this->ctx = ctx;
-  this->cdnStream =
-      std::make_unique<cspot::CDNTrackStream>(this->accessKeyFetcher);
-
-  this->trackInfo = {};
-}
-
-TrackProvider::~TrackProvider() {
-  pb_release(Track_fields, &trackInfo);
-  pb_release(Episode_fields, &trackInfo);
-}
-
-std::shared_ptr<cspot::CDNTrackStream> TrackProvider::loadFromTrackRef(
-    TrackReference& trackRef) {
-  auto track = std::make_shared<cspot::CDNTrackStream>(this->accessKeyFetcher);
-  this->currentTrackReference = track;
-  this->trackIdInfo = trackRef;
-
-  queryMetadata();
-  return track;
-}
-
-void TrackProvider::queryMetadata() {
-  std::string requestUrl = string_format(
-      "hm://metadata/3/%s/%s",
-      trackIdInfo.type == TrackReference::Type::TRACK ? "track" : "episode",
-      bytesToHexString(trackIdInfo.gid).c_str());
-  CSPOT_LOG(debug, "Requesting track metadata from %s", requestUrl.c_str());
-
-  auto responseHandler = [this](MercurySession::Response& res) {
-    this->onMetadataResponse(res);
-  };
-
-  // Execute the request
-  ctx->session->execute(MercurySession::RequestType::GET, requestUrl,
-                        responseHandler);
-}
-
-void TrackProvider::onMetadataResponse(MercurySession::Response& res) {
-  CSPOT_LOG(debug, "Got track metadata response");
-
-  int alternativeCount, filesCount = 0;
-  bool canPlay = false;
-  AudioFile* selectedFiles;
-  std::vector<uint8_t> trackId, fileId;
-
-  if (trackIdInfo.type == TrackReference::Type::TRACK) {
-    pb_release(Track_fields, &trackInfo);
-    assert(res.parts.size() > 0);
-    pbDecode(trackInfo, Track_fields, res.parts[0]);
-    CSPOT_LOG(info, "Track name: %s", trackInfo.name);
-    CSPOT_LOG(info, "Track duration: %d", trackInfo.duration);
-
-    CSPOT_LOG(debug, "trackInfo.restriction.size() = %d",
-              trackInfo.restriction_count);
-
-    if (doRestrictionsApply(trackInfo.restriction,
-                            trackInfo.restriction_count)) {
-      // Go through alternatives
-      for (int x = 0; x < trackInfo.alternative_count; x++) {
-        if (!doRestrictionsApply(trackInfo.alternative[x].restriction,
-                                 trackInfo.alternative[x].restriction_count)) {
-          selectedFiles = trackInfo.alternative[x].file;
-          filesCount = trackInfo.alternative[x].file_count;
-          trackId = pbArrayToVector(trackInfo.alternative[x].gid);
-          break;
-        }
-      }
-    } else {
-      selectedFiles = trackInfo.file;
-      filesCount = trackInfo.file_count;
-      trackId = pbArrayToVector(trackInfo.gid);
-    }
-
-    // Set track's metadata
-    auto trackRef = this->currentTrackReference.lock();
-
-    auto imageId =
-        pbArrayToVector(trackInfo.album.cover_group.image[0].file_id);
-
-    trackRef->trackInfo.trackId = bytesToHexString(trackIdInfo.gid);
-    trackRef->trackInfo.name = std::string(trackInfo.name);
-    trackRef->trackInfo.album = std::string(trackInfo.album.name);
-    trackRef->trackInfo.artist = std::string(trackInfo.artist[0].name);
-    trackRef->trackInfo.imageUrl =
-        "https://i.scdn.co/image/" + bytesToHexString(imageId);
-    trackRef->trackInfo.duration = trackInfo.duration;
-  } else {
-    pb_release(Episode_fields, &episodeInfo);
-    assert(res.parts.size() > 0);
-    pbDecode(episodeInfo, Episode_fields, res.parts[0]);
-
-    CSPOT_LOG(info, "Episode name: %s", episodeInfo.name);
-    CSPOT_LOG(info, "Episode duration: %d", episodeInfo.duration);
-
-    CSPOT_LOG(debug, "episodeInfo.restriction.size() = %d",
-              episodeInfo.restriction_count);
-    if (!doRestrictionsApply(episodeInfo.restriction,
-                             episodeInfo.restriction_count)) {
-      selectedFiles = episodeInfo.file;
-      filesCount = episodeInfo.file_count;
-      trackId = pbArrayToVector(episodeInfo.gid);
-    }
-
-    auto trackRef = this->currentTrackReference.lock();
-
-    auto imageId = pbArrayToVector(episodeInfo.covers->image[0].file_id);
-
-    trackRef->trackInfo.trackId = bytesToHexString(trackIdInfo.gid);
-    trackRef->trackInfo.name = std::string(episodeInfo.name);
-    trackRef->trackInfo.album = "";
-    trackRef->trackInfo.artist = "",
-    trackRef->trackInfo.imageUrl =
-        "https://i.scdn.co/image/" + bytesToHexString(imageId);
-    trackRef->trackInfo.duration = episodeInfo.duration;
-  }
-
-  for (int x = 0; x < filesCount; x++) {
-    CSPOT_LOG(debug, "File format: %d", selectedFiles[x].format);
-    if (selectedFiles[x].format == ctx->config.audioFormat) {
-      fileId = pbArrayToVector(selectedFiles[x].file_id);
-      break;  // If file found stop searching
-    }
-
-    // Fallback to OGG Vorbis 96kbps
-    if (fileId.size() == 0 &&
-        selectedFiles[x].format == AudioFormat_OGG_VORBIS_96) {
-      fileId = pbArrayToVector(selectedFiles[x].file_id);
-    }
-  }
-
-  // No viable files found for playback
-  if (fileId.size() == 0) {
-    CSPOT_LOG(info, "File not available for playback");
-    // no alternatives for song
-    if (!this->currentTrackReference.expired()) {
-      auto trackRef = this->currentTrackReference.lock();
-      trackRef->status = CDNTrackStream::Status::FAILED;
-      trackRef->trackReady->give();
-    }
-    return;
-  }
-
-  this->fetchFile(fileId, trackId);
-}
-
-void TrackProvider::fetchFile(const std::vector<uint8_t>& fileId,
-                              const std::vector<uint8_t>& trackId) {
-  ctx->session->requestAudioKey(
-      trackId, fileId,
-      [this, fileId](bool success, const std::vector<uint8_t>& audioKey) {
-        if (success) {
-          CSPOT_LOG(info, "Got audio key");
-          if (!this->currentTrackReference.expired()) {
-            auto ref = this->currentTrackReference.lock();
-            ref->fetchFile(fileId, audioKey);
-          }
-
-        } else {
-          CSPOT_LOG(error, "Failed to get audio key");
-          if (!this->currentTrackReference.expired()) {
-            auto ref = this->currentTrackReference.lock();
-            ref->fail();
-          }
-        }
-      });
-}
-
-bool countryListContains(char* countryList, char* country) {
-  uint16_t countryList_length = strlen(countryList);
-  for (int x = 0; x < countryList_length; x += 2) {
-    if (countryList[x] == country[0] && countryList[x + 1] == country[1]) {
-      return true;
-    }
-  }
-  return false;
-}
-
-bool TrackProvider::doRestrictionsApply(Restriction* restrictions, int count) {
-  for (int x = 0; x < count; x++) {
-    if (restrictions[x].countries_allowed != nullptr) {
-      return !countryListContains(restrictions[x].countries_allowed,
-                                  (char*)ctx->config.countryCode.c_str());
-    }
-
-    if (restrictions[x].countries_forbidden != nullptr) {
-      return countryListContains(restrictions[x].countries_forbidden,
-                                 (char*)ctx->config.countryCode.c_str());
-    }
-  }
-
-  return false;
-}
-
-bool TrackProvider::canPlayTrack(int altIndex) {
-  if (altIndex < 0) {
-
-  } else {
-    for (int x = 0; x < trackInfo.alternative[altIndex].restriction_count;
-         x++) {
-      if (trackInfo.alternative[altIndex].restriction[x].countries_allowed !=
-          nullptr) {
-        return countryListContains(
-            trackInfo.alternative[altIndex].restriction[x].countries_allowed,
-            (char*)ctx->config.countryCode.c_str());
-      }
-
-      if (trackInfo.alternative[altIndex].restriction[x].countries_forbidden !=
-          nullptr) {
-        return !countryListContains(
-            trackInfo.alternative[altIndex].restriction[x].countries_forbidden,
-            (char*)ctx->config.countryCode.c_str());
-      }
-    }
-  }
-  return true;
-}

+ 603 - 0
components/spotify/cspot/src/TrackQueue.cpp

@@ -0,0 +1,603 @@
+#include "TrackQueue.h"
+#include <pb_decode.h>
+
+#include <algorithm>
+#include <functional>
+#include <memory>
+#include <mutex>
+
+#include "AccessKeyFetcher.h"
+#include "BellTask.h"
+#include "CDNAudioFile.h"
+#include "CSpotContext.h"
+#include "HTTPClient.h"
+#include "Logger.h"
+#include "Utils.h"
+#include "WrappedSemaphore.h"
+#ifdef BELL_ONLY_CJSON
+#include "cJSON.h"
+#else
+#include "nlohmann/json.hpp"      // for basic_json<>::object_t, basic_json
+#include "nlohmann/json_fwd.hpp"  // for json
+#endif
+#include "protobuf/metadata.pb.h"
+
+using namespace cspot;
+namespace TrackDataUtils {
+bool countryListContains(char* countryList, const char* country) {
+  uint16_t countryList_length = strlen(countryList);
+  for (int x = 0; x < countryList_length; x += 2) {
+    if (countryList[x] == country[0] && countryList[x + 1] == country[1]) {
+      return true;
+    }
+  }
+  return false;
+}
+
+bool doRestrictionsApply(Restriction* restrictions, int count,
+                         const char* country) {
+  for (int x = 0; x < count; x++) {
+    if (restrictions[x].countries_allowed != nullptr) {
+      return !countryListContains(restrictions[x].countries_allowed, country);
+    }
+
+    if (restrictions[x].countries_forbidden != nullptr) {
+      return countryListContains(restrictions[x].countries_forbidden, country);
+    }
+  }
+
+  return false;
+}
+
+bool canPlayTrack(Track& trackInfo, int altIndex, const char* country) {
+  if (altIndex < 0) {
+
+  } else {
+    for (int x = 0; x < trackInfo.alternative[altIndex].restriction_count;
+         x++) {
+      if (trackInfo.alternative[altIndex].restriction[x].countries_allowed !=
+          nullptr) {
+        return countryListContains(
+            trackInfo.alternative[altIndex].restriction[x].countries_allowed,
+            country);
+      }
+
+      if (trackInfo.alternative[altIndex].restriction[x].countries_forbidden !=
+          nullptr) {
+        return !countryListContains(
+            trackInfo.alternative[altIndex].restriction[x].countries_forbidden,
+            country);
+      }
+    }
+  }
+  return true;
+}
+}  // namespace TrackDataUtils
+
+void TrackInfo::loadPbTrack(Track* pbTrack, const std::vector<uint8_t>& gid) {
+  // Generate ID based on GID
+  trackId = bytesToHexString(gid);
+
+  name = std::string(pbTrack->name);
+
+  if (pbTrack->artist_count > 0) {
+    // Handle artist data
+    artist = std::string(pbTrack->artist[0].name);
+  }
+
+  if (pbTrack->has_album) {
+    // Handle album data
+    album = std::string(pbTrack->album.name);
+
+    if (pbTrack->album.has_cover_group &&
+        pbTrack->album.cover_group.image_count > 0) {
+      auto imageId =
+          pbArrayToVector(pbTrack->album.cover_group.image[0].file_id);
+      imageUrl = "https://i.scdn.co/image/" + bytesToHexString(imageId);
+    }
+  }
+
+  duration = pbTrack->duration;
+}
+
+void TrackInfo::loadPbEpisode(Episode* pbEpisode,
+                              const std::vector<uint8_t>& gid) {
+  // Generate ID based on GID
+  trackId = bytesToHexString(gid);
+
+  name = std::string(pbEpisode->name);
+
+  if (pbEpisode->covers->image_count > 0) {
+    // Handle episode info
+    auto imageId = pbArrayToVector(pbEpisode->covers->image[0].file_id);
+    imageUrl = "https://i.scdn.co/image/" + bytesToHexString(imageId);
+  }
+
+  duration = pbEpisode->duration;
+}
+
+QueuedTrack::QueuedTrack(TrackReference& ref,
+                         std::shared_ptr<cspot::Context> ctx,
+                         uint32_t requestedPosition)
+    : requestedPosition(requestedPosition), ctx(ctx) {
+  this->ref = ref;
+
+  loadedSemaphore = std::make_shared<bell::WrappedSemaphore>();
+  state = State::QUEUED;
+}
+
+QueuedTrack::~QueuedTrack() {
+  state = State::FAILED;
+  loadedSemaphore->give();
+
+  if (pendingMercuryRequest != 0) {
+    ctx->session->unregister(pendingMercuryRequest);
+  }
+
+  if (pendingAudioKeyRequest != 0) {
+    ctx->session->unregisterAudioKey(pendingAudioKeyRequest);
+  }
+}
+
+std::shared_ptr<cspot::CDNAudioFile> QueuedTrack::getAudioFile() {
+  if (state != State::READY) {
+    return nullptr;
+  }
+
+  return std::make_shared<cspot::CDNAudioFile>(cdnUrl, audioKey);
+}
+
+void QueuedTrack::stepParseMetadata(Track* pbTrack, Episode* pbEpisode) {
+  int alternativeCount, filesCount = 0;
+  bool canPlay = false;
+  AudioFile* selectedFiles = nullptr;
+
+  const char* countryCode = ctx->config.countryCode.c_str();
+
+  if (ref.type == TrackReference::Type::TRACK) {
+    CSPOT_LOG(info, "Track name: %s", pbTrack->name);
+    CSPOT_LOG(info, "Track duration: %d", pbTrack->duration);
+
+    CSPOT_LOG(debug, "trackInfo.restriction.size() = %d",
+              pbTrack->restriction_count);
+
+    // Check if we can play the track, if not, try alternatives
+    if (TrackDataUtils::doRestrictionsApply(
+            pbTrack->restriction, pbTrack->restriction_count, countryCode)) {
+      // Go through alternatives
+      for (int x = 0; x < pbTrack->alternative_count; x++) {
+        if (!TrackDataUtils::doRestrictionsApply(
+                pbTrack->alternative[x].restriction,
+                pbTrack->alternative[x].restriction_count, countryCode)) {
+          selectedFiles = pbTrack->alternative[x].file;
+          filesCount = pbTrack->alternative[x].file_count;
+          trackId = pbArrayToVector(pbTrack->alternative[x].gid);
+          break;
+        }
+      }
+    } else {
+      // We can play the track
+      selectedFiles = pbTrack->file;
+      filesCount = pbTrack->file_count;
+      trackId = pbArrayToVector(pbTrack->gid);
+    }
+
+    if (trackId.size() > 0) {
+      // Load track information
+      trackInfo.loadPbTrack(pbTrack, trackId);
+    }
+  } else {
+    // Handle episodes
+    CSPOT_LOG(info, "Episode name: %s", pbEpisode->name);
+    CSPOT_LOG(info, "Episode duration: %d", pbEpisode->duration);
+
+    CSPOT_LOG(debug, "episodeInfo.restriction.size() = %d",
+              pbEpisode->restriction_count);
+
+    // Check if we can play the episode
+    if (!TrackDataUtils::doRestrictionsApply(pbEpisode->restriction,
+                                             pbEpisode->restriction_count,
+                                             countryCode)) {
+      selectedFiles = pbEpisode->file;
+      filesCount = pbEpisode->file_count;
+      trackId = pbArrayToVector(pbEpisode->gid);
+
+      // Load track information
+      trackInfo.loadPbEpisode(pbEpisode, trackId);
+    }
+  }
+
+  // Find playable file
+  for (int x = 0; x < filesCount; x++) {
+    CSPOT_LOG(debug, "File format: %d", selectedFiles[x].format);
+    if (selectedFiles[x].format == ctx->config.audioFormat) {
+      fileId = pbArrayToVector(selectedFiles[x].file_id);
+      break;  // If file found stop searching
+    }
+
+    // Fallback to OGG Vorbis 96kbps
+    if (fileId.size() == 0 &&
+        selectedFiles[x].format == AudioFormat_OGG_VORBIS_96) {
+      fileId = pbArrayToVector(selectedFiles[x].file_id);
+    }
+  }
+
+  // No viable files found for playback
+  if (fileId.size() == 0) {
+    CSPOT_LOG(info, "File not available for playback");
+
+    // no alternatives for song
+    state = State::FAILED;
+    loadedSemaphore->give();
+    return;
+  }
+
+  // Assign track identifier
+  identifier = bytesToHexString(fileId);
+
+  state = State::KEY_REQUIRED;
+}
+
+void QueuedTrack::stepLoadAudioFile(
+    std::mutex& trackListMutex,
+    std::shared_ptr<bell::WrappedSemaphore> updateSemaphore) {
+  // Request audio key
+  this->pendingAudioKeyRequest = ctx->session->requestAudioKey(
+      trackId, fileId,
+      [this, &trackListMutex, updateSemaphore](
+          bool success, const std::vector<uint8_t>& audioKey) {
+        std::scoped_lock lock(trackListMutex);
+
+        if (success) {
+          CSPOT_LOG(info, "Got audio key");
+          this->audioKey =
+              std::vector<uint8_t>(audioKey.begin() + 4, audioKey.end());
+
+          state = State::CDN_REQUIRED;
+        } else {
+          CSPOT_LOG(error, "Failed to get audio key");
+          state = State::FAILED;
+          loadedSemaphore->give();
+        }
+        updateSemaphore->give();
+      });
+
+  state = State::PENDING_KEY;
+}
+
+void QueuedTrack::stepLoadCDNUrl(const std::string& accessKey) {
+  if (accessKey.size() == 0) {
+    // Wait for access key
+    return;
+  }
+
+  // Request CDN URL
+  CSPOT_LOG(info, "Received access key, fetching CDN URL...");
+
+  try {
+
+    std::string requestUrl = string_format(
+        "https://api.spotify.com/v1/storage-resolve/files/audio/interactive/"
+        "%s?alt=json&product=9",
+        bytesToHexString(fileId).c_str());
+
+    auto req = bell::HTTPClient::get(
+        requestUrl, {bell::HTTPClient::ValueHeader(
+                        {"Authorization", "Bearer " + accessKey})});
+
+    // Wait for response
+    std::string_view result = req->body();
+
+#ifdef BELL_ONLY_CJSON
+    cJSON* jsonResult = cJSON_Parse(result.data());
+    cdnUrl = cJSON_GetArrayItem(cJSON_GetObjectItem(jsonResult, "cdnurl"), 0)
+                 ->valuestring;
+    cJSON_Delete(jsonResult);
+#else
+    auto jsonResult = nlohmann::json::parse(result);
+    cdnUrl = jsonResult["cdnurl"][0];
+#endif
+
+    CSPOT_LOG(info, "Received CDN URL, %s", cdnUrl.c_str());
+    state = State::READY;
+    loadedSemaphore->give();
+  } catch (...) {
+    CSPOT_LOG(error, "Cannot fetch CDN URL");
+    state = State::FAILED;
+    loadedSemaphore->give();
+  }
+}
+
+void QueuedTrack::expire() {
+  if (state != State::QUEUED) {
+    state = State::FAILED;
+    loadedSemaphore->give();
+  }
+}
+
+void QueuedTrack::stepLoadMetadata(
+    Track* pbTrack, Episode* pbEpisode, std::mutex& trackListMutex,
+    std::shared_ptr<bell::WrappedSemaphore> updateSemaphore) {
+
+  // Prepare request ID
+  std::string requestUrl = string_format(
+      "hm://metadata/3/%s/%s",
+      ref.type == TrackReference::Type::TRACK ? "track" : "episode",
+      bytesToHexString(ref.gid).c_str());
+
+  auto responseHandler = [this, pbTrack, pbEpisode, &trackListMutex,
+                          updateSemaphore](MercurySession::Response& res) {
+    std::scoped_lock lock(trackListMutex);
+
+    if (res.parts.size() == 0) {
+      // Invalid metadata, cannot proceed
+      state = State::FAILED;
+      updateSemaphore->give();
+      loadedSemaphore->give();
+      return;
+    }
+
+    // Parse the metadata
+    if (ref.type == TrackReference::Type::TRACK) {
+      pb_release(Track_fields, pbTrack);
+      pbDecode(*pbTrack, Track_fields, res.parts[0]);
+    } else {
+      pb_release(Episode_fields, pbEpisode);
+      pbDecode(*pbEpisode, Episode_fields, res.parts[0]);
+    }
+
+    // Parse received metadata
+    stepParseMetadata(pbTrack, pbEpisode);
+
+    updateSemaphore->give();
+  };
+  // Execute the request
+  pendingMercuryRequest = ctx->session->execute(
+      MercurySession::RequestType::GET, requestUrl, responseHandler);
+
+  // Set the state to pending
+  state = State::PENDING_META;
+}
+
+TrackQueue::TrackQueue(std::shared_ptr<cspot::Context> ctx,
+                       std::shared_ptr<cspot::PlaybackState> state)
+    : bell::Task("CSpotTrackQueue", 1024 * 32, 2, 1),
+      playbackState(state),
+      ctx(ctx) {
+  accessKeyFetcher = std::make_shared<cspot::AccessKeyFetcher>(ctx);
+  processSemaphore = std::make_shared<bell::WrappedSemaphore>();
+  playableSemaphore = std::make_shared<bell::WrappedSemaphore>();
+
+  // Assign encode callback to track list
+  playbackState->innerFrame.state.track.funcs.encode =
+      &TrackReference::pbEncodeTrackList;
+  playbackState->innerFrame.state.track.arg = &currentTracks;
+  pbTrack = Track_init_zero;
+  pbEpisode = Episode_init_zero;
+
+  // Start the task
+  startTask();
+};
+
+TrackQueue::~TrackQueue() {
+  stopTask();
+
+  std::scoped_lock lock(tracksMutex);
+
+  pb_release(Track_fields, &pbTrack);
+  pb_release(Episode_fields, &pbEpisode);
+}
+
+TrackInfo TrackQueue::getTrackInfo(std::string_view identifier) {
+  for (auto& track : preloadedTracks) {
+    if (track->identifier == identifier)
+      return track->trackInfo;
+  }
+  return TrackInfo{};
+}
+
+void TrackQueue::runTask() {
+  isRunning = true;
+
+  std::scoped_lock lock(runningMutex);
+
+  std::deque<std::shared_ptr<QueuedTrack>> trackQueue;
+
+  while (isRunning) {
+    processSemaphore->twait(100);
+
+    // Make sure we have the newest access key
+    accessKey = accessKeyFetcher->getAccessKey();
+
+    int loadedIndex = currentTracksIndex;
+
+    // No tracks loaded yet
+    if (loadedIndex < 0) {
+      continue;
+    } else {
+      std::scoped_lock lock(tracksMutex);
+
+      trackQueue = preloadedTracks;
+    }
+
+    for (auto& track : trackQueue) {
+      if (track) {
+        this->processTrack(track);
+      }
+    }
+  }
+}
+
+void TrackQueue::stopTask() {
+  if (isRunning) {
+    isRunning = false;
+    processSemaphore->give();
+    std::scoped_lock lock(runningMutex);
+  }
+}
+
+std::shared_ptr<QueuedTrack> TrackQueue::consumeTrack(
+    std::shared_ptr<QueuedTrack> prevTrack, int& offset) {
+  std::scoped_lock lock(tracksMutex);
+
+  if (currentTracksIndex == -1 || currentTracksIndex >= currentTracks.size()) {
+    return nullptr;
+  }
+
+  // No previous track, return head
+  if (prevTrack == nullptr) {
+    offset = 0;
+
+    return preloadedTracks[0];
+  }
+
+  // if (currentTracksIndex + preloadedTracks.size() >= currentTracks.size()) {
+  //   offset = -1;
+
+  //   // Last track in queue
+  //   return nullptr;
+  // }
+
+  auto prevTrackIter =
+      std::find(preloadedTracks.begin(), preloadedTracks.end(), prevTrack);
+
+  if (prevTrackIter != preloadedTracks.end()) {
+    // Get offset of next track
+    offset = prevTrackIter - preloadedTracks.begin() + 1;
+  } else {
+    offset = 0;
+  }
+
+  if (offset >= preloadedTracks.size()) {
+    // Last track in preloaded queue
+    return nullptr;
+  }
+
+  // Return the current track
+  return preloadedTracks[offset];
+}
+
+void TrackQueue::processTrack(std::shared_ptr<QueuedTrack> track) {
+  switch (track->state) {
+    case QueuedTrack::State::QUEUED:
+      track->stepLoadMetadata(&pbTrack, &pbEpisode, tracksMutex,
+                              processSemaphore);
+      break;
+    case QueuedTrack::State::KEY_REQUIRED:
+      track->stepLoadAudioFile(tracksMutex, processSemaphore);
+      break;
+    case QueuedTrack::State::CDN_REQUIRED:
+      track->stepLoadCDNUrl(accessKey);
+
+      if (track->state == QueuedTrack::State::READY) {
+        if (preloadedTracks.size() < MAX_TRACKS_PRELOAD) {
+          // Queue a new track to preload
+          queueNextTrack(preloadedTracks.size());
+        }
+      }
+      break;
+    default:
+      // Do not perform any action
+      break;
+  }
+}
+
+bool TrackQueue::queueNextTrack(int offset, uint32_t positionMs) {
+  const int requestedRefIndex = offset + currentTracksIndex;
+  if (requestedRefIndex < 0 || requestedRefIndex >= currentTracks.size()) {
+    return false;
+  }
+
+  if (offset < 0) {
+    preloadedTracks.push_front(std::make_shared<QueuedTrack>(
+        currentTracks[requestedRefIndex], ctx, positionMs));
+  } else {
+    preloadedTracks.push_back(std::make_shared<QueuedTrack>(
+        currentTracks[requestedRefIndex], ctx, positionMs));
+  }
+
+  return true;
+}
+
+bool TrackQueue::skipTrack(SkipDirection dir, bool expectNotify) {
+  bool canSkipNext = currentTracks.size() > currentTracksIndex + 1;
+  bool canSkipPrev = currentTracksIndex > 0;
+
+  if ((dir == SkipDirection::NEXT && canSkipNext) ||
+      (dir == SkipDirection::PREV && canSkipPrev)) {
+    std::scoped_lock lock(tracksMutex);
+    if (dir == SkipDirection::NEXT) {
+      preloadedTracks.pop_front();
+
+      if (!queueNextTrack(preloadedTracks.size() + 1)) {
+        CSPOT_LOG(info, "Failed to queue next track");
+      }
+
+      currentTracksIndex++;
+    } else {
+      queueNextTrack(-1);
+
+      if (preloadedTracks.size() > MAX_TRACKS_PRELOAD) {
+        preloadedTracks.pop_back();
+      }
+
+      currentTracksIndex--;
+    }
+
+    // Update frame data
+    playbackState->innerFrame.state.playing_track_index = currentTracksIndex;
+
+    if (expectNotify) {
+      // Reset position to zero
+      notifyPending = true;
+    }
+
+    return true;
+  }
+
+  return false;
+}
+
+bool TrackQueue::hasTracks() {
+  std::scoped_lock lock(tracksMutex);
+
+  return currentTracks.size() > 0;
+}
+
+bool TrackQueue::isFinished() {
+  std::scoped_lock lock(tracksMutex);
+  return currentTracksIndex >= currentTracks.size() - 1;
+}
+
+void TrackQueue::updateTracks(uint32_t requestedPosition, bool initial) {
+  std::scoped_lock lock(tracksMutex);
+
+  if (initial) {
+    // Clear preloaded tracks
+    preloadedTracks.clear();
+
+    // Copy requested track list
+    currentTracks = playbackState->remoteTracks;
+
+    currentTracksIndex = playbackState->innerFrame.state.playing_track_index;
+
+    if (currentTracksIndex < currentTracks.size()) {
+      // Push a song on the preloaded queue
+      queueNextTrack(0, requestedPosition);
+    }
+
+    // We already updated track meta, mark it
+    notifyPending = true;
+
+    playableSemaphore->give();
+  } else {
+    // Clear preloaded tracks
+    preloadedTracks.clear();
+
+    // Copy requested track list
+    currentTracks = playbackState->remoteTracks;
+
+    // Push a song on the preloaded queue
+    queueNextTrack(0, requestedPosition);
+  }
+}

+ 156 - 0
components/spotify/cspot/src/TrackReference.cpp

@@ -0,0 +1,156 @@
+#include "TrackReference.h"
+
+#include "NanoPBExtensions.h"
+#include "Utils.h"
+#include "protobuf/spirc.pb.h"
+
+using namespace cspot;
+
+static constexpr auto base62Alphabet =
+    "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+TrackReference::TrackReference() : type(Type::TRACK) {}
+
+void TrackReference::decodeURI() {
+  if (gid.size() == 0) {
+    // Episode GID is being fetched via base62 encoded URI
+    auto idString = uri.substr(uri.find_last_of(":") + 1, uri.size());
+    gid = {0};
+
+    std::string_view alphabet(base62Alphabet);
+    for (int x = 0; x < idString.size(); x++) {
+      size_t d = alphabet.find(idString[x]);
+      gid = bigNumMultiply(gid, 62);
+      gid = bigNumAdd(gid, d);
+    }
+
+#if __cplusplus >= 202002L
+    if (uri.starts_with("episode")) {
+#else
+    if (uri.find("episode") == 0) {    
+#endif    
+      type = Type::EPISODE;
+    }
+  }
+}
+
+bool TrackReference::operator==(const TrackReference& other) const {
+  return other.gid == gid && other.uri == uri;
+}
+
+bool TrackReference::pbEncodeTrackList(pb_ostream_t* stream,
+                                       const pb_field_t* field,
+                                       void* const* arg) {
+  auto trackQueue = *static_cast<std::vector<TrackReference>*>(*arg);
+  static TrackRef msg = TrackRef_init_zero;
+
+  // Prepare nanopb callbacks
+  msg.context.funcs.encode = &bell::nanopb::encodeString;
+  msg.uri.funcs.encode = &bell::nanopb::encodeString;
+  msg.gid.funcs.encode = &bell::nanopb::encodeVector;
+  msg.queued.funcs.encode = &bell::nanopb::encodeBoolean;
+
+  for (auto trackRef : trackQueue) {
+    if (!pb_encode_tag_for_field(stream, field)) {
+      return false;
+    }
+
+    msg.gid.arg = &trackRef.gid;
+    msg.uri.arg = &trackRef.uri;
+    msg.context.arg = &trackRef.context;
+    msg.queued.arg = &trackRef.queued;
+
+    if (!pb_encode_submessage(stream, TrackRef_fields, &msg)) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+bool TrackReference::pbDecodeTrackList(pb_istream_t* stream,
+                                       const pb_field_t* field, void** arg) {
+  auto trackQueue = static_cast<std::vector<TrackReference>*>(*arg);
+
+  // Push a new reference
+  trackQueue->push_back(TrackReference());
+
+  auto& track = trackQueue->back();
+
+  bool eof = false;
+  pb_wire_type_t wire_type;
+  pb_istream_t substream;
+  uint32_t tag;
+
+  while (!eof) {
+    if (!pb_decode_tag(stream, &wire_type, &tag, &eof)) {
+      // Decoding failed and not eof
+      if (!eof) {
+        return false;
+      }
+      // EOF
+    } else {
+      switch (tag) {
+        case TrackRef_uri_tag:
+        case TrackRef_context_tag:
+        case TrackRef_gid_tag: {
+          // Make substream
+          if (!pb_make_string_substream(stream, &substream)) {
+
+            return false;
+          }
+
+          uint8_t* destBuffer = nullptr;
+
+          // Handle GID
+          if (tag == TrackRef_gid_tag) {
+            track.gid.resize(substream.bytes_left);
+            destBuffer = &track.gid[0];
+          } else if (tag == TrackRef_context_tag) {
+            track.context.resize(substream.bytes_left);
+
+            destBuffer = reinterpret_cast<uint8_t*>(&track.context[0]);
+          } else if (tag == TrackRef_uri_tag) {
+            track.uri.resize(substream.bytes_left);
+
+            destBuffer = reinterpret_cast<uint8_t*>(&track.uri[0]);
+          }
+
+          if (!pb_read(&substream, destBuffer, substream.bytes_left)) {
+            return false;
+          }
+
+          // Close substream
+          if (!pb_close_string_substream(stream, &substream)) {
+            return false;
+          }
+
+          break;
+        }
+        case TrackRef_queued_tag: {
+          uint32_t queuedValue;
+
+          // Decode boolean
+          if (!pb_decode_varint32(stream, &queuedValue)) {
+            return false;
+          }
+
+          // Cast down to bool
+          track.queued = (bool)queuedValue;
+
+          break;
+        }
+        default:
+          // Field not known, skip
+          pb_skip_field(stream, wire_type);
+
+          break;
+      }
+    }
+  }
+
+  // Fill in GID when only URI is provided
+  track.decodeURI();
+
+  return true;
+}