|  | @@ -30,10 +30,16 @@
 | 
	
		
			
				|  |  |  #include "cspot_private.h"
 | 
	
		
			
				|  |  |  #include "cspot_sink.h"
 | 
	
		
			
				|  |  |  #include "platform_config.h"
 | 
	
		
			
				|  |  | +#include "nvs_utilities.h"
 | 
	
		
			
				|  |  |  #include "tools.h"
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  static class cspotPlayer *player;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +static const struct {
 | 
	
		
			
				|  |  | +    const char *ns;
 | 
	
		
			
				|  |  | +    const char *credentials;
 | 
	
		
			
				|  |  | +} spotify_ns = { .ns = "spotify", .credentials = "credentials" };
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  /****************************************************************************************
 | 
	
		
			
				|  |  |   * Player's main class  & task
 | 
	
		
			
				|  |  |   */
 | 
	
	
		
			
				|  | @@ -42,7 +48,11 @@ class cspotPlayer : public bell::Task {
 | 
	
		
			
				|  |  |  private:
 | 
	
		
			
				|  |  |      std::string name;
 | 
	
		
			
				|  |  |      bell::WrappedSemaphore clientConnected;
 | 
	
		
			
				|  |  | -    std::atomic<bool> isPaused, isConnected;
 | 
	
		
			
				|  |  | +    std::atomic<bool> isPaused;
 | 
	
		
			
				|  |  | +    enum states { ABORT, LINKED, DISCO };
 | 
	
		
			
				|  |  | +    std::atomic<states> state;
 | 
	
		
			
				|  |  | +    std::string credentials;
 | 
	
		
			
				|  |  | +    bool zeroConf;
 | 
	
		
			
				|  |  |          
 | 
	
		
			
				|  |  |      int startOffset, volume = 0, bitrate = 160;
 | 
	
		
			
				|  |  |      httpd_handle_t serverHandle;
 | 
	
	
		
			
				|  | @@ -57,6 +67,7 @@ private:
 | 
	
		
			
				|  |  |      void eventHandler(std::unique_ptr<cspot::SpircHandler::Event> event);
 | 
	
		
			
				|  |  |      void trackHandler(void);
 | 
	
		
			
				|  |  |      size_t pcmWrite(uint8_t *pcm, size_t bytes, std::string_view trackId);
 | 
	
		
			
				|  |  | +    void enableZeroConf(void);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      void runTask();
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -79,8 +90,25 @@ cspotPlayer::cspotPlayer(const char* name, httpd_handle_t server, int port, cspo
 | 
	
		
			
				|  |  |      if ((item = cJSON_GetObjectItem(config, "volume")) != NULL) volume = item->valueint;
 | 
	
		
			
				|  |  |      if ((item = cJSON_GetObjectItem(config, "bitrate")) != NULL) bitrate = item->valueint;   
 | 
	
		
			
				|  |  |      if ((item = cJSON_GetObjectItem(config, "deviceName") ) != NULL) this->name = item->valuestring;
 | 
	
		
			
				|  |  | -    else this->name = name;
 | 
	
		
			
				|  |  | -    cJSON_Delete(config);
 | 
	
		
			
				|  |  | +    else this->name = name; 
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    if ((item = cJSON_GetObjectItem(config, "zeroConf")) != NULL) {
 | 
	
		
			
				|  |  | +        zeroConf = item->valueint;
 | 
	
		
			
				|  |  | +        cJSON_Delete(config);
 | 
	
		
			
				|  |  | +    } else {
 | 
	
		
			
				|  |  | +        zeroConf = true;
 | 
	
		
			
				|  |  | +        cJSON_AddNumberToObject(config, "zeroConf", 1);
 | 
	
		
			
				|  |  | +        config_set_cjson_str_and_free("cspot_config", config);
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    // get optional credentials from own NVS
 | 
	
		
			
				|  |  | +    if (!zeroConf) {
 | 
	
		
			
				|  |  | +        char *credentials = (char*) get_nvs_value_alloc_for_partition(NVS_DEFAULT_PART_NAME, spotify_ns.ns, NVS_TYPE_STR, spotify_ns.credentials, NULL);
 | 
	
		
			
				|  |  | +        if (credentials) {
 | 
	
		
			
				|  |  | +            this->credentials = credentials;
 | 
	
		
			
				|  |  | +            free(credentials); 
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      if (bitrate != 96 && bitrate != 160 && bitrate != 320) bitrate = 160;
 | 
	
		
			
				|  |  |  }
 | 
	
	
		
			
				|  | @@ -207,7 +235,7 @@ void cspotPlayer::eventHandler(std::unique_ptr<cspot::SpircHandler::Event> event
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |      case cspot::SpircHandler::EventType::DISC:
 | 
	
		
			
				|  |  |          cmdHandler(CSPOT_DISC);
 | 
	
		
			
				|  |  | -        isConnected = false;
 | 
	
		
			
				|  |  | +        state = DISCO;
 | 
	
		
			
				|  |  |          break;
 | 
	
		
			
				|  |  |      case cspot::SpircHandler::EventType::SEEK: {
 | 
	
		
			
				|  |  |          cmdHandler(CSPOT_SEEK, std::get<int>(event->data));
 | 
	
	
		
			
				|  | @@ -265,7 +293,7 @@ void cspotPlayer::command(cspot_event_t event) {
 | 
	
		
			
				|  |  |       * generate any cspot::event */
 | 
	
		
			
				|  |  |      case CSPOT_DISC:
 | 
	
		
			
				|  |  |          cmdHandler(CSPOT_DISC);
 | 
	
		
			
				|  |  | -        isConnected = false;
 | 
	
		
			
				|  |  | +        state = ABORT;
 | 
	
		
			
				|  |  |          break;
 | 
	
		
			
				|  |  |      // spirc->setRemoteVolume does not generate a cspot::event so call cmdHandler
 | 
	
		
			
				|  |  |      case CSPOT_VOLUME_UP:
 | 
	
	
		
			
				|  | @@ -285,34 +313,48 @@ void cspotPlayer::command(cspot_event_t event) {
 | 
	
		
			
				|  |  |  	}
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -void cspotPlayer::runTask() {
 | 
	
		
			
				|  |  | +void cspotPlayer::enableZeroConf(void) {
 | 
	
		
			
				|  |  |      httpd_uri_t request = {
 | 
	
		
			
				|  |  |  		.uri = "/spotify_info",
 | 
	
		
			
				|  |  |  		.method = HTTP_GET,
 | 
	
		
			
				|  |  |  		.handler = ::handleGET,
 | 
	
		
			
				|  |  |  		.user_ctx = NULL,
 | 
	
		
			
				|  |  | -	};
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | +    };
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  |      // register GET and POST handler for built-in server
 | 
	
		
			
				|  |  |      httpd_register_uri_handler(serverHandle, &request);
 | 
	
		
			
				|  |  |      request.method = HTTP_POST;
 | 
	
		
			
				|  |  |      request.handler = ::handlePOST;
 | 
	
		
			
				|  |  |      httpd_register_uri_handler(serverHandle, &request);
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    // construct blob for that player
 | 
	
		
			
				|  |  | -    blob = std::make_unique<cspot::LoginBlob>(name);
 | 
	
		
			
				|  |  | +        
 | 
	
		
			
				|  |  | +    CSPOT_LOG(info, "ZeroConf mode (port %d)", serverPort);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      // 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"} });
 | 
	
		
			
				|  |  | -            
 | 
	
		
			
				|  |  | +                                       { {"VERSION", "1.0"}, {"CPath", "/spotify_info"}, {"Stack", "SP"} });    
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | + 
 | 
	
		
			
				|  |  | +void cspotPlayer::runTask() {
 | 
	
		
			
				|  |  | +    bool useZeroConf = zeroConf;
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    // construct blob for that player
 | 
	
		
			
				|  |  | +    blob = std::make_unique<cspot::LoginBlob>(name);
 | 
	
		
			
				|  |  | +                  
 | 
	
		
			
				|  |  |      CSPOT_LOG(info, "CSpot instance service name %s (id %s)", blob->getDeviceName().c_str(), blob->getDeviceId().c_str());
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    if (!zeroConf && !credentials.empty()) {
 | 
	
		
			
				|  |  | +        blob->loadJson(credentials);
 | 
	
		
			
				|  |  | +        CSPOT_LOG(info, "Reusable credentials mode");
 | 
	
		
			
				|  |  | +    } else {      
 | 
	
		
			
				|  |  | +        // whether we want it or not we must use ZeroConf
 | 
	
		
			
				|  |  | +        useZeroConf = true;
 | 
	
		
			
				|  |  | +        enableZeroConf();
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      // gone with the wind...
 | 
	
		
			
				|  |  |      while (1) {
 | 
	
		
			
				|  |  | -        clientConnected.wait();
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        CSPOT_LOG(info, "Spotify client connected for %s", name.c_str());
 | 
	
		
			
				|  |  | +        if (useZeroConf) clientConnected.wait();
 | 
	
		
			
				|  |  | +        CSPOT_LOG(info, "Spotify client launched for %s", name.c_str());
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          auto ctx = cspot::Context::createFromBlob(blob);
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -321,12 +363,26 @@ void cspotPlayer::runTask() {
 | 
	
		
			
				|  |  |          else ctx->config.audioFormat = AudioFormat_OGG_VORBIS_160;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          ctx->session->connectWithRandomAp();
 | 
	
		
			
				|  |  | -        auto token = ctx->session->authenticate(blob);
 | 
	
		
			
				|  |  | +        ctx->config.authData = ctx->session->authenticate(blob);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          // Auth successful
 | 
	
		
			
				|  |  | -        if (token.size() > 0) {
 | 
	
		
			
				|  |  | +        if (ctx->config.authData.size() > 0) {
 | 
	
		
			
				|  |  | +            // we might have been forced to use zeroConf, so store credentials and reset zeroConf usage
 | 
	
		
			
				|  |  | +            if (!zeroConf) {
 | 
	
		
			
				|  |  | +                useZeroConf = false;
 | 
	
		
			
				|  |  | +                // can't call store_nvs... from a task running on EXTRAM stack
 | 
	
		
			
				|  |  | +                TimerHandle_t timer = xTimerCreate( "credentials", 1, pdFALSE, strdup(ctx->getCredentialsJson().c_str()),
 | 
	
		
			
				|  |  | +                            [](TimerHandle_t xTimer) {
 | 
	
		
			
				|  |  | +                                auto credentials = (char*) pvTimerGetTimerID(xTimer);
 | 
	
		
			
				|  |  | +                                store_nvs_value_len_for_partition(NVS_DEFAULT_PART_NAME, spotify_ns.ns, NVS_TYPE_STR, spotify_ns.credentials, credentials, 0);
 | 
	
		
			
				|  |  | +                                free(credentials);
 | 
	
		
			
				|  |  | +                                xTimerDelete(xTimer, portMAX_DELAY);
 | 
	
		
			
				|  |  | +                            } );
 | 
	
		
			
				|  |  | +                xTimerStart(timer, portMAX_DELAY);
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |              spirc = std::make_unique<cspot::SpircHandler>(ctx);
 | 
	
		
			
				|  |  | -            isConnected = true;            
 | 
	
		
			
				|  |  | +            state = LINKED;
 | 
	
		
			
				|  |  |  			
 | 
	
		
			
				|  |  |              // set call back to calculate a hash on trackId
 | 
	
		
			
				|  |  |              spirc->getTrackPlayer()->setDataCallback(
 | 
	
	
		
			
				|  | @@ -347,7 +403,7 @@ void cspotPlayer::runTask() {
 | 
	
		
			
				|  |  |              cmdHandler(CSPOT_VOLUME, volume);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              // exit when player has stopped (received a DISC)
 | 
	
		
			
				|  |  | -            while (isConnected) {
 | 
	
		
			
				|  |  | +            while (state == LINKED) {
 | 
	
		
			
				|  |  |                  ctx->session->handlePacket();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |                  // low-accuracy polling events
 | 
	
	
		
			
				|  | @@ -371,23 +427,32 @@ void cspotPlayer::runTask() {
 | 
	
		
			
				|  |  |                          spirc->setPause(true);
 | 
	
		
			
				|  |  |                      }
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  | +                
 | 
	
		
			
				|  |  | +                // on disconnect, stay in the core loop unless we are in ZeroConf mode
 | 
	
		
			
				|  |  | +                if (state == DISCO) {
 | 
	
		
			
				|  |  | +                    // update volume then
 | 
	
		
			
				|  |  | +                    cJSON *config = config_alloc_get_cjson("cspot_config");
 | 
	
		
			
				|  |  | +                    cJSON_DeleteItemFromObject(config, "volume");
 | 
	
		
			
				|  |  | +                    cJSON_AddNumberToObject(config, "volume", volume);
 | 
	
		
			
				|  |  | +                    config_set_cjson_str_and_free("cspot_config", config);
 | 
	
		
			
				|  |  | +                    
 | 
	
		
			
				|  |  | +                    // in ZeroConf mod, stay connected (in this loop)
 | 
	
		
			
				|  |  | +                    if (!zeroConf) state = LINKED;
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              spirc->disconnect();
 | 
	
		
			
				|  |  |              spirc.reset();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              CSPOT_LOG(info, "disconnecting player %s", name.c_str());
 | 
	
		
			
				|  |  | +        } else {
 | 
	
		
			
				|  |  | +            CSPOT_LOG(error, "failed authentication, forcing ZeroConf");
 | 
	
		
			
				|  |  | +            if (!useZeroConf) enableZeroConf();
 | 
	
		
			
				|  |  | +            useZeroConf = true;
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | +            
 | 
	
		
			
				|  |  |          // we want to release memory ASAP and for sure
 | 
	
		
			
				|  |  | -        ctx.reset();
 | 
	
		
			
				|  |  | -        token.clear();
 | 
	
		
			
				|  |  | -        
 | 
	
		
			
				|  |  | -        // update volume when we disconnect
 | 
	
		
			
				|  |  | -        cJSON *config = config_alloc_get_cjson("cspot_config");
 | 
	
		
			
				|  |  | -        cJSON_DeleteItemFromObject(config, "volume");
 | 
	
		
			
				|  |  | -        cJSON_AddNumberToObject(config, "volume", volume);
 | 
	
		
			
				|  |  | -        config_set_cjson_str_and_free("cspot_config", config);
 | 
	
		
			
				|  |  | +        ctx.reset();      
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 |