Skip to content

error touch envent This code works well with the stream and clicking the home and back buttons both work, but the touch event is not recognized. Help me. #6655

@roremi

Description

@roremi

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('⚠️ Cannot get device resolution:', err.message);
}
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('⚠️ Failed to remove forward');
}
process.exit(0);
});

start();

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions