diff --git a/CHANGELOG b/CHANGELOG index 7dff3e67..c367c5a9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +2024-01-16 + - catch-up with cspot latest + - refactor airplay flush/first packet + - new libFLAC that supports multi-stream OggFlac + - fix output threshold + 2024-01-10 - add OggFlac to stream metadata - fix OggFlac deadlock in flac callback when not enough data in streambuf diff --git a/components/codecs/lib/libFLAC.a b/components/codecs/lib/libFLAC.a index 4e16b03c..38777277 100644 Binary files a/components/codecs/lib/libFLAC.a and b/components/codecs/lib/libFLAC.a differ diff --git a/components/raop/rtp.c b/components/raop/rtp.c index ebe96bef..d7972571 100644 --- a/components/raop/rtp.c +++ b/components/raop/rtp.c @@ -126,11 +126,6 @@ typedef struct rtp_s { u32_t rtp, time; u8_t status; } synchro; - struct { - u32_t time; - seq_t seqno; - u32_t rtptime; - } record; int latency; // rtp hold depth in samples u32_t resent_req, resent_rec; // total resent + recovered frames u32_t silent_frames; // total silence frames @@ -147,8 +142,8 @@ typedef struct rtp_s { #endif struct alac_codec_s *alac_codec; - int flush_seqno; - bool playing; + int first_seqno; + enum { RTP_WAIT, RTP_STREAM, RTP_PLAY } state; int stalled; raop_data_cb_t data_cb; raop_cmd_cb_t cmd_cb; @@ -231,7 +226,7 @@ rtp_resp_t rtp_init(struct in_addr host, int latency, char *aeskey, char *aesiv, ctx->rtp_host.sin_family = AF_INET; ctx->rtp_host.sin_addr.s_addr = INADDR_ANY; pthread_mutex_init(&ctx->ab_mutex, 0); - ctx->flush_seqno = -1; + ctx->first_seqno = -1; ctx->latency = latency; ctx->ab_read = ctx->ab_write; @@ -339,24 +334,23 @@ void rtp_end(rtp_t *ctx) /*---------------------------------------------------------------------------*/ bool rtp_flush(rtp_t *ctx, unsigned short seqno, unsigned int rtptime, bool exit_locked) -{ - bool rc = true; - u32_t now = gettime_ms(); +{ + pthread_mutex_lock(&ctx->ab_mutex); + + // always store flush seqno as we only want stricly above it, even when equal to RECORD + ctx->first_seqno = seqno; + bool flushed = false; - if (now < ctx->record.time + 250 || (ctx->record.seqno == seqno && ctx->record.rtptime == rtptime)) { - rc = false; - LOG_ERROR("[%p]: FLUSH ignored as same as RECORD (%hu - %u)", ctx, seqno, rtptime); - } else { - pthread_mutex_lock(&ctx->ab_mutex); - buffer_reset(ctx->audio_buffer); - ctx->playing = false; - ctx->flush_seqno = seqno; - if (!exit_locked) pthread_mutex_unlock(&ctx->ab_mutex); + // no need to stop playing if recent or equal to record - but first_seqno is needed + if (ctx->state == RTP_PLAY) { + buffer_reset(ctx->audio_buffer); + ctx->state = RTP_WAIT; + flushed = true; + LOG_INFO("[%p]: FLUSH packets below %hu - %u", ctx, seqno, rtptime); } - - LOG_INFO("[%p]: flush %hu %u", ctx, seqno, rtptime); - - return rc; + + if (!exit_locked || !flushed) pthread_mutex_unlock(&ctx->ab_mutex); + return flushed; } /*---------------------------------------------------------------------------*/ @@ -367,11 +361,9 @@ void rtp_flush_release(rtp_t *ctx) { /*---------------------------------------------------------------------------*/ void rtp_record(rtp_t *ctx, unsigned short seqno, unsigned rtptime) { - ctx->record.seqno = seqno; - ctx->record.rtptime = rtptime; - ctx->record.time = gettime_ms(); - - LOG_INFO("[%p]: record %hu %u", ctx, seqno, rtptime); + ctx->first_seqno = (seqno || rtptime) ? seqno : -1; + ctx->state = RTP_WAIT; + LOG_INFO("[%p]: record %hu - %u", ctx, seqno, rtptime); } /*---------------------------------------------------------------------------*/ @@ -442,26 +434,50 @@ static void alac_decode(rtp_t *ctx, s16_t *dest, char *buf, int len, u16_t *outs /*---------------------------------------------------------------------------*/ static void buffer_put_packet(rtp_t *ctx, seq_t seqno, unsigned rtptime, bool first, char *data, int len) { abuf_t *abuf = NULL; - u32_t playtime; pthread_mutex_lock(&ctx->ab_mutex); - - if (!ctx->playing) { - if ((ctx->flush_seqno == -1 || seq_order(ctx->flush_seqno, seqno)) && - (ctx->synchro.status & RTP_SYNC) && (ctx->synchro.status & NTP_SYNC)) { - ctx->ab_write = seqno-1; - ctx->ab_read = seqno; - ctx->flush_seqno = -1; - ctx->playing = true; - ctx->resent_req = ctx->resent_rec = ctx->silent_frames = ctx->discarded = 0; - playtime = ctx->synchro.time + ((rtptime - ctx->synchro.rtp) * 10) / (RAOP_SAMPLE_RATE / 100); - ctx->cmd_cb(RAOP_PLAY, playtime); - } else { - pthread_mutex_unlock(&ctx->ab_mutex); - return; - } - } + /* if we have received a RECORD with a seqno, then this is the first allowed rtp sequence number + * and we are in RTP_WAIT state. If seqno was 0, then we are waiting for a flush that will tell + * us what should be our first allowed packet but we must accept everything, wait and clean when + * we the it arrives. This means that first packet moves us to RTP_STREAM state where we accept + * frames but wait for the FLUSH. If this was a FLUSH while playing, then we are also in RTP_WAIT + * state but we do have an allowed seqno and we should not accept any frame before we have it */ + + // if we have a pending first seqno and we are below, always ignore it + if (ctx->first_seqno != -1 && seq_order(seqno, ctx->first_seqno)) { + pthread_mutex_unlock(&ctx->ab_mutex); + return; + } + + if (ctx->state == RTP_WAIT) { + ctx->ab_write = seqno - 1; + ctx->ab_read = ctx->ab_write + 1; + ctx->resent_req = ctx->resent_rec = ctx->silent_frames = ctx->discarded = 0; + if (ctx->first_seqno != -1) { + LOG_INFO("[%p]: 1st accepted packet:%d, now playing", ctx, seqno); + ctx->state = RTP_PLAY; + ctx->first_seqno = -1; + u32_t playtime = ctx->synchro.time + ((rtptime - ctx->synchro.rtp) * 10) / (RAOP_SAMPLE_RATE / 100); + ctx->cmd_cb(RAOP_PLAY, playtime); + } else { + ctx->state = RTP_STREAM; + LOG_INFO("[%p]: 1st accepted packet:%hu, waiting for FLUSH", ctx, seqno); + } + } else if (ctx->state == RTP_STREAM && ctx->first_seqno != -1 && seq_order(ctx->first_seqno, seqno + 1)) { + // now we're talking, but first discard all packets with a seqno below first_seqno AND not ready + while (seq_order(ctx->ab_read, ctx->first_seqno) || + !ctx->audio_buffer[BUFIDX(ctx->ab_read)].ready) { + ctx->audio_buffer[BUFIDX(ctx->ab_read)].ready = false; + ctx->ab_read++; + } + LOG_INFO("[%p]: done waiting for FLUSH with packet:%d, now playing starting:%hu", ctx, seqno, ctx->ab_read); + ctx->state = RTP_PLAY; + ctx->first_seqno = -1; + u32_t playtime = ctx->synchro.time + ((rtptime - ctx->synchro.rtp) * 10) / (RAOP_SAMPLE_RATE / 100); + ctx->cmd_cb(RAOP_PLAY, playtime); + } + if (seqno == (u16_t) (ctx->ab_write+1)) { // expected packet abuf = ctx->audio_buffer + BUFIDX(seqno); @@ -475,7 +491,7 @@ static void buffer_put_packet(rtp_t *ctx, seq_t seqno, unsigned rtptime, bool fi ctx->ab_read = seqno; } else { // request re-send missed frames and evaluate resent date as a whole *after* - rtp_request_resend(ctx, ctx->ab_write + 1, seqno-1); + if (ctx->state == RTP_PLAY) rtp_request_resend(ctx, ctx->ab_write + 1, seqno-1); // resend date is after all requests have been sent u32_t now = gettime_ms(); @@ -528,7 +544,7 @@ static void buffer_push_packet(rtp_t *ctx) { u32_t now, playtime, hold = max((ctx->latency * 1000) / (8 * RAOP_SAMPLE_RATE), 100); // not ready to play yet - if (!ctx->playing || ctx->synchro.status != (RTP_SYNC | NTP_SYNC)) return; + if (ctx->state != RTP_PLAY || ctx->synchro.status != (RTP_SYNC | NTP_SYNC)) return; // there is always at least one frame in the buffer do { diff --git a/components/spotify/cspot/bell/external/civetweb/civetweb.c b/components/spotify/cspot/bell/external/civetweb/civetweb.c index 5fc4703f..5a7cb2d5 100644 --- a/components/spotify/cspot/bell/external/civetweb/civetweb.c +++ b/components/spotify/cspot/bell/external/civetweb/civetweb.c @@ -22482,9 +22482,13 @@ mg_init_library(unsigned features) file_mutex_init = pthread_mutex_init(&global_log_file_lock, &pthread_mutex_attr); if (file_mutex_init == 0) { +#ifdef WINSOCK_START /* Start WinSock */ WSADATA data; failed = wsa = WSAStartup(MAKEWORD(2, 2), &data); +#else + failed = wsa = 0; +#endif } #else mutexattr_init = pthread_mutexattr_init(&pthread_mutex_attr); @@ -22498,7 +22502,9 @@ mg_init_library(unsigned features) if (failed) { #if defined(_WIN32) if (wsa == 0) { +#ifdef WINSOCK_START (void)WSACleanup(); +#endif } if (file_mutex_init == 0) { (void)pthread_mutex_destroy(&global_log_file_lock); @@ -22598,7 +22604,9 @@ mg_exit_library(void) #endif #if defined(_WIN32) +#ifdef WINSOCK_START (void)WSACleanup(); +#endif (void)pthread_mutex_destroy(&global_log_file_lock); #else (void)pthread_mutexattr_destroy(&pthread_mutex_attr); diff --git a/components/spotify/cspot/bell/main/io/BellHTTPServer.cpp b/components/spotify/cspot/bell/main/io/BellHTTPServer.cpp index 77cdb30d..feb34a53 100644 --- a/components/spotify/cspot/bell/main/io/BellHTTPServer.cpp +++ b/components/spotify/cspot/bell/main/io/BellHTTPServer.cpp @@ -12,6 +12,8 @@ using namespace bell; +std::mutex BellHTTPServer::initMutex; + class WebSocketHandler : public CivetWebSocketHandler { public: BellHTTPServer::WSDataHandler dataHandler; @@ -187,6 +189,7 @@ bool BellHTTPServer::handlePost(CivetServer* server, } BellHTTPServer::BellHTTPServer(int serverPort) { + std::lock_guard lock(initMutex); mg_init_library(0); BELL_LOG(info, "HttpServer", "Server listening on port %d", serverPort); this->serverPort = serverPort; @@ -197,6 +200,11 @@ BellHTTPServer::BellHTTPServer(int serverPort) { server = std::make_unique(civetWebOptions); } +BellHTTPServer::~BellHTTPServer() { + std::lock_guard lock(initMutex); + mg_exit_library(); +} + std::unique_ptr BellHTTPServer::makeJsonResponse( const std::string& json, int status) { auto response = std::make_unique(); diff --git a/components/spotify/cspot/bell/main/io/include/BellHTTPServer.h b/components/spotify/cspot/bell/main/io/include/BellHTTPServer.h index bc7f78bd..6572343a 100644 --- a/components/spotify/cspot/bell/main/io/include/BellHTTPServer.h +++ b/components/spotify/cspot/bell/main/io/include/BellHTTPServer.h @@ -19,6 +19,7 @@ namespace bell { class BellHTTPServer : public CivetHandler { public: BellHTTPServer(int serverPort); + ~BellHTTPServer(); enum class WSState { CONNECTED, READY, CLOSED }; @@ -100,6 +101,8 @@ class BellHTTPServer : public CivetHandler { std::mutex responseMutex; HTTPHandler notFoundHandler; + static std::mutex initMutex; + bool handleGet(CivetServer* server, struct mg_connection* conn); bool handlePost(CivetServer* server, struct mg_connection* conn); }; diff --git a/components/spotify/cspot/bell/main/platform/linux/MDNSService.cpp b/components/spotify/cspot/bell/main/platform/linux/MDNSService.cpp index 56cf9f0f..3f52e5a0 100644 --- a/components/spotify/cspot/bell/main/platform/linux/MDNSService.cpp +++ b/components/spotify/cspot/bell/main/platform/linux/MDNSService.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #if __has_include("avahi-client/client.h") #include @@ -41,8 +42,9 @@ class implMDNSService : public MDNSService { #endif static struct mdnsd* mdnsServer; static in_addr_t host; + static std::atomic instances; - implMDNSService(struct mdns_service* service) : service(service){}; + implMDNSService(struct mdns_service* service) : service(service){ instances++; }; #ifndef BELL_DISABLE_AVAHI implMDNSService(AvahiEntryGroup* avahiGroup) : avahiGroup(avahiGroup){}; #endif @@ -51,6 +53,7 @@ class implMDNSService : public MDNSService { struct mdnsd* implMDNSService::mdnsServer = NULL; in_addr_t implMDNSService::host = INADDR_ANY; +std::atomic implMDNSService::instances = 0; static std::mutex registerMutex; #ifndef BELL_DISABLE_AVAHI AvahiClient* implMDNSService::avahiClient = NULL; @@ -66,11 +69,21 @@ void implMDNSService::unregisterService() { #ifndef BELL_DISABLE_AVAHI if (avahiGroup) { avahi_entry_group_free(avahiGroup); + if (!--instances && implMDNSService::avahiClient) { + avahi_client_free(implMDNSService::avahiClient); + avahi_simple_poll_free(implMDNSService::avahiPoll); + implMDNSService::avahiClient = nullptr; + implMDNSService::avahiPoll = nullptr; + } } else #endif { mdns_service_remove(implMDNSService::mdnsServer, service); - } + if (!--instances && implMDNSService::mdnsServer) { + mdnsd_stop(implMDNSService::mdnsServer); + implMDNSService::mdnsServer = nullptr; + } + } } std::unique_ptr MDNSService::registerService( @@ -180,19 +193,14 @@ std::unique_ptr MDNSService::registerService( std::string type(serviceType + "." + serviceProto + ".local"); BELL_LOG(info, "MDNS", "using built-in mDNS for %s", serviceName.c_str()); - struct mdns_service* mdnsService = + auto service = mdnsd_register_svc(implMDNSService::mdnsServer, serviceName.c_str(), - type.c_str(), servicePort, NULL, txt.data()); - if (mdnsService) { - auto service = - mdnsd_register_svc(implMDNSService::mdnsServer, serviceName.c_str(), type.c_str(), servicePort, NULL, txt.data()); - return std::make_unique(service); - } + if (service) return std::make_unique(service); } BELL_LOG(error, "MDNS", "cannot start any mDNS listener for %s", serviceName.c_str()); - return NULL; + return nullptr; } diff --git a/components/spotify/cspot/bell/main/platform/win32/MDNSService.cpp b/components/spotify/cspot/bell/main/platform/win32/MDNSService.cpp index 7ee64a17..62fa75f7 100644 --- a/components/spotify/cspot/bell/main/platform/win32/MDNSService.cpp +++ b/components/spotify/cspot/bell/main/platform/win32/MDNSService.cpp @@ -19,13 +19,12 @@ using namespace bell; class implMDNSService : public MDNSService { private: struct mdns_service* service; - void unregisterService(void) { - mdns_service_remove(implMDNSService::mdnsServer, service); - }; + void unregisterService(void); public: static struct mdnsd* mdnsServer; - implMDNSService(struct mdns_service* service) : service(service){}; + static std::atomic instances; + implMDNSService(struct mdns_service* service) : service(service) { instances++; }; }; /** @@ -33,8 +32,18 @@ class implMDNSService : public MDNSService { **/ struct mdnsd* implMDNSService::mdnsServer = NULL; +std::atomic implMDNSService::instances = 0; + static std::mutex registerMutex; +void implMDNSService::unregisterService() { + mdns_service_remove(implMDNSService::mdnsServer, service); + if (!--instances && implMDNSService::mdnsServer) { + mdnsd_stop(implMDNSService::mdnsServer); + implMDNSService::mdnsServer = nullptr; + } +} + std::unique_ptr MDNSService::registerService( const std::string& serviceName, const std::string& serviceType, const std::string& serviceProto, const std::string& serviceHost, @@ -94,5 +103,5 @@ std::unique_ptr MDNSService::registerService( mdnsd_register_svc(implMDNSService::mdnsServer, serviceName.c_str(), type.c_str(), servicePort, NULL, txt.data()); - return std::make_unique(service); + return service ? std::make_unique(service) : nullptr; } diff --git a/components/spotify/cspot/bell/main/utilities/include/BellTask.h b/components/spotify/cspot/bell/main/utilities/include/BellTask.h index 7f0fc3be..d8b8eb71 100644 --- a/components/spotify/cspot/bell/main/utilities/include/BellTask.h +++ b/components/spotify/cspot/bell/main/utilities/include/BellTask.h @@ -72,7 +72,11 @@ class Task { (LPTHREAD_START_ROUTINE)taskEntryFunc, this, 0, NULL); return thread != NULL; #else - return (pthread_create(&thread, NULL, taskEntryFunc, this) == 0); + if (!pthread_create(&thread, NULL, taskEntryFunc, this)) { + pthread_detach(thread); + return true; + } + return false; #endif } @@ -108,13 +112,7 @@ class Task { #endif static void* taskEntryFunc(void* This) { - Task* self = (Task*)This; - self->runTask(); -#if _WIN32 - WaitForSingleObject(self->thread, INFINITE); -#else - pthread_join(self->thread, NULL); -#endif + ((Task*)This)->runTask(); return NULL; } }; diff --git a/components/spotify/cspot/include/TrackQueue.h b/components/spotify/cspot/include/TrackQueue.h index 60c1d318..8e2f0c37 100644 --- a/components/spotify/cspot/include/TrackQueue.h +++ b/components/spotify/cspot/include/TrackQueue.h @@ -24,7 +24,7 @@ class CDNAudioFile; // Used in got track info event struct TrackInfo { std::string name, album, artist, imageUrl, trackId; - uint32_t duration; + uint32_t duration, number, discNumber; void loadPbTrack(Track* pbTrack, const std::vector& gid); void loadPbEpisode(Episode* pbEpisode, const std::vector& gid); diff --git a/components/spotify/cspot/protobuf/metadata.proto b/components/spotify/cspot/protobuf/metadata.proto index 6dc8a7c3..dc1fad12 100644 --- a/components/spotify/cspot/protobuf/metadata.proto +++ b/components/spotify/cspot/protobuf/metadata.proto @@ -32,6 +32,8 @@ message Artist { message Track { optional bytes gid = 1; optional string name = 2; + optional sint32 number = 5; + optional sint32 disc_number = 6; optional sint32 duration = 0x7; optional Album album = 0x3; repeated Artist artist = 0x4; @@ -44,6 +46,7 @@ message Episode { optional bytes gid = 1; optional string name = 2; optional sint32 duration = 7; + optional sint32 number = 65; repeated AudioFile file = 12; repeated Restriction restriction = 0x4B; optional ImageGroup covers = 0x44; diff --git a/components/spotify/cspot/src/TrackQueue.cpp b/components/spotify/cspot/src/TrackQueue.cpp index f2860387..26ec0a27 100644 --- a/components/spotify/cspot/src/TrackQueue.cpp +++ b/components/spotify/cspot/src/TrackQueue.cpp @@ -97,6 +97,8 @@ void TrackInfo::loadPbTrack(Track* pbTrack, const std::vector& gid) { } } + number = pbTrack->has_number ? pbTrack->number : 0; + discNumber = pbTrack->has_disc_number ? pbTrack->disc_number : 0; duration = pbTrack->duration; } @@ -113,6 +115,8 @@ void TrackInfo::loadPbEpisode(Episode* pbEpisode, imageUrl = "https://i.scdn.co/image/" + bytesToHexString(imageId); } + number = pbEpisode->has_number ? pbEpisode->number : 0; + discNumber = 0; duration = pbEpisode->duration; } diff --git a/components/squeezelite/output.c b/components/squeezelite/output.c index 982b6466..e5291f3b 100644 --- a/components/squeezelite/output.c +++ b/components/squeezelite/output.c @@ -60,7 +60,7 @@ frames_t _output_frames(frames_t avail) { silence = false; // start when threshold met - if (output.state == OUTPUT_BUFFER && (frames * BYTES_PER_FRAME) > output.threshold * output.next_sample_rate / 10 && frames > output.start_frames) { + if (output.state == OUTPUT_BUFFER && frames > output.threshold * output.next_sample_rate / 10 && frames > output.start_frames) { output.state = OUTPUT_RUNNING; LOG_INFO("start buffer frames: %u", frames); wake_controller();