Feature(web): Add toolbar audio toggle button
This commit is contained in:
@@ -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<CScreenSpyDlg*>(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)
|
void CMy2015RemoteDlg::CloseWebRemoteDesktopByClientID(uint64_t clientID)
|
||||||
{
|
{
|
||||||
CScreenSpyDlg* targetDlg = nullptr;
|
CScreenSpyDlg* targetDlg = nullptr;
|
||||||
|
|||||||
@@ -373,6 +373,7 @@ public:
|
|||||||
void RemoveRemoteWindow(HWND wnd);
|
void RemoveRemoteWindow(HWND wnd);
|
||||||
void CloseRemoteDesktopByClientID(uint64_t clientID);
|
void CloseRemoteDesktopByClientID(uint64_t clientID);
|
||||||
void CloseWebRemoteDesktopByClientID(uint64_t clientID); // Only close Web session dialog
|
void CloseWebRemoteDesktopByClientID(uint64_t clientID); // Only close Web session dialog
|
||||||
|
bool PostWebAudioToggle(uint64_t clientID); // 给 Web 会话 ScreenSpy 投递音频开关消息
|
||||||
CDialogBase* m_pActiveSession = nullptr; // 当前活动会话窗口指针 / NULL 表示无
|
CDialogBase* m_pActiveSession = nullptr; // 当前活动会话窗口指针 / NULL 表示无
|
||||||
void UpdateActiveRemoteSession(CDialogBase* sess);
|
void UpdateActiveRemoteSession(CDialogBase* sess);
|
||||||
CDialogBase* GetActiveRemoteSession();
|
CDialogBase* GetActiveRemoteSession();
|
||||||
|
|||||||
@@ -224,6 +224,8 @@ CScreenSpyDlg::CScreenSpyDlg(CMy2015RemoteDlg* Parent, Server* IOCPServer, CONTE
|
|||||||
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
|
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
|
||||||
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
|
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
|
||||||
WebService().NotifyResolutionChange(m_ClientID, width, height);
|
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(MM_WOM_DONE, &CScreenSpyDlg::OnWaveOutDone)
|
||||||
ON_MESSAGE(WM_RECVFILEV2_CHUNK, &CScreenSpyDlg::OnRecvFileV2Chunk)
|
ON_MESSAGE(WM_RECVFILEV2_CHUNK, &CScreenSpyDlg::OnRecvFileV2Chunk)
|
||||||
ON_MESSAGE(WM_RECVFILEV2_COMPLETE, &CScreenSpyDlg::OnRecvFileV2Complete)
|
ON_MESSAGE(WM_RECVFILEV2_COMPLETE, &CScreenSpyDlg::OnRecvFileV2Complete)
|
||||||
|
ON_MESSAGE(WM_AUDIO_TOGGLE_FROM_WEB, &CScreenSpyDlg::OnAudioToggleFromWeb)
|
||||||
ON_WM_DROPFILES()
|
ON_WM_DROPFILES()
|
||||||
ON_WM_CAPTURECHANGED()
|
ON_WM_CAPTURECHANGED()
|
||||||
END_MESSAGE_MAP()
|
END_MESSAGE_MAP()
|
||||||
@@ -3521,6 +3524,11 @@ void CScreenSpyDlg::DisableAudio()
|
|||||||
}
|
}
|
||||||
|
|
||||||
Mprintf("[Audio Web] 禁用音频(来自 web 命令)\n");
|
Mprintf("[Audio Web] 禁用音频(来自 web 命令)\n");
|
||||||
|
|
||||||
|
// 广播状态给所有正在观看本设备的 web 客户端
|
||||||
|
if (WebService().IsRunning()) {
|
||||||
|
WebService().NotifyAudioState(m_ClientID, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3539,8 +3547,23 @@ void CScreenSpyDlg::EnableAudio()
|
|||||||
}
|
}
|
||||||
|
|
||||||
Mprintf("[Audio Web] 启用音频(来自 web 命令)\n");
|
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)
|
void CScreenSpyDlg::OnAudioData(BYTE* pData, UINT32 len)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ extern "C"
|
|||||||
// 文件接收消息(用于将工作线程的文件数据转发到主线程处理)
|
// 文件接收消息(用于将工作线程的文件数据转发到主线程处理)
|
||||||
#define WM_RECVFILEV2_CHUNK (WM_USER + 0x200)
|
#define WM_RECVFILEV2_CHUNK (WM_USER + 0x200)
|
||||||
#define WM_RECVFILEV2_COMPLETE (WM_USER + 0x201)
|
#define WM_RECVFILEV2_COMPLETE (WM_USER + 0x201)
|
||||||
|
// 来自 web 命令的音频开关,PostMessage 到对话框的 UI 线程,避免 WS 线程
|
||||||
|
// 直接动 waveOut 句柄
|
||||||
|
#define WM_AUDIO_TOGGLE_FROM_WEB (WM_USER + 0x202)
|
||||||
|
|
||||||
// ScreenSpyDlg 系统菜单命令 ID
|
// ScreenSpyDlg 系统菜单命令 ID
|
||||||
enum {
|
enum {
|
||||||
@@ -363,6 +366,7 @@ public:
|
|||||||
void StopAudioPlayback(); // 停止音频播放
|
void StopAudioPlayback(); // 停止音频播放
|
||||||
void DisableAudio(); // 禁用音频(从网页命令)
|
void DisableAudio(); // 禁用音频(从网页命令)
|
||||||
void EnableAudio(); // 启用音频(从网页命令)
|
void EnableAudio(); // 启用音频(从网页命令)
|
||||||
|
LRESULT OnAudioToggleFromWeb(WPARAM wParam, LPARAM lParam); // PostMessage 处理器
|
||||||
void SendAudioCtrl(BYTE enable, BYTE persist); // 发送音频控制命令
|
void SendAudioCtrl(BYTE enable, BYTE persist); // 发送音频控制命令
|
||||||
void FeedAudioBuffers(); // 填充音频缓冲区
|
void FeedAudioBuffers(); // 填充音频缓冲区
|
||||||
void SendAudioToWeb(const BYTE* pAudioData, UINT32 len, const WAVEFORMATEX* pFormat, BYTE compression); // 发送音频到网页 (compression=AudioCompression)
|
void SendAudioToWeb(const BYTE* pAudioData, UINT32 len, const WAVEFORMATEX* pFormat, BYTE compression); // 发送音频到网页 (compression=AudioCompression)
|
||||||
|
|||||||
@@ -396,6 +396,8 @@ void CWebService::ServerThread(int port) {
|
|||||||
HandleKey(ws_ptr, msg);
|
HandleKey(ws_ptr, msg);
|
||||||
} else if (cmd == "rdp_reset") {
|
} else if (cmd == "rdp_reset") {
|
||||||
HandleRdpReset(ws_ptr, token);
|
HandleRdpReset(ws_ptr, token);
|
||||||
|
} else if (cmd == "audio_toggle") {
|
||||||
|
HandleAudioToggle(ws_ptr, token);
|
||||||
} else if (cmd == "get_salt") {
|
} else if (cmd == "get_salt") {
|
||||||
HandleGetSalt(ws_ptr, msg);
|
HandleGetSalt(ws_ptr, msg);
|
||||||
} else if (cmd == "create_user") {
|
} 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 width = 0, height = 0;
|
||||||
|
int audio_enabled = -1; // -1 = unknown yet (前端走 audio_state 事件兜底)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(m_DeviceCacheMutex);
|
std::lock_guard<std::mutex> lock(m_DeviceCacheMutex);
|
||||||
auto it = m_DeviceCache.find(device_id);
|
auto it = m_DeviceCache.find(device_id);
|
||||||
if (it != m_DeviceCache.end()) {
|
if (it != m_DeviceCache.end()) {
|
||||||
width = it->second->screen_width;
|
width = it->second->screen_width;
|
||||||
height = it->second->screen_height;
|
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["width"] = width;
|
||||||
res["height"] = height;
|
res["height"] = height;
|
||||||
}
|
}
|
||||||
|
if (audio_enabled >= 0) {
|
||||||
|
res["audio_enabled"] = (audio_enabled != 0);
|
||||||
|
}
|
||||||
res["algorithm"] = "h264";
|
res["algorithm"] = "h264";
|
||||||
|
|
||||||
Json::StreamWriterBuilder builder;
|
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<std::mutex> 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
|
// 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<std::mutex> lock(m_DeviceCacheMutex);
|
||||||
|
auto it = m_DeviceCache.find(device_id);
|
||||||
|
if (it == m_DeviceCache.end()) {
|
||||||
|
m_DeviceCache[device_id] = std::make_shared<WebDeviceInfo>();
|
||||||
|
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<std::mutex> 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) {
|
void CWebService::BroadcastCursor(uint64_t device_id, uint8_t cursor_index) {
|
||||||
if (m_bStopping) return;
|
if (m_bStopping) return;
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ struct WebDeviceInfo {
|
|||||||
int screen_width;
|
int screen_width;
|
||||||
int screen_height;
|
int screen_height;
|
||||||
bool online;
|
bool online;
|
||||||
|
// 当前会话的音频开关。-1=未知(客户端 BITMAPINFO 还没回来),0=关,1=开
|
||||||
|
int audio_enabled = -1;
|
||||||
|
|
||||||
// Keyframe cache for new web clients
|
// Keyframe cache for new web clients
|
||||||
std::vector<uint8_t> keyframe_cache;
|
std::vector<uint8_t> keyframe_cache;
|
||||||
@@ -98,6 +100,10 @@ public:
|
|||||||
// Resolution change notification
|
// Resolution change notification
|
||||||
void NotifyResolutionChange(uint64_t device_id, int width, int height);
|
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)
|
// Cursor change notification (called from ScreenSpyDlg)
|
||||||
void BroadcastCursor(uint64_t device_id, uint8_t cursor_index);
|
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 HandleMouse(void* ws_ptr, const std::string& msg);
|
||||||
void HandleKey(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 HandleRdpReset(void* ws_ptr, const std::string& token);
|
||||||
|
void HandleAudioToggle(void* ws_ptr, const std::string& token);
|
||||||
|
|
||||||
// Token management
|
// Token management
|
||||||
std::string GenerateToken(const std::string& username, const std::string& role);
|
std::string GenerateToken(const std::string& username, const std::string& role);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ require (
|
|||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/klauspost/compress v1.18.2
|
github.com/klauspost/compress v1.18.2
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.34.0
|
||||||
|
golang.org/x/sync v0.20.0
|
||||||
golang.org/x/text v0.32.0
|
golang.org/x/text v0.32.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
)
|
)
|
||||||
@@ -14,6 +15,5 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // 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
|
golang.org/x/sys v0.12.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -51,6 +51,15 @@ func (h *wsHub) dispatch(c *wsClient, cmd string, raw []byte) {
|
|||||||
h.handleConnect(c, raw)
|
h.handleConnect(c, raw)
|
||||||
case "rdp_reset":
|
case "rdp_reset":
|
||||||
h.handleRdpReset(c, raw)
|
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":
|
case "mouse":
|
||||||
h.handleMouse(c, raw)
|
h.handleMouse(c, raw)
|
||||||
case "key":
|
case "key":
|
||||||
|
|||||||
@@ -737,6 +737,7 @@
|
|||||||
.toolbar-btn-bar:hover { background: rgba(255,255,255,0.2); }
|
.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 { background: rgba(52,199,89,0.8); }
|
||||||
.toolbar-btn-bar.active:hover { background: rgba(52,199,89,1); }
|
.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 { opacity: 0.4; cursor: not-allowed; }
|
||||||
.toolbar-btn-bar:disabled:hover { background: rgba(255,255,255,0.1); }
|
.toolbar-btn-bar:disabled:hover { background: rgba(255,255,255,0.1); }
|
||||||
#screen-page:fullscreen .screen-toolbar { display: none; }
|
#screen-page:fullscreen .screen-toolbar { display: none; }
|
||||||
@@ -1174,6 +1175,7 @@
|
|||||||
<div class="toolbar-right">
|
<div class="toolbar-right">
|
||||||
<span id="screen-status" class="screen-status connecting">Connecting...</span>
|
<span id="screen-status" class="screen-status connecting">Connecting...</span>
|
||||||
<button class="toolbar-btn-bar" id="btn-rdp-reset-bar" onclick="sendRdpReset()" title="RDP Reset">↻</button>
|
<button class="toolbar-btn-bar" id="btn-rdp-reset-bar" onclick="sendRdpReset()" title="RDP Reset">↻</button>
|
||||||
|
<button class="toolbar-btn-bar" id="btn-audio-bar" onclick="toggleAudio()" title="Mute audio">🔊</button>
|
||||||
<button class="toolbar-btn-bar" id="btn-mouse-bar" onclick="toggleControl()" title="Mouse Control">🖱</button>
|
<button class="toolbar-btn-bar" id="btn-mouse-bar" onclick="toggleControl()" title="Mouse Control">🖱</button>
|
||||||
<button class="toolbar-btn-bar" id="btn-keyboard-bar" onclick="toggleKeyboard()" title="Keyboard" disabled>⌨</button>
|
<button class="toolbar-btn-bar" id="btn-keyboard-bar" onclick="toggleKeyboard()" title="Keyboard" disabled>⌨</button>
|
||||||
<button class="fullscreen-btn" onclick="toggleFullscreen()" title="Fullscreen (F11)">⛶</button>
|
<button class="fullscreen-btn" onclick="toggleFullscreen()" title="Fullscreen (F11)">⛶</button>
|
||||||
@@ -1548,11 +1550,22 @@
|
|||||||
// Wait for resolution_changed message
|
// Wait for resolution_changed message
|
||||||
updateScreenStatus('waiting', 'Waiting for video...');
|
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 {
|
} else {
|
||||||
updateScreenStatus('error', msg.msg);
|
updateScreenStatus('error', msg.msg);
|
||||||
setTimeout(() => showPage('devices-page'), 2000);
|
setTimeout(() => showPage('devices-page'), 2000);
|
||||||
}
|
}
|
||||||
break;
|
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':
|
case 'term_ready':
|
||||||
termState.ready = true;
|
termState.ready = true;
|
||||||
document.getElementById('term-status-info').textContent =
|
document.getElementById('term-status-info').textContent =
|
||||||
@@ -2463,6 +2476,9 @@
|
|||||||
document.getElementById('device-name').textContent = currentDevice.name;
|
document.getElementById('device-name').textContent = currentDevice.name;
|
||||||
document.getElementById('frame-info').textContent = '';
|
document.getElementById('frame-info').textContent = '';
|
||||||
updateScreenStatus('connecting');
|
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');
|
showPage('screen-page');
|
||||||
ws.send(JSON.stringify({ cmd: 'connect', id: String(id), token }));
|
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)
|
// Detect touch device (mobile/tablet)
|
||||||
const isTouchDevice = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
|
const isTouchDevice = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user