2828#include < utility>
2929#include < cstring>
3030#include < thread>
31+ #ifdef _WIN32
32+ #include < winsock2.h>
33+ #include < ws2tcpip.h>
34+ #else
35+ #include < netinet/tcp.h>
36+ #endif
3137#include " NfsAnlzFetcher.h"
3238#include " WaveformCache.h"
3339#include " AppSettings.h"
@@ -500,6 +506,7 @@ class DbServerClient : private juce::Thread
500506 static constexpr int kMaxArtCacheEntries = 64 ;
501507 static constexpr int kMaxConnections = 6 ;
502508 static constexpr int kReconnectCooldownMs = 5000 ;
509+ static constexpr double kIdleTimeoutMs = 30000.0 ; // close connections idle for >30s
503510
504511 // NXS2 extension constants for color waveform
505512 static constexpr uint32_t kNxs2ExtRequest = 0x2c04 ;
@@ -558,6 +565,7 @@ class DbServerClient : private juce::Thread
558565 uint8_t contextPlayer = 0 ; // player number used in setupQueryContext
559566 uint32_t txId = 0 ;
560567 double lastFailTime = 0.0 ;
568+ double lastActivityTime = 0.0 ; // hiRes ms -- for idle timeout
561569
562570 bool isConnected () const { return socket && socket->isConnected (); }
563571
@@ -572,6 +580,7 @@ class DbServerClient : private juce::Thread
572580 contextPlayer = 0 ;
573581 txId = 0 ;
574582 dbPort = 0 ;
583+ lastActivityTime = 0.0 ;
575584 playerIP.clear ();
576585 }
577586 };
@@ -908,6 +917,7 @@ class DbServerClient : private juce::Thread
908917 conn.lastFailTime = 0.0 ; // intentional close, not a failure
909918 break ; // fall through to new connection below
910919 }
920+ conn.lastActivityTime = juce::Time::getMillisecondCounterHiRes ();
911921 return &conn;
912922 }
913923 }
@@ -962,6 +972,19 @@ class DbServerClient : private juce::Thread
962972 return nullptr ;
963973 }
964974
975+ // Disable Nagle's algorithm -- ensure each dbserver message is sent as
976+ // a single TCP segment. Some players fail to parse messages that arrive
977+ // split across multiple packets.
978+ {
979+ auto fd = sock->getRawSocketHandle ();
980+ if (fd >= 0 )
981+ {
982+ int flag = 1 ;
983+ setsockopt (fd, IPPROTO_TCP , TCP_NODELAY ,
984+ reinterpret_cast <const char *>(&flag), sizeof (flag));
985+ }
986+ }
987+
965988 // Step 3: Send initial handshake -- [0x11, 0x00000001]
966989 {
967990 uint8_t hello[] = { 0x11 , 0x00 , 0x00 , 0x00 , 0x01 };
@@ -986,6 +1009,7 @@ class DbServerClient : private juce::Thread
9861009 slot->playerIP = playerIP;
9871010 slot->dbPort = dbPort;
9881011 slot->contextSetUp = false ;
1012+ slot->lastActivityTime = now;
9891013 slot->txId = 0 ;
9901014 slot->lastFailTime = 0.0 ;
9911015
@@ -1001,8 +1025,37 @@ class DbServerClient : private juce::Thread
10011025 return slot;
10021026 }
10031027
1004- // / Discover the dbserver port by querying TCP 12523
1028+ // / Discover the dbserver port by querying TCP 12523.
1029+ // / Retries a few times because the player may not be ready to respond
1030+ // / immediately after booting or loading media.
10051031 int discoverDbPort (const juce::String& playerIP)
1032+ {
1033+ static constexpr int kMaxRetries = 3 ;
1034+ static constexpr int kRetryDelayMs = 1000 ;
1035+
1036+ for (int attempt = 0 ; attempt < kMaxRetries ; ++attempt)
1037+ {
1038+ if (attempt > 0 )
1039+ {
1040+ DBG (" DbServerClient: port discovery retry " + juce::String (attempt)
1041+ + " /" + juce::String (kMaxRetries - 1 ) + " for " + playerIP);
1042+ juce::Thread::sleep (kRetryDelayMs );
1043+ if (!isRunningFlag.load (std::memory_order_relaxed))
1044+ return 0 ;
1045+ }
1046+
1047+ int port = discoverDbPortOnce (playerIP);
1048+ if (port > 0 )
1049+ return port;
1050+ }
1051+
1052+ DBG (" DbServerClient: port discovery failed after " + juce::String (kMaxRetries )
1053+ + " attempts for " + playerIP);
1054+ return 0 ;
1055+ }
1056+
1057+ // / Single attempt at port discovery via TCP 12523
1058+ int discoverDbPortOnce (const juce::String& playerIP)
10061059 {
10071060 DBG (" DbServerClient: discovering db port on " + playerIP + " :12523" );
10081061
@@ -1040,9 +1093,10 @@ class DbServerClient : private juce::Thread
10401093 int port = (int (response[0 ]) << 8 ) | response[1 ];
10411094 DBG (" DbServerClient: discovered db port " + juce::String (port) + " on " + playerIP);
10421095
1043- if (port <= 0 || port > 65535 )
1096+ if (port <= 0 || port >= 65535 )
10441097 {
1045- DBG (" DbServerClient: invalid port " + juce::String (port));
1098+ DBG (" DbServerClient: invalid port " + juce::String (port)
1099+ + " (0xFFFF = no service available)" );
10461100 return 0 ;
10471101 }
10481102
@@ -2182,6 +2236,25 @@ class DbServerClient : private juce::Thread
21822236
21832237 if (threadShouldExit ()) break ;
21842238
2239+ // Close idle connections — prevents zombie TCP connections from
2240+ // holding CDJ NFS slots (CDJ-2000NXS2 has limited slots).
2241+ {
2242+ double now = juce::Time::getMillisecondCounterHiRes ();
2243+ for (auto & conn : connections)
2244+ {
2245+ if (conn.isConnected () && conn.lastActivityTime > 0.0
2246+ && (now - conn.lastActivityTime ) > kIdleTimeoutMs )
2247+ {
2248+ DBG (" DbServerClient: closing idle connection to "
2249+ + conn.playerIP + " :" + juce::String (conn.dbPort )
2250+ + " (idle " + juce::String ((int )((now - conn.lastActivityTime ) / 1000.0 )) + " s)" );
2251+ conn.close ();
2252+ }
2253+ }
2254+ }
2255+
2256+ if (threadShouldExit ()) break ;
2257+
21852258 // Process all queued requests
21862259 while (true )
21872260 {
0 commit comments