2
0

Shim.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. /*
  2. * This software is released under the MIT License.
  3. * https://opensource.org/licenses/MIT
  4. *
  5. */
  6. #include <string>
  7. #include <streambuf>
  8. #include <Session.h>
  9. #include <PlainConnection.h>
  10. #include <memory>
  11. #include <vector>
  12. #include <iostream>
  13. #include <inttypes.h>
  14. #include <fstream>
  15. #include <stdarg.h>
  16. #include <ApResolve.h>
  17. #include "MDNSService.h"
  18. #include "SpircHandler.h"
  19. #include "LoginBlob.h"
  20. #include "CentralAudioBuffer.h"
  21. #include "Logger.h"
  22. #include "Utils.h"
  23. #include "esp_http_server.h"
  24. #include "cspot_private.h"
  25. #include "cspot_sink.h"
  26. #include "platform_config.h"
  27. #include "tools.h"
  28. //#include "time.h"
  29. /*
  30. #include <stdio.h>
  31. #include <string.h>
  32. #include <inttypes.h>
  33. #include "sdkconfig.h"
  34. #include "freertos/FreeRTOS.h"
  35. #include "freertos/task.h"
  36. #include "esp_system.h"
  37. #include "esp_wifi.h"
  38. #include "esp_event.h"
  39. #include "esp_log.h"
  40. #include "esp_http_server.h"
  41. #include <ConstantParameters.h>
  42. #include <Session.h>
  43. #include <SpircController.h>
  44. #include <MercuryManager.h>
  45. #include <ZeroconfAuthenticator.h>
  46. #include <ApResolve.h>
  47. #include <HTTPServer.h>
  48. #include "ConfigJSON.h"
  49. #include "Logger.h"
  50. #include "platform_config.h"
  51. #include "tools.h"
  52. #include "cspot_private.h"
  53. #include "cspot_sink.h"
  54. */
  55. static const char *TAG = "cspot";
  56. class cspotPlayer *player;
  57. /****************************************************************************************
  58. * Chunk manager class (task)
  59. */
  60. class chunkManager : public bell::Task {
  61. public:
  62. std::atomic<bool> isRunning = true;
  63. std::atomic<bool> isPaused = true;
  64. chunkManager(std::shared_ptr<bell::CentralAudioBuffer> centralAudioBuffer, std::function<void()> trackHandler, std::function<void(const uint8_t*, size_t)> audioHandler);
  65. void teardown();
  66. private:
  67. std::shared_ptr<bell::CentralAudioBuffer> centralAudioBuffer;
  68. std::function<void()> trackHandler;
  69. std::function<void(const uint8_t*, size_t)> audioHandler;
  70. std::mutex runningMutex;
  71. void runTask() override;
  72. };
  73. chunkManager::chunkManager(std::shared_ptr<bell::CentralAudioBuffer> centralAudioBuffer,
  74. std::function<void()> trackHandler, std::function<void(const uint8_t*, size_t)> audioHandler)
  75. : bell::Task("player", 4 * 1024, 0, 0) {
  76. this->centralAudioBuffer = centralAudioBuffer;
  77. this->trackHandler = trackHandler;
  78. this->audioHandler = audioHandler;
  79. startTask();
  80. }
  81. void chunkManager::runTask() {
  82. std::scoped_lock lock(runningMutex);
  83. size_t lastHash = 0;
  84. while (isRunning) {
  85. if (isPaused) {
  86. BELL_SLEEP_MS(100);
  87. continue;
  88. }
  89. auto chunk = centralAudioBuffer->readChunk();
  90. if (!chunk || chunk->pcmSize == 0) {
  91. BELL_SLEEP_MS(50);
  92. continue;
  93. }
  94. // receiving first chunk of new track from Spotify server
  95. if (lastHash != chunk->trackHash) {
  96. CSPOT_LOG(info, "hash update %x => %x", lastHash, chunk->trackHash);
  97. lastHash = chunk->trackHash;
  98. trackHandler();
  99. }
  100. audioHandler(chunk->pcmData, chunk->pcmSize);
  101. }
  102. }
  103. void chunkManager::teardown() {
  104. isRunning = false;
  105. std::scoped_lock lock(runningMutex);
  106. }
  107. /****************************************************************************************
  108. * Player's main class & task
  109. */
  110. class cspotPlayer : public bell::Task {
  111. private:
  112. std::string name;
  113. bool playback = false;
  114. bell::WrappedSemaphore clientConnected;
  115. std::shared_ptr<bell::CentralAudioBuffer> centralAudioBuffer;
  116. TimerHandle_t trackTimer;
  117. int startOffset, volume = 0, bitrate = 160;
  118. httpd_handle_t serverHandle;
  119. int serverPort;
  120. cspot_cmd_cb_t cmdHandler;
  121. cspot_data_cb_t dataHandler;
  122. std::shared_ptr<cspot::LoginBlob> blob;
  123. std::unique_ptr<cspot::SpircHandler> spirc;
  124. std::unique_ptr<chunkManager> chunker;
  125. void eventHandler(std::unique_ptr<cspot::SpircHandler::Event> event);
  126. void trackHandler(void);
  127. void runTask();
  128. public:
  129. std::atomic<bool> trackNotify = false;
  130. cspotPlayer(const char*, httpd_handle_t, int, cspot_cmd_cb_t, cspot_data_cb_t);
  131. ~cspotPlayer();
  132. esp_err_t handleGET(httpd_req_t *request);
  133. esp_err_t handlePOST(httpd_req_t *request);
  134. };
  135. cspotPlayer::cspotPlayer(const char* name, httpd_handle_t server, int port, cspot_cmd_cb_t cmdHandler, cspot_data_cb_t dataHandler) :
  136. bell::Task("playerInstance", 32 * 1024, 0, 0),
  137. serverHandle(server), serverPort(port),
  138. cmdHandler(cmdHandler), dataHandler(dataHandler) {
  139. cJSON *item, *config = config_alloc_get_cjson("cspot_config");
  140. if ((item = cJSON_GetObjectItem(config, "volume")) != NULL) volume = item->valueint;
  141. if ((item = cJSON_GetObjectItem(config, "bitrate")) != NULL) bitrate = item->valueint;
  142. if ((item = cJSON_GetObjectItem(config, "deviceName") ) != NULL) this->name = item->valuestring;
  143. else this->name = name;
  144. cJSON_Delete(config);
  145. if (bitrate != 96 && bitrate != 160 && bitrate != 320) bitrate = 160;
  146. }
  147. cspotPlayer::~cspotPlayer() {
  148. }
  149. extern "C" {
  150. static esp_err_t handleGET(httpd_req_t *request) {
  151. return player->handleGET(request);
  152. }
  153. static esp_err_t handlePOST(httpd_req_t *request) {
  154. return player->handlePOST(request);
  155. }
  156. static void trackTimerHandler(TimerHandle_t xTimer) {
  157. player->trackNotify = true;
  158. }
  159. }
  160. esp_err_t cspotPlayer::handleGET(httpd_req_t *request) {
  161. std::string body = this->blob->buildZeroconfInfo();
  162. if (body.size() == 0) {
  163. CSPOT_LOG(info, "cspot empty blob's body on GET");
  164. return ESP_ERR_HTTPD_INVALID_REQ;
  165. }
  166. httpd_resp_set_hdr(request, "Content-type", "application/json");
  167. httpd_resp_set_hdr(request, "Content-length", std::to_string(body.size()).c_str());
  168. httpd_resp_send(request, body.c_str(), body.size());
  169. return ESP_OK;
  170. }
  171. esp_err_t cspotPlayer::handlePOST(httpd_req_t *request) {
  172. cJSON* response= cJSON_CreateObject();
  173. cJSON_AddNumberToObject(response, "status", 101);
  174. cJSON_AddStringToObject(response, "statusString", "ERROR-OK");
  175. cJSON_AddNumberToObject(response, "spotifyError", 0);
  176. // get body if any (add '\0' at the end if used as string)
  177. if (request->content_len) {
  178. char* body = (char*) calloc(1, request->content_len + 1);
  179. int size = httpd_req_recv(request, body, request->content_len);
  180. // I know this is very crude and unsafe...
  181. url_decode(body);
  182. char *key = strtok(body, "&");
  183. std::map<std::string, std::string> queryMap;
  184. while (key) {
  185. char *value = strchr(key, '=');
  186. *value++ = '\0';
  187. queryMap[key] = value;
  188. key = strtok(NULL, "&");
  189. };
  190. free(body);
  191. // Pass user's credentials to the blob and give the token
  192. blob->loadZeroconfQuery(queryMap);
  193. clientConnected.give();
  194. }
  195. char *responseStr = cJSON_PrintUnformatted(response);
  196. cJSON_Delete(response);
  197. esp_err_t rc = httpd_resp_send(request, responseStr, strlen(responseStr));
  198. free(responseStr);
  199. return rc;
  200. }
  201. void cspotPlayer::eventHandler(std::unique_ptr<cspot::SpircHandler::Event> event) {
  202. switch (event->eventType) {
  203. case cspot::SpircHandler::EventType::PLAYBACK_START: {
  204. centralAudioBuffer->clearBuffer();
  205. // we are not playing anymore
  206. xTimerStop(trackTimer, portMAX_DELAY);
  207. trackNotify = false;
  208. playback = false;
  209. // memorize position for when track's beginning will be detected
  210. startOffset = std::get<int>(event->data);
  211. cmdHandler(CSPOT_START, 44100);
  212. CSPOT_LOG(info, "start track <%s>", spirc->getTrackPlayer()->getCurrentTrackInfo().name.c_str());
  213. // Spotify servers do not send volume at connection
  214. spirc->setRemoteVolume(volume);
  215. break;
  216. }
  217. case cspot::SpircHandler::EventType::PLAY_PAUSE: {
  218. bool pause = std::get<bool>(event->data);
  219. cmdHandler(pause ? CSPOT_PAUSE : CSPOT_PLAY);
  220. chunker->isPaused = pause;
  221. break;
  222. }
  223. case cspot::SpircHandler::EventType::TRACK_INFO: {
  224. auto trackInfo = std::get<cspot::CDNTrackStream::TrackInfo>(event->data);
  225. cmdHandler(CSPOT_TRACK, trackInfo.duration, startOffset, trackInfo.artist.c_str(),
  226. trackInfo.album.c_str(), trackInfo.name.c_str(), trackInfo.imageUrl.c_str());
  227. spirc->updatePositionMs(startOffset);
  228. startOffset = 0;
  229. break;
  230. }
  231. case cspot::SpircHandler::EventType::NEXT:
  232. case cspot::SpircHandler::EventType::PREV:
  233. case cspot::SpircHandler::EventType::FLUSH: {
  234. // FLUSH is sent when there is no next, just clean everything
  235. centralAudioBuffer->clearBuffer();
  236. cmdHandler(CSPOT_FLUSH);
  237. break;
  238. }
  239. case cspot::SpircHandler::EventType::DISC:
  240. centralAudioBuffer->clearBuffer();
  241. xTimerStop(trackTimer, portMAX_DELAY);
  242. cmdHandler(CSPOT_DISC);
  243. chunker->teardown();
  244. break;
  245. case cspot::SpircHandler::EventType::SEEK: {
  246. centralAudioBuffer->clearBuffer();
  247. cmdHandler(CSPOT_SEEK, std::get<int>(event->data));
  248. break;
  249. }
  250. case cspot::SpircHandler::EventType::DEPLETED:
  251. CSPOT_LOG(info, "playlist ended, no track left to play");
  252. break;
  253. case cspot::SpircHandler::EventType::VOLUME:
  254. volume = std::get<int>(event->data);
  255. cmdHandler(CSPOT_VOLUME, volume);
  256. break;
  257. default:
  258. break;
  259. }
  260. }
  261. void cspotPlayer::trackHandler(void) {
  262. if (playback) {
  263. uint32_t remains;
  264. auto trackInfo = spirc->getTrackPlayer()->getCurrentTrackInfo();
  265. // if this is not first track, estimate when the current one will finish
  266. cmdHandler(CSPOT_REMAINING, &remains);
  267. if (remains > 100) xTimerChangePeriod(trackTimer, pdMS_TO_TICKS(remains), portMAX_DELAY);
  268. else trackNotify = true;
  269. CSPOT_LOG(info, "next track <%s> in cspot buffers, remaining %d ms", trackInfo.name.c_str(), remains);
  270. } else {
  271. trackNotify = true;
  272. playback = true;
  273. }
  274. }
  275. void cspotPlayer::runTask() {
  276. httpd_uri_t request = {
  277. .uri = "/spotify_info",
  278. .method = HTTP_GET,
  279. .handler = ::handleGET,
  280. .user_ctx = NULL,
  281. };
  282. // register GET and POST handler for built-in server
  283. httpd_register_uri_handler(serverHandle, &request);
  284. request.method = HTTP_POST;
  285. request.handler = ::handlePOST;
  286. httpd_register_uri_handler(serverHandle, &request);
  287. // construct blob for that player
  288. blob = std::make_unique<cspot::LoginBlob>(name);
  289. // Register mdns service, for spotify to find us
  290. bell::MDNSService::registerService( blob->getDeviceName(), "_spotify-connect", "_tcp", "", serverPort,
  291. { {"VERSION", "1.0"}, {"CPath", "/spotify_info"}, {"Stack", "SP"} });
  292. static int count = 0;
  293. // gone with the wind...
  294. while (1) {
  295. clientConnected.wait();
  296. CSPOT_LOG(info, "Spotify client connected for %s", name.c_str());
  297. centralAudioBuffer = std::make_shared<bell::CentralAudioBuffer>(32);
  298. auto ctx = cspot::Context::createFromBlob(blob);
  299. if (bitrate == 320) ctx->config.audioFormat = AudioFormat_OGG_VORBIS_320;
  300. else if (bitrate == 96) ctx->config.audioFormat = AudioFormat_OGG_VORBIS_96;
  301. else ctx->config.audioFormat = AudioFormat_OGG_VORBIS_160;
  302. ctx->session->connectWithRandomAp();
  303. auto token = ctx->session->authenticate(blob);
  304. // Auth successful
  305. if (token.size() > 0) {
  306. trackTimer = xTimerCreate("trackTimer", pdMS_TO_TICKS(1000), pdFALSE, NULL, trackTimerHandler);
  307. spirc = std::make_unique<cspot::SpircHandler>(ctx);
  308. // set call back to calculate a hash on trackId
  309. spirc->getTrackPlayer()->setDataCallback(
  310. [this](uint8_t* data, size_t bytes, std::string_view trackId, size_t sequence) {
  311. return centralAudioBuffer->writePCM(data, bytes, sequence);
  312. });
  313. // set event (PLAY, VOLUME...) handler
  314. spirc->setEventHandler(
  315. [this](std::unique_ptr<cspot::SpircHandler::Event> event) {
  316. eventHandler(std::move(event));
  317. });
  318. // Start handling mercury messages
  319. ctx->session->startTask();
  320. // Create a player, pass the tack handler
  321. chunker = std::make_unique<chunkManager>(centralAudioBuffer,
  322. [this](void) {
  323. return trackHandler();
  324. },
  325. [this](const uint8_t* data, size_t bytes) {
  326. return dataHandler(data, bytes);
  327. });
  328. // exit when player has stopped (received a DISC)
  329. while (chunker->isRunning) {
  330. ctx->session->handlePacket();
  331. // inform Spotify that next track has started (don't need to be super accurate)
  332. if (trackNotify) {
  333. CSPOT_LOG(info, "next track's audio has reached DAC");
  334. spirc->notifyAudioReachedPlayback();
  335. trackNotify = false;
  336. }
  337. }
  338. xTimerDelete(trackTimer, portMAX_DELAY);
  339. spirc->disconnect();
  340. CSPOT_LOG(info, "disconnecting player %s", name.c_str());
  341. }
  342. // we want to release memory ASAP and fore sure
  343. centralAudioBuffer.reset();
  344. ctx.reset();
  345. token.clear();
  346. // update volume when we disconnect
  347. cJSON *item, *config = config_alloc_get_cjson("cspot_config");
  348. cJSON_DeleteItemFromObject(config, "volume");
  349. cJSON_AddNumberToObject(config, "volume", volume);
  350. config_set_cjson_str_and_free("cspot_config", config);
  351. }
  352. }
  353. /****************************************************************************************
  354. * API to create and start a cspot instance
  355. */
  356. struct cspot_s* cspot_create(const char *name, httpd_handle_t server, int port, cspot_cmd_cb_t cmd_cb, cspot_data_cb_t data_cb) {
  357. bell::setDefaultLogger();
  358. player = new cspotPlayer(name, server, port, cmd_cb, data_cb);
  359. player->startTask();
  360. return (cspot_s*) player;
  361. }
  362. /****************************************************************************************
  363. * Commands sent by local buttons/actions
  364. */
  365. bool cspot_cmd(struct cspot_s* ctx, cspot_event_t event, void *param) {
  366. // we might have no controller left
  367. /*
  368. if (!spircController.use_count()) return false;
  369. switch(event) {
  370. case CSPOT_PREV:
  371. spircController->prevSong();
  372. break;
  373. case CSPOT_NEXT:
  374. spircController->nextSong();
  375. break;
  376. case CSPOT_TOGGLE:
  377. spircController->playToggle();
  378. break;
  379. case CSPOT_PAUSE:
  380. spircController->setPause(true);
  381. break;
  382. case CSPOT_PLAY:
  383. spircController->setPause(false);
  384. break;
  385. case CSPOT_DISC:
  386. spircController->disconnect();
  387. break;
  388. case CSPOT_STOP:
  389. spircController->stopPlayer();
  390. break;
  391. case CSPOT_VOLUME_UP:
  392. spircController->adjustVolume(MAX_VOLUME / 100 + 1);
  393. break;
  394. case CSPOT_VOLUME_DOWN:
  395. spircController->adjustVolume(-(MAX_VOLUME / 100 + 1));
  396. break;
  397. default:
  398. break;
  399. }
  400. */
  401. return true;
  402. }