feat(cdp): Phase 2 - real-time UI mirroring via screencast + remote input
This commit is contained in:
@@ -25,6 +25,8 @@ class CDPClient {
|
||||
this.pollInterval = null;
|
||||
this.onChatUpdate = null; // callback(html)
|
||||
this.onDisconnect = null; // callback()
|
||||
this.onScreencastFrame = null; // callback({data, metadata})
|
||||
this.screencastRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -268,7 +270,7 @@ class CDPClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* 스크린샷 (Phase 2 미리보기용)
|
||||
* 스크린샷 (미리보기용)
|
||||
*/
|
||||
async captureScreenshot() {
|
||||
if (!this.connected || !this.client) return null;
|
||||
@@ -280,6 +282,118 @@ class CDPClient {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Phase 2: Screencast & Remote Input ────────────────
|
||||
|
||||
/**
|
||||
* 실시간 화면 스트리밍 시작
|
||||
* Page.startScreencast로 프레임을 수신하고 onScreencastFrame 콜백으로 전달
|
||||
*/
|
||||
async startScreencast(options = {}) {
|
||||
if (!this.connected || !this.client) return false;
|
||||
if (this.screencastRunning) return true;
|
||||
|
||||
const {
|
||||
format = 'jpeg',
|
||||
quality = 60,
|
||||
maxWidth = 1280,
|
||||
maxHeight = 900,
|
||||
everyNthFrame = 2,
|
||||
} = options;
|
||||
|
||||
try {
|
||||
// 프레임 이벤트 리스너 등록
|
||||
this.client.Page.screencastFrame(async (params) => {
|
||||
// ACK — 이걸 보내야 다음 프레임이 옴
|
||||
try {
|
||||
await this.client.Page.screencastFrameAck({ sessionId: params.sessionId });
|
||||
} catch { /* ignore */ }
|
||||
|
||||
if (this.onScreencastFrame) {
|
||||
this.onScreencastFrame({
|
||||
data: params.data, // base64 JPEG
|
||||
metadata: params.metadata, // {offsetTop, pageScaleFactor, deviceWidth, deviceHeight, ...}
|
||||
sessionId: params.sessionId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await this.client.Page.startScreencast({
|
||||
format,
|
||||
quality,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
everyNthFrame,
|
||||
});
|
||||
|
||||
this.screencastRunning = true;
|
||||
console.log(`[CDP] Screencast 시작 (${maxWidth}x${maxHeight}, q${quality}, every ${everyNthFrame}th frame)`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('[CDP] Screencast 시작 오류:', err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 실시간 화면 스트리밍 중지
|
||||
*/
|
||||
async stopScreencast() {
|
||||
if (!this.connected || !this.client || !this.screencastRunning) return;
|
||||
try {
|
||||
await this.client.Page.stopScreencast();
|
||||
} catch { /* ignore */ }
|
||||
this.screencastRunning = false;
|
||||
this.onScreencastFrame = null;
|
||||
console.log('[CDP] Screencast 중지');
|
||||
}
|
||||
|
||||
/**
|
||||
* 원격 입력 이벤트 디스패치
|
||||
* @param {Object} evt — { type: 'mouse'|'key'|'scroll', ... }
|
||||
*/
|
||||
async dispatchInput(evt) {
|
||||
if (!this.connected || !this.client) return;
|
||||
|
||||
try {
|
||||
switch (evt.type) {
|
||||
case 'mouse':
|
||||
await this.client.Input.dispatchMouseEvent({
|
||||
type: evt.action, // mousePressed, mouseReleased, mouseMoved
|
||||
x: evt.x,
|
||||
y: evt.y,
|
||||
button: evt.button || 'left',
|
||||
clickCount: evt.clickCount || 1,
|
||||
modifiers: evt.modifiers || 0,
|
||||
});
|
||||
break;
|
||||
|
||||
case 'scroll':
|
||||
await this.client.Input.dispatchMouseEvent({
|
||||
type: 'mouseWheel',
|
||||
x: evt.x,
|
||||
y: evt.y,
|
||||
deltaX: evt.deltaX || 0,
|
||||
deltaY: evt.deltaY || 0,
|
||||
});
|
||||
break;
|
||||
|
||||
case 'key':
|
||||
await this.client.Input.dispatchKeyEvent({
|
||||
type: evt.action, // keyDown, keyUp, char
|
||||
key: evt.key,
|
||||
code: evt.code || '',
|
||||
text: evt.text || '',
|
||||
nativeVirtualKeyCode: evt.keyCode || 0,
|
||||
windowsVirtualKeyCode: evt.keyCode || 0,
|
||||
modifiers: evt.modifiers || 0,
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
// 입력 이벤트 오류는 빈번할 수 있으므로 조용히 처리
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CDPClient;
|
||||
|
||||
Reference in New Issue
Block a user