diff --git a/cspot/bell b/cspot/bell index d755889..2bb0407 160000 --- a/cspot/bell +++ b/cspot/bell @@ -1 +1 @@ -Subproject commit d755889c67e08a25e72cfdb1fd2dae1c2afb4b9a +Subproject commit 2bb040741d923291d4908e36c15e5cf6bc1a8b7b diff --git a/cspot/include/CDNAudioFile.h b/cspot/include/CDNAudioFile.h index 19f3001..1b93fb0 100644 --- a/cspot/include/CDNAudioFile.h +++ b/cspot/include/CDNAudioFile.h @@ -26,6 +26,16 @@ class CDNAudioFile { * @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); #else /** * @brief Opens connection to the provided cdn url, and fetches track header. @@ -34,8 +44,8 @@ class CDNAudioFile { * * @returns char* where to read data from */ - uint8_t* openStream(size_t&); -#endif + uint8_t* openStream(ssize_t&); + /** * @brief Read and decrypt part of the cdn stream * @@ -46,6 +56,7 @@ class CDNAudioFile { */ long readBytes(uint8_t* dst, size_t bytes); +#endif /** * @brief Returns current position in CDN stream */ diff --git a/cspot/include/DeviceStateHandler.h b/cspot/include/DeviceStateHandler.h index e5cc124..598e264 100644 --- a/cspot/include/DeviceStateHandler.h +++ b/cspot/include/DeviceStateHandler.h @@ -55,8 +55,9 @@ class DeviceStateHandler { }; typedef std::function StateCallback; + std::function onClose; - DeviceStateHandler(std::shared_ptr); + DeviceStateHandler(std::shared_ptr, std::function); ~DeviceStateHandler(); void disconnect(); @@ -101,10 +102,11 @@ class DeviceStateHandler { void parseCommand(std::vector& data); void skip(CommandType dir, bool notify); - void unreference(char* string) { - if (string != NULL) - free(string); - string = NULL; + void unreference(char** string) { + if (*string != NULL) { + free(*string); + *string = NULL; + } } static void reloadTrackList(void*); diff --git a/cspot/include/TrackPlayer.h b/cspot/include/TrackPlayer.h index 6223f41..8a63d65 100644 --- a/cspot/include/TrackPlayer.h +++ b/cspot/include/TrackPlayer.h @@ -44,7 +44,7 @@ class TrackPlayer : bell::Task { TrackPlayer(std::shared_ptr ctx, std::shared_ptr trackQueue, TrackEndedCallback onTrackEnd, - TrackChangedCallback onTrackChanged); + TrackChangedCallback onTrackChanged, bool* track_repeat); ~TrackPlayer(); void loadTrackFromRef(TrackReference& ref, size_t playbackMs, @@ -54,7 +54,7 @@ class TrackPlayer : bell::Task { SeekableCallback spaces_available = nullptr); // CDNTrackStream::TrackInfo getCurrentTrackInfo(); - void seekMs(size_t ms); + void seekMs(size_t ms, bool loading = true); void resetState(bool paused = false); #ifndef CONFIG_BELL_NOCODEC @@ -85,6 +85,7 @@ class TrackPlayer : bell::Task { // Playback control std::atomic currentSongPlaying; + bool* repeating_track_; std::mutex playbackMutex; std::mutex dataOutMutex; @@ -95,11 +96,7 @@ class TrackPlayer : bell::Task { int currentSection; #endif -#ifndef CONFIG_BELL_NOCODEC - std::vector pcmBuffer = std::vector(1024); -#else std::vector pcmBuffer = std::vector(1024); -#endif bool autoStart = false; diff --git a/cspot/include/TrackQueue.h b/cspot/include/TrackQueue.h index 5ee6457..f021829 100644 --- a/cspot/include/TrackQueue.h +++ b/cspot/include/TrackQueue.h @@ -58,7 +58,9 @@ class QueuedTrack { std::string identifier; uint32_t playingTrackIndex; uint32_t requestedPosition; + AudioFormat audioFormat; bool loading = false; + uint8_t retries = 0; // PB data Track pbTrack = Track_init_zero; @@ -84,8 +86,6 @@ class QueuedTrack { void stepLoadCDNUrl(const std::string& accessKey); - void expire(); - private: std::shared_ptr ctx; std::shared_ptr playableSemaphore; @@ -126,10 +126,10 @@ class TrackQueue : public bell::Task { std::shared_ptr ctx; std::shared_ptr processSemaphore; - bool isRunning = false; + std::atomic isRunning = false; std::string accessKey; - void processTrack(std::shared_ptr track); + bool processTrack(std::shared_ptr track); }; } // namespace cspot \ No newline at end of file diff --git a/cspot/protobuf/connect.options b/cspot/protobuf/connect.options index 63c63f8..3ef99a4 100644 --- a/cspot/protobuf/connect.options +++ b/cspot/protobuf/connect.options @@ -1,79 +1,99 @@ -DeviceInfo.name type:FT_POINTER -DeviceInfo.device_id type:FT_POINTER -DeviceInfo.client_id type:FT_POINTER -DeviceInfo.spirc_version type:FT_POINTER -//DeviceInfo.supported_types type:FT_POINTER -DeviceInfo.device_software_version type:FT_POINTER -DeviceInfo.capabilities max_count:27, fixed_count:false -DeviceInfo.model type:FT_POINTER -DeviceInfo.brand type:FT_POINTER -PutStateRequest.callback_url type:FT_POINTER -PutStateRequest.last_command_sent_by_device_id type:FT_POINTER -Capabilities.stringValue max_count:50, max_size: 50 -Capabilities.intValue max_count:50 -Capabilities.supported_types type:FT_POINTER -Capabilities.Supported_typesEntry type:FT_POINTER -Cluster.device max_count:10 fixed_count:false -Cluster.DeviceEntry.key type:FT_POINTER -Cluster.active_device_id type:FT_POINTER -ProvidedTrack.uri type:FT_POINTER -ProvidedTrack.uid type:FT_POINTER -ProvidedTrack.metadata max_count:30, fixed_count:false -ProvidedTrack.MetadataEntry type:FT_POINTER -ProvidedTrack.removed type:FT_POINTER -ProvidedTrack.blocked type:FT_POINTER -ProvidedTrack.provider type:FT_POINTER -ProvidedTrack.album_uri type:FT_POINTER -ProvidedTrack.disallow_reasons type:FT_POINTER -ProvidedTrack.artist_uri type:FT_POINTER -ProvidedTrack.disallow_undecided type:FT_POINTER -PlayOrigin.feature_identifier type:FT_POINTER -PlayOrigin.feature_version type:FT_POINTER -PlayOrigin.view_uri type:FT_POINTER -PlayOrigin.external_referrer type:FT_POINTER -PlayOrigin.referrer_identifier type:FT_POINTER -PlayOrigin.device_identifier type:FT_POINTER -PlayOrigin.feature_classes type:FT_POINTER -PlayerState.playback_id type:FT_POINTER +DeviceInfo.name type + : FT_POINTER DeviceInfo.device_id type + : FT_POINTER DeviceInfo.client_id type : FT_POINTER DeviceInfo + .spirc_version type + : FT_POINTER + //DeviceInfo.supported_types type:FT_POINTER + DeviceInfo.device_software_version type + : FT_POINTER DeviceInfo.model type + : FT_POINTER DeviceInfo.brand type + : FT_POINTER PutStateRequest.callback_url type + : FT_POINTER PutStateRequest.last_command_sent_by_device_id type + : FT_POINTER Capabilities.supported_types type + : FT_POINTER Capabilities.supported_typesEntry type + : FT_POINTER Cluster.device max_count : 10 fixed_count + : false Cluster.DeviceEntry.key type + : FT_POINTER Cluster.active_device_id type + : FT_POINTER ProvidedTrack.uri type + : FT_POINTER ProvidedTrack.uid type + : FT_POINTER ProvidedTrack.metadata max_count : 30, + fixed_count + : false ProvidedTrack.MetadataEntry type + : FT_POINTER ProvidedTrack.removed type + : FT_POINTER ProvidedTrack.blocked type + : FT_POINTER ProvidedTrack.provider type + : FT_POINTER ProvidedTrack.album_uri type + : FT_POINTER ProvidedTrack.disallow_reasons type + : FT_POINTER ProvidedTrack.artist_uri type + : FT_POINTER ProvidedTrack.disallow_undecided type + : FT_POINTER PlayOrigin.feature_identifier type + : FT_POINTER PlayOrigin.feature_version type + : FT_POINTER PlayOrigin.view_uri type + : FT_POINTER PlayOrigin.external_referrer type + : FT_POINTER PlayOrigin.referrer_identifier type + : FT_POINTER PlayOrigin.device_identifier type + : FT_POINTER PlayOrigin.feature_classes type : FT_POINTER PlayerState + .playback_id type + : FT_POINTER -ContextPlayerOptions.context_enhancement max_count:2, fixed_count:false -ContextPlayerOptions.ContextEnhancementEntry type:FT_POINTER + ContextPlayerOptions.context_enhancement max_count : 2, + fixed_count : false ContextPlayerOptions + .ContextEnhancementEntry type : FT_POINTER -Restrictions type:FT_POINTER + Restrictions type + : FT_POINTER -PlayerState.context_uri type:FT_POINTER -PlayerState.context_url type:FT_POINTER -PlayerState.context_metadata type:FT_POINTER -PlayerState.ContextMetadataEntry type:FT_POINTER -PlayerState.page_metadata type:FT_POINTER -PlayerState.PageMetadataEntry type:FT_POINTER -PlayerState.session_id type:FT_POINTER -PlayerState.queue_revision type:FT_POINTER -PlayerState.entity_uri type:FT_POINTER -PlayerState.playback_provider type:FT_POINTER -PlayerState.random_message type:FT_POINTER + PlayerState.context_uri type + : FT_POINTER PlayerState.context_url type + : FT_POINTER PlayerState.context_metadata type + : FT_POINTER PlayerState.ContextMetadataEntry type + : FT_POINTER PlayerState.page_metadata type + : FT_POINTER PlayerState.PageMetadataEntry type + : FT_POINTER PlayerState.session_id type + : FT_POINTER PlayerState.queue_revision type + : FT_POINTER PlayerState.entity_uri type + : FT_POINTER PlayerState.playback_provider type : FT_POINTER PlayerState + .random_message type + : FT_POINTER -Restrictions.disallow_pausing_reasons type:FT_POINTER -Restrictions.disallow_resuming_reasons type:FT_POINTER -Restrictions.disallow_seeking_reasons type:FT_POINTER -Restrictions.disallow_peeking_prev_reasons type:FT_POINTER -Restrictions.disallow_peeking_next_reasons type:FT_POINTER -Restrictions.disallow_skipping_prev_reasons type:FT_POINTER -Restrictions.disallow_skipping_next_reasons type:FT_POINTER -Restrictions.disallow_toggling_repeat_context_reasons type:FT_POINTER -Restrictions.disallow_toggling_repeat_track_reasons type:FT_POINTER -Restrictions.disallow_toggling_shuffle_reasons type:FT_POINTER -Restrictions.disallow_set_queue_reasons type:FT_POINTER -Restrictions.disallow_interrupting_playback_reasons type:FT_POINTER -Restrictions.disallow_transferring_playback_reasons type:FT_POINTER -Restrictions.disallow_remote_control_reasons type:FT_POINTER -Restrictions.disallow_inserting_into_next_tracks_reasons type:FT_POINTER -Restrictions.disallow_inserting_into_context_tracks_reasons type:FT_POINTER -Restrictions.disallow_reordering_in_next_tracks_reasons type:FT_POINTER -Restrictions.disallow_reordering_in_context_tracks_reasons type:FT_POINTER -Restrictions.disallow_removing_from_next_tracks_reasons type:FT_POINTER -Restrictions.disallow_removing_from_context_tracks_reasons type:FT_POINTER -Restrictions.disallow_updating_context_reasons type:FT_POINTER -Restrictions.disallow_playing_reasons type:FT_POINTER -Restrictions.disallow_stopping_reasons type:FT_POINTER -Restrictions.disallow_loading_context_reasons type:FT_POINTER \ No newline at end of file + Restrictions.disallow_pausing_reasons type : FT_POINTER Restrictions + .disallow_resuming_reasons type : FT_POINTER Restrictions + .disallow_seeking_reasons type : FT_POINTER Restrictions + .disallow_peeking_prev_reasons type + : FT_POINTER Restrictions + .disallow_peeking_next_reasons type + : FT_POINTER Restrictions + .disallow_skipping_prev_reasons type + : FT_POINTER Restrictions + .disallow_skipping_next_reasons type + : FT_POINTER Restrictions + .disallow_toggling_repeat_context_reasons type + : FT_POINTER Restrictions + .disallow_toggling_repeat_track_reasons type + : FT_POINTER Restrictions + .disallow_toggling_shuffle_reasons type + : FT_POINTER Restrictions + .disallow_set_queue_reasons type : FT_POINTER Restrictions + .disallow_interrupting_playback_reasons type + : FT_POINTER Restrictions + .disallow_transferring_playback_reasons type + : FT_POINTER Restrictions + .disallow_remote_control_reasons type + : FT_POINTER Restrictions + .disallow_inserting_into_next_tracks_reasons type + : FT_POINTER Restrictions + .disallow_inserting_into_context_tracks_reasons type + : FT_POINTER Restrictions + .disallow_reordering_in_next_tracks_reasons type + : FT_POINTER Restrictions + .disallow_reordering_in_context_tracks_reasons type + : FT_POINTER Restrictions + .disallow_removing_from_next_tracks_reasons type + : FT_POINTER Restrictions + .disallow_removing_from_context_tracks_reasons type + : FT_POINTER Restrictions + .disallow_updating_context_reasons type + : FT_POINTER Restrictions + .disallow_playing_reasons type : FT_POINTER Restrictions + .disallow_stopping_reasons type + : FT_POINTER Restrictions.disallow_loading_context_reasons type : FT_POINTER \ No newline at end of file diff --git a/cspot/src/CDNAudioFile.cpp b/cspot/src/CDNAudioFile.cpp index ed88f26..8439796 100644 --- a/cspot/src/CDNAudioFile.cpp +++ b/cspot/src/CDNAudioFile.cpp @@ -80,6 +80,81 @@ void CDNAudioFile::openStream() { this->lastRequestPosition = 0; this->lastRequestCapacity = 0; } + +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; + + if (position + bytes >= this->totalFileSize) { + return 0; + } + + // // Opus tries to read header, use prefetched data + if (offsetPosition < OPUS_HEADER_SIZE && + bytes + offsetPosition <= OPUS_HEADER_SIZE) { + memcpy(dst, this->header.data() + offsetPosition, bytes); + position += bytes; + return bytes; + } + + // // Opus tries to read footer, use prefetched data + if (offsetPosition >= (actualFileSize - this->footer.size())) { + size_t toReadBytes = bytes; + + if ((position + bytes) > this->totalFileSize) { + // Tries to read outside of bounds, truncate + toReadBytes = this->totalFileSize - position; + } + + size_t footerOffset = + offsetPosition - (actualFileSize - this->footer.size()); + memcpy(dst, this->footer.data() + footerOffset, toReadBytes); + + position += toReadBytes; + return toReadBytes; + } + + // Data not in the headers. Make sense of whats going on. + // Position in bounds :) + if (offsetPosition >= this->lastRequestPosition && + offsetPosition < this->lastRequestPosition + this->lastRequestCapacity) { + size_t toRead = bytes; + + if ((toRead + offsetPosition) > + this->lastRequestPosition + lastRequestCapacity) { + toRead = this->lastRequestPosition + lastRequestCapacity - offsetPosition; + } + + memcpy(dst, this->httpBuffer.data() + offsetPosition - lastRequestPosition, + toRead); + position += toRead; + + return toRead; + } else { + size_t requestPosition = (offsetPosition) - ((offsetPosition) % 16); + if (this->enableRequestMargin && requestPosition > SEEK_MARGIN_SIZE) { + requestPosition = (offsetPosition - SEEK_MARGIN_SIZE) - + ((offsetPosition - SEEK_MARGIN_SIZE) % 16); + this->enableRequestMargin = false; + } + + this->httpConnection->get( + cdnUrl, {bell::HTTPClient::RangeHeader::range( + requestPosition, requestPosition + HTTP_BUFFER_SIZE - 1)}); + this->lastRequestPosition = requestPosition; + this->lastRequestCapacity = this->httpConnection->contentLength(); + + this->httpConnection->stream().read((char*)this->httpBuffer.data(), + lastRequestCapacity); + this->decrypt(this->httpBuffer.data(), lastRequestCapacity, + + this->lastRequestPosition); + + return readBytes(dst, bytes); + } + + return bytes; +} #else /** * @brief Opens a connection to the CDN URL and fills the first buffer with track header data. @@ -87,7 +162,7 @@ void CDNAudioFile::openStream() { * @param header_size Reference to a size_t variable where the size of the header is stored. * @return Pointer to the beginning of the HTTP buffer where the track header data is stored. */ -uint8_t* CDNAudioFile::openStream(size_t& header_size) { +uint8_t* CDNAudioFile::openStream(ssize_t& header_size) { // Open connection, fill first buffer this->httpConnection = bell::HTTPClient::get( @@ -98,8 +173,7 @@ uint8_t* CDNAudioFile::openStream(size_t& header_size) { } this->lastRequestPosition = 0; this->lastRequestCapacity = this->httpConnection->contentLength(); - this->totalFileSize = - this->httpConnection->totalLength() - SPOTIFY_OPUS_HEADER; + this->totalFileSize = this->httpConnection->totalLength(); this->httpConnection->stream().read((char*)this->httpBuffer.data(), lastRequestCapacity); @@ -109,7 +183,6 @@ uint8_t* CDNAudioFile::openStream(size_t& header_size) { header_size = this->position; return &httpBuffer[0]; } -#endif /** * @brief Finds the position of the first audio frame in the HTTP response. @@ -124,14 +197,23 @@ long CDNAudioFile::getHeader() { for (int i = 0; i < 3; ++i) { offset += 26; - uint32_t segmentCount = httpBuffer[offset]; + if (offset >= HTTP_BUFFER_SIZE) { + return HTTP_BUFFER_SIZE; + } + uint8_t segmentCount = httpBuffer[offset]; uint32_t segmentEnd = segmentCount + offset + 1; ++offset; for (uint32_t j = offset; j < segmentEnd; ++j) { + if (offset >= HTTP_BUFFER_SIZE) { + return HTTP_BUFFER_SIZE; + } offset += httpBuffer[j]; } + if (offset >= HTTP_BUFFER_SIZE) { + return HTTP_BUFFER_SIZE; + } offset += segmentCount; } @@ -139,69 +221,49 @@ long CDNAudioFile::getHeader() { } long CDNAudioFile::readBytes(uint8_t* dst, size_t bytes) { - size_t offsetPosition = position + SPOTIFY_OPUS_HEADER; - size_t actualFileSize = this->totalFileSize + SPOTIFY_OPUS_HEADER; - if (position + bytes >= this->totalFileSize) { - return 0; - } - -#ifndef CONFIG_BELL_NOCODEC - // // Opus tries to read header, use prefetched data - if (offsetPosition < OPUS_HEADER_SIZE && - bytes + offsetPosition <= OPUS_HEADER_SIZE) { - memcpy(dst, this->header.data() + offsetPosition, bytes); - position += bytes; - return bytes; - } - - // // Opus tries to read footer, use prefetched data - if (offsetPosition >= (actualFileSize - this->footer.size())) { - size_t toReadBytes = bytes; - - if ((position + bytes) > this->totalFileSize) { - // Tries to read outside of bounds, truncate - toReadBytes = this->totalFileSize - position; + if (position >= this->totalFileSize - 1) { + return 0; + } else { + CSPOT_LOG(info, "Truncating read to %d bytes", + this->totalFileSize - position); + bytes = this->totalFileSize - position; } - - size_t footerOffset = - offsetPosition - (actualFileSize - this->footer.size()); - memcpy(dst, this->footer.data() + footerOffset, toReadBytes); - - position += toReadBytes; - return toReadBytes; } - // Data not in the headers. Make sense of whats going on. -#endif // Position in bounds :) - if (offsetPosition >= this->lastRequestPosition && - offsetPosition < this->lastRequestPosition + this->lastRequestCapacity) { + if (position >= this->lastRequestPosition && + position < this->lastRequestPosition + this->lastRequestCapacity) { size_t toRead = bytes; - if ((toRead + offsetPosition) > - this->lastRequestPosition + lastRequestCapacity) { - toRead = this->lastRequestPosition + lastRequestCapacity - offsetPosition; + if ((toRead + position) > this->lastRequestPosition + lastRequestCapacity) { + toRead = this->lastRequestPosition + lastRequestCapacity - position; } - memcpy(dst, this->httpBuffer.data() + offsetPosition - lastRequestPosition, + memcpy(dst, this->httpBuffer.data() + position - lastRequestPosition, toRead); position += toRead; return toRead; } else { - size_t requestPosition = (offsetPosition) - ((offsetPosition) % 16); + size_t requestPosition = (position) - ((position) % 16); if (this->enableRequestMargin && requestPosition > SEEK_MARGIN_SIZE) { - requestPosition = (offsetPosition - SEEK_MARGIN_SIZE) - - ((offsetPosition - SEEK_MARGIN_SIZE) % 16); + requestPosition = + (position - SEEK_MARGIN_SIZE) - ((position - SEEK_MARGIN_SIZE) % 16); this->enableRequestMargin = false; } + // Ensure the request range is within file bounds + size_t endPosition = requestPosition + HTTP_BUFFER_SIZE - 1; + if (endPosition > this->totalFileSize) { + endPosition = this->totalFileSize - 1; // Cap the range to the file size + } + // Only request if the range is valid if (!this->httpConnection->get( - cdnUrl, - {bell::HTTPClient::RangeHeader::range( - requestPosition, requestPosition + HTTP_BUFFER_SIZE - 1)})) - return -1; + cdnUrl, {bell::HTTPClient::RangeHeader::range(requestPosition, + endPosition)})) { + return -1; // Handle error if range is invalid or request fails + } this->lastRequestPosition = requestPosition; this->lastRequestCapacity = this->httpConnection->contentLength(); @@ -216,12 +278,16 @@ long CDNAudioFile::readBytes(uint8_t* dst, size_t bytes) { return bytes; } +#endif size_t CDNAudioFile::getSize() { return this->totalFileSize; } void CDNAudioFile::decrypt(uint8_t* dst, size_t nbytes, size_t pos) { + if (audioKey.size() != 16 && audioKey.size() != 24 && audioKey.size() != 32) { + throw std::runtime_error("Invalid AES key length"); + } auto calculatedIV = bigNumAdd(audioAESIV, pos / 16); this->crypto->aesCTRXcrypt(this->audioKey, calculatedIV, dst, nbytes); diff --git a/cspot/src/DeviceStateHandler.cpp b/cspot/src/DeviceStateHandler.cpp index 4d8631e..b28845e 100644 --- a/cspot/src/DeviceStateHandler.cpp +++ b/cspot/src/DeviceStateHandler.cpp @@ -24,34 +24,79 @@ using namespace cspot; +#if defined(_WIN32) || defined(_WIN64) +char* strndup(const char* str, size_t n) { + if (!str) + return nullptr; // Handle null input gracefully + size_t len = std::strlen(str); + if (len > n) + len = n; // Limit to n characters + char* copy = (char*)std::malloc(len + 1); // Allocate memory + if (!copy) + return nullptr; // Return null if allocation fails + std::memcpy(copy, str, len); // Copy the characters + copy[len] = '\0'; // Null-terminate the string + return copy; +} +#endif static DeviceStateHandler* handler; -void DeviceStateHandler::reloadTrackList(void*) { - - if (handler->reloadPreloadedTracks) { - handler->needsToBeSkipped = true; - while (!handler->trackQueue->playableSemaphore->twait(1)) {}; - handler->trackPlayer->start(); - handler->trackPlayer->resetState(); - handler->reloadPreloadedTracks = false; - handler->sendCommand(CommandType::PLAYBACK_START); - } - if (!handler->offset) { - if (handler->trackQueue->preloadedTracks.size()) - handler->trackQueue->preloadedTracks.clear(); - handler->trackQueue->preloadedTracks.push_back( - std::make_shared( - handler->currentTracks[handler->offset], handler->ctx, - handler->trackQueue->playableSemaphore, - handler->offsetFromStartInMillis)); - handler->device.player_state.track = - handler->currentTracks[handler->offset]; - handler->offsetFromStartInMillis = 0; - handler->offset++; +void DeviceStateHandler::reloadTrackList(void* data) { + if (data == NULL) { + if (handler->reloadPreloadedTracks) { + handler->needsToBeSkipped = true; + while (!handler->trackQueue->playableSemaphore->twait(1)) {}; + handler->trackPlayer->start(); + handler->trackPlayer->resetState(); + handler->reloadPreloadedTracks = false; + handler->sendCommand(CommandType::PLAYBACK_START); + handler->device.player_state.track = handler->currentTracks[0]; + } + if (!handler->offset) { + if (handler->trackQueue->preloadedTracks.size()) + handler->trackQueue->preloadedTracks.clear(); + handler->trackQueue->preloadedTracks.push_back( + std::make_shared( + handler->currentTracks[handler->offset], handler->ctx, + handler->trackQueue->playableSemaphore, + handler->offsetFromStartInMillis)); + handler->device.player_state.track = + handler->currentTracks[handler->offset]; + handler->offsetFromStartInMillis = 0; + handler->offset++; + } + if (!handler->trackQueue->preloadedTracks.size()) { + handler->trackQueue->preloadedTracks.push_back( + std::make_shared( + handler->currentTracks[handler->offset - 1], handler->ctx, + handler->trackQueue->playableSemaphore, + handler->offsetFromStartInMillis)); + handler->offsetFromStartInMillis = 0; + } + if (handler->currentTracks.size() > + handler->trackQueue->preloadedTracks.size() + handler->offset) { + while (handler->currentTracks.size() > + handler->trackQueue->preloadedTracks.size() + + handler->offset && + handler->trackQueue->preloadedTracks.size() < 3) { + handler->trackQueue->preloadedTracks.push_back( + std::make_shared( + handler->currentTracks + [handler->offset + + handler->trackQueue->preloadedTracks.size() - 1], + handler->ctx, handler->trackQueue->playableSemaphore)); + } + } + if (handler->playerStateChanged) { + handler->putPlayerState( + PutStateReason::PutStateReason_PLAYER_STATE_CHANGED); + handler->playerStateChanged = false; + } } if (strcmp(handler->currentTracks[handler->offset - 1].uri, "spotify:delimiter") == 0 && - handler->device.player_state.is_playing) { + handler->device.player_state.is_playing && + handler->currentTracks.size() <= handler->offset) { handler->ctx->playbackMetrics->end_reason = cspot::PlaybackMetrics::REMOTE; handler->ctx->playbackMetrics->end_source = "unknown"; handler->trackPlayer->stop(); @@ -66,49 +111,26 @@ void DeviceStateHandler::reloadTrackList(void*) { handler->device.player_state.has_restrictions = false; handler->putPlayerState(); handler->sendCommand(CommandType::DISC); +#ifndef CONFIG_CSPOT_STAY_CONNECTED_ON_TRANSFER + handler->disconnect(); +#endif return; } - if (!handler->trackQueue->preloadedTracks.size()) { - handler->trackQueue->preloadedTracks.push_back( - std::make_shared( - handler->currentTracks[handler->offset - 1], handler->ctx, - handler->trackQueue->playableSemaphore, - handler->offsetFromStartInMillis)); - handler->offsetFromStartInMillis = 0; - } - if (handler->currentTracks.size() > - handler->trackQueue->preloadedTracks.size() + handler->offset) { - while (handler->currentTracks.size() > - handler->trackQueue->preloadedTracks.size() + handler->offset && - handler->trackQueue->preloadedTracks.size() < 3) { - handler->trackQueue->preloadedTracks.push_back( - std::make_shared( - handler - ->currentTracks[handler->offset + - handler->trackQueue->preloadedTracks.size() - - 1], - handler->ctx, handler->trackQueue->playableSemaphore)); - } - } - if (handler->playerStateChanged) { - handler->putPlayerState( - PutStateReason::PutStateReason_PLAYER_STATE_CHANGED); - handler->playerStateChanged = false; - } handler->resolvingContext.store(false); //CSPOT_LOG(info,"heap_memory_check-safe = %i",heap_caps_check_integrity_all(true)); } -DeviceStateHandler::DeviceStateHandler(std::shared_ptr ctx) { +DeviceStateHandler::DeviceStateHandler(std::shared_ptr ctx, + std::function onClose) { handler = this; this->ctx = ctx; + this->onClose = onClose; this->trackQueue = std::make_shared(ctx); this->playerContext = std::make_shared( ctx, &this->device.player_state, ¤tTracks, &offset); auto onTrackEnd = [this](bool loaded) { if (!loaded) {} - CSPOT_LOG(debug, "Ended track, needs_to_be_skipped = %s", - needsToBeSkipped ? "true" : "false"); + CSPOT_LOG(debug, "Ended track"); if (needsToBeSkipped) { if (this->device.player_state.options.repeating_track) this->trackQueue->preloadedTracks[0]->requestedPosition = 0; @@ -122,17 +144,13 @@ DeviceStateHandler::DeviceStateHandler(std::shared_ptr ctx) { sendCommand(CommandType::DEPLETED); if ((uint32_t)currentTracks.size() / 2 <= offset && !this->resolvingContext) { - //CSPOT_LOG(info,"heap_memory_check-safe = %i",heap_caps_check_integrity_all(true)); this->resolvingContext.store(true); playerContext->resolveTracklist(metadata_map, reloadTrackList); - //CSPOT_LOG(info,"heap_memory_check-safe = %i",heap_caps_check_integrity_all(true)); } }; auto onTrackChanged = [this](std::shared_ptr track, bool new_track = false) { - CSPOT_LOG(debug, "Track loaded, new_track = %s", - new_track ? "true" : "false"); if (new_track) { this->device.player_state.timestamp = this->trackQueue->preloadedTracks[0] @@ -146,8 +164,9 @@ DeviceStateHandler::DeviceStateHandler(std::shared_ptr ctx) { putPlayerState(); }; - this->trackPlayer = std::make_shared(ctx, trackQueue, onTrackEnd, - onTrackChanged); + this->trackPlayer = std::make_shared( + ctx, trackQueue, onTrackEnd, onTrackChanged, + &device.player_state.options.repeating_track); CSPOT_LOG(info, "Started player"); auto connectStateSubscription = [this](MercurySession::Response res) { @@ -168,6 +187,10 @@ DeviceStateHandler::DeviceStateHandler(std::shared_ptr ctx) { pb_release(SetVolumeCommand_fields, &newVolume); } } else if (strstr(res.mercuryHeader.uri, "cluster")) { + if (0) { // will send cluster info if new device logged in, but connect_state/cluster is called way too often(for example during each song) too send each time a putPlayerStateRequest + //if(is_active){ + putPlayerState(); + } } else CSPOT_LOG(debug, "Unknown connect_state, uri : %s", res.mercuryHeader.uri); @@ -208,6 +231,9 @@ DeviceStateHandler::DeviceStateHandler(std::shared_ptr ctx) { this->putDeviceState(PutStateReason::PutStateReason_BECAME_INACTIVE); CSPOT_LOG(debug, "Device changed"); sendCommand(CommandType::DISC); +#ifndef CONFIG_CSPOT_STAY_CONNECTED_ON_TRANSFER + this->disconnect(); +#endif } } }; @@ -312,7 +338,7 @@ DeviceStateHandler::DeviceStateHandler(std::shared_ptr ctx) { DeviceStateHandler::~DeviceStateHandler() { TrackReference::clearProvidedTracklist(¤tTracks); - currentTracks.clear(); + device.player_state.track = ProvidedTrack_init_zero; pb_release(Device_fields, &device); } @@ -431,7 +457,7 @@ void DeviceStateHandler::putPlayerState(PutStateReason put_state_reason) { device.player_state.restrictions.disallow_pausing_reasons[0] = strdup("not_playing"); } - + // Update player state with current track information device.player_state.restrictions.disallow_toggling_repeat_context_reasons = (char**)calloc(3, sizeof(char*)); device.player_state.restrictions @@ -470,6 +496,10 @@ void DeviceStateHandler::putPlayerState(PutStateReason put_state_reason) { device.player_state.has_index = false; device.player_state.has_restrictions = true; + if (device.player_state.play_origin.feature_classes != NULL) { + free(device.player_state.play_origin.feature_classes); + device.player_state.play_origin.feature_classes = NULL; + } } else { device.player_state.index = ContextIndex{true, device.player_state.track.page, true, @@ -513,14 +543,27 @@ void DeviceStateHandler::putPlayerState(PutStateReason put_state_reason) { } void DeviceStateHandler::disconnect() { + if (this->is_active) { + this->ctx->playbackMetrics->end_reason = PlaybackMetrics::REMOTE; + this->ctx->playbackMetrics->end_source = "unknown"; + this->trackPlayer->stop(); + this->is_active = false; + if (device.player_state.has_restrictions) + pb_release(Restrictions_fields, &device.player_state.restrictions); + device.player_state.restrictions = Restrictions_init_zero; + device.player_state.has_restrictions = false; + this->putDeviceState(PutStateReason::PutStateReason_BECAME_INACTIVE); + CSPOT_LOG(debug, "Device changed"); + sendCommand(CommandType::DISC); + } + this->trackQueue->preloadedTracks.clear(); this->trackQueue->stopTask(); - this->trackPlayer->stop(); this->ctx->session->disconnect(); + this->onClose(); } void DeviceStateHandler::skip(CommandType dir, bool notify) { if (dir == CommandType::SKIP_NEXT) { - std::scoped_lock lock(trackQueue->tracksMutex); this->device.player_state.track = currentTracks[offset]; if (this->device.player_state.track.full_metadata_count > this->device.player_state.track.metadata_count) @@ -544,7 +587,6 @@ void DeviceStateHandler::skip(CommandType dir, bool notify) { } else if (trackQueue->preloadedTracks[0]->trackMetrics->getPosition() >= 3000 && offset > 1) { - std::scoped_lock lock(trackQueue->tracksMutex); trackQueue->preloadedTracks.pop_back(); offset--; trackQueue->preloadedTracks.push_front(std::make_shared( @@ -567,7 +609,13 @@ void DeviceStateHandler::skip(CommandType dir, bool notify) { void DeviceStateHandler::parseCommand(std::vector& data) { if (data.size() <= 2) return; - auto jsonResult = nlohmann::json::parse(data); + nlohmann::json jsonResult; + try { + jsonResult = nlohmann::json::parse(data); + } catch (const nlohmann::json::parse_error&) { + CSPOT_LOG(error, "Failed to parse command"); + return; // Parsing failed + } last_message_id = jsonResult.value("message_id", last_message_id); auto command = jsonResult.find("command"); @@ -578,8 +626,6 @@ void DeviceStateHandler::parseCommand(std::vector& data) { command->at("endpoint").get().c_str()); auto options = command->find("options"); - - // std::cout<at("endpoint") == "transfer") { if (is_active) return; @@ -592,6 +638,8 @@ void DeviceStateHandler::parseCommand(std::vector& data) { } } } + if (this->playerContext->next_page_url != NULL) + unreference(&(this->playerContext->next_page_url)); this->playerContext->radio_offset = 0; this->device.player_state.has_is_playing = true; this->device.player_state.is_playing = true; @@ -621,14 +669,13 @@ void DeviceStateHandler::parseCommand(std::vector& data) { auto responseHandler = [this](MercurySession::Response res) { if (res.fail || !res.parts.size()) return; - std::scoped_lock lock(trackQueue->tracksMutex); cspot::TrackReference::clearProvidedTracklist(¤tTracks); currentTracks = {}; Cluster cluster = {}; for (int i = this->device.player_state.context_metadata_count - 1; i >= 0; i--) { - unreference(this->device.player_state.context_metadata[i].key); - unreference(this->device.player_state.context_metadata[i].value); + unreference(&(this->device.player_state.context_metadata[i].key)); + unreference(&(this->device.player_state.context_metadata[i].value)); } free(this->device.player_state.context_metadata); this->device.player_state.context_metadata = NULL; @@ -657,11 +704,11 @@ void DeviceStateHandler::parseCommand(std::vector& data) { for (int i = 0; i < 16; i++) { random_bytes.push_back(d(ctx->rng)); } - unreference(this->device.player_state.session_id); + unreference(&(this->device.player_state.session_id)); this->device.player_state.session_id = strdup(bytesToHexString(random_bytes).c_str()); - unreference(this->device.player_state.playback_id); + unreference(&(this->device.player_state.playback_id)); random_bytes.clear(); for (int i = 0; i < 16; i++) { random_bytes.push_back(d(ctx->rng)); @@ -671,7 +718,7 @@ void DeviceStateHandler::parseCommand(std::vector& data) { this->currentTracks.insert(this->currentTracks.begin(), this->device.player_state.track); - offset = 1; + offset = 0; queuePacket = {&offset, ¤tTracks}; this->putDeviceState( @@ -690,8 +737,9 @@ void DeviceStateHandler::parseCommand(std::vector& data) { handler->trackPlayer->stop(); sendCommand(CommandType::DEPLETED); #endif - playerContext->radio_offset = 0; - std::scoped_lock lock(trackQueue->tracksMutex); + if (this->playerContext->next_page_url != NULL) + unreference(&(this->playerContext->next_page_url)); + this->playerContext->radio_offset = 0; trackQueue->preloadedTracks.clear(); uint8_t queued = 0; ProvidedTrack track = ProvidedTrack_init_zero; @@ -700,7 +748,7 @@ void DeviceStateHandler::parseCommand(std::vector& data) { this->device.player_state.has_track = true; } for (int i = 0; i < currentTracks.size(); i++) { - if (i > this->offset || + if (i > this->offset || currentTracks[i].provider == NULL || strcmp(currentTracks[i].provider, "queue") != 0) { cspot::TrackReference::pbReleaseProvidedTrack(¤tTracks[i]); currentTracks.erase(currentTracks.begin() + i); @@ -742,7 +790,10 @@ void DeviceStateHandler::parseCommand(std::vector& data) { auto options = command->find("options"); int64_t playlist_offset = 0; if (options != command->end()) { - if (options->find("player_options_override") != options->end()) + if (options->find("player_options_override") != options->end() && + options->at("player_options_override") + .find("shuffling_context") != + options->at("player_options_override").end()) device.player_state.options.shuffling_context = options->at("player_options_override").at("shuffling_context"); else @@ -774,8 +825,8 @@ void DeviceStateHandler::parseCommand(std::vector& data) { .context_enhancement_count) { for (auto& enhamcement : this->device.player_state.options.context_enhancement) { - this->unreference(enhamcement.key); - this->unreference(enhamcement.value); + this->unreference(&(enhamcement.key)); + this->unreference(&(enhamcement.value)); } this->device.player_state.options.context_enhancement_count = 0; } @@ -790,8 +841,8 @@ void DeviceStateHandler::parseCommand(std::vector& data) { } for (int i = this->device.player_state.context_metadata_count - 1; i >= 0; i--) { - unreference(this->device.player_state.context_metadata[i].key); - unreference(this->device.player_state.context_metadata[i].value); + unreference(&(this->device.player_state.context_metadata[i].key)); + unreference(&(this->device.player_state.context_metadata[i].value)); } free(this->device.player_state.context_metadata); this->device.player_state.context_metadata = @@ -810,33 +861,32 @@ void DeviceStateHandler::parseCommand(std::vector& data) { if (this->device.player_state.options.context_enhancement_count) { for (auto& enhamcement : this->device.player_state.options.context_enhancement) { - this->unreference(enhamcement.key); - this->unreference(enhamcement.value); + this->unreference(&(enhamcement.key)); + this->unreference(&(enhamcement.value)); } this->device.player_state.options.context_enhancement_count = 0; } context_metadata_map.clear(); for (int i = this->device.player_state.context_metadata_count - 1; i >= 0; i--) { - unreference(this->device.player_state.context_metadata[i].key); - unreference(this->device.player_state.context_metadata[i].value); + unreference(&(this->device.player_state.context_metadata[i].key)); + unreference(&(this->device.player_state.context_metadata[i].value)); } free(this->device.player_state.context_metadata); this->device.player_state.context_metadata = NULL; this->device.player_state.context_metadata_count = 0; } - unreference(this->device.player_state.context_uri); + unreference(&(this->device.player_state.context_uri)); this->device.player_state.context_uri = PlayerContext::createStringReferenceIfFound(command->at("context"), "uri"); - unreference(this->device.player_state.context_url); + unreference(&(this->device.player_state.context_url)); this->device.player_state.context_url = PlayerContext::createStringReferenceIfFound(command->at("context"), "url"); - reloadPreloadedTracks = true; - this->trackPlayer->start(); + //this->trackPlayer->start(); uint8_t metadata_offset = 0; for (auto metadata_entry : metadata_map) { track.metadata[metadata_offset].key = @@ -877,14 +927,25 @@ void DeviceStateHandler::parseCommand(std::vector& data) { } track.full_metadata_count = metadata_offset; track.metadata_count = metadata_offset; - track.provider = strdup("context"); + if (this->device.player_state.context_url != NULL && + strchr(this->device.player_state.context_url, ':') != NULL) { + track.provider = + strndup(this->device.player_state.context_url, + strchr(this->device.player_state.context_url, ':') - + this->device.player_state.context_url); + } else if (strchr(this->device.player_state.context_uri, ':') != + strrchr(this->device.player_state.context_uri, ':')) { + track.provider = strdup("context"); + } if (track.uri) { currentTracks.insert(currentTracks.begin(), track); device.player_state.track = track; - } - offset = 1; - playerContext->resolveTracklist(metadata_map, reloadTrackList, true, - true); + } else + pb_release(ProvidedTrack_fields, &track); + offset = 0; + this->playerContext->resolveTracklist(metadata_map, reloadTrackList, + true, true); + CSPOT_LOG(info, "Tracklist reloaded"); } else if (command->at("endpoint") == "pause") { device.player_state.is_paused = true; device.player_state.has_is_paused = true; @@ -938,7 +999,8 @@ void DeviceStateHandler::parseCommand(std::vector& data) { } else if (command->at("endpoint") == "seek_to") { #ifndef CONFIG_BELL_NOCODEC - needsToBeSkipped = false; + if (!this->trackQueue->preloadedTracks[0]->loading) + needsToBeSkipped = false; #endif if (command->at("relative") == "beginning") { //relative this->device.player_state.has_position_as_of_timestamp = true; @@ -946,14 +1008,17 @@ void DeviceStateHandler::parseCommand(std::vector& data) { command->at("value").get(); this->device.player_state.timestamp = this->ctx->timeProvider->getSyncedTimestamp(); - this->trackPlayer->seekMs(command->at("value").get()); + this->trackPlayer->seekMs( + command->at("value").get(), + this->trackQueue->preloadedTracks[0]->loading); } else if (command->at("relative") == "current") { this->device.player_state.has_position_as_of_timestamp = true; this->device.player_state.position_as_of_timestamp = command->at("value").get() + command->at("position").get(); this->trackPlayer->seekMs( - this->device.player_state.position_as_of_timestamp); + this->device.player_state.position_as_of_timestamp, + this->trackQueue->preloadedTracks[0]->loading); this->device.player_state.timestamp = this->ctx->timeProvider->getSyncedTimestamp(); } @@ -962,7 +1027,6 @@ void DeviceStateHandler::parseCommand(std::vector& data) { (int32_t)this->device.player_state.position_as_of_timestamp); this->putPlayerState(); } else if (command->at("endpoint") == "add_to_queue") { - std::scoped_lock lock(trackQueue->tracksMutex); uint8_t queuedOffset = 0; //look up already queued tracks for (uint8_t i = offset; i < currentTracks.size(); i++) { @@ -987,14 +1051,14 @@ void DeviceStateHandler::parseCommand(std::vector& data) { } #ifndef CONFIG_BELL_NOCODEC this->trackPlayer->seekMs( - trackQueue->preloadedTracks[0]->trackMetrics->getPosition()); + trackQueue->preloadedTracks[0]->trackMetrics->getPosition(), + this->trackQueue->preloadedTracks[0]->loading); sendCommand( CommandType::SEEK, (int32_t)this->device.player_state.position_as_of_timestamp); #endif this->putPlayerState(); } else if (command->at("endpoint") == "set_queue") { - std::scoped_lock lock(trackQueue->tracksMutex); uint8_t queuedOffset = 0, newQueuedOffset = 0; //look up already queued tracks for (uint8_t i = offset; i < currentTracks.size(); i++) { @@ -1057,14 +1121,15 @@ void DeviceStateHandler::parseCommand(std::vector& data) { } #ifndef CONFIG_BELL_NOCODEC this->trackPlayer->seekMs( - trackQueue->preloadedTracks[0]->trackMetrics->getPosition()); + trackQueue->preloadedTracks[0]->trackMetrics->getPosition(), + this->trackQueue->preloadedTracks[0]->loading); sendCommand( CommandType::SEEK, (int32_t)this->device.player_state.position_as_of_timestamp); #endif this->putPlayerState(); } else if (command->at("endpoint") == "update_context") { - unreference(this->device.player_state.session_id); + unreference(&(this->device.player_state.session_id)); this->device.player_state.session_id = PlayerContext::createStringReferenceIfFound(*command, "session_id"); @@ -1090,17 +1155,17 @@ void DeviceStateHandler::parseCommand(std::vector& data) { } } else if (command->at("endpoint") == "set_shuffling_context") { if (context_uri.size()) { - unreference(this->device.player_state.context_uri); + unreference(&(this->device.player_state.context_uri)); this->device.player_state.context_uri = strdup(context_uri.c_str()); } if (context_url.size()) { - unreference(this->device.player_state.context_url); + unreference(&(this->device.player_state.context_url)); this->device.player_state.context_url = strdup(context_url.c_str()); } for (int i = this->device.player_state.context_metadata_count - 1; i >= 0; i--) { - unreference(this->device.player_state.context_metadata[i].key); - unreference(this->device.player_state.context_metadata[i].value); + unreference(&(this->device.player_state.context_metadata[i].key)); + unreference(&(this->device.player_state.context_metadata[i].value)); } free(this->device.player_state.context_metadata); this->device.player_state.context_metadata = @@ -1130,14 +1195,14 @@ void DeviceStateHandler::parseCommand(std::vector& data) { this->device.player_state.options.context_enhancement_count = 1; } else { if (this->device.player_state.options.context_enhancement_count) { + unreference(&( + this->device.player_state.options.context_enhancement[0].key)); unreference( - this->device.player_state.options.context_enhancement[0].key); - unreference( - this->device.player_state.options.context_enhancement[0].value); + &(this->device.player_state.options.context_enhancement[0] + .value)); this->device.player_state.options.context_enhancement_count = 0; } } - std::scoped_lock lock(trackQueue->tracksMutex); playerStateChanged = true; this->trackQueue->preloadedTracks.erase( this->trackQueue->preloadedTracks.begin(), @@ -1159,7 +1224,8 @@ void DeviceStateHandler::parseCommand(std::vector& data) { .shuffling_context)); #ifndef CONFIG_BELL_NOCODEC this->trackPlayer->seekMs( - trackQueue->preloadedTracks[0]->trackMetrics->getPosition()); + trackQueue->preloadedTracks[0]->trackMetrics->getPosition(), + this->trackQueue->preloadedTracks[0]->loading); sendCommand( CommandType::SEEK, (int32_t)this->device.player_state.position_as_of_timestamp); diff --git a/cspot/src/MercurySession.cpp b/cspot/src/MercurySession.cpp index 1915d71..2dc3e78 100644 --- a/cspot/src/MercurySession.cpp +++ b/cspot/src/MercurySession.cpp @@ -116,8 +116,8 @@ void MercurySession::handleReconnection() { void MercurySession::reconnect() { while (isRunning) { try { - this->conn = nullptr; this->shanConn = nullptr; + this->conn = nullptr; this->partials.clear(); // Reset connections this->connectWithRandomAp(); @@ -178,8 +178,8 @@ void MercurySession::unregisterAudioKey(uint32_t sequenceId) { void MercurySession::disconnect() { CSPOT_LOG(info, "Disconnecting mercury session"); isRunning.store(false); - conn->close(); std::scoped_lock lock(this->isRunningMutex); + conn->close(); } std::string MercurySession::getCountryCode() { @@ -389,6 +389,8 @@ uint64_t MercurySession::executeSubscription(RequestType method, this->sequenceId += 1; try { + while (isReconnecting) + BELL_SLEEP_MS(100); this->shanConn->sendPacket( static_cast::type>(method), sequenceIdBytes); diff --git a/cspot/src/PlayerContext.cpp b/cspot/src/PlayerContext.cpp index e2f651b..8f6be6e 100644 --- a/cspot/src/PlayerContext.cpp +++ b/cspot/src/PlayerContext.cpp @@ -86,11 +86,65 @@ T getFromJsonObject(nlohmann::json::value_type& jsonObject, const char* key) { * @param[in] secondTry If true, use the first track in the tracklist as the context URI * instead of the context URI from the player state. */ +// Helper function to split a string by a delimiter +std::vector split(const std::string& s, char delimiter) { + std::vector tokens; + std::string token; + std::istringstream tokenStream(s); + while (std::getline(tokenStream, token, delimiter)) { + tokens.push_back(token); + } + return tokens; +} + +// Helper function to join a vector of strings with a delimiter +std::string join(const std::vector& vec, char delimiter) { + std::ostringstream result; + for (size_t i = 0; i < vec.size(); ++i) { + result << vec[i]; + if (i < vec.size() - 1) { + result << delimiter; + } + } + return result.str(); +} + +// Function to process the next_page_url +char* processNextPageUrl(const std::string& url, size_t trackLimit, + uint64_t* radio_offset) { + const std::string key = "prev_tracks="; + size_t startPos = url.find(key); + if (startPos == std::string::npos) { + return NULL; // No prev_tracks found + } + startPos += key.length(); + + // Find the end of the prev_tracks parameter + size_t endPos = url.find('&', startPos); + std::string prevTracks = (endPos == std::string::npos) + ? url.substr(startPos) + : url.substr(startPos, endPos - startPos); + + // Split, cap, and join + std::vector tracks = split(prevTracks, ','); + if (tracks.size() > trackLimit) { + *radio_offset += tracks.size() - trackLimit; + tracks.erase(tracks.begin(), tracks.end() - trackLimit); + } + std::string newPrevTracks = join(tracks, ','); + + // Rebuild the URL + std::string rebuiltUrl = url.substr(0, startPos) + newPrevTracks + + "&offset=" + std::to_string(*radio_offset); + return strdup(rebuiltUrl.c_str()); +} void PlayerContext::autoplayQuery( std::vector> metadata_map, void (*responseFunction)(void*), bool secondTry) { if (next_page_url != NULL) return resolveRadio(metadata_map, responseFunction, next_page_url); + if (playerState->context_uri == NULL) + secondTry = true; std::string requestUrl = string_format("hm://autoplay-enabled/query?uri=%s", secondTry ? tracks->at(0).uri : playerState->context_uri); @@ -100,20 +154,22 @@ void PlayerContext::autoplayQuery( if (res.fail || !res.parts.size() || !res.parts[0].size()) { if (!secondTry) return autoplayQuery(metadata_map, responseFunction, true); - else - return; // responseFunction(NULL); + //else + //return responseFunction((void*)radio_offset); } std::string resolve_autoplay = std::string(res.parts[0].begin(), res.parts[0].end()); std::string requestUrl; { - if (strcmp(tracks->back().provider, "context") == 0) + if (tracks->back().provider && + (strcmp(tracks->back().provider, "context") == 0 || + playerState->context_uri == NULL)) requestUrl = string_format( - "hm://radio-apollo/v3/stations/%s?autoplay=true&offset=%i", - &resolve_autoplay[0], tracks->back().original_index); + "hm://radio-apollo/v3/stations/%s?autoplay=true", //&offset=%i", + &resolve_autoplay[0]); //, tracks->back().original_index); else { requestUrl = "hm://radio-apollo/v3/tracks/" + - (std::string)playerState->context_uri + + (std::string)tracks->at(0).uri + "?autoplay=true&count=50&isVideo=false&prev_tracks="; bool copiedTracks = false; auto trackRef = @@ -139,30 +195,42 @@ void PlayerContext::autoplayQuery( void PlayerContext::resolveRadio( std::vector> metadata_map, void (*responseFunction)(void*), char* url) { - CSPOT_LOG(debug, "Resolve radio : %s", &url[0]); + CSPOT_LOG(debug, "Resolve radio"); auto responseHandler = [this, metadata_map, responseFunction](MercurySession::Response res) { if (res.fail || !res.parts.size()) - return responseFunction(NULL); + return responseFunction((void*)radio_offset); if (!res.parts[0].size()) - return responseFunction(NULL); + return responseFunction((void*)radio_offset); // remove old_tracks, keep 5 tracks in memory - int remove_tracks = ((int)*index) - 5; - if (remove_tracks > 0) { - cspot::TrackReference::deleteTracksInRange(tracks, 0, remove_tracks - 1); - *index = (uint8_t)(remove_tracks < 0 ? 5 + remove_tracks : 5); + if (*index > 5) { + cspot::TrackReference::deleteTracksInRange(tracks, 0, *index - 5); + *index = 4; + } + if (!nlohmann::json::accept(res.parts[0])) { + return responseFunction((void*)radio_offset); } auto jsonResult = nlohmann::json::parse(res.parts[0]); context_uri = jsonResult.value("uri", context_uri); if (next_page_url != NULL) free(next_page_url); - next_page_url = createStringReferenceIfFound(jsonResult, "next_page_url"); + + auto urlObject = jsonResult.find("next_page_url"); + if (urlObject != jsonResult.end()) { + next_page_url = processNextPageUrl(urlObject.value(), 100, &radio_offset); + } + std::vector> metadata = metadata_map; metadata.push_back(std::make_pair("context_uri", context_uri)); metadata.push_back(std::make_pair("entity_uri", context_uri)); + metadata.push_back(std::make_pair("iteration", "0")); metadata.insert(metadata.begin(), std::make_pair("autoplay.is_autoplay", "true")); metadata.push_back(std::make_pair("track_player", "audio")); + metadata.push_back( + std::make_pair("actions.skipping_next_past_track", "resume")); + metadata.push_back( + std::make_pair("actions.skipping_prev_past_track", "resume")); jsonToTracklist(tracks, metadata, jsonResult["tracks"], "autoplay", 0); radio_offset++; responseFunction(NULL); @@ -297,12 +365,12 @@ uint8_t PlayerContext::jsonToTracklist( copiedTracks++; offset++; } - if (offset == json_tracks.size()) { + if (offset == json_tracks.size() && !radio) { ProvidedTrack new_track = ProvidedTrack_init_zero; new_track.uri = strdup("spotify:delimiter"); new_track.uid = strdup("delimiter0"); - new_track.provider = strdup("context"); - new_track.removed = strdup("context/delimiter"); + new_track.provider = strdup(provider); + new_track.removed = strdup((std::string(provider) + "/delimiter").c_str()); new_track.metadata[0] = {strdup("hidden"), strdup("true")}; new_track.metadata[1] = {strdup("actions.skipping_next_past_track"), strdup("resume")}; @@ -311,6 +379,7 @@ uint8_t PlayerContext::jsonToTracklist( new_track.metadata_count = 3; new_track.full_metadata_count = 3; tracks->push_back(new_track); + CSPOT_LOG(debug, "Adding delimiter to tracklist"); } return copiedTracks; } @@ -319,214 +388,149 @@ void PlayerContext::resolveTracklist( std::vector> metadata_map, void (*responseFunction)(void*), bool changed_state, bool trackIsPartOfContext) { - // MAX_TRACKS if (changed_state) { - //free next_page_url - if (next_page_url != NULL) - free(next_page_url); - next_page_url = NULL; - //new Playlist was loaded, check if there is a delimiter in tracklist and if, delete all after + //new Playlist/context was loaded, check if there is a delimiter in tracklist and if, delete all after for (int i = 0; i < tracks->size(); i++) { if (tracks->at(i).uri && strstr(tracks->at(i).uri, "spotify:delimiter")) { + CSPOT_LOG(debug, + "Deleting all tracks after delimiter, current tracklist " + "size: %i, index: %i", + tracks->size(), i); cspot::TrackReference::deleteTracksInRange(tracks, i, tracks->size() - 1); break; } } } - + //if current track's provider is autoplay, skip loading the tracklist and query autoplay + if (playerState->track.provider == NULL || + strcmp(playerState->track.provider, "autoplay") == 0) { + return autoplayQuery(metadata_map, responseFunction); + } else + radio_offset = 0; + if (playerState->context_uri == NULL) + return responseFunction((void*)radio_offset); //if last track was no radio track, resolve tracklist - if ((playerState->track.provider == NULL || - strcmp(playerState->track.provider, "autoplay")) != 0 && - playerState->context_uri != NULL) { - std::string requestUrl = "hm://context-resolve/v1/%s"; - if (playerState->options.shuffling_context && playerState->context_url) - requestUrl = string_format(requestUrl, &playerState->context_url[10]); - else - requestUrl = string_format(requestUrl, playerState->context_uri); - CSPOT_LOG(debug, "Resolve tracklist, url: %s", &requestUrl[0]); - - auto responseHandler = [this, metadata_map, responseFunction, changed_state, - trackIsPartOfContext]( - MercurySession::Response res) { - if (res.fail || !res.parts.size()) - return; - if (!res.parts[0].size()) - return; - auto jsonResult = nlohmann::json::parse(res.parts[0]); - uint8_t copy_tracks = 0; - if (tracks->size()) { - // remove old_tracks, keep 5 tracks in memory - int remove_tracks = ((int)*index) - 5; - if (remove_tracks > 0) - cspot::TrackReference::deleteTracksInRange(tracks, 0, - remove_tracks - 1); - *index = (uint8_t)(remove_tracks < 0 ? 5 + remove_tracks : 5); - auto trackref = tracks->end() - 1; - //if last track was a queued track/delimiter, try to look for a normal track as lookup reference - while (trackref != tracks->begin() && - (strcmp(trackref->provider, "context") != 0 || - trackref->removed != NULL)) { - trackref--; - } - //if no normal track was found, resolve radio - if (strcmp(trackref->provider, "queue") == 0) - return autoplayQuery(metadata_map, responseFunction); - looking_for_playlisttrack:; - //if last track was a smart_shuffled track - if (trackref != tracks->begin()) { - if (trackref->removed != NULL || - strcmp(trackref->provider, "context") != - 0) { //is a delimiter || is queued - trackref--; - goto looking_for_playlisttrack; - } - for (int i = 0; i < trackref->full_metadata_count; i++) - if (trackref->metadata[i].key && - strcmp(trackref->metadata[i].key, "provider") == 0 && - !playerState->options - .context_enhancement_count) { //was a smart_shuffle-track, but smart_shuffle is no more - trackref--; - goto looking_for_playlisttrack; - } - } + std::string requestUrl = "hm://context-resolve/v1/%s"; + if (playerState->options.shuffling_context && + playerState->options.context_enhancement_count) + requestUrl = string_format(requestUrl, &playerState->context_url[10]); + else + requestUrl = string_format(requestUrl, playerState->context_uri); + CSPOT_LOG(debug, "Resolve context, url: %s", &requestUrl[0]); - if (trackref == tracks->begin() && - strcmp(trackref->uri, "spotify:delimiter") == 0) - return; - //if track available were all smart_shuffle_tracks, load Tracklist from 0; - if (trackref == tracks->begin()) { - for (int i = 0; - i < (trackref->full_metadata_count > trackref->metadata_count - ? trackref->full_metadata_count - : trackref->metadata_count); - i++) - if ((strcmp(trackref->metadata[i].key, "provider") == 0 && - !playerState->options.context_enhancement_count)) { - jsonToTracklist(tracks, metadata_map, - jsonResult["pages"][0]["tracks"], "context", 0, 0, - playerState->options.shuffling_context, false); - return responseFunction(NULL); - } - } - - //look for trackreference - for (int i = 0; i < jsonResult["pages"].size(); i++) { - uint32_t offset = 0; - if (!copy_tracks) { - for (auto track : jsonResult["pages"][i]["tracks"]) { - if (strcmp(track["uri"].get().c_str(), - trackref->uri) == 0) { - copy_tracks = 1; + auto responseHandler = [this, metadata_map, responseFunction, changed_state, + trackIsPartOfContext](MercurySession::Response res) { + if (res.fail || !res.parts.size()) + return responseFunction((void*)radio_offset); + if (!res.parts[0].size()) + return responseFunction((void*)radio_offset); + auto jsonResult = nlohmann::json::parse(res.parts[0]); + uint8_t pageIndex = 0; + uint32_t offset = 0; + bool smartShuffledTrack = false, foundTrack = false; + std::vector::iterator trackref = tracks->begin(); + if (tracks->size()) { + // do all the look up magic before deleting tracks + trackref = tracks->end() - 1; + //if last track in tracklist was a queued track/delimiter, try to look for a normal track as lookup reference + while (trackref != tracks->begin()) { + smartShuffledTrack = false; + if (trackref->removed == NULL) { // is not a delimiter + if (strcmp(trackref->provider, "context") == 0) { + for (int i = 0; i < trackref->full_metadata_count; i++) { + if (strcmp(trackref->metadata[i].key, "provider") == 0 && + !playerState->options + .context_enhancement_count) { //was a smart_shuffle-track, but smart_shuffle is no more + smartShuffledTrack = true; break; } - offset++; } + break; } - //if trackreference was found - if (copy_tracks) { - if (changed_state) { - createIndexBasedOnTracklist( - tracks, jsonResult["pages"][i]["tracks"], - playerState->options.shuffling_context, i); - if (jsonResult["pages"][i]["tracks"].at(0).find( - METADATA_STRING) != - jsonResult["pages"][i]["tracks"].at(0).end() && - jsonResult["pages"][i]["tracks"] - .at(0) - .find(METADATA_STRING) - ->find(SMART_SHUFFLE_STRING) != - jsonResult["pages"][i]["tracks"] - .at(0) - .find(METADATA_STRING) - ->end()) { - if (playerState->options.shuffling_context) { - if (alternative_index[0] != offset) { - for (auto& index_ : alternative_index) - if (index_ == offset) { - index_ = alternative_index[0]; - alternative_index[0] = offset; - break; - } - } - } - } + } + CSPOT_LOG(debug, "trackref: %s", trackref->uri); + trackref--; + } + if (trackref->removed != NULL) { + if (tracks->size() == 1) + return responseFunction((void*)radio_offset); + else + return autoplayQuery(metadata_map, responseFunction); + } + CSPOT_LOG(debug, "Last track in tracklist: %s", trackref->uri); + if (!smartShuffledTrack || + playerState->options.context_enhancement_count) { + for (pageIndex = 0; pageIndex < jsonResult["pages"].size(); + pageIndex++) { + offset = 0; + for (auto track : jsonResult["pages"][pageIndex]["tracks"]) { + if (strcmp(track["uri"].get().c_str(), + trackref->uri) == 0) { + foundTrack = true; + break; } - copy_tracks = jsonToTracklist( - tracks, metadata_map, jsonResult["pages"][i]["tracks"], - "context", offset, i, playerState->options.shuffling_context, - true); - if (copy_tracks) + //??if(foundTrack) break; + if (foundTrack) break; + offset++; } + if (foundTrack) + break; } + //if trackreference was found } - if (!copy_tracks) { - if (this->playerState->options.repeating_context || !tracks->size()) { - if (*index >= tracks->size()) { - for (int i = 0; i < tracks->size(); i++) { - cspot::TrackReference::pbReleaseProvidedTrack(&tracks->at(i)); - } - tracks->clear(); - *index = 0; - } else - *index = 1; - createIndexBasedOnTracklist(tracks, jsonResult["pages"][0]["tracks"], - playerState->options.shuffling_context, - 0); - jsonToTracklist(tracks, metadata_map, - jsonResult["pages"][0]["tracks"], "context", 0, 0, - playerState->options.shuffling_context, false); - playerState->track = tracks->back(); + } + if (!foundTrack) { + pageIndex = 0; + offset = 0; + } + CSPOT_LOG(debug, "Context at page %i, offset %i", pageIndex, offset); + //delete tracks ? + //if tracklist is in a new state, create index based on tracklist + if (changed_state) { + createIndexBasedOnTracklist( + tracks, jsonResult["pages"][pageIndex]["tracks"], + playerState->options.shuffling_context, pageIndex); - if (*index >= tracks->size() && tracks->size()) { - ProvidedTrack new_track = ProvidedTrack_init_zero; - new_track.uri = strdup("spotify:delimiter"); - new_track.uid = strdup("uiddelimiter0"); - new_track.provider = strdup("context"); - new_track.removed = strdup("context/delimiter"); - new_track.metadata[new_track.metadata_count].key = strdup("hidden"); - new_track.metadata[new_track.metadata_count].value = strdup("true"); - new_track.metadata_count++; - new_track.metadata[new_track.metadata_count].key = - strdup("actions.skipping_next_past_track"); - new_track.metadata[new_track.metadata_count].value = - strdup("resume"); - new_track.metadata_count++; - new_track.metadata[new_track.metadata_count].key = - strdup("actions.advancing_past_track"); - new_track.metadata[new_track.metadata_count].value = - strdup("resume"); - new_track.metadata_count++; - new_track.metadata[new_track.metadata_count].key = - strdup("iteration"); - new_track.metadata[new_track.metadata_count].value = strdup("0"); - new_track.metadata_count++; - for (auto metadata : metadata_map) { - new_track.metadata[new_track.metadata_count].key = - strdup(metadata.first.c_str()); - new_track.metadata[new_track.metadata_count].value = - strdup(metadata.second.c_str()); - new_track.metadata_count++; + //if smart_shuffle is tur + if (playerState->options.shuffling_context) { + if (alternative_index[trackref - tracks->begin()] != offset) { + for (auto& index_ : alternative_index) + if (index_ == offset) { + index_ = alternative_index[trackref - tracks->begin()]; + alternative_index[trackref - tracks->begin()] = offset; + break; } - - tracks->insert(tracks->begin(), new_track); - } - } else if (trackIsPartOfContext) { - jsonToTracklist(tracks, metadata_map, - jsonResult["pages"][0]["tracks"], "context", - tracks->at(0).original_index + 1, 0, - playerState->options.shuffling_context, false); - - } else - return autoplayQuery(metadata_map, responseFunction); + } } - responseFunction(NULL); - }; - ctx->session->execute(MercurySession::RequestType::GET, requestUrl, - responseHandler); + } - } else - autoplayQuery(metadata_map, responseFunction); + // remove played tracks, keep 5 tracks in memory + if (*index > 5) { + cspot::TrackReference::deleteTracksInRange(tracks, 0, *index - 5); + *index = 4; + } + CSPOT_LOG( + debug, + "Current tracklist size: %i, loading tracklist from page %i, offset %i", + tracks->size(), pageIndex, offset); + + offset = jsonToTracklist( + tracks, metadata_map, jsonResult["pages"][pageIndex]["tracks"], + "context", offset, pageIndex, playerState->options.shuffling_context, + foundTrack); + if (offset > 1) { + CSPOT_LOG(debug, "Tracklist populated with %i tracks", offset); + return responseFunction(NULL); + } else if (playerState->options.repeating_context) { + jsonToTracklist(tracks, metadata_map, + jsonResult["pages"][pageIndex]["tracks"], "context", 0, + pageIndex, playerState->options.shuffling_context); + } else + return autoplayQuery(metadata_map, responseFunction); + }; + ctx->session->execute(MercurySession::RequestType::GET, requestUrl, + responseHandler); } \ No newline at end of file diff --git a/cspot/src/ShannonConnection.cpp b/cspot/src/ShannonConnection.cpp index ebfe091..5737dcf 100644 --- a/cspot/src/ShannonConnection.cpp +++ b/cspot/src/ShannonConnection.cpp @@ -16,7 +16,9 @@ using namespace cspot; ShannonConnection::ShannonConnection() {} -ShannonConnection::~ShannonConnection() {} +ShannonConnection::~ShannonConnection() { + std::scoped_lock lock(this->writeMutex, this->readMutex); +} void ShannonConnection::wrapConnection( std::shared_ptr conn, std::vector& sendKey, diff --git a/cspot/src/TrackPlayer.cpp b/cspot/src/TrackPlayer.cpp index c4a73e4..97f4edc 100644 --- a/cspot/src/TrackPlayer.cpp +++ b/cspot/src/TrackPlayer.cpp @@ -58,13 +58,15 @@ static long vorbisTellCb(TrackPlayer* self) { TrackPlayer::TrackPlayer(std::shared_ptr ctx, std::shared_ptr trackQueue, TrackEndedCallback onTrackEnd, - TrackChangedCallback onTrackChanged) + TrackChangedCallback onTrackChanged, + bool* repeating_track) : bell::Task("cspot_player", 48 * 1024, 5, 1) { this->ctx = ctx; this->onTrackEnd = onTrackEnd; this->onTrackChanged = onTrackChanged; this->trackQueue = trackQueue; this->playbackSemaphore = std::make_unique(5); + repeating_track_ = repeating_track; #ifndef CONFIG_BELL_NOCODEC // Initialize vorbis callbacks @@ -111,10 +113,11 @@ void TrackPlayer::resetState(bool paused) { CSPOT_LOG(info, "Resetting state"); } -void TrackPlayer::seekMs(size_t ms) { +void TrackPlayer::seekMs(size_t ms, bool loading) { #ifndef CONFIG_BELL_NOCODEC - if (inFuture) { + if (!loading) { // We're in the middle of the next track, so we need to reset the player in order to seek + CSPOT_LOG(info, "Resetting state"); resetState(); } #endif @@ -134,10 +137,8 @@ void TrackPlayer::runTask() { bool endOfQueueReached = false; while (isRunning) { - CSPOT_LOG(error, "new Track stream"); bool properStream = true; this->trackQueue->playableSemaphore->wait(); - CSPOT_LOG(error, "all good with stream"); // Last track was interrupted, reset to default if (pendingReset) { @@ -155,8 +156,8 @@ void TrackPlayer::runTask() { if (pendingReset) { continue; } - - newTrack = trackQueue->consumeTrack(track, trackOffset); + if (!*repeating_track_ || newTrack == nullptr) + newTrack = trackQueue->consumeTrack(track, trackOffset); if (newTrack == nullptr) { if (trackOffset == -1) { @@ -173,35 +174,40 @@ void TrackPlayer::runTask() { this->ctx->playbackMetrics->trackMetrics = track->trackMetrics; inFuture = trackOffset > 0; - + uint8_t retries = 10; while (track->state != QueuedTrack::State::READY && - track->state != QueuedTrack::State::FAILED) { + track->state != QueuedTrack::State::FAILED && retries-- > 0) { BELL_SLEEP_MS(100); - CSPOT_LOG(error, "track in state %i", (int)track->state); + CSPOT_LOG(error, "Track in state %i", (int)track->state); } - if (track->state == QueuedTrack::State::FAILED) { + if (track->state != QueuedTrack::State::READY) { CSPOT_LOG(error, "Track failed to load, skipping it"); + if (track->ref.removed != NULL) + this->onTrackChanged(track, false); this->onTrackEnd(true); continue; } - + track->playingTrackIndex = tracksPlayed; currentSongPlaying = true; track->trackMetrics->startTrack(); - + retries = 3; { std::scoped_lock lock(playbackMutex); bool skipped = 0; currentTrackStream = track->getAudioFile(); - // Open the stream #ifndef CONFIG_BELL_NOCODEC currentTrackStream->openStream(); #else - size_t start_offset = 0; + ssize_t start_offset = 0; uint8_t* headerBuf = currentTrackStream->openStream(start_offset); + if (start_offset < 0) { + CSPOT_LOG(error, "Track failed to load, skipping it"); + this->onTrackEnd(true); + continue; + } #endif - CSPOT_LOG(info, "opend stream"); if (pendingReset || !currentSongPlaying) { continue; } @@ -226,7 +232,6 @@ void TrackPlayer::runTask() { } track->written_bytes += start_offset; - CSPOT_LOG(info, "start offset at %i", start_offset); float duration_lambda = 1.0 * (currentTrackStream->getSize() - start_offset) / track->trackInfo.duration; @@ -285,10 +290,41 @@ void TrackPlayer::runTask() { #endif if (ret < 0) { - CSPOT_LOG(error, "An error has occured in the stream %d", ret); - currentSongPlaying = false; - properStream = false; - eof = true; + if (retries == 0) { + CSPOT_LOG(error, "Track failed to reload, skipping it"); + currentSongPlaying = false; + properStream = false; + eof = true; + } else { + CSPOT_LOG(error, "An error has occured in the stream %d", ret); + retries--; + CSPOT_LOG(error, "Retries left:%d", retries); +#ifndef CONFIG_BELL_NOCODEC + ov_clear(&vorbisFile); +#endif + size_t pos = this->currentTrackStream->getPosition(); + + this->currentTrackStream = nullptr; + this->currentTrackStream = track->getAudioFile(); + + // Open the stream +#ifndef CONFIG_BELL_NOCODEC + currentTrackStream->openStream(); + int32_t r = + ov_open_callbacks(this, &vorbisFile, NULL, 0, vorbisCallbacks); +#else + ssize_t start_offset = 0; + uint8_t* headerBuf = currentTrackStream->openStream(start_offset); + if (start_offset < 0) { + CSPOT_LOG(error, "Track failed to reload, skipping it"); + currentSongPlaying = false; + properStream = false; + eof = true; + + } else +#endif + this->currentTrackStream->seek(pos); + } } else { if (ret == 0) { CSPOT_LOG(info, "EOF"); diff --git a/cspot/src/TrackQueue.cpp b/cspot/src/TrackQueue.cpp index bd6b62b..98889fc 100644 --- a/cspot/src/TrackQueue.cpp +++ b/cspot/src/TrackQueue.cpp @@ -129,23 +129,22 @@ QueuedTrack::QueuedTrack( int64_t requestedPosition) : requestedPosition((uint32_t)requestedPosition), ctx(ctx) { trackMetrics = std::make_shared(ctx, requestedPosition); - loadedSemaphore = std::make_shared(); this->playableSemaphore = playableSemaphore; this->ref = ref; + this->audioFormat = ctx->config.audioFormat; if (!strstr(ref.uri, "spotify:delimiter")) { this->gid = base62Decode(ref.uri); state = State::QUEUED; } else { state = State::FAILED; playableSemaphore->give(); - loadedSemaphore->give(); } } QueuedTrack::~QueuedTrack() { + //if (state < State::READY) + // playableSemaphore->give(); state = State::FAILED; - loadedSemaphore->give(); - //playableSemaphore->give(); if (pendingMercuryRequest != 0) { ctx->session->unregister(pendingMercuryRequest); @@ -177,9 +176,6 @@ void QueuedTrack::stepParseMetadata(Track* pbTrack, Episode* pbEpisode) { 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)) { @@ -210,9 +206,6 @@ void QueuedTrack::stepParseMetadata(Track* pbTrack, Episode* pbEpisode) { 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, @@ -229,7 +222,7 @@ void QueuedTrack::stepParseMetadata(Track* pbTrack, Episode* pbEpisode) { // 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) { + if (selectedFiles[x].format == audioFormat) { fileId = pbArrayToVector(selectedFiles[x].file_id); break; // If file found stop searching } @@ -238,6 +231,7 @@ void QueuedTrack::stepParseMetadata(Track* pbTrack, Episode* pbEpisode) { if (fileId.size() == 0 && selectedFiles[x].format == AudioFormat_OGG_VORBIS_96) { fileId = pbArrayToVector(selectedFiles[x].file_id); + CSPOT_LOG(info, "Falling back to OGG Vorbis 96kbps"); } } @@ -247,7 +241,6 @@ void QueuedTrack::stepParseMetadata(Track* pbTrack, Episode* pbEpisode) { // no alternatives for song state = State::FAILED; - loadedSemaphore->give(); playableSemaphore->give(); return; } @@ -271,12 +264,21 @@ void QueuedTrack::stepLoadAudioFile( std::vector(audioKey.begin() + 4, audioKey.end()); state = State::CDN_REQUIRED; + updateSemaphore->give(); } else { CSPOT_LOG(error, "Failed to get audio key"); - state = State::FAILED; - playableSemaphore->give(); + retries++; + state = State::KEY_REQUIRED; + if (retries > 10) { + if (audioFormat > AudioFormat_OGG_VORBIS_96) { + audioFormat = (AudioFormat)(audioFormat - 1); + state = State::QUEUED; + } else { + state = State::FAILED; + playableSemaphore->give(); + } + } } - updateSemaphore->give(); }); state = State::PENDING_KEY; @@ -316,23 +318,13 @@ void QueuedTrack::stepLoadCDNUrl(const std::string& accessKey) { // 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(); } playableSemaphore->give(); } -void QueuedTrack::expire() { - if (state != State::QUEUED && state != State::FAILED) { - state = State::FAILED; - loadedSemaphore->give(); - playableSemaphore->give(); - } -} - void QueuedTrack::stepLoadMetadata( Track* pbTrack, Episode* pbEpisode, std::mutex& trackListMutex, std::shared_ptr updateSemaphore) { @@ -351,7 +343,6 @@ void QueuedTrack::stepLoadMetadata( // Invalid metadata, cannot proceed state = State::FAILED; updateSemaphore->give(); - loadedSemaphore->give(); playableSemaphore->give(); return; } @@ -380,7 +371,7 @@ void QueuedTrack::stepLoadMetadata( } TrackQueue::TrackQueue(std::shared_ptr ctx) - : bell::Task("CSpotTrackQueue", 1024 * 32, 2, 1), ctx(ctx) { + : bell::Task("CSpotTrackQueue", 1024 * 48, 2, 1), ctx(ctx) { accessKeyFetcher = std::make_shared(ctx); processSemaphore = std::make_shared(); playableSemaphore = std::make_shared(); @@ -417,11 +408,8 @@ void TrackQueue::runTask() { } for (auto& track : trackQueue) { - std::scoped_lock lock(tracksMutex); if (track) { - this->processTrack(track); - if (track->state != QueuedTrack::State::FAILED && - track->state != QueuedTrack::State::READY) + if (this->processTrack(track)) break; } } @@ -461,7 +449,7 @@ std::shared_ptr TrackQueue::consumeTrack( return preloadedTracks[offset]; } -void TrackQueue::processTrack(std::shared_ptr track) { +bool TrackQueue::processTrack(std::shared_ptr track) { switch (track->state) { case QueuedTrack::State::QUEUED: track->stepLoadMetadata(&track->pbTrack, &track->pbEpisode, tracksMutex, @@ -473,7 +461,9 @@ void TrackQueue::processTrack(std::shared_ptr track) { case QueuedTrack::State::CDN_REQUIRED: track->stepLoadCDNUrl(accessKey); default: + return false; // Do not perform any action break; } + return true; } diff --git a/targets/cli/main.cpp b/targets/cli/main.cpp index 536d959..58fe9f7 100644 --- a/targets/cli/main.cpp +++ b/targets/cli/main.cpp @@ -135,100 +135,109 @@ int main(int argc, char** argv) { isRunning = false; }; - try { - auto args = CommandLineArguments::parse(argc, argv); - if (args->shouldShowHelp) { - std::cout << "Usage: cspotcli [OPTION]...\n"; - std::cout << "Emulate a Spotify connect speaker.\n"; - std::cout << "\n"; - std::cout << "Run without any arguments to authenticate by using mDNS on " - "the local network (open the spotify app and CSpot should " - "appear as a device on the local network). \n"; - std::cout << "Alternatively you can specify a username and password to " - "login with."; - std::cout << "\n"; - std::cout << "-u, --username your spotify username\n"; - std::cout << "-p, --password your spotify password, note that " - "if you use facebook login you can set a password in your " - "account settings\n"; - std::cout << "-c, --credentials json file to store/load reusable " - "credentials\n"; - std::cout << "-b, --bitrate bitrate (320, 160, 96)\n"; - std::cout << "\n"; - std::cout << "ddd 2022\n"; - return 0; - } + while (true) { + try { + auto args = CommandLineArguments::parse(argc, argv); + if (args->shouldShowHelp) { + std::cout << "Usage: cspotcli [OPTION]...\n"; + std::cout << "Emulate a Spotify connect speaker.\n"; + std::cout << "\n"; + std::cout + << "Run without any arguments to authenticate by using mDNS on " + "the local network (open the spotify app and CSpot should " + "appear as a device on the local network). \n"; + std::cout << "Alternatively you can specify a username and password to " + "login with."; + std::cout << "\n"; + std::cout << "-u, --username your spotify username\n"; + std::cout + << "-p, --password your spotify password, note that " + "if you use facebook login you can set a password in your " + "account settings\n"; + std::cout + << "-c, --credentials json file to store/load reusable " + "credentials\n"; + std::cout << "-b, --bitrate bitrate (320, 160, 96)\n"; + std::cout << "\n"; + std::cout << "ddd 2022\n"; + return 0; + } - // Create a login blob, pass a device name - auto loginBlob = std::make_shared("CSpot player"); + // Create a login blob, pass a device name + auto loginBlob = std::make_shared("CSpot player"); - // Login using Command line arguments - if (!args->username.empty()) { - loginBlob->loadUserPass(args->username, args->password); - loggedInSemaphore->give(); - } - // reusable credentials - else if (!args->credentials.empty()) { - std::ifstream file(args->credentials); - std::ostringstream credentials; - credentials << file.rdbuf(); - loginBlob->loadJson(credentials.str()); - loggedInSemaphore->give(); - } - // ZeroconfAuthenticator - else { - zeroconfServer->blob = loginBlob; - zeroconfServer->onAuthSuccess = [loggedInSemaphore]() { + // Login using Command line arguments + if (!args->username.empty()) { + loginBlob->loadUserPass(args->username, args->password); loggedInSemaphore->give(); - }; - zeroconfServer->registerHandlers(); - } - - // Wait for authentication to complete - loggedInSemaphore->wait(); - auto ctx = cspot::Context::createFromBlob(loginBlob); - - // Apply preferences - if (args->setBitrate) { - ctx->config.audioFormat = args->bitrate; - } - - CSPOT_LOG(info, "Creating player"); - ctx->session->connectWithRandomAp(); - ctx->config.authData = ctx->session->authenticate(loginBlob); - - // Auth successful - if (ctx->config.authData.size() > 0) { - // when credentials file is set, then store reusable credentials - if (!args->credentials.empty()) { - std::ofstream file(args->credentials); - file << ctx->getCredentialsJson(); } - - // Start DeviceStateHandler - auto handler = std::make_shared(ctx); - - // Start handling mercury messages - ctx->session->startTask(); - - // Create a player, pass the handler - auto player = std::make_shared(std::move(audioSink), handler); - - // If we wanted to handle multiple devices, we would halt this loop - // when a new zeroconf login is requested, and reinitialize the session - while (isRunning) { - ctx->session->handlePacket(); + // reusable credentials + else if (!args->credentials.empty()) { + std::ifstream file(args->credentials); + std::ostringstream credentials; + credentials << file.rdbuf(); + loginBlob->loadJson(credentials.str()); + loggedInSemaphore->give(); } + // ZeroconfAuthenticator + else { + zeroconfServer->blob = loginBlob; + zeroconfServer->onAuthSuccess = [loggedInSemaphore]() { + loggedInSemaphore->give(); + }; + zeroconfServer->registerHandlers(); + } + while (true) { + // Wait for authentication to complete + loggedInSemaphore->wait(); + isRunning = true; + auto ctx = cspot::Context::createFromBlob(loginBlob); + + // Apply preferences + if (args->setBitrate) { + ctx->config.audioFormat = args->bitrate; + } - // Never happens, but required for above case - handler->disconnect(); - player->disconnect(); + CSPOT_LOG(info, "Creating player"); + ctx->session->connectWithRandomAp(); + ctx->config.authData = ctx->session->authenticate(loginBlob); + + // Auth successful + if (ctx->config.authData.size() > 0) { + // when credentials file is set, then store reusable credentials + if (!args->credentials.empty()) { + std::ofstream file(args->credentials); + file << ctx->getCredentialsJson(); + } + + // Start DeviceStateHandler + auto handler = std::make_shared( + ctx, zeroconfServer->onClose); + + // Start handling mercury messages + ctx->session->startTask(); + + // Create a player, pass the handler + auto player = + std::make_shared(std::move(audioSink), handler); + + // If we wanted to handle multiple devices, we would halt this loop + // when a new zeroconf login is requested, and reinitialize the session + while (isRunning) { + ctx->session->handlePacket(); + } + + // Never happens, but required for above case + handler->disconnect(); + player->disconnect(); + } + } + } catch (std::invalid_argument e) { + std::cout << "Invalid options passed: " << e.what() << "\n"; + std::cout << "Pass --help for more informaton. \n"; + continue; + //return 1; // we exit with an non-zero exit code } - - } catch (std::invalid_argument e) { - std::cout << "Invalid options passed: " << e.what() << "\n"; - std::cout << "Pass --help for more informaton. \n"; - return 1; // we exit with an non-zero exit code } return 0; diff --git a/targets/esp32/components/VS1053/include/VS1053.h b/targets/esp32/components/VS1053/include/VS1053.h index 41f5907..6326a18 100644 --- a/targets/esp32/components/VS1053/include/VS1053.h +++ b/targets/esp32/components/VS1053/include/VS1053.h @@ -77,7 +77,7 @@ class VS1053_TRACK { tsCancel = 5, tsCancelAwait = 6, tsStopped = 7 - } state = tsPlaybackStart; + } state = tsStopped; size_t header_size = 0; size_t track_id; StaticStreamBuffer_t xStaticStreamBuffer; diff --git a/targets/esp32/components/VS1053/src/VS1053.cpp b/targets/esp32/components/VS1053/src/VS1053.cpp index 66ac2c1..bf9151f 100644 --- a/targets/esp32/components/VS1053/src/VS1053.cpp +++ b/targets/esp32/components/VS1053/src/VS1053.cpp @@ -122,7 +122,7 @@ esp_err_t VS1053_SINK::init(spi_host_device_t SPI, load_user_code(PLUGIN, PLUGIN_SIZE); #endif vTaskDelay(100 / portTICK_PERIOD_MS); - xTaskCreate(vs_feed, "track_feed", 4098 * 4, (void*)this, 10, &task_handle); + xTaskCreate(vs_feed, "track_feed", 4098 * 4, (void*)this, 4, &task_handle); //xTaskCreatePinnedToCore(vs_feed, "track_feed", 1028 * 20, (void*)this, 1, &task_handle, 1); return ESP_OK; } @@ -189,6 +189,8 @@ bool VS1053_SINK::is_cancelled(VS1053_TRACK::VS_TRACK_STATE* state, void VS1053_SINK::delete_all_tracks(void) { if (this->tracks.size() > 1) this->tracks.erase(tracks.begin() + 1, tracks.end()); + if (!this->tracks.size()) + return; if (this->tracks[0]->state != VS1053_TRACK::VS_TRACK_STATE::tsStopped) new_state(this->tracks[0]->state, VS1053_TRACK::VS_TRACK_STATE::tsCancel); } @@ -499,7 +501,7 @@ void VS1053_SINK::load_user_code(const unsigned short* plugin, void VS1053_SINK::await_data_request() { while (!gpio_get_level((gpio_num_t)CONFIG_GPIO_VS_DREQ)) - taskYIELD(); + vTaskDelay(10 / portTICK_PERIOD_MS); } // WRITE/READ FUNCTIONS diff --git a/targets/esp32/main/CMakeLists.txt b/targets/esp32/main/CMakeLists.txt index 9fdd96d..ac3b4d3 100644 --- a/targets/esp32/main/CMakeLists.txt +++ b/targets/esp32/main/CMakeLists.txt @@ -1,23 +1,23 @@ cmake_minimum_required(VERSION 3.5) -# Main target sources -file(GLOB SOURCES "*.cpp" "*.c") +#Main target sources + file(GLOB SOURCES + "*.cpp" + "*.c") -# Configure the target -idf_component_register( - SRCS ${SOURCES} - INCLUDE_DIRS "." - REQUIRES mdns spiffs esp_wifi nvs_flash protocol_examples_common VS1053 fatfs -) -idf_build_set_property(COMPILE_OPTIONS "-fdiagnostics-color=always" APPEND) +#Configure the target + idf_component_register(SRCS ${SOURCES} INCLUDE_DIRS ".") + idf_build_set_property(COMPILE_OPTIONS + "-fdiagnostics-color=always" APPEND) -# Build static library, do not build test executables -option(BUILD_SHARED_LIBS OFF) -option(BUILD_TESTING OFF) +#Build static library, do not build test executables + option(BUILD_SHARED_LIBS OFF) option(BUILD_TESTING OFF) -# Import cspot library -add_subdirectory("../../../cspot" ${CMAKE_CURRENT_BINARY_DIR}/cspot) +#Import cspot library + add_subdirectory( + "../../../cspot" ${CMAKE_CURRENT_BINARY_DIR} / cspot) -# Configure the target -target_link_libraries(${COMPONENT_LIB} PUBLIC cspot) -target_compile_options(${COMPONENT_LIB} PRIVATE -std=gnu++17) +#Configure the target + target_link_libraries(${COMPONENT_LIB} PUBLIC cspot) + target_compile_options(${COMPONENT_LIB} PRIVATE - + std = gnu++ 17) diff --git a/targets/esp32/main/EspPlayer.cpp b/targets/esp32/main/EspPlayer.cpp index 365743f..66147c1 100644 --- a/targets/esp32/main/EspPlayer.cpp +++ b/targets/esp32/main/EspPlayer.cpp @@ -18,9 +18,9 @@ #include "StreamInfo.h" // for BitWidth, BitWidth::BW_16 #include "TrackPlayer.h" // for TrackPlayer -EspPlayer::EspPlayer(std::unique_ptr sink, +EspPlayer::EspPlayer(std::shared_ptr sink, std::shared_ptr handler) - : bell::Task("player", 32 * 1024, 0, 1) { + : bell::Task("player", 12 * 1024, 0, 1) { this->handler = handler; this->audioSink = std::move(sink); @@ -93,23 +93,16 @@ EspPlayer::EspPlayer(std::unique_ptr sink, void EspPlayer::feedData(uint8_t* data, size_t len, size_t trackId) { size_t toWrite = len; - if (!len) { - tracks.at(0)->trackMetrics->endTrack(); - this->handler->ctx->playbackMetrics->sendEvent(tracks[0]); - if (this->playlistEnd) { - tracks.clear(); + while (toWrite > 0) { + this->current_hash = trackId; + size_t written = + this->circularBuffer->write(data + (len - toWrite), toWrite); + if (written == 0) { + BELL_SLEEP_MS(10); } - } else - while (toWrite > 0) { - this->current_hash = trackId; - size_t written = - this->circularBuffer->write(data + (len - toWrite), toWrite); - if (written == 0) { - BELL_SLEEP_MS(10); - } - toWrite -= written; - } + toWrite -= written; + } } void EspPlayer::runTask() { @@ -130,15 +123,6 @@ void EspPlayer::runTask() { this->audioSink->feedPCMFrames(outBuf.data(), read); if (read == 0) { - if (this->playlistEnd) { - this->playlistEnd = false; - if (tracks.size()) { - tracks.at(0)->trackMetrics->endTrack(); - this->handler->ctx->playbackMetrics->sendEvent(tracks[0]); - tracks.clear(); - } - lastHash = 0; - } BELL_SLEEP_MS(10); continue; } else { @@ -146,7 +130,13 @@ void EspPlayer::runTask() { if (lastHash) { tracks.at(0)->trackMetrics->endTrack(); this->handler->ctx->playbackMetrics->sendEvent(tracks[0]); - tracks.pop_front(); + CSPOT_LOG(info, "TRACK ENDED, new track %i", current_hash); + if (tracks.size()) { + tracks.pop_front(); + if (this->playlistEnd) { + tracks.clear(); + } + } this->handler->trackPlayer->onTrackEnd(true); } lastHash = current_hash; diff --git a/targets/esp32/main/EspPlayer.h b/targets/esp32/main/EspPlayer.h index dfcc2bb..713f278 100644 --- a/targets/esp32/main/EspPlayer.h +++ b/targets/esp32/main/EspPlayer.h @@ -20,14 +20,14 @@ class DeviceStateHandler; class EspPlayer : public bell::Task { public: - EspPlayer(std::unique_ptr sink, + EspPlayer(std::shared_ptr sink, std::shared_ptr handler); void disconnect(); private: std::string currentTrackId; std::shared_ptr handler; - std::unique_ptr audioSink; + std::shared_ptr audioSink; std::shared_ptr circularBuffer; std::deque> tracks = {}; void feedData(uint8_t* data, size_t len, size_t); diff --git a/targets/esp32/main/Kconfig.projbuild b/targets/esp32/main/Kconfig.projbuild index cd416e5..977a9ba 100644 --- a/targets/esp32/main/Kconfig.projbuild +++ b/targets/esp32/main/Kconfig.projbuild @@ -67,6 +67,7 @@ menu "CSPOT Configuration" config CSPOT_STATUS_LED_TYPE_RMT bool "RMT - Addressable LED" endchoice + choice CSPOT_LOGIN prompt "Login type" default CSPOT_LOGIN_ZEROCONF @@ -81,7 +82,7 @@ menu "CSPOT Configuration" bool "Login with password" endchoice - menu "username & password" + menu "Username & password" visible if CSPOT_LOGIN_PASS config CSPOT_LOGIN_USERNAME string "Spotify username" @@ -93,6 +94,39 @@ menu "CSPOT Configuration" login with username and password endmenu + choice CSPOT_DISCOVERY_MODE + prompt "CSpot Device Visibility Mode" + default CSPOT_DISCOVERY_MODE_OPEN + + config CSPOT_DISCOVERY_MODE_OPEN + bool "Always visible in local network" + help + In this mode, the mDNS announcement remains active, making the device + visible to all devices on the local network, even while it is actively + connected and controlled. Playback rights can be transferred to another + device without disconnecting. + + config CSPOT_DISCOVERY_MODE_VISIBLE_ON_DISCONNECTED + bool "Visible only when disconnected" + help + In this mode, the mDNS announcement is disabled when the device is + actively connected and controlled. The device becomes visible on the + local network only after it is disconnected or playback rights are + transferred. + endchoice + + config CSPOT_STAY_CONNECTED_ON_TRANSFER + bool "Stay connected on playback transfer" + help + Enable this option to keep the CSpot device logged in and accessible during + a playback rights transfer. If enabled, the device will remain accessible + to other Spotify devices logged in with the same account after playback transfer. + + Note: If the device visibility is set to "Visible only when disconnected," + the mDNS announcement will remain disabled during playback and playback transfers. + The device will only announce itself on the local network after it is + disconnected or an explicit mDNS "/close" command is received. + config UPDATE_FUTURE_TRACKS int "Send tracks to spotify" range 0 100 diff --git a/targets/esp32/main/VSPlayer.cpp b/targets/esp32/main/VSPlayer.cpp index 2b81e30..dc4c29c 100644 --- a/targets/esp32/main/VSPlayer.cpp +++ b/targets/esp32/main/VSPlayer.cpp @@ -70,8 +70,11 @@ VSPlayer::VSPlayer(std::shared_ptr handler, case cspot::DeviceStateHandler::CommandType::FLUSH: this->track->empty_feed(); break; - //case cspot::DeviceStateHandler::CommandType::SEEK: - //break; + case cspot::DeviceStateHandler::CommandType::SKIP_NEXT: + [[fallthrough]]; + case cspot::DeviceStateHandler::CommandType::SKIP_PREV: + this->vsSink->stop_feed(); + break; case cspot::DeviceStateHandler::CommandType::PLAYBACK_START: if (this->track != nullptr) { this->track = nullptr; @@ -118,7 +121,7 @@ void VSPlayer::state_callback(uint8_t state) { } if (state == 7) { currentTrack->trackMetrics->endTrack(); - this->handler->ctx->playbackMetrics->sendEvent(currentTrack); + //this->handler->ctx->playbackMetrics->sendEvent(currentTrack); if (futureTrack != nullptr) { currentTrack = futureTrack; futureTrack = nullptr; diff --git a/targets/esp32/main/main.cpp b/targets/esp32/main/main.cpp index af2c305..9e3b39b 100644 --- a/targets/esp32/main/main.cpp +++ b/targets/esp32/main/main.cpp @@ -44,8 +44,8 @@ static EventGroupHandle_t s_wifi_event_group; #define WIFI_CONNECTED_BIT BIT0 #define WIFI_FAIL_BIT BIT1 -#define EXAMPLE_ESP_WIFI_SSID CONFIG_EXAMPLE_WIFI_SSID -#define EXAMPLE_ESP_WIFI_PASS CONFIG_EXAMPLE_WIFI_PASSWORD +//#define EXAMPLE_ESP_WIFI_SSID CONFIG_EXAMPLE_WIFI_SSID +//#define EXAMPLE_ESP_WIFI_PASS CONFIG_EXAMPLE_WIFI_PASSWORD #define WIFI_AP_MAXIMUM_RETRY 5 #define DEVICE_NAME CONFIG_CSPOT_DEVICE_NAME @@ -92,11 +92,18 @@ class ZeroconfAuthenticator { // Use bell's HTTP server to handle the authentication, although anything can be used std::unique_ptr server; + std::unique_ptr mdnsService; std::shared_ptr blob; std::function onAuthSuccess; std::function onClose; + void registerMdnsService() { + this->mdnsService = bell::MDNSService::registerService( + blob->getDeviceName(), "_spotify-connect", "_tcp", "", serverPort, + {{"VERSION", "1.0"}, {"CPath", "/spotify_info"}, {"Stack", "SP"}}); + } + void registerHandlers() { this->server = std::make_unique(serverPort); @@ -105,6 +112,7 @@ class ZeroconfAuthenticator { }); server->registerGet("/close", [this](struct mg_connection* conn) { + CSPOT_LOG(info, "Closing connection"); this->onClose(); return this->server->makeEmptyResponse(); }); @@ -130,23 +138,30 @@ class ZeroconfAuthenticator { for (int i = 0; i < num; i++) { queryMap[hd[i].name] = hd[i].value; } + if (1) { //queryMap["userName"] != blob->getUserName()) { - CSPOT_LOG(info, "Received zeroauth POST data"); + CSPOT_LOG(info, "Received zeroauth POST data"); - // Pass user's credentials to the blob - blob->loadZeroconfQuery(queryMap); + // Pass user's credentials to the blob + blob->loadZeroconfQuery(queryMap); - // We have the blob, proceed to login - onAuthSuccess(); + // We have the blob, proceed to login +#ifndef CONFIG_CSPOT_DISCOVERY_MODE_OPEN + mdnsService->unregisterService(); +#else + onClose(); +#endif + onAuthSuccess(); + } else { + CSPOT_LOG(debug, "User already logged in, skipping auth"); + } } return server->makeJsonResponse(obj.dump()); }); // Register mdns service, for spotify to find us - bell::MDNSService::registerService( - blob->getDeviceName(), "_spotify-connect", "_tcp", "", serverPort, - {{"VERSION", "1.0"}, {"CPath", "/spotify_info"}, {"Stack", "SP"}}); + this->registerMdnsService(); std::cout << "Waiting for spotify app to connect..." << std::endl; } }; @@ -155,11 +170,12 @@ class CSpotTask : public bell::Task { private: //std::unique_ptr handler; #ifndef CONFIG_BELL_NOCODEC - std::unique_ptr audioSink; + std::shared_ptr audioSink; #endif + std::unique_ptr zeroconfServer; public: - CSpotTask() : bell::Task("cspot", 32 * 1024, 0, 0) { + CSpotTask() : bell::Task("cspot", 16 * 1024, 0, 0) { startTask(); } void runTask() { @@ -172,22 +188,22 @@ class CSpotTask : public bell::Task { initAudioSink(audioSink); #else #ifdef CONFIG_CSPOT_SINK_INTERNAL - auto audioSink = std::make_unique(); + auto audioSink = std::make_shared(); #endif #ifdef CONFIG_CSPOT_SINK_AC101 - auto audioSink = std::make_unique(); + auto audioSink = std::make_shared(); #endif #ifdef CONFIG_CSPOT_SINK_ES8388 - auto audioSink = std::make_unique(); + auto audioSink = std::make_shared(); #endif #ifdef CONFIG_CSPOT_SINK_ES9018 - auto audioSink = std::make_unique(); + auto audioSink = std::make_shared(); #endif #ifdef CONFIG_CSPOT_SINK_PCM5102 - auto audioSink = std::make_unique(); + auto audioSink = std::make_shared(); #endif #ifdef CONFIG_CSPOT_SINK_TAS5711 - auto audioSink = std::make_unique(); + auto audioSink = std::make_shared(); #endif audioSink->setParams(44100, 2, 16); audioSink->volumeChanged(160); @@ -195,14 +211,22 @@ class CSpotTask : public bell::Task { auto loggedInSemaphore = std::make_shared(); - auto zeroconfServer = std::make_unique(); + std::atomic isRunningInDiscoveryMode = true; std::atomic isRunning = true; + this->zeroconfServer = std::make_unique(); + + auto loginBlob = std::make_shared(DEVICE_NAME); - zeroconfServer->onClose = [&isRunning]() { + this->zeroconfServer->onClose = [this, &isRunning, &loginBlob]() { isRunning = false; +#ifndef CONFIG_CSPOT_DISCOVERY_MODE_OPEN + CSPOT_LOG(info, "Waiting for spotify app to connect..."); + loginBlob = std::make_shared(DEVICE_NAME); + this->zeroconfServer->blob = loginBlob; + this->zeroconfServer->registerMdnsService(); +#endif }; - auto loginBlob = std::make_shared(DEVICE_NAME); #ifdef CONFIG_CSPOT_LOGIN_PASS loginBlob->loadUserPass(CONFIG_CSPOT_LOGIN_USERNAME, CONFIG_CSPOT_LOGIN_PASSWORD); @@ -210,48 +234,53 @@ class CSpotTask : public bell::Task { #else zeroconfServer->blob = loginBlob; - zeroconfServer->onAuthSuccess = [loggedInSemaphore]() { - loggedInSemaphore->give(); + zeroconfServer->onAuthSuccess = [loggedInSemaphore, &isRunning]() { + if (!isRunning) + loggedInSemaphore->give(); }; zeroconfServer->registerHandlers(); #endif - loggedInSemaphore->wait(); - auto ctx = cspot::Context::createFromBlob(loginBlob); - CSPOTConnecting:; - try { - ctx->session->connectWithRandomAp(); - ctx->config.authData = ctx->session->authenticate(loginBlob); - if (ctx->config.authData.size() > 0) { - // when credentials file is set, then store reusable credentials - - // Start device handler task - auto handler = std::make_shared(ctx); - - // Start handling mercury messages - ctx->session->startTask(); - - // Create a player, pass the handler + while (true) { + loggedInSemaphore->wait(); + isRunning = true; + auto ctx = cspot::Context::createFromBlob(loginBlob); + CSPOTConnecting:; + try { + ctx->session->connectWithRandomAp(); + ctx->config.authData = ctx->session->authenticate(loginBlob); + if (ctx->config.authData.size() > 0) { + // when credentials file is set, then store reusable credentials + + // Start device handler task + auto handler = std::make_shared( + ctx, zeroconfServer->onClose); + + // Start handling mercury messages + ctx->session->startTask(); + + // Create a player, pass the handler #ifndef CONFIG_BELL_NOCODEC - auto player = std::make_shared(std::move(audioSink), - std::move(handler)); + auto player = std::make_shared(audioSink, handler); #else - auto player = std::make_shared(std::move(handler), - std::move(audioSink)); + auto player = std::make_shared(handler, audioSink); #endif - // If we wanted to handle multiple devices, we would halt this loop - // when a new zeroconf login is requested, and reinitialize the session - uint8_t taskCount = 0; - while (isRunning) { - ctx->session->handlePacket(); + // If we wanted to handle multiple devices, we would halt this loop + // when a new zeroconf login is requested, and reinitialize the session + uint8_t taskCount = 0; + while (isRunning) { + ctx->session->handlePacket(); + } + + // Never happens, but required for above case + handler->disconnect(); + player->disconnect(); + } else { + std::cout << "Failed to authenticate" << std::endl; } - - // Never happens, but required for above case - handler->disconnect(); - player->disconnect(); + } catch (std::exception& e) { + std::cout << "Error while connecting " << e.what() << std::endl; + goto CSPOTConnecting; } - } catch (std::exception& e) { - std::cout << "Error while connecting " << e.what() << std::endl; - goto CSPOTConnecting; } } }; @@ -320,13 +349,11 @@ void wifi_init_sta(void) { WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, &instance_any_id)); ESP_ERROR_CHECK(esp_event_handler_instance_register( IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, &instance_got_ip)); - wifi_config_t wifi_config = { - .sta = - { - .ssid = EXAMPLE_ESP_WIFI_SSID, - .password = EXAMPLE_ESP_WIFI_PASS, - }, - }; + wifi_config_t wifi_config = {}; + strcpy(reinterpret_cast(wifi_config.sta.ssid), + CONFIG_EXAMPLE_WIFI_SSID); + strcpy(reinterpret_cast(wifi_config.sta.password), + CONFIG_EXAMPLE_WIFI_PASSWORD); ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); ESP_ERROR_CHECK(esp_wifi_start()); @@ -342,11 +369,11 @@ void wifi_init_sta(void) { /* xEventGroupWaitBits() returns the bits before the call returned, hence we can test which event actually * happened. */ if (bits & WIFI_CONNECTED_BIT) { - ESP_LOGI(TAG, "connected to ap SSID:%s password:%s", EXAMPLE_ESP_WIFI_SSID, - EXAMPLE_ESP_WIFI_PASS); + ESP_LOGI(TAG, "connected to ap SSID:%s password:%s", + CONFIG_EXAMPLE_WIFI_SSID, CONFIG_EXAMPLE_WIFI_SSID); } else if (bits & WIFI_FAIL_BIT) { ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s", - EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS); + CONFIG_EXAMPLE_WIFI_SSID, CONFIG_EXAMPLE_WIFI_SSID); } else { ESP_LOGE(TAG, "UNEXPECTED EVENT"); } diff --git a/targets/esp32/sdkconfig.defaults b/targets/esp32/sdkconfig.defaults index 2bd5878..3cd924f 100644 --- a/targets/esp32/sdkconfig.defaults +++ b/targets/esp32/sdkconfig.defaults @@ -1129,7 +1129,7 @@ CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES=y CONFIG_LWIP_TIMERS_ONDEMAND=y CONFIG_LWIP_MAX_SOCKETS=16 # CONFIG_LWIP_USE_ONLY_LWIP_SELECT is not set -# CONFIG_LWIP_SO_LINGER is not set +CONFIG_LWIP_SO_LINGER=y CONFIG_LWIP_SO_REUSE=y CONFIG_LWIP_SO_REUSE_RXTOALL=y # CONFIG_LWIP_SO_RCVBUF is not set