-
-
Notifications
You must be signed in to change notification settings - Fork 12.6k
Description
const net = require('net');
const WebSocket = require('ws');
const { spawn, execSync } = require('child_process');
// CẤU HÌNH THIẾT BỊ
const SERIAL = '29f25b993d017ece';
const PORT = 12345;
const SERVER_PATH = '/data/local/tmp/scrcpy-server.jar';
// ========== CONTROL MESSAGE TYPES ==========
const ControlMessageType = {
INJECT_KEYCODE: 0,
INJECT_TEXT: 1,
INJECT_TOUCH_EVENT: 2,
INJECT_SCROLL_EVENT: 3,
BACK_OR_SCREEN_ON: 4,
EXPAND_NOTIFICATION_PANEL: 5,
EXPAND_SETTINGS_PANEL: 6,
COLLAPSE_PANELS: 7,
GET_CLIPBOARD: 8,
SET_CLIPBOARD: 9,
SET_DISPLAY_POWER: 10,
ROTATE_DEVICE: 11,
};
const MotionEvent = {
ACTION_DOWN: 0,
ACTION_UP: 1,
ACTION_MOVE: 2,
ACTION_CANCEL: 3,
BUTTON_PRIMARY: 1, // Dùng cho ngón tay (hoặc chuột trái)
BUTTON_NONE: 0
};
const KeyEvent = {
ACTION_DOWN: 0,
ACTION_UP: 1,
KEYCODE_BACK: 4,
KEYCODE_HOME: 3,
KEYCODE_APP_SWITCH: 187,
KEYCODE_ENTER: 66,
KEYCODE_DEL: 67,
};
// ========== BINARY HELPERS ==========
class BinaryWriter {
static write64BE(buffer, offset, value) {
const bigValue = typeof value === 'bigint' ? value : BigInt(value);
buffer.writeBigUInt64BE(bigValue, offset);
return offset + 8;
}
static writeInt32BE(buffer, offset, value) {
buffer.writeInt32BE(value, offset);
return offset + 4;
}
static write16BE(buffer, offset, value) {
buffer.writeUInt16BE(value, offset);
return offset + 2;
}
static write8(buffer, offset, value) {
buffer.writeUInt8(value, offset);
return offset + 1;
}
static floatToU16FP(f) {
if (f < 0.0) f = 0.0;
if (f > 1.0) f = 1.0;
let u = Math.floor(f * 65536);
if (u >= 0xFFFF) u = 0xFFFF;
return u;
}
}
// ========== MESSAGES CLASSES ==========
class TouchControlMessage {
constructor(action, pointerId, x, y, screenWidth, screenHeight, pressure, actionButton, buttons) {
this.action = action;
this.pointerId = typeof pointerId === 'bigint' ? pointerId : BigInt(pointerId);
this.x = x;
this.y = y;
this.screenWidth = screenWidth;
this.screenHeight = screenHeight;
this.pressure = pressure;
this.actionButton = actionButton;
this.buttons = buttons;
}
toBuffer() {
const buffer = Buffer.alloc(32);
let offset = 0;
offset = BinaryWriter.write8(buffer, offset, ControlMessageType.INJECT_TOUCH_EVENT);
offset = BinaryWriter.write8(buffer, offset, this.action);
offset = BinaryWriter.write64BE(buffer, offset, this.pointerId);
offset = BinaryWriter.writeInt32BE(buffer, offset, this.x);
offset = BinaryWriter.writeInt32BE(buffer, offset, this.y);
offset = BinaryWriter.write16BE(buffer, offset, this.screenWidth);
offset = BinaryWriter.write16BE(buffer, offset, this.screenHeight);
const pressureValue = BinaryWriter.floatToU16FP(this.pressure);
offset = BinaryWriter.write16BE(buffer, offset, pressureValue);
offset = BinaryWriter.writeInt32BE(buffer, offset, this.actionButton);
BinaryWriter.writeInt32BE(buffer, offset, this.buttons);
return buffer;
}
toString() {
const actionName = ['DOWN', 'UP', 'MOVE', 'CANCEL'][this.action] || 'UNKNOWN';
return `Touch{${actionName} id=${this.pointerId} pos=(${this.x},${this.y}) btns=${this.buttons}}`;
}
}
class KeyCodeMessage {
constructor(action, keycode, repeat = 0, metastate = 0) {
this.action = action;
this.keycode = keycode;
this.repeat = repeat;
this.metastate = metastate;
}
toBuffer() {
const buffer = Buffer.alloc(14);
let offset = 0;
offset = BinaryWriter.write8(buffer, offset, ControlMessageType.INJECT_KEYCODE);
offset = BinaryWriter.write8(buffer, offset, this.action);
offset = BinaryWriter.writeInt32BE(buffer, offset, this.keycode);
offset = BinaryWriter.writeInt32BE(buffer, offset, this.repeat);
BinaryWriter.writeInt32BE(buffer, offset, this.metastate);
return buffer;
}
}
class TextMessage {
constructor(text) {
this.text = text;
}
toBuffer() {
const textBuffer = Buffer.from(this.text, 'utf8');
const textLength = Math.min(textBuffer.length, 300);
const buffer = Buffer.alloc(1 + 4 + textLength);
let offset = 0;
offset = BinaryWriter.write8(buffer, offset, ControlMessageType.INJECT_TEXT);
offset = BinaryWriter.writeInt32BE(buffer, offset, textLength);
textBuffer.copy(buffer, offset, 0, textLength);
return buffer;
}
}
class BackOrScreenOnMessage {
constructor(action) {
this.action = action;
}
toBuffer() {
const buffer = Buffer.alloc(2);
buffer.writeUInt8(ControlMessageType.BACK_OR_SCREEN_ON, 0);
buffer.writeUInt8(this.action, 1);
return buffer;
}
}
class SimpleCommandMessage {
constructor(type) {
this.type = type;
}
toBuffer() {
const buffer = Buffer.alloc(1);
buffer.writeUInt8(this.type, 0);
return buffer;
}
}
// ========== SCRCPY CONTROL HANDLER ==========
class ScrcpyControlHandler {
constructor(controlSocket, deviceRes, videoRes) {
this.controlSocket = controlSocket;
this.deviceRes = deviceRes;
this.videoRes = videoRes;
this.touchStates = new Map();
this.scaleX = deviceRes.width / videoRes.width;
this.scaleY = deviceRes.height / videoRes.height;
console.log(`🎮 Scale: x=${this.scaleX.toFixed(4)}, y=${this.scaleY.toFixed(4)}`);
}
transformCoordinates(videoX, videoY) {
const deviceX = Math.round(videoX * this.scaleX);
const deviceY = Math.round(videoY * this.scaleY);
const clampedX = Math.max(0, Math.min(this.deviceRes.width - 1, deviceX));
const clampedY = Math.max(0, Math.min(this.deviceRes.height - 1, deviceY));
return { x: clampedX, y: clampedY };
}
sendMessage(message) {
if (!this.controlSocket || this.controlSocket.destroyed) {
console.error('❌ Control socket not available');
return false;
}
try {
const buffer = message.toBuffer();
this.controlSocket.write(buffer);
return true;
} catch (err) {
console.error('❌ Send error:', err.message);
return false;
}
}
// --- CÁC HÀM GỬI TOUCH (đặt actionButton/buttons = BUTTON_PRIMARY) ---
sendTouchDown(videoX, videoY, pointerId = 0) {
const { x, y } = this.transformCoordinates(videoX, videoY);
console.log(`\n👇 TOUCH DOWN: (${x}, ${y})`);
const msg = new TouchControlMessage(
MotionEvent.ACTION_DOWN,
pointerId,
x, y,
this.deviceRes.width,
this.deviceRes.height,
1.0, // Pressure
MotionEvent.BUTTON_PRIMARY, // phải là 1
MotionEvent.BUTTON_PRIMARY // phải là 1
);
return this.sendMessage(msg);
}
sendTouchMove(videoX, videoY, pointerId = 0) {
const { x, y } = this.transformCoordinates(videoX, videoY);
const msg = new TouchControlMessage(
MotionEvent.ACTION_MOVE,
pointerId,
x, y,
this.deviceRes.width,
this.deviceRes.height,
1.0,
MotionEvent.BUTTON_PRIMARY,
MotionEvent.BUTTON_PRIMARY
);
return this.sendMessage(msg);
}
sendTouchUp(videoX, videoY, pointerId = 0) {
const { x, y } = this.transformCoordinates(videoX, videoY);
console.log(`👆 TOUCH UP: (${x}, ${y})\n`);
const msg = new TouchControlMessage(
MotionEvent.ACTION_UP,
pointerId,
x, y,
this.deviceRes.width,
this.deviceRes.height,
0.0, // Pressure = 0
MotionEvent.BUTTON_PRIMARY,
MotionEvent.BUTTON_PRIMARY
);
return this.sendMessage(msg);
}
// --- CÁC LOGIC CAO CẤP (TAP, SWIPE) ---
async tap(videoX, videoY) {
this.sendTouchDown(videoX, videoY);
await new Promise(resolve => setTimeout(resolve, 50));
this.sendTouchUp(videoX, videoY);
}
async swipe(videoX1, videoY1, videoX2, videoY2, duration = 300) {
console.log(`\n👉 SWIPE from (${videoX1},${videoY1}) to (${videoX2},${videoY2})`);
this.sendTouchDown(videoX1, videoY1);
const steps = Math.max(10, Math.floor(duration / 16));
for (let i = 1; i < steps; i++) {
const t = i / steps;
const x = videoX1 + (videoX2 - videoX1) * t;
const y = videoY1 + (videoY2 - videoY1) * t;
this.sendTouchMove(x, y);
await new Promise(resolve => setTimeout(resolve, duration / steps));
}
this.sendTouchUp(videoX2, videoY2);
}
async longPress(videoX, videoY, duration = 1000) {
console.log(`\n👇 LONG PRESS at (${videoX}, ${videoY})`);
this.sendTouchDown(videoX, videoY);
await new Promise(resolve => setTimeout(resolve, duration));
this.sendTouchUp(videoX, videoY);
}
// --- CÁC HÀM KHÁC (KEY, TEXT, COMMANDS) ---
sendText(text) {
console.log(`\n⌨️ TEXT: "${text}"`);
const msg = new TextMessage(text);
return this.sendMessage(msg);
}
async sendKey(keycode) {
console.log(`\n🔘 KEY: ${keycode}`);
const downMsg = new KeyCodeMessage(KeyEvent.ACTION_DOWN, keycode);
this.sendMessage(downMsg);
await new Promise(resolve => setTimeout(resolve, 50));
const upMsg = new KeyCodeMessage(KeyEvent.ACTION_UP, keycode);
this.sendMessage(upMsg);
}
async sendBackKey() { return this.sendKey(KeyEvent.KEYCODE_BACK); }
async sendHomeKey() { return this.sendKey(KeyEvent.KEYCODE_HOME); }
async sendRecentKey() { return this.sendKey(KeyEvent.KEYCODE_APP_SWITCH); }
async sendEnterKey() { return this.sendKey(KeyEvent.KEYCODE_ENTER); }
async sendDeleteKey() { return this.sendKey(KeyEvent.KEYCODE_DEL); }
expandNotificationPanel() { return this.sendMessage(new SimpleCommandMessage(ControlMessageType.EXPAND_NOTIFICATION_PANEL)); }
expandSettingsPanel() { return this.sendMessage(new SimpleCommandMessage(ControlMessageType.EXPAND_SETTINGS_PANEL)); }
collapsePanels() { return this.sendMessage(new SimpleCommandMessage(ControlMessageType.COLLAPSE_PANELS)); }
rotateDevice() { return this.sendMessage(new SimpleCommandMessage(ControlMessageType.ROTATE_DEVICE)); }
// --- XỬ LÝ MESSAGE TỪ CLIENT ---
async handleTouchMessage(clientId, data) {
try {
const msg = JSON.parse(data);
if (msg.type !== 'touch') return;
if (!this.touchStates.has(clientId)) {
this.touchStates.set(clientId, {
isDown: false, startX: 0, startY: 0, lastX: 0, lastY: 0, startTime: 0
});
}
const state = this.touchStates.get(clientId);
if (msg.x < 0 || msg.x >= this.videoRes.width || msg.y < 0 || msg.y >= this.videoRes.height) {
return;
}
switch (msg.action) {
case 'tap':
await this.tap(msg.x, msg.y);
break;
case 'down':
state.isDown = true;
state.startX = msg.x; state.startY = msg.y;
state.lastX = msg.x; state.lastY = msg.y;
state.startTime = Date.now();
this.sendTouchDown(msg.x, msg.y);
break;
case 'move':
if (state.isDown) {
this.sendTouchMove(msg.x, msg.y);
state.lastX = msg.x; state.lastY = msg.y;
}
break;
case 'up':
if (state.isDown) {
this.sendTouchUp(msg.x, msg.y);
state.isDown = false;
}
break;
case 'longpress':
await this.longPress(msg.x, msg.y, msg.duration || 1000);
break;
}
} catch (err) {
console.error('❌ Handle touch error:', err.message);
}
}
clearClientState(clientId) {
const state = this.touchStates.get(clientId);
if (state && state.isDown) {
this.sendTouchUp(state.lastX, state.lastY);
}
this.touchStates.delete(clientId);
}
}
// ========== UTILITY FUNCTIONS ==========
function getDeviceResolution() {
try {
const output = execSync(adb -s ${SERIAL} shell wm size, { encoding: 'utf8' });
const match = output.match(/Physical size: (\d+)x(\d+)/);
if (match) {
return { width: parseInt(match[1]), height: parseInt(match[2]) };
}
} catch (err) {
console.error('
}
return { width: 1080, height: 2400 };
}
function checkServerExists() {
try {
const result = execSync(
adb -s ${SERIAL} shell "[ -f ${SERVER_PATH} ] && echo EXISTS || echo NOT_FOUND",
{ encoding: 'utf8' }
).trim();
return result === 'EXISTS';
} catch {
return false;
}
}
function ensureServerExists() {
console.log('🔍 Checking server file...');
if (checkServerExists()) {
console.log('✅ Server file already exists');
return Promise.resolve();
}
console.log('📤 Pushing server file...');
return new Promise((resolve, reject) => {
const push = spawn('adb', ['-s', SERIAL, 'push', 'scrcpy-server.jar', SERVER_PATH]);
push.on('close', (code) => {
if (code === 0) {
console.log('✅ Server file pushed');
resolve();
} else {
reject(new Error('Push failed'));
}
});
});
}
// ========== MAIN ==========
async function start() {
try {
await ensureServerExists();
const deviceRes = getDeviceResolution();
console.log(`📱 Device: ${deviceRes.width}x${deviceRes.height}`);
const maxSize = 720;
let videoWidth, videoHeight;
if (deviceRes.width > deviceRes.height) {
videoWidth = maxSize;
videoHeight = Math.round((deviceRes.height / deviceRes.width) * maxSize);
} else {
videoHeight = maxSize;
videoWidth = Math.round((deviceRes.width / deviceRes.height) * maxSize);
}
const videoRes = { width: videoWidth, height: videoHeight };
console.log(`📹 Video: ${videoWidth}x${videoHeight}`);
console.log('🔧 Port forward (SAME PORT for both!)...');
execSync(`adb -s ${SERIAL} forward tcp:${PORT} localabstract:scrcpy`);
console.log('🚀 Starting scrcpy server...');
const server = spawn('adb', [
'-s', SERIAL, 'shell',
`CLASSPATH=${SERVER_PATH}`,
'app_process', '/', 'com.genymobile.scrcpy.Server', '3.3.4',
'tunnel_forward=true',
'audio=false',
'control=true',
'video_codec=h264',
'max_size=720',
'video_bit_rate=1000000',
'max_fps=60',
'send_codec_meta=true',
'send_frame_meta=false',
'send_dummy_byte=false'
]);
server.stdout.on('data', d => console.log('📱', d.toString().trim()));
server.stderr.on('data', d => console.log('⚠️', d.toString().trim()));
await new Promise(resolve => setTimeout(resolve, 3000));
// ✅ FIRST CONNECTION = VIDEO
console.log('📹 Connecting video socket (1st connection)...');
const videoSocket = net.connect(PORT, '127.0.0.1');
videoSocket.setNoDelay(true);
await new Promise((resolve, reject) => {
videoSocket.once('connect', () => {
console.log('✅ Video socket connected');
resolve();
});
videoSocket.once('error', reject);
});
// ✅ SECOND CONNECTION = CONTROL
console.log('🎮 Connecting control socket (2nd connection)...');
const controlSocket = net.connect(PORT, '127.0.0.1');
controlSocket.setNoDelay(true);
await new Promise((resolve, reject) => {
controlSocket.once('connect', () => {
console.log('✅ Control socket connected');
resolve();
});
controlSocket.once('error', reject);
});
const controlHandler = new ScrcpyControlHandler(controlSocket, deviceRes, videoRes);
console.log('✅ Binary control protocol ready!\n');
const initChunks = [];
let headerReceived = false;
let isInitPhase = true;
let initChunkCount = 0;
const clients = new Map();
let clientIdCounter = 0;
videoSocket.on('data', (data) => {
if (isInitPhase) {
if (!headerReceived && data.length >= 12) {
data = data.slice(12);
headerReceived = true;
}
if (headerReceived) {
initChunks.push(Buffer.from(data));
initChunkCount++;
if (initChunkCount >= 5) {
isInitPhase = false;
console.log('✅ Video init complete!\n');
}
}
}
if (!isInitPhase && headerReceived) {
clients.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(data, { binary: true });
}
});
}
});
videoSocket.on('error', (err) => console.error('❌ Video:', err.message));
videoSocket.on('close', () => {
console.error('❌ Video closed');
clients.forEach(ws => ws.close());
});
controlSocket.on('error', (err) => console.error('❌ Control:', err.message));
controlSocket.on('close', () => console.error('❌ Control closed'));
const wss = new WebSocket.Server({ port: 8080, perMessageDeflate: false });
wss.on('connection', (ws) => {
const clientId = ++clientIdCounter;
clients.set(clientId, ws);
console.log(`\n✅ Client ${clientId} connected`);
ws.send(JSON.stringify({
type: 'device_info',
deviceWidth: deviceRes.width,
deviceHeight: deviceRes.height,
videoWidth: videoWidth,
videoHeight: videoHeight
}));
if (initChunks.length > 0) {
initChunks.forEach(chunk => ws.send(chunk, { binary: true }));
}
ws.on('message', async (data) => {
try {
const msg = JSON.parse(data.toString());
if (msg.type === 'key') {
switch (msg.key) {
case 'back': await controlHandler.sendBackKey(); break;
case 'home': await controlHandler.sendHomeKey(); break;
case 'recent': await controlHandler.sendRecentKey(); break;
case 'enter': await controlHandler.sendEnterKey(); break;
case 'delete': await controlHandler.sendDeleteKey(); break;
}
} else if (msg.type === 'touch') {
await controlHandler.handleTouchMessage(clientId, data.toString());
} else if (msg.type === 'text') {
controlHandler.sendText(msg.text);
} else if (msg.type === 'swipe') {
await controlHandler.swipe(msg.x1, msg.y1, msg.x2, msg.y2, msg.duration || 300);
} else if (msg.type === 'longpress') {
await controlHandler.longPress(msg.x, msg.y, msg.duration || 1000);
} else if (msg.type === 'keycode') {
await controlHandler.sendKey(msg.keycode);
} else if (msg.type === 'expand_notification') {
controlHandler.expandNotificationPanel();
} else if (msg.type === 'expand_settings') {
controlHandler.expandSettingsPanel();
} else if (msg.type === 'collapse_panels') {
controlHandler.collapsePanels();
} else if (msg.type === 'rotate') {
controlHandler.rotateDevice();
}
} catch (err) {
console.error('❌ Message error:', err.message);
}
});
ws.on('close', () => {
clients.delete(clientId);
controlHandler.clearClientState(clientId);
console.log(`🔌 Client ${clientId} disconnected`);
});
ws.on('error', (err) => {
console.error(`❌ Client ${clientId}:`, err.message);
});
});
console.log('\n🚀 SERVER READY!\n');
console.log('🌐 WebSocket: ws://localhost:8080');
console.log('⚡ Binary Protocol (32 bytes per touch)');
console.log('📏 Connection order: 1=Video, 2=Control\n');
} catch (err) {
console.error('❌ Fatal:', err.message);
process.exit(1);
}
}
process.on('SIGINT', () => {
console.log('\n🛑 Shutting down...');
try {
execSync(adb -s ${SERIAL} forward --remove tcp:${PORT});
console.log('✅ Port forward removed');
} catch (e) {
console.error('
}
process.exit(0);
});
start();