Keep-alive like librespot (core/src/session.rs): the server sends a Ping every ~2 min; send the Pong 60 s later, not at once. With an immediate Pong the server reset the AP connection (ECONNRESET) on the third Ping, i.e. every 6 minutes, and the Spotify app lost the device. --- a/cspot/include/MercurySession.h +++ b/cspot/include/MercurySession.h @@ -120,6 +120,14 @@ unsigned long long timestampDiff; unsigned long long lastPingTimestamp = -1; + + // Keep-alive as librespot does it: Ping -> wait 60 s -> Pong -> PongAck. Answering at once made + // the server reset the connection on the third Ping (every 6 minutes). + const int PONG_DELAY_MS = 60 * 1000; + bool pongPending = false; + unsigned long long pongDueTimestamp = 0; + std::vector pongData; + void sendPendingPong(); std::string countryCode = ""; std::mutex isRunningMutex; --- a/cspot/src/MercurySession.cpp +++ b/cspot/src/MercurySession.cpp @@ -45,7 +45,10 @@ timeProvider->syncWithPingPacket(packet.data); this->lastPingTimestamp = timeProvider->getSyncedTimestamp(); - this->shanConn->sendPacket(0x49, packet.data); + // Pong 60 s later (sent from triggerTimeout(), called on every 3 s receive timeout) + this->pongData = packet.data; + this->pongDueTimestamp = this->lastPingTimestamp + PONG_DELAY_MS; + this->pongPending = true; } else { this->packetQueue.push(packet); } @@ -80,6 +83,7 @@ BELL_SLEEP_MS(100); lastPingTimestamp = timeProvider->getSyncedTimestamp(); + pongPending = false; // a Ping on the old connection needs no Pong on the new one isReconnecting = false; this->executeEstabilishedCallback = true; @@ -98,9 +102,30 @@ this->connectionReadyCallback = callback; } +void MercurySession::sendPendingPong() { + if (!pongPending || + timeProvider->getSyncedTimestamp() < pongDueTimestamp) + return; + pongPending = false; + std::shared_ptr shanConn; + { + std::scoped_lock lock(connMutex); + shanConn = this->shanConn; + } + if (!shanConn) + return; + try { + shanConn->sendPacket(0x49, pongData); + CSPOT_LOG(info, "Sent delayed Pong"); + } catch (...) { + CSPOT_LOG(error, "Failed to send Pong"); + } +} + bool MercurySession::triggerTimeout() { if (!isRunning) return true; + sendPendingPong(); auto currentTimestamp = timeProvider->getSyncedTimestamp(); if (currentTimestamp - this->lastPingTimestamp > PING_TIMEOUT_MS) {