diff --git a/docs/JSON-RPC.md b/docs/JSON-RPC.md index e9111187f4..6b6859b30e 100644 --- a/docs/JSON-RPC.md +++ b/docs/JSON-RPC.md @@ -129,6 +129,42 @@ Results: | result.version | string | The Jamulus version. | +### jamulusclient/connect + +Connects to a server previously discovered via jamulusclient/pollServerList on the given directory. Blocks for a short time (up to a few seconds) while the outcome of the connection attempt is determined. + +Parameters: + +| Name | Type | Description | +| --- | --- | --- | +| params.directory | string | Socket address of the directory the server was discovered from. Must have already been queried via jamulusclient/pollServerList. | +| params.server | string | Socket address of the server to connect to, as returned in params.servers[*].address of jamulusclient/serverListReceived. | + +Results: + +| Name | Type | Description | +| --- | --- | --- | +| result | string | "ok"; "Not Found" if the directory hasn't been polled, or the server address is invalid; "Unauthorized" if the server requires accepting a licence, which isn't supported over this API; "Gone (server no longer listed)" if the server is not, or is no longer, present in the directory's server list; "Upgrade Required (obsolete protocol, upgrade Jamulus)" if the server requires a newer protocol version than this client supports; or "Insufficient Storage (Full)" if the server is full. If the local audio interface fails to start, an error is returned instead of a result. | + + +### jamulusclient/disconnect + +Disconnects from the server that was connected via jamulusclient/connect. + +Parameters: + +| Name | Type | Description | +| --- | --- | --- | +| params.directory | string | Socket address of the directory that was passed to jamulusclient/connect. | +| params.server | string | Socket address of the server that was passed to jamulusclient/connect. | + +Results: + +| Name | Type | Description | +| --- | --- | --- | +| result | string | "ok"; "Not Found" if params.directory/params.server do not match the connection established by the most recent jamulusclient/connect call; or, reflecting the outcome of that call, "Unauthorized", "Gone (server no longer listed)", or "Upgrade Required (obsolete protocol, upgrade Jamulus)". | + + ### jamulusclient/getChannelInfo Returns the client's profile information. @@ -143,7 +179,7 @@ Results: | Name | Type | Description | | --- | --- | --- | -| result.id | number | The channel ID. | +| result.id | number | The channel ID assigned by the server, or -1 if not currently connected. | | result.name | string | The musician’s name. | | result.skillLevel | string | The musician’s skill level (beginner, intermediate, expert, or null). | | result.countryId | number | The musician’s country ID (see QLocale::Country). | @@ -188,6 +224,23 @@ Results: | result.clients | array | The client list. See jamulusclient/clientListReceived for the format. | +### jamulusclient/getDirectories + +Returns the list of directories in the same order as presented in Jamulus. + +Parameters: + +| Name | Type | Description | +| --- | --- | --- | +| params | object | No parameters (empty object). | + +Results: + +| Name | Type | Description | +| --- | --- | --- | +| result | array | Array of directory socket address strings, usable as params.directory in jamulusclient/pollServerList. | + + ### jamulusclient/getMidiDevices Returns a list of available MIDI input devices. diff --git a/src/client.cpp b/src/client.cpp index fb9bab5478..15cde195db 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -47,6 +47,8 @@ #include "client.h" #include "settings.h" #include "util.h" +#include +#include /* Implementation *************************************************************/ CClient::CClient ( const quint16 iPortNumber, @@ -92,7 +94,8 @@ CClient::CClient ( const quint16 iPortNumber, bJitterBufferOK ( true ), bMuteMeInPersonalMix ( bNMuteMeInPersonalMix ), iServerSockBufNumFrames ( DEF_NET_BUF_SIZE_NUM_BL ), - bRawAudioIsSupported ( false ) + bRawAudioIsSupported ( false ), + iMyChannelID ( INVALID_INDEX ) { int iOpusError; @@ -1003,6 +1006,8 @@ void CClient::OnClientIDReceived ( int iServerChanID ) SetRemoteChanGain ( iChanID, 0, false ); } + iMyChannelID = iChanID; + emit ClientIDReceived ( iChanID ); } @@ -1059,6 +1064,9 @@ void CClient::Stop() bRawAudioIsSupported = false; Init(); + // no longer connected, so no valid channel ID (see GetMyChannelID()) + iMyChannelID = INVALID_INDEX; + // wait for approx. 100 ms to make sure no audio packet is still in the // network queue causing the channel to be reconnected right after having // received the disconnect message (seems not to gain much, disconnect is @@ -1088,6 +1096,118 @@ void CClient::Stop() // Allow hibernation or display dimming if the app is running again (Windows) SetThreadExecutionState ( ES_CONTINUOUS ); #endif + + // Let every caller of Stop() (the UI's Disconnect button, the JSON-RPC API, or our own + // ConnectToServer() dropping a stale connection before reconnecting) rely on this single + // signal for post-disconnect cleanup, instead of each having to duplicate it. This is in + // addition to the same signal being emitted when the channel notices a timeout on its own. + emit Disconnected(); +} + +CClient::EConnectResult CClient::ConnectToServer ( const QString& strServerAddress, QString* pstrErrorMessage ) +{ + if ( IsRunning() ) + { + Stop(); + } + + if ( !SetServerAddr ( strServerAddress ) ) + { + return CR_INVALID_ADDRESS; + } + + // Wait for the connection attempt to resolve into a known outcome, collecting the result from + // whichever signal the connection process triggers first. On success, we wait for both the client + // ID and the version info so a server that turns out to require a newer client is still caught + // rather than being missed because we already quit the loop. + bool bGotClientID = false; + bool bGotVersion = false; + EConnectResult eResult = CR_OK; + QEventLoop loop; + + auto conClientID = connect ( this, &CClient::ClientIDReceived, [&] ( int /* unused */ ) { + bGotClientID = true; + if ( bGotVersion ) + { + loop.quit(); + } + } ); + + auto conVersion = connect ( this, &CClient::VersionAndOSReceived, [&] ( COSUtil::EOpSystemType /* unused */, QString strServerVersion ) { + int iServerSuffixIndex; + QVersionNumber serverVersion = QVersionNumber::fromString ( strServerVersion, &iServerSuffixIndex ); + + int iMySuffixIndex; + QVersionNumber myVersion = QVersionNumber::fromString ( VERSION, &iMySuffixIndex ); + + // only compare release versions (a dev/beta suffix means we cannot reliably compare) + if ( ( strServerVersion.size() == iServerSuffixIndex ) && ( QVersionNumber::compare ( serverVersion, myVersion ) > 0 ) ) + { + eResult = CR_UPGRADE_REQUIRED; + loop.quit(); + return; + } + + bGotVersion = true; + if ( bGotClientID ) + { + loop.quit(); + } + } ); + + auto conFull = connect ( getConnLessProtocol(), &CProtocol::ServerFullMesReceived, [&]() { + eResult = CR_FULL; + loop.quit(); + } ); + + auto conLicence = connect ( this, &CClient::LicenceRequired, [&] ( ELicenceType /* unused */ ) { + eResult = CR_UNAUTHORIZED; + loop.quit(); + } ); + + auto conGone = connect ( getConnLessProtocol(), &CProtocol::CLDisconnection, [&] ( CHostAddress InetAddr ) { + if ( InetAddr.toString() == strServerAddress ) + { + eResult = CR_GONE; + loop.quit(); + } + } ); + + QTimer::singleShot ( 3000, &loop, &QEventLoop::quit ); + + try + { + Start(); + loop.exec(); + } + catch ( const CGenErr& generr ) + { + disconnect ( conClientID ); + disconnect ( conVersion ); + disconnect ( conFull ); + disconnect ( conLicence ); + disconnect ( conGone ); + + if ( pstrErrorMessage ) + { + *pstrErrorMessage = generr.GetErrorText(); + } + return CR_SOUND_DEVICE_ERROR; + } + + disconnect ( conClientID ); + disconnect ( conVersion ); + disconnect ( conFull ); + disconnect ( conLicence ); + disconnect ( conGone ); + + if ( ( eResult == CR_OK ) && !bGotClientID ) + { + // no response of any kind before the timeout + eResult = CR_GONE; + } + + return eResult; } void CClient::Init() diff --git a/src/client.h b/src/client.h index fb77930723..213f8a6eb0 100644 --- a/src/client.h +++ b/src/client.h @@ -163,6 +163,35 @@ class CClient : public QObject bool IsCallbackEntered() const { return Sound.IsCallbackEntered(); } bool SetServerAddr ( QString strNAddr ); + // The address most recently passed to SetServerAddr()/ConnectToServer(), i.e. the server we are + // currently connected (or attempting to connect) to. + const CHostAddress& GetServerAddress() const { return Channel.GetAddress(); } + + // The client channel ID assigned to us by the server (see the ClientIDReceived signal), or + // INVALID_INDEX if not currently connected. + int GetMyChannelID() const { return iMyChannelID; } + + // Outcome of ConnectToServer(). Used by both the client UI and the JSON-RPC API so they agree on + // what a connection attempt resulted in. + enum EConnectResult + { + CR_OK, + CR_INVALID_ADDRESS, // strServerAddress could not be parsed + CR_SOUND_DEVICE_ERROR, // the audio interface could not be started (see pstrErrorMessage) + CR_UNAUTHORIZED, // the server requires accepting a licence before use + CR_GONE, // the server did not respond, or told us it is disconnecting us + CR_UPGRADE_REQUIRED, // the server requires a newer protocol version than this client supports + CR_FULL // the server is full + }; + + // Sets the server address and starts the client, then blocks for a short time (up to a few seconds) + // while the outcome of the connection attempt is determined. On CR_SOUND_DEVICE_ERROR, *pstrErrorMessage + // (if given) is set to a human-readable description of the audio failure. Does not stop the client on a + // non-CR_OK outcome; callers decide whether/when to give up on the attempt (e.g. the UI keeps the + // connection alive on CR_UNAUTHORIZED so the licence dialog, wired independently to LicenceRequired, + // can still let the user accept and continue). + EConnectResult ConnectToServer ( const QString& strServerAddress, QString* pstrErrorMessage = nullptr ); + // IPv6 Available bool IsIPv6Available() { return bIPv6Available; } @@ -439,6 +468,9 @@ class CClient : public QObject int iServerSockBufNumFrames; bool bRawAudioIsSupported; + // see GetMyChannelID() + int iMyChannelID; + // for ping measurement QElapsedTimer PreciseTime; diff --git a/src/clientdlg.cpp b/src/clientdlg.cpp index b102c34724..36ff4f8a74 100644 --- a/src/clientdlg.cpp +++ b/src/clientdlg.cpp @@ -1236,65 +1236,123 @@ void CClientDlg::OnCLPingTimeWithNumClientsReceived ( CHostAddress InetAddr, int ConnectDlg.SetPingTimeAndNumClientsResult ( InetAddr, iPingTime, iNumClients ); } +// NOTE (deferred, not yet implemented): Connect()/OnClientIDReceived()/OnDisconnected() are event-driven +// (they react to a state *transition*). A future pull-based CClientDlg::Refresh() - callable any time to +// make the GUI match current CClient state, e.g. on startup - is a natural next step, but two gaps need a +// decision first: +// 1. No stored "friendly" server display name: CClient only exposes the raw server address +// (GetServerAddress()); the nicer name (e.g. a server list entry's name) is a transient parameter to +// this method, never retained. A stateless Refresh() would have to fall back to the raw address, which +// is a real (if minor) behavioural difference from what this method shows today. +// 2. MainMixerBoard->HideAll() (in OnDisconnected()) calls StoreAllFaderSettings() first, a one-time +// persistence action, not pure state-reflection; and the timer .start() calls, while idempotent, are +// side effects rather than display updates. A strict "reads state, alters nothing" Refresh() would +// need to either exclude these or explicitly accept them as in-scope. +// This is left as-is intentionally for now: unifying these into Refresh() touches the same connect/disconnect +// code paths already reworked to fix the JSON-RPC API/GUI sync bugs, and is a large enough behavioural change +// to warrant its own separate, focused review. void CClientDlg::Connect ( const QString& strSelectedAddress, const QString& strMixerBoardLabel ) { - // set address and check if address is valid - if ( pClient->SetServerAddr ( strSelectedAddress ) ) + // Delegate to the same method the JSON-RPC API's jamulusclient/connect uses (CClient::ConnectToServer), + // so both agree on what constitutes success vs. each failure mode. This blocks for a short time (up to a + // few seconds) while the outcome is determined, so disable the button for the duration to avoid + // re-entering this method via a second click while we are waiting. + butConnect->setEnabled ( false ); + QString strErrorMessage; + const CClient::EConnectResult eResult = pClient->ConnectToServer ( strSelectedAddress, &strErrorMessage ); + butConnect->setEnabled ( true ); + + switch ( eResult ) { - // try to start client, if error occurred, do not go in - // running state but show error message - try - { - if ( !pClient->IsRunning() ) - { - pClient->Start(); - } - } + case CClient::CR_INVALID_ADDRESS: + // matches the previous behaviour: silently do nothing for an invalid address + return; - catch ( const CGenErr& generr ) - { - // show error message and return the function - QMessageBox::critical ( this, APP_NAME, generr.GetErrorText(), "Close", nullptr ); - return; - } + case CClient::CR_SOUND_DEVICE_ERROR: + // show error message and return the function + QMessageBox::critical ( this, APP_NAME, strErrorMessage, "Close", nullptr ); + return; + + case CClient::CR_GONE: + pClient->Stop(); + QMessageBox::warning ( this, APP_NAME, tr ( "The server did not respond. It may no longer be available." ), "Close", nullptr ); + return; + + case CClient::CR_FULL: + pClient->Stop(); + QMessageBox::warning ( this, APP_NAME, tr ( "This server is full." ), "Close", nullptr ); + return; - // hide label connect to server - lblConnectToServer->hide(); - lbrInputLevelL->setEnabled ( true ); - lbrInputLevelR->setEnabled ( true ); + case CClient::CR_UNAUTHORIZED: + // handled by OnLicenceRequired (wired directly to CClient::LicenceRequired), which shows the + // licence dialog and disconnects if declined; the connection stays up until then. + break; - // change connect button text to "disconnect" - butConnect->setText ( tr ( "&Disconnect" ) ); + case CClient::CR_UPGRADE_REQUIRED: + // advisory only, similar to the existing "update available" notice; does not block the connection + QMessageBox::information ( + this, APP_NAME, tr ( "This server is running a newer version of Jamulus than you are. Consider upgrading." ), "Close", nullptr ); + break; - // set server name in audio mixer group box title - MainMixerBoard->SetServerName ( strMixerBoardLabel ); + case CClient::CR_OK: + break; + } - // start timer for level meter bar and ping time measurement - TimerSigMet.start ( LEVELMETER_UPDATE_TIME_MS ); - TimerBuffersLED.start ( BUFFER_LED_UPDATE_TIME_MS ); - TimerPing.start ( PING_UPDATE_TIME_MS ); - TimerCheckAudioDeviceOk.start ( CHECK_AUDIO_DEV_OK_TIME_MS ); // is single shot timer + // The rest of the "we are now connected" GUI setup (button text, timers, etc.) happens in + // OnClientIDReceived, triggered by CClient::ClientIDReceived, which fires regardless of whether + // this method or the JSON-RPC API's jamulusclient/connect initiated the connection. That handler + // does not know a human-friendly display name, so override its default (the raw server address) + // with the one this dialog knows about (e.g. the server list's name for the chosen entry). + MainMixerBoard->SetServerName ( strMixerBoardLabel ); +} - // audio feedback detection - if ( pSettings->bEnableFeedbackDetection ) - { - TimerDetectFeedback.start ( DETECT_FEEDBACK_TIME_MS ); // single shot timer - bDetectFeedback = true; - } +void CClientDlg::OnClientIDReceived ( int iChanID ) +{ + MainMixerBoard->SetMyChannelID ( iChanID ); + + // "we are now connected" GUI setup. Triggered here (rather than directly in Connect()) so it also + // applies when the JSON-RPC API's jamulusclient/connect established the connection instead. We have + // no human-friendly display name at this point, so default to the raw server address; Connect() + // overrides this afterwards with a nicer name when it has one. + MainMixerBoard->SetServerName ( pClient->GetServerAddress().toString() ); + + // hide label connect to server + lblConnectToServer->hide(); + lbrInputLevelL->setEnabled ( true ); + lbrInputLevelR->setEnabled ( true ); + + // change connect button text to "disconnect" + butConnect->setText ( tr ( "&Disconnect" ) ); + + // start timer for level meter bar and ping time measurement + TimerSigMet.start ( LEVELMETER_UPDATE_TIME_MS ); + TimerBuffersLED.start ( BUFFER_LED_UPDATE_TIME_MS ); + TimerPing.start ( PING_UPDATE_TIME_MS ); + TimerCheckAudioDeviceOk.start ( CHECK_AUDIO_DEV_OK_TIME_MS ); // is single shot timer + + // audio feedback detection + if ( pSettings->bEnableFeedbackDetection ) + { + TimerDetectFeedback.start ( DETECT_FEEDBACK_TIME_MS ); // single shot timer + bDetectFeedback = true; } } void CClientDlg::Disconnect() { - // only stop client if currently running, in case we received - // the stopped message, the client is already stopped but the - // connect/disconnect button and other GUI controls must be - // updated + // Just stop the client if it is currently running; the actual GUI cleanup happens in + // OnDisconnected(), triggered by CClient::Disconnected. CClient::Stop() always emits that + // signal, so this reaches the same cleanup regardless of who else calls Stop() on our + // behalf (the JSON-RPC API's jamulusclient/disconnect, or CClient::ConnectToServer() + // dropping a stale connection before reconnecting). if ( pClient->IsRunning() ) { pClient->Stop(); } +} +void CClientDlg::OnDisconnected() +{ // change connect button text to "connect" butConnect->setText ( tr ( "C&onnect" ) ); diff --git a/src/clientdlg.h b/src/clientdlg.h index 6a34cc66f0..c8b30b4025 100644 --- a/src/clientdlg.h +++ b/src/clientdlg.h @@ -244,7 +244,7 @@ public slots: ConnectDlg.SetConnClientsList ( InetAddr, vecChanInfo ); } - void OnClientIDReceived ( int iChanID ) { MainMixerBoard->SetMyChannelID ( iChanID ); } + void OnClientIDReceived ( int iChanID ); void OnMuteStateHasChangedReceived ( int iChanID, bool bIsMuted ) { MainMixerBoard->SetRemoteFaderIsMute ( iChanID, bIsMuted ); } @@ -254,7 +254,7 @@ public slots: } void OnConnectDlgAccepted(); - void OnDisconnected() { Disconnect(); } + void OnDisconnected(); void OnGUIDesignChanged(); void OnMeterStyleChanged(); void OnRecorderStateReceived ( ERecorderState eRecorderState ); diff --git a/src/clientrpc.cpp b/src/clientrpc.cpp index 9ba7060a1d..e64edeb1fb 100644 --- a/src/clientrpc.cpp +++ b/src/clientrpc.cpp @@ -132,8 +132,9 @@ CClientRpc::CClientRpc ( CClient* pClient, CClientSettings* pSettings, CRpcServe /// @param {string} params.servers[*].city - Server city. connect ( pClient->getConnLessProtocol(), &CProtocol::CLServerListReceived, - [=] ( CHostAddress /* unused */, CVector vecServerInfo ) { - QJsonArray arrServerInfo; + [=] ( CHostAddress InetAddr, CVector vecServerInfo ) { + QJsonArray arrServerInfo; + QSet setServerAddresses; for ( const auto& serverInfo : vecServerInfo ) { QJsonObject objServerInfo{ @@ -144,8 +145,12 @@ CClientRpc::CClientRpc ( CClient* pClient, CClientSettings* pSettings, CRpcServe { "city", serverInfo.strCity }, }; arrServerInfo.append ( objServerInfo ); + setServerAddresses.insert ( serverInfo.HostAddr.toString() ); pClient->CreateCLServerListPingMes ( serverInfo.HostAddr ); } + // remember which servers this directory listed, so jamulusclient/connect can tell + // a delisted server apart from one that was never seen in the first place + m_mapDirectoryServers[InetAddr.toString()] = setServerAddresses; pRpcServer->BroadcastNotification ( "jamulusclient/serverListReceived", QJsonObject{ { "servers", arrServerInfo }, @@ -166,7 +171,14 @@ CClientRpc::CClientRpc ( CClient* pClient, CClientSettings* pSettings, CRpcServe /// @rpc_notification jamulusclient/disconnected /// @brief Emitted when the client is disconnected from the server. /// @param {object} params - No parameters (empty object). - connect ( pClient, &CClient::Disconnected, [=]() { pRpcServer->BroadcastNotification ( "jamulusclient/disconnected", QJsonObject{} ); } ); + connect ( pClient, &CClient::Disconnected, [=]() { + // keep our record of the current connection (used by jamulusclient/disconnect) in sync, + // whether the disconnect was initiated by us, the server, or the desktop UI + m_strConnectedDirectory.clear(); + m_strConnectedServer.clear(); + m_strConnectStatus.clear(); + pRpcServer->BroadcastNotification ( "jamulusclient/disconnected", QJsonObject{} ); + } ); /// @rpc_notification jamulusclient/recorderState /// @brief Emitted when the client is connected to a server whose recorder state changes. @@ -205,6 +217,156 @@ CClientRpc::CClientRpc ( CClient* pClient, CClientSettings* pSettings, CRpcServe response["result"] = "ok"; } ); + /// @rpc_method jamulusclient/getDirectories + /// @brief Returns the list of directories in the same order as presented in Jamulus. + /// @param {object} params - No parameters (empty object). + /// @result {array} result - Array of directory socket address strings, usable as params.directory in jamulusclient/pollServerList. + pRpcServer->HandleMethod ( "jamulusclient/getDirectories", [=] ( const QJsonObject& params, QJsonObject& response ) { + QJsonArray arrDirectories; + // built-in directories in UI order + for ( int i = AT_DEFAULT; i < AT_CUSTOM; i++ ) + { + arrDirectories.append ( NetworkUtil::GetDirectoryAddress ( static_cast ( i ), "" ) ); + } + // custom directories — stored newest-first, displayed oldest-first (reverse iteration) + for ( int i = MAX_NUM_SERVER_ADDR_ITEMS - 1; i >= 0; i-- ) + { + if ( !m_pSettings->vstrDirectoryAddress[i].isEmpty() ) + { + arrDirectories.append ( m_pSettings->vstrDirectoryAddress[i] ); + } + } + response["result"] = arrDirectories; + Q_UNUSED ( params ); + } ); + + /// @rpc_method jamulusclient/connect + /// @brief Connects to a server previously discovered via jamulusclient/pollServerList on the given directory. + /// Blocks for a short time (up to a few seconds) while the outcome of the connection attempt is determined. + /// @param {string} params.directory - Socket address of the directory the server was discovered from. Must + /// have already been queried via jamulusclient/pollServerList. + /// @param {string} params.server - Socket address of the server to connect to, as returned in + /// params.servers[*].address of jamulusclient/serverListReceived. + /// @result {string} result - "ok"; "Not Found" if the directory hasn't been polled, or the server address is + /// invalid; "Unauthorized" if the server requires accepting a licence, which isn't supported over this API; + /// "Gone (server no longer listed)" if the server is not, or is no longer, present in the directory's server + /// list; "Upgrade Required (obsolete protocol, upgrade Jamulus)" if the server requires a newer protocol + /// version than this client supports; or "Insufficient Storage (Full)" if the server is full. If the local + /// audio interface fails to start, an error is returned instead of a result. + pRpcServer->HandleMethod ( "jamulusclient/connect", [=] ( const QJsonObject& params, QJsonObject& response ) { + auto jsonDirectory = params["directory"]; + auto jsonServer = params["server"]; + if ( !jsonDirectory.isString() || !jsonServer.isString() ) + { + response["error"] = + CRpcServer::CreateJsonRpcError ( CRpcServer::iErrInvalidParams, "Invalid params: directory and server must be strings" ); + return; + } + + // Resolve the directory the same way jamulusclient/pollServerList does, so it matches the key that the + // jamulusclient/serverListReceived handler cached the server list under. + CHostAddress haDirectoryAddress; + const QString strDirectoryKey = + NetworkUtil::ParseNetworkAddress ( jsonDirectory.toString(), haDirectoryAddress, false ) ? haDirectoryAddress.toString() : QString(); + + if ( strDirectoryKey.isEmpty() || !m_mapDirectoryServers.contains ( strDirectoryKey ) ) + { + response["result"] = "Not Found"; + return; + } + + const QString strServer = jsonServer.toString(); + + if ( !m_mapDirectoryServers[strDirectoryKey].contains ( strServer ) ) + { + response["result"] = "Gone (server no longer listed)"; + return; + } + + // Delegate the actual connection attempt to the same method the desktop UI's "Connect" button uses + // (CClientDlg::Connect), so both agree on what constitutes success vs. each failure mode. + QString strErrorMessage; + const CClient::EConnectResult eResult = pClient->ConnectToServer ( strServer, &strErrorMessage ); + + if ( eResult == CClient::CR_SOUND_DEVICE_ERROR ) + { + response["error"] = CRpcServer::CreateJsonRpcError ( 1, "Could not start the audio interface: " + strErrorMessage ); + return; + } + + switch ( eResult ) + { + case CClient::CR_OK: + response["result"] = "ok"; + break; + case CClient::CR_INVALID_ADDRESS: + response["result"] = "Not Found"; + break; + case CClient::CR_UNAUTHORIZED: + response["result"] = "Unauthorized"; + break; + case CClient::CR_GONE: + response["result"] = "Gone (server no longer listed)"; + break; + case CClient::CR_UPGRADE_REQUIRED: + response["result"] = "Upgrade Required (obsolete protocol, upgrade Jamulus)"; + break; + case CClient::CR_FULL: + response["result"] = "Insufficient Storage (Full)"; + break; + default: + break; + } + + // Unlike the desktop UI (which keeps the connection alive on CR_UNAUTHORIZED/CR_UPGRADE_REQUIRED so + // the user can interact with the licence dialog, or simply because it never blocked on stale + // versions), there is no interactive fallback here, so any non-ok outcome ends the attempt. + if ( ( eResult != CClient::CR_OK ) && pClient->IsRunning() ) + { + pClient->Stop(); + } + + m_strConnectedDirectory = jsonDirectory.toString(); + m_strConnectedServer = strServer; + m_strConnectStatus = response["result"].toString(); + } ); + + /// @rpc_method jamulusclient/disconnect + /// @brief Disconnects from the server that was connected via jamulusclient/connect. + /// @param {string} params.directory - Socket address of the directory that was passed to jamulusclient/connect. + /// @param {string} params.server - Socket address of the server that was passed to jamulusclient/connect. + /// @result {string} result - "ok"; "Not Found" if params.directory/params.server do not match the connection + /// established by the most recent jamulusclient/connect call; or, reflecting the outcome of that call, + /// "Unauthorized", "Gone (server no longer listed)", or "Upgrade Required (obsolete protocol, upgrade + /// Jamulus)". + pRpcServer->HandleMethod ( "jamulusclient/disconnect", [=] ( const QJsonObject& params, QJsonObject& response ) { + auto jsonDirectory = params["directory"]; + auto jsonServer = params["server"]; + if ( !jsonDirectory.isString() || !jsonServer.isString() ) + { + response["error"] = + CRpcServer::CreateJsonRpcError ( CRpcServer::iErrInvalidParams, "Invalid params: directory and server must be strings" ); + return; + } + + if ( ( jsonDirectory.toString() != m_strConnectedDirectory ) || ( jsonServer.toString() != m_strConnectedServer ) ) + { + response["result"] = "Not Found"; + return; + } + + // capture before Stop(): it synchronously emits CClient::Disconnected, which our own handler + // above (kept in sync with the desktop UI's cleanup) reacts to by clearing these fields + const QString strPreviousStatus = m_strConnectStatus; + + if ( pClient->IsRunning() ) + { + pClient->Stop(); + } + + response["result"] = strPreviousStatus; + } ); + /// @rpc_method jamulus/getMode /// @brief Returns the current mode, i.e. whether Jamulus is running as a server or client. /// @param {object} params - No parameters (empty object). @@ -228,7 +390,7 @@ CClientRpc::CClientRpc ( CClient* pClient, CClientSettings* pSettings, CRpcServe /// @rpc_method jamulusclient/getChannelInfo /// @brief Returns the client's profile information. /// @param {object} params - No parameters (empty object). - /// @result {number} result.id - The channel ID. + /// @result {number} result.id - The channel ID assigned by the server, or -1 if not currently connected. /// @result {string} result.name - The musician’s name. /// @result {string} result.skillLevel - The musician’s skill level (beginner, intermediate, expert, or null). /// @result {number} result.countryId - The musician’s country ID (see QLocale::Country). @@ -239,7 +401,7 @@ CClientRpc::CClientRpc ( CClient* pClient, CClientSettings* pSettings, CRpcServe /// @result {string} result.skillLevel - Your skill level (beginner, intermediate, expert, or null). pRpcServer->HandleMethod ( "jamulusclient/getChannelInfo", [=] ( const QJsonObject& params, QJsonObject& response ) { QJsonObject result{ - // TODO: We cannot include "id" here is pClient->ChannelInfo is a CChannelCoreInfo which lacks that field. + { "id", pClient->GetMyChannelID() }, { "name", pClient->ChannelInfo.strName }, { "countryId", pClient->ChannelInfo.eCountry }, { "country", QLocale::countryToString ( pClient->ChannelInfo.eCountry ) }, diff --git a/src/clientrpc.h b/src/clientrpc.h index bae565208a..9e6d2116ba 100644 --- a/src/clientrpc.h +++ b/src/clientrpc.h @@ -47,6 +47,7 @@ #pragma once +#include #include "client.h" #include "util.h" #include "rpcserver.h" @@ -64,4 +65,14 @@ class CClientRpc : public QObject CClientSettings* m_pSettings; QJsonArray arrStoredChanInfo; static QJsonValue SerializeSkillLevel ( ESkillLevel skillLevel ); + + // last server list received per directory (keyed by CHostAddress::toString() of the directory), + // used to check whether a server is (still) listed before connecting to it + QMap> m_mapDirectoryServers; + + // identifies the connection established (or attempted) by the most recent jamulusclient/connect + // call, along with its outcome, so jamulusclient/disconnect can validate its params against it + QString m_strConnectedDirectory; + QString m_strConnectedServer; + QString m_strConnectStatus; };