Feature: Implement initial macOS SimpleRemoter client

This commit is contained in:
yuanyuanxiang
2026-04-29 23:25:32 +02:00
parent 7a90d217f3
commit f2a184e760
23 changed files with 2958 additions and 21 deletions

View File

@@ -156,7 +156,13 @@ CScreenSpyDlg::CScreenSpyDlg(CMy2015RemoteDlg* Parent, Server* IOCPServer, CONTE
LPBYTE pClientID = m_ContextObject->InDeCompressedBuffer.GetBuffer(41);
if (pClientID) {
m_ClientID = *((uint64_t*)pClientID);
Mprintf("[ScreenSpyDlg] Parsed clientID in constructor: %llu\n", m_ClientID);
// Notify web clients of resolution (important for clients that only send TOKEN_BITMAPINFO once)
if (WebService().IsRunning()) {
int width = m_BitmapInfor_Full->bmiHeader.biWidth;
int height = abs(m_BitmapInfor_Full->bmiHeader.biHeight);
WebService().NotifyResolutionChange(m_ClientID, width, height);
}
}
// 从客户端配置初始化自适应质量状态 (QualityLevel: -2=关闭, -1=自适应, 0-5=具体等级)
@@ -758,6 +764,12 @@ BOOL CScreenSpyDlg::OnInitDialog()
// 注册屏幕上下文到 WebService用于 Web 端鼠标/键盘控制)
WebService().RegisterScreenContext(m_ClientID, m_ContextObject);
// Hide window if this session was triggered by web client
if (WebService().IsWebTriggered(m_ClientID) && WebService().GetHideWebSessions()) {
m_bHide = true;
ShowWindow(SW_HIDE);
}
return TRUE;
}
@@ -1299,6 +1311,24 @@ VOID CScreenSpyDlg::DrawNextScreenDiff(bool keyFrame)
break;
}
case ALGORITHM_H264: {
// Decode locally if dialog is visible
if (!m_bHide && NextScreenLength > 0) {
if (Decode((LPBYTE)NextScreenData, NextScreenLength)) {
bChange = TRUE;
}
}
// Broadcast H264 keyframe to web clients
if (NextScreenLength > 0 && WebService().IsRunning()) {
std::vector<uint8_t> packet(4 + 1 + 4 + NextScreenLength);
uint32_t deviceIdLow = (uint32_t)(m_ClientID & 0xFFFFFFFF);
uint8_t frameType = 1; // Keyframe
uint32_t dataLen = (uint32_t)NextScreenLength;
memcpy(packet.data(), &deviceIdLow, 4);
packet[4] = frameType;
memcpy(packet.data() + 5, &dataLen, 4);
memcpy(packet.data() + 9, NextScreenData, NextScreenLength);
WebService().BroadcastH264Frame(m_ClientID, packet.data(), packet.size());
}
break;
}
default:

View File

@@ -1298,6 +1298,11 @@ inline std::string GetWebPageHTML() {
}
function initDecoder(width, height) {
decoderWidth = width;
decoderHeight = height;
needKeyframe = false;
decodeTimestamp = 0;
// Clear canvas before resizing to prevent residual content
ctx.setTransform(1, 0, 0, 1, 0, 0); // Reset transform
ctx.clearRect(0, 0, canvas.width, canvas.height);
@@ -1319,6 +1324,10 @@ inline std::string GetWebPageHTML() {
lastFrameTime = performance.now();
decoder = new VideoDecoder({
output: (frame) => {
// Check if frame dimensions match canvas
if (frame.displayWidth !== canvas.width || frame.displayHeight !== canvas.height) {
console.warn(`Frame size mismatch: frame=${frame.displayWidth}x${frame.displayHeight}, canvas=${canvas.width}x${canvas.height}`);
}
ctx.drawImage(frame, 0, 0);
frame.close();
frameCount++;
@@ -1330,7 +1339,7 @@ inline std::string GetWebPageHTML() {
document.getElementById('frame-info').textContent = width + 'x' + height + ' @ ' + fps + ' fps';
}
},
error: (e) => { console.error('Decoder error:', e); updateScreenStatus('error', 'Decode error'); }
error: (e) => { console.error('Decoder error:', e); needKeyframe = true; }
});
decoder.configure({
codec: 'avc1.42E01E',
@@ -1340,20 +1349,50 @@ inline std::string GetWebPageHTML() {
});
}
let decoderWidth = 0, decoderHeight = 0, needKeyframe = false;
let decodeTimestamp = 0; // Monotonically increasing timestamp for decoder
function handleBinaryFrame(data) {
if (!decoder || decoder.state !== 'configured') return;
const view = new DataView(data);
const deviceId = view.getUint32(0, true);
const frameType = view.getUint8(4);
const dataLen = view.getUint32(5, true);
const isKeyframe = frameType === 1;
// If decoder is closed or errored, wait for keyframe to reinitialize
if (!decoder || decoder.state === 'closed') {
if (isKeyframe && decoderWidth > 0) {
console.log('Reinitializing decoder on keyframe');
initDecoder(decoderWidth, decoderHeight);
needKeyframe = false;
} else {
needKeyframe = true;
return;
}
}
if (decoder.state !== 'configured') return;
// Skip delta frames if we need a keyframe
if (needKeyframe && !isKeyframe) return;
if (isKeyframe) needKeyframe = false;
const h264Data = new Uint8Array(data, 9, dataLen);
try {
// Check decoder queue to avoid overwhelming it (but never skip keyframes)
if (!isKeyframe && decoder.decodeQueueSize > 10) {
needKeyframe = true; // Need keyframe to resync after skipping
return;
}
decoder.decode(new EncodedVideoChunk({
type: frameType === 1 ? 'key' : 'delta',
timestamp: performance.now() * 1000,
type: isKeyframe ? 'key' : 'delta',
timestamp: decodeTimestamp++,
data: h264Data
}));
} catch (e) { console.error('Decode error:', e); }
} catch (e) {
console.error('Decode error:', e);
needKeyframe = true;
}
}
)HTML";

View File

@@ -1428,11 +1428,9 @@ void CWebService::BroadcastH264Frame(uint64_t device_id, const uint8_t* data, si
// Broadcast to all watching clients
std::lock_guard<std::mutex> lock(m_ClientsMutex);
int sent_count = 0;
for (auto& [ws_ptr, client] : m_Clients) {
if (client.watch_device_id == device_id) {
SendBinary(ws_ptr, data, len);
sent_count++;
}
}
// Cache keyframe (check FrameType byte at offset 4)