#include "SpircHandler.h" #include // for uint8_t #include // for shared_ptr, make_unique, unique_ptr #include // for remove_extent_t #include // for move #include "BellLogger.h" // for AbstractLogger #include "CSpotContext.h" // for Context::ConfigState, Context (ptr only) #include "Logger.h" // for CSPOT_LOG #include "MercurySession.h" // for MercurySession, MercurySession::Response #include "NanoPBHelper.h" // for pbDecode #include "Packet.h" // for cspot #include "PlaybackState.h" // for PlaybackState, PlaybackState::State #include "TrackPlayer.h" // for TrackPlayer #include "TrackQueue.h" #include "TrackReference.h" // for TrackReference #include "Utils.h" // for stringHexToBytes #include "pb_decode.h" // for pb_release #include "protobuf/spirc.pb.h" // for Frame, State, Frame_fields, MessageTy... using namespace cspot; SpircHandler::SpircHandler(std::shared_ptr ctx) { this->playbackState = std::make_shared(ctx); this->trackQueue = std::make_shared(ctx, playbackState); auto EOFCallback = [this]() { if (trackQueue->isFinished()) { sendEvent(EventType::DEPLETED); } }; auto trackLoadedCallback = [this](std::shared_ptr track, bool paused = false) { playbackState->setPlaybackState(paused ? PlaybackState::State::Paused : PlaybackState::State::Playing); playbackState->updatePositionMs(track->requestedPosition); this->notify(); // Send playback start event, pause/unpause per request sendEvent(EventType::PLAYBACK_START, (int)track->requestedPosition); sendEvent(EventType::PLAY_PAUSE, paused); }; this->ctx = ctx; this->trackPlayer = std::make_shared( ctx, trackQueue, EOFCallback, trackLoadedCallback); // Subscribe to mercury on session ready ctx->session->setConnectedHandler([this]() { this->subscribeToMercury(); }); } void SpircHandler::subscribeToMercury() { auto responseLambda = [this](MercurySession::Response& res) { if (res.fail) return; sendCmd(MessageType_kMessageTypeHello); CSPOT_LOG(debug, "Sent kMessageTypeHello!"); // Assign country code this->ctx->config.countryCode = this->ctx->session->getCountryCode(); }; auto subscriptionLambda = [this](MercurySession::Response& res) { if (res.fail) return; CSPOT_LOG(debug, "Received subscription response"); this->handleFrame(res.parts[0]); }; ctx->session->executeSubscription( MercurySession::RequestType::SUB, "hm://remote/user/" + ctx->config.username + "/", responseLambda, subscriptionLambda); } void SpircHandler::loadTrackFromURI(const std::string& uri) {} void SpircHandler::notifyAudioEnded() { playbackState->updatePositionMs(0); notify(); trackPlayer->resetState(true); } void SpircHandler::notifyAudioReachedPlayback() { int offset = 0; // get HEAD track auto currentTrack = trackQueue->consumeTrack(nullptr, offset); // Do not execute when meta is already updated if (trackQueue->notifyPending) { trackQueue->notifyPending = false; playbackState->updatePositionMs(currentTrack->requestedPosition); // Reset position in queued track currentTrack->requestedPosition = 0; } else { trackQueue->skipTrack(TrackQueue::SkipDirection::NEXT, false); playbackState->updatePositionMs(0); // we moved to next track, re-acquire currentTrack again currentTrack = trackQueue->consumeTrack(nullptr, offset); } this->notify(); sendEvent(EventType::TRACK_INFO, currentTrack->trackInfo); } void SpircHandler::updatePositionMs(uint32_t position) { playbackState->updatePositionMs(position); notify(); } void SpircHandler::disconnect() { this->trackQueue->stopTask(); this->trackPlayer->stop(); this->ctx->session->disconnect(); } void SpircHandler::handleFrame(std::vector& data) { // Decode received spirc frame playbackState->decodeRemoteFrame(data); switch (playbackState->remoteFrame.typ) { case MessageType_kMessageTypeNotify: { CSPOT_LOG(debug, "Notify frame"); // Pause the playback if another player took control if (playbackState->isActive() && playbackState->remoteFrame.device_state.is_active) { CSPOT_LOG(debug, "Another player took control, pausing playback"); playbackState->setActive(false); this->trackPlayer->stop(); sendEvent(EventType::DISC); } break; } case MessageType_kMessageTypeSeek: { this->trackPlayer->seekMs(playbackState->remoteFrame.position); playbackState->updatePositionMs(playbackState->remoteFrame.position); notify(); sendEvent(EventType::SEEK, (int)playbackState->remoteFrame.position); break; } case MessageType_kMessageTypeVolume: playbackState->setVolume(playbackState->remoteFrame.volume); this->notify(); sendEvent(EventType::VOLUME, (int)playbackState->remoteFrame.volume); break; case MessageType_kMessageTypePause: setPause(true); break; case MessageType_kMessageTypePlay: setPause(false); break; case MessageType_kMessageTypeNext: if (nextSong()) { sendEvent(EventType::NEXT); } break; case MessageType_kMessageTypePrev: if (previousSong()) { sendEvent(EventType::PREV); } break; case MessageType_kMessageTypeLoad: { this->trackPlayer->start(); CSPOT_LOG(debug, "Load frame %d!", playbackState->remoteTracks.size()); if (playbackState->remoteTracks.size() == 0) { CSPOT_LOG(info, "No tracks in frame, stopping playback"); break; } playbackState->setActive(true); playbackState->updatePositionMs(playbackState->remoteFrame.position); playbackState->setPlaybackState(PlaybackState::State::Playing); playbackState->syncWithRemote(); // Update track list in case we have a new one trackQueue->updateTracks(playbackState->remoteFrame.state.position_ms, true); this->notify(); // Stop the current track, if any trackPlayer->resetState(); break; } case MessageType_kMessageTypeReplace: { CSPOT_LOG(debug, "Got replace frame %d", playbackState->remoteTracks.size()); playbackState->syncWithRemote(); // 1st track is the current one, but update the position bool cleared = trackQueue->updateTracks( playbackState->remoteFrame.state.position_ms + ctx->timeProvider->getSyncedTimestamp() - playbackState->innerFrame.state.position_measured_at); this->notify(); // need to re-load all if streaming track is completed if (cleared) { sendEvent(EventType::FLUSH); trackPlayer->resetState(); } break; } case MessageType_kMessageTypeShuffle: { CSPOT_LOG(debug, "Got shuffle frame"); this->notify(); break; } case MessageType_kMessageTypeRepeat: { CSPOT_LOG(debug, "Got repeat frame"); this->notify(); break; } default: break; } } void SpircHandler::setRemoteVolume(int volume) { playbackState->setVolume(volume); notify(); } void SpircHandler::notify() { this->sendCmd(MessageType_kMessageTypeNotify); } bool SpircHandler::skipSong(TrackQueue::SkipDirection dir) { bool skipped = trackQueue->skipTrack(dir); // Reset track state trackPlayer->resetState(!skipped); // send NEXT or PREV event only when successful return skipped; } bool SpircHandler::nextSong() { return skipSong(TrackQueue::SkipDirection::NEXT); } bool SpircHandler::previousSong() { return skipSong(TrackQueue::SkipDirection::PREV); } std::shared_ptr SpircHandler::getTrackPlayer() { return this->trackPlayer; } void SpircHandler::sendCmd(MessageType typ) { // Serialize current player state auto encodedFrame = playbackState->encodeCurrentFrame(typ); auto responseLambda = [=](MercurySession::Response& res) { }; auto parts = MercurySession::DataParts({encodedFrame}); ctx->session->execute(MercurySession::RequestType::SEND, "hm://remote/user/" + ctx->config.username + "/", responseLambda, parts); } void SpircHandler::setEventHandler(EventHandler handler) { this->eventHandler = handler; } void SpircHandler::setPause(bool isPaused) { if (isPaused) { CSPOT_LOG(debug, "External pause command"); playbackState->setPlaybackState(PlaybackState::State::Paused); } else { CSPOT_LOG(debug, "External play command"); playbackState->setPlaybackState(PlaybackState::State::Playing); } notify(); sendEvent(EventType::PLAY_PAUSE, isPaused); } void SpircHandler::sendEvent(EventType type) { auto event = std::make_unique(); event->eventType = type; event->data = {}; eventHandler(std::move(event)); } void SpircHandler::sendEvent(EventType type, EventData data) { auto event = std::make_unique(); event->eventType = type; event->data = data; eventHandler(std::move(event)); }