diff --git a/server/2015Remote/2015RemoteDlg.cpp b/server/2015Remote/2015RemoteDlg.cpp index 594e013..4197f98 100644 --- a/server/2015Remote/2015RemoteDlg.cpp +++ b/server/2015Remote/2015RemoteDlg.cpp @@ -9651,6 +9651,25 @@ void CMy2015RemoteDlg::CloseRemoteDesktopByClientID(uint64_t clientID) } } +bool CMy2015RemoteDlg::PostWebAudioToggle(uint64_t clientID) +{ + HWND hWnd = NULL; + EnterCriticalSection(&m_cs); + for (auto& pair : m_RemoteWnds) { + CScreenSpyDlg* dlg = dynamic_cast(pair.second); + if (dlg && dlg->GetClientID() == clientID && dlg->IsWebSession()) { + hWnd = dlg->GetSafeHwnd(); + break; + } + } + LeaveCriticalSection(&m_cs); + if (hWnd && ::IsWindow(hWnd)) { + // PostMessage 把活儿丢到对话框的 UI 线程,避免 WS 线程动 waveOut 句柄 + return ::PostMessage(hWnd, WM_AUDIO_TOGGLE_FROM_WEB, 0, 0) != 0; + } + return false; +} + void CMy2015RemoteDlg::CloseWebRemoteDesktopByClientID(uint64_t clientID) { CScreenSpyDlg* targetDlg = nullptr; diff --git a/server/2015Remote/2015RemoteDlg.h b/server/2015Remote/2015RemoteDlg.h index 8aad020..7730d7f 100644 --- a/server/2015Remote/2015RemoteDlg.h +++ b/server/2015Remote/2015RemoteDlg.h @@ -373,6 +373,7 @@ public: void RemoveRemoteWindow(HWND wnd); void CloseRemoteDesktopByClientID(uint64_t clientID); void CloseWebRemoteDesktopByClientID(uint64_t clientID); // Only close Web session dialog + bool PostWebAudioToggle(uint64_t clientID); // 给 Web 会话 ScreenSpy 投递音频开关消息 CDialogBase* m_pActiveSession = nullptr; // 当前活动会话窗口指针 / NULL 表示无 void UpdateActiveRemoteSession(CDialogBase* sess); CDialogBase* GetActiveRemoteSession(); diff --git a/server/2015Remote/ScreenSpyDlg.cpp b/server/2015Remote/ScreenSpyDlg.cpp index 388fe28..f583491 100644 --- a/server/2015Remote/ScreenSpyDlg.cpp +++ b/server/2015Remote/ScreenSpyDlg.cpp @@ -224,6 +224,8 @@ CScreenSpyDlg::CScreenSpyDlg(CMy2015RemoteDlg* Parent, Server* IOCPServer, CONTE int width = m_BitmapInfor_Full->bmiHeader.biWidth; int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight); WebService().NotifyResolutionChange(m_ClientID, width, height); + // 透传客户端初始的音频开/关状态给 web,让前端按钮显示正确 + WebService().NotifyAudioState(m_ClientID, m_Settings.AudioEnabled != 0); } } @@ -568,6 +570,7 @@ BEGIN_MESSAGE_MAP(CScreenSpyDlg, CDialog) ON_MESSAGE(MM_WOM_DONE, &CScreenSpyDlg::OnWaveOutDone) ON_MESSAGE(WM_RECVFILEV2_CHUNK, &CScreenSpyDlg::OnRecvFileV2Chunk) ON_MESSAGE(WM_RECVFILEV2_COMPLETE, &CScreenSpyDlg::OnRecvFileV2Complete) + ON_MESSAGE(WM_AUDIO_TOGGLE_FROM_WEB, &CScreenSpyDlg::OnAudioToggleFromWeb) ON_WM_DROPFILES() ON_WM_CAPTURECHANGED() END_MESSAGE_MAP() @@ -3521,6 +3524,11 @@ void CScreenSpyDlg::DisableAudio() } Mprintf("[Audio Web] 禁用音频(来自 web 命令)\n"); + + // 广播状态给所有正在观看本设备的 web 客户端 + if (WebService().IsRunning()) { + WebService().NotifyAudioState(m_ClientID, false); + } } } @@ -3539,9 +3547,24 @@ void CScreenSpyDlg::EnableAudio() } Mprintf("[Audio Web] 启用音频(来自 web 命令)\n"); + + if (WebService().IsRunning()) { + WebService().NotifyAudioState(m_ClientID, true); + } } } +// 由 PostMessage 从 WS 线程派发到 UI 线程;根据当前状态翻转 +LRESULT CScreenSpyDlg::OnAudioToggleFromWeb(WPARAM /*wParam*/, LPARAM /*lParam*/) +{ + if (m_Settings.AudioEnabled) { + DisableAudio(); + } else { + EnableAudio(); + } + return 0; +} + void CScreenSpyDlg::OnAudioData(BYTE* pData, UINT32 len) { if (len < 1) return; diff --git a/server/2015Remote/ScreenSpyDlg.h b/server/2015Remote/ScreenSpyDlg.h index 967facb..91062d8 100644 --- a/server/2015Remote/ScreenSpyDlg.h +++ b/server/2015Remote/ScreenSpyDlg.h @@ -93,6 +93,9 @@ extern "C" // 文件接收消息(用于将工作线程的文件数据转发到主线程处理) #define WM_RECVFILEV2_CHUNK (WM_USER + 0x200) #define WM_RECVFILEV2_COMPLETE (WM_USER + 0x201) +// 来自 web 命令的音频开关,PostMessage 到对话框的 UI 线程,避免 WS 线程 +// 直接动 waveOut 句柄 +#define WM_AUDIO_TOGGLE_FROM_WEB (WM_USER + 0x202) // ScreenSpyDlg 系统菜单命令 ID enum { @@ -363,6 +366,7 @@ public: void StopAudioPlayback(); // 停止音频播放 void DisableAudio(); // 禁用音频(从网页命令) void EnableAudio(); // 启用音频(从网页命令) + LRESULT OnAudioToggleFromWeb(WPARAM wParam, LPARAM lParam); // PostMessage 处理器 void SendAudioCtrl(BYTE enable, BYTE persist); // 发送音频控制命令 void FeedAudioBuffers(); // 填充音频缓冲区 void SendAudioToWeb(const BYTE* pAudioData, UINT32 len, const WAVEFORMATEX* pFormat, BYTE compression); // 发送音频到网页 (compression=AudioCompression) diff --git a/server/2015Remote/WebService.cpp b/server/2015Remote/WebService.cpp index b38e38e..f4855a3 100644 --- a/server/2015Remote/WebService.cpp +++ b/server/2015Remote/WebService.cpp @@ -396,6 +396,8 @@ void CWebService::ServerThread(int port) { HandleKey(ws_ptr, msg); } else if (cmd == "rdp_reset") { HandleRdpReset(ws_ptr, token); + } else if (cmd == "audio_toggle") { + HandleAudioToggle(ws_ptr, token); } else if (cmd == "get_salt") { HandleGetSalt(ws_ptr, msg); } else if (cmd == "create_user") { @@ -689,14 +691,16 @@ void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t } } - // Get screen dimensions from device info cache (may not be available yet) + // Get screen dimensions + audio state from device info cache (may not be ready) int width = 0, height = 0; + int audio_enabled = -1; // -1 = unknown yet (前端走 audio_state 事件兜底) { std::lock_guard lock(m_DeviceCacheMutex); auto it = m_DeviceCache.find(device_id); if (it != m_DeviceCache.end()) { width = it->second->screen_width; height = it->second->screen_height; + audio_enabled = it->second->audio_enabled; } } @@ -710,6 +714,9 @@ void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t res["width"] = width; res["height"] = height; } + if (audio_enabled >= 0) { + res["audio_enabled"] = (audio_enabled != 0); + } res["algorithm"] = "h264"; Json::StreamWriterBuilder builder; @@ -1002,6 +1009,33 @@ void CWebService::HandleRdpReset(void* ws_ptr, const std::string& token) { } } +void CWebService::HandleAudioToggle(void* ws_ptr, const std::string& token) { + std::string username, role; + if (!ValidateToken(token, username, role)) { + SendText(ws_ptr, BuildJsonResponse("audio_toggle_result", false, "Invalid token")); + return; + } + + uint64_t device_id = 0; + { + std::lock_guard lock(m_ClientsMutex); + auto it = m_Clients.find(ws_ptr); + if (it != m_Clients.end()) device_id = it->second.watch_device_id; + } + if (device_id == 0) { + SendText(ws_ptr, BuildJsonResponse("audio_toggle_result", false, "No device connected")); + return; + } + + // 投递到 ScreenSpyDlg 的 UI 线程;那边会调用 Enable/DisableAudio 并通过 + // NotifyAudioState 把新状态广播给所有 watching 的 web 客户端 + if (!m_pParentDlg || !m_pParentDlg->PostWebAudioToggle(device_id)) { + SendText(ws_ptr, BuildJsonResponse("audio_toggle_result", false, "No active screen session")); + return; + } + SendText(ws_ptr, BuildJsonResponse("audio_toggle_result", true)); +} + ////////////////////////////////////////////////////////////////////////// // User Management Handlers ////////////////////////////////////////////////////////////////////////// @@ -1699,6 +1733,37 @@ void CWebService::NotifyResolutionChange(uint64_t device_id, int width, int heig } } +void CWebService::NotifyAudioState(uint64_t device_id, bool enabled) { + if (m_bStopping) return; + + // 缓存最新状态,新加入的 web 客户端通过 connect_result 取到初值 + { + std::lock_guard lock(m_DeviceCacheMutex); + auto it = m_DeviceCache.find(device_id); + if (it == m_DeviceCache.end()) { + m_DeviceCache[device_id] = std::make_shared(); + it = m_DeviceCache.find(device_id); + } + it->second->audio_enabled = enabled ? 1 : 0; + } + + Json::Value res; + res["cmd"] = "audio_state"; + res["id"] = device_id; + res["enabled"] = enabled; + + Json::StreamWriterBuilder builder; + builder["indentation"] = ""; + std::string json = Json::writeString(builder, res); + + std::lock_guard lock(m_ClientsMutex); + for (auto& [ws_ptr, client] : m_Clients) { + if (client.watch_device_id == device_id) { + SendText(ws_ptr, json); + } + } +} + void CWebService::BroadcastCursor(uint64_t device_id, uint8_t cursor_index) { if (m_bStopping) return; diff --git a/server/2015Remote/WebService.h b/server/2015Remote/WebService.h index 04bbd0e..3da10b9 100644 --- a/server/2015Remote/WebService.h +++ b/server/2015Remote/WebService.h @@ -55,6 +55,8 @@ struct WebDeviceInfo { int screen_width; int screen_height; bool online; + // 当前会话的音频开关。-1=未知(客户端 BITMAPINFO 还没回来),0=关,1=开 + int audio_enabled = -1; // Keyframe cache for new web clients std::vector keyframe_cache; @@ -98,6 +100,10 @@ public: // Resolution change notification void NotifyResolutionChange(uint64_t device_id, int width, int height); + // Audio enable/disable notification — pushes current state to all web + // clients watching this device and caches it for newcomers. + void NotifyAudioState(uint64_t device_id, bool enabled); + // Cursor change notification (called from ScreenSpyDlg) void BroadcastCursor(uint64_t device_id, uint8_t cursor_index); @@ -129,6 +135,7 @@ private: void HandleMouse(void* ws_ptr, const std::string& msg); void HandleKey(void* ws_ptr, const std::string& msg); void HandleRdpReset(void* ws_ptr, const std::string& token); + void HandleAudioToggle(void* ws_ptr, const std::string& token); // Token management std::string GenerateToken(const std::string& username, const std::string& role); diff --git a/server/go/go.mod b/server/go/go.mod index 9bfa5d7..e62f88f 100644 --- a/server/go/go.mod +++ b/server/go/go.mod @@ -7,6 +7,7 @@ require ( github.com/gorilla/websocket v1.5.3 github.com/klauspost/compress v1.18.2 github.com/rs/zerolog v1.34.0 + golang.org/x/sync v0.20.0 golang.org/x/text v0.32.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) @@ -14,6 +15,5 @@ require ( require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect - golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.12.0 // indirect ) diff --git a/server/go/web/ws_handlers.go b/server/go/web/ws_handlers.go index 44087dc..b84d1e6 100644 --- a/server/go/web/ws_handlers.go +++ b/server/go/web/ws_handlers.go @@ -51,6 +51,15 @@ func (h *wsHub) dispatch(c *wsClient, cmd string, raw []byte) { h.handleConnect(c, raw) case "rdp_reset": h.handleRdpReset(c, raw) + case "audio_toggle": + // Audio capture/forwarding is not yet ported from the C++ WebService + // (see project_go_webservice_port). Reply explicitly so the front-end + // console shows why the toolbar button has no effect, instead of the + // request being silently dropped by the default case. + c.queue(mustJSON(map[string]any{ + "cmd": "audio_toggle_result", "ok": false, + "msg": "Audio toggle not supported on Go server yet", + })) case "mouse": h.handleMouse(c, raw) case "key": diff --git a/server/web/index.html b/server/web/index.html index 9811147..e1d7963 100644 --- a/server/web/index.html +++ b/server/web/index.html @@ -737,6 +737,7 @@ .toolbar-btn-bar:hover { background: rgba(255,255,255,0.2); } .toolbar-btn-bar.active { background: rgba(52,199,89,0.8); } .toolbar-btn-bar.active:hover { background: rgba(52,199,89,1); } + .toolbar-btn-bar.muted { opacity: 0.55; } .toolbar-btn-bar:disabled { opacity: 0.4; cursor: not-allowed; } .toolbar-btn-bar:disabled:hover { background: rgba(255,255,255,0.1); } #screen-page:fullscreen .screen-toolbar { display: none; } @@ -1174,6 +1175,7 @@
Connecting... + @@ -1548,11 +1550,22 @@ // Wait for resolution_changed message updateScreenStatus('waiting', 'Waiting for video...'); } + // Audio state may or may not be cached yet on the server. + // If not, the audio_state event below will populate it. + if (typeof msg.audio_enabled === 'boolean') { + applyAudioState(msg.audio_enabled); + } } else { updateScreenStatus('error', msg.msg); setTimeout(() => showPage('devices-page'), 2000); } break; + case 'audio_state': + applyAudioState(!!msg.enabled); + break; + case 'audio_toggle_result': + if (!msg.ok) console.warn('[Audio] toggle failed:', msg.msg); + break; case 'term_ready': termState.ready = true; document.getElementById('term-status-info').textContent = @@ -2463,6 +2476,9 @@ document.getElementById('device-name').textContent = currentDevice.name; document.getElementById('frame-info').textContent = ''; updateScreenStatus('connecting'); + // Default the audio button to "on" optimistically; server will + // correct via connect_result.audio_enabled or audio_state event. + applyAudioState(true); showPage('screen-page'); ws.send(JSON.stringify({ cmd: 'connect', id: String(id), token })); } @@ -2906,6 +2922,26 @@ } } + // Reflect server-confirmed audio on/off on the toolbar icon. Server is + // authoritative — toggleAudio() does not flip state locally; it only + // sends the request and waits for the audio_state broadcast. + function applyAudioState(enabled) { + audioEnabled = !!enabled; + const btn = document.getElementById('btn-audio-bar'); + if (btn) { + // 0x1F50A speaker / 0x1F507 muted speaker + btn.innerHTML = audioEnabled ? '🔊' : '🔇'; + btn.title = audioEnabled ? 'Mute audio' : 'Unmute audio'; + btn.classList.toggle('muted', !audioEnabled); + } + } + + function toggleAudio() { + if (ws && ws.readyState === WebSocket.OPEN && token) { + ws.send(JSON.stringify({ cmd: 'audio_toggle', token })); + } + } + // Detect touch device (mobile/tablet) const isTouchDevice = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);