/**
* scrcpy control protocol message encoder
*
* Based on scrcpy control protocol documentation:
* - All values are big-endian
* - Touch events are 32 bytes
* - Key events are 14 bytes
* - Text injection is variable length
*
* Reference: https://github.com/ArtifactForms/scrcpy/blob/master/CONTROL-PROTOCOL.md
*/
// Message type constants (from scrcpy control_msg.h)
export const MSG_TYPE_INJECT_KEYCODE = 0;
export const MSG_TYPE_INJECT_TEXT = 1;
export const MSG_TYPE_INJECT_TOUCH_EVENT = 2;
export const MSG_TYPE_INJECT_SCROLL_EVENT = 3;
export const MSG_TYPE_BACK_OR_SCREEN_ON = 4;
export const MSG_TYPE_EXPAND_NOTIFICATION_PANEL = 5;
export const MSG_TYPE_EXPAND_SETTINGS_PANEL = 6;
export const MSG_TYPE_COLLAPSE_PANELS = 7;
export const MSG_TYPE_GET_CLIPBOARD = 8;
export const MSG_TYPE_SET_CLIPBOARD = 9;
export const MSG_TYPE_SET_SCREEN_POWER_MODE = 10;
export const MSG_TYPE_ROTATE_DEVICE = 11;
export const MSG_TYPE_UHID_CREATE = 12;
export const MSG_TYPE_UHID_INPUT = 13;
export const MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 14;
// Touch action constants (Android MotionEvent)
export const AMOTION_EVENT_ACTION_DOWN = 0;
export const AMOTION_EVENT_ACTION_UP = 1;
export const AMOTION_EVENT_ACTION_MOVE = 2;
export const AMOTION_EVENT_ACTION_CANCEL = 3;
export const AMOTION_EVENT_ACTION_POINTER_DOWN = 5;
export const AMOTION_EVENT_ACTION_POINTER_UP = 6;
// Key action constants (Android KeyEvent)
export const AKEY_EVENT_ACTION_DOWN = 0;
export const AKEY_EVENT_ACTION_UP = 1;
// Mouse button constants
export const AMOTION_EVENT_BUTTON_PRIMARY = 1;
export const AMOTION_EVENT_BUTTON_SECONDARY = 2;
export const AMOTION_EVENT_BUTTON_TERTIARY = 4;
// Screen power modes
export const SCREEN_POWER_MODE_OFF = 0;
export const SCREEN_POWER_MODE_NORMAL = 2;
/**
* Encode an inject touch event message (32 bytes)
*
* Format:
* - type: 1 byte (MSG_TYPE_INJECT_TOUCH_EVENT = 2)
* - action: 1 byte (DOWN=0, UP=1, MOVE=2)
* - pointer_id: 8 bytes (bigint, -1 for mouse)
* - x: 4 bytes (position as uint32)
* - y: 4 bytes (position as uint32)
* - screen_width: 2 bytes (uint16)
* - screen_height: 2 bytes (uint16)
* - pressure: 2 bytes (uint16, normalized 0-65535)
* - action_button: 4 bytes (uint32)
* - buttons: 4 bytes (uint32)
*/
export function encodeInjectTouchEvent(
action: number,
pointerId: bigint,
x: number,
y: number,
screenWidth: number,
screenHeight: number,
pressure: number = 1.0,
actionButton: number = 0,
buttons: number = 0
): Buffer {
const buf = Buffer.alloc(32);
let offset = 0;
// type (1 byte)
buf.writeUInt8(MSG_TYPE_INJECT_TOUCH_EVENT, offset);
offset += 1;
// action (1 byte)
buf.writeUInt8(action, offset);
offset += 1;
// pointer_id (8 bytes, big-endian)
buf.writeBigInt64BE(pointerId, offset);
offset += 8;
// x position (4 bytes, big-endian)
buf.writeUInt32BE(Math.floor(x), offset);
offset += 4;
// y position (4 bytes, big-endian)
buf.writeUInt32BE(Math.floor(y), offset);
offset += 4;
// screen_width (2 bytes, big-endian)
buf.writeUInt16BE(screenWidth, offset);
offset += 2;
// screen_height (2 bytes, big-endian)
buf.writeUInt16BE(screenHeight, offset);
offset += 2;
// pressure (2 bytes, normalized to 0-65535)
const pressureNorm = Math.floor(Math.max(0, Math.min(1, pressure)) * 65535);
buf.writeUInt16BE(pressureNorm, offset);
offset += 2;
// action_button (4 bytes, big-endian)
buf.writeUInt32BE(actionButton, offset);
offset += 4;
// buttons (4 bytes, big-endian)
buf.writeUInt32BE(buttons, offset);
return buf;
}
/**
* Encode an inject keycode message (14 bytes)
*
* Format:
* - type: 1 byte (MSG_TYPE_INJECT_KEYCODE = 0)
* - action: 1 byte (DOWN=0, UP=1)
* - keycode: 4 bytes (Android keycode, big-endian)
* - repeat: 4 bytes (repeat count, big-endian)
* - metastate: 4 bytes (modifier keys, big-endian)
*/
export function encodeInjectKeycode(
action: number,
keycode: number,
repeat: number = 0,
metastate: number = 0
): Buffer {
const buf = Buffer.alloc(14);
let offset = 0;
// type (1 byte)
buf.writeUInt8(MSG_TYPE_INJECT_KEYCODE, offset);
offset += 1;
// action (1 byte)
buf.writeUInt8(action, offset);
offset += 1;
// keycode (4 bytes, big-endian)
buf.writeUInt32BE(keycode, offset);
offset += 4;
// repeat (4 bytes, big-endian)
buf.writeUInt32BE(repeat, offset);
offset += 4;
// metastate (4 bytes, big-endian)
buf.writeUInt32BE(metastate, offset);
return buf;
}
/**
* Encode an inject text message (variable length)
*
* Format:
* - type: 1 byte (MSG_TYPE_INJECT_TEXT = 1)
* - length: 4 bytes (text length in bytes, big-endian)
* - text: variable (UTF-8 encoded)
*/
export function encodeInjectText(text: string): Buffer {
const textBytes = Buffer.from(text, "utf8");
const buf = Buffer.alloc(5 + textBytes.length);
let offset = 0;
// type (1 byte)
buf.writeUInt8(MSG_TYPE_INJECT_TEXT, offset);
offset += 1;
// length (4 bytes, big-endian)
buf.writeUInt32BE(textBytes.length, offset);
offset += 4;
// text (variable length)
textBytes.copy(buf, offset);
return buf;
}
/**
* Encode an inject scroll event message (21 bytes)
*
* Format:
* - type: 1 byte (MSG_TYPE_INJECT_SCROLL_EVENT = 3)
* - x: 4 bytes (position as uint32, big-endian)
* - y: 4 bytes (position as uint32, big-endian)
* - screen_width: 2 bytes (uint16, big-endian)
* - screen_height: 2 bytes (uint16, big-endian)
* - hscroll: 4 bytes (int32, big-endian, horizontal scroll amount)
* - vscroll: 4 bytes (int32, big-endian, vertical scroll amount)
*/
export function encodeInjectScrollEvent(
x: number,
y: number,
screenWidth: number,
screenHeight: number,
hscroll: number,
vscroll: number
): Buffer {
const buf = Buffer.alloc(21);
let offset = 0;
// type (1 byte)
buf.writeUInt8(MSG_TYPE_INJECT_SCROLL_EVENT, offset);
offset += 1;
// x position (4 bytes, big-endian)
buf.writeUInt32BE(Math.floor(x), offset);
offset += 4;
// y position (4 bytes, big-endian)
buf.writeUInt32BE(Math.floor(y), offset);
offset += 4;
// screen_width (2 bytes, big-endian)
buf.writeUInt16BE(screenWidth, offset);
offset += 2;
// screen_height (2 bytes, big-endian)
buf.writeUInt16BE(screenHeight, offset);
offset += 2;
// hscroll (4 bytes, signed int32, big-endian)
buf.writeInt32BE(Math.floor(hscroll), offset);
offset += 4;
// vscroll (4 bytes, signed int32, big-endian)
buf.writeInt32BE(Math.floor(vscroll), offset);
return buf;
}
/**
* Encode a back or screen on message (2 bytes)
*
* Format:
* - type: 1 byte (MSG_TYPE_BACK_OR_SCREEN_ON = 4)
* - action: 1 byte (DOWN=0, UP=1)
*/
export function encodeBackOrScreenOn(action: number): Buffer {
const buf = Buffer.alloc(2);
buf.writeUInt8(MSG_TYPE_BACK_OR_SCREEN_ON, 0);
buf.writeUInt8(action, 1);
return buf;
}
/**
* Encode expand notification panel message (1 byte)
*/
export function encodeExpandNotificationPanel(): Buffer {
const buf = Buffer.alloc(1);
buf.writeUInt8(MSG_TYPE_EXPAND_NOTIFICATION_PANEL, 0);
return buf;
}
/**
* Encode expand settings panel message (1 byte)
*/
export function encodeExpandSettingsPanel(): Buffer {
const buf = Buffer.alloc(1);
buf.writeUInt8(MSG_TYPE_EXPAND_SETTINGS_PANEL, 0);
return buf;
}
/**
* Encode collapse panels message (1 byte)
*/
export function encodeCollapsePanels(): Buffer {
const buf = Buffer.alloc(1);
buf.writeUInt8(MSG_TYPE_COLLAPSE_PANELS, 0);
return buf;
}
/**
* Encode get clipboard message (2 bytes)
*
* Format:
* - type: 1 byte (MSG_TYPE_GET_CLIPBOARD = 8)
* - copy_key: 1 byte (0 = no copy key)
*/
export function encodeGetClipboard(copyKey: number = 0): Buffer {
const buf = Buffer.alloc(2);
buf.writeUInt8(MSG_TYPE_GET_CLIPBOARD, 0);
buf.writeUInt8(copyKey, 1);
return buf;
}
/**
* Encode set clipboard message (variable length)
*
* Format:
* - type: 1 byte (MSG_TYPE_SET_CLIPBOARD = 9)
* - sequence: 8 bytes (big-endian, for async acknowledgment)
* - paste: 1 byte (1 to paste immediately, 0 to just set)
* - length: 4 bytes (text length in bytes, big-endian)
* - text: variable (UTF-8 encoded)
*/
export function encodeSetClipboard(
text: string,
sequence: bigint = BigInt(0),
paste: boolean = false
): Buffer {
const textBytes = Buffer.from(text, "utf8");
const buf = Buffer.alloc(14 + textBytes.length);
let offset = 0;
// type (1 byte)
buf.writeUInt8(MSG_TYPE_SET_CLIPBOARD, offset);
offset += 1;
// sequence (8 bytes, big-endian)
buf.writeBigUInt64BE(sequence, offset);
offset += 8;
// paste (1 byte)
buf.writeUInt8(paste ? 1 : 0, offset);
offset += 1;
// length (4 bytes, big-endian)
buf.writeUInt32BE(textBytes.length, offset);
offset += 4;
// text (variable length)
textBytes.copy(buf, offset);
return buf;
}
/**
* Encode set screen power mode message (2 bytes)
*
* Format:
* - type: 1 byte (MSG_TYPE_SET_SCREEN_POWER_MODE = 10)
* - mode: 1 byte (OFF=0, NORMAL=2)
*/
export function encodeSetScreenPowerMode(mode: number): Buffer {
const buf = Buffer.alloc(2);
buf.writeUInt8(MSG_TYPE_SET_SCREEN_POWER_MODE, 0);
buf.writeUInt8(mode, 1);
return buf;
}
/**
* Encode rotate device message (1 byte)
*/
export function encodeRotateDevice(): Buffer {
const buf = Buffer.alloc(1);
buf.writeUInt8(MSG_TYPE_ROTATE_DEVICE, 0);
return buf;
}
// ============================================================================
// High-level convenience functions
// ============================================================================
/**
* Create touch down + up messages for a tap at the given coordinates.
* Returns array of buffers to send in sequence.
*/
export function encodeTap(
x: number,
y: number,
screenWidth: number,
screenHeight: number,
pointerId: bigint = BigInt(-1)
): Buffer[] {
return [
encodeInjectTouchEvent(
AMOTION_EVENT_ACTION_DOWN,
pointerId,
x,
y,
screenWidth,
screenHeight,
1.0,
AMOTION_EVENT_BUTTON_PRIMARY,
AMOTION_EVENT_BUTTON_PRIMARY
),
encodeInjectTouchEvent(
AMOTION_EVENT_ACTION_UP,
pointerId,
x,
y,
screenWidth,
screenHeight,
0,
AMOTION_EVENT_BUTTON_PRIMARY,
0
),
];
}
/**
* Create touch down, move, up messages for a swipe gesture.
* Returns array of buffers to send in sequence.
*/
export function encodeSwipe(
x1: number,
y1: number,
x2: number,
y2: number,
screenWidth: number,
screenHeight: number,
steps: number = 20,
pointerId: bigint = BigInt(-1)
): Buffer[] {
const messages: Buffer[] = [];
// Touch down at start
messages.push(
encodeInjectTouchEvent(
AMOTION_EVENT_ACTION_DOWN,
pointerId,
x1,
y1,
screenWidth,
screenHeight,
1.0,
AMOTION_EVENT_BUTTON_PRIMARY,
AMOTION_EVENT_BUTTON_PRIMARY
)
);
// Move events
for (let i = 1; i <= steps; i++) {
const t = i / steps;
const x = x1 + (x2 - x1) * t;
const y = y1 + (y2 - y1) * t;
messages.push(
encodeInjectTouchEvent(
AMOTION_EVENT_ACTION_MOVE,
pointerId,
x,
y,
screenWidth,
screenHeight,
1.0,
0,
AMOTION_EVENT_BUTTON_PRIMARY
)
);
}
// Touch up at end
messages.push(
encodeInjectTouchEvent(
AMOTION_EVENT_ACTION_UP,
pointerId,
x2,
y2,
screenWidth,
screenHeight,
0,
AMOTION_EVENT_BUTTON_PRIMARY,
0
)
);
return messages;
}
/**
* Create touch down, wait, up messages for a long press.
* Note: The actual delay must be handled by the caller between sending down and up.
*/
export function encodeLongPressStart(
x: number,
y: number,
screenWidth: number,
screenHeight: number,
pointerId: bigint = BigInt(-1)
): Buffer {
return encodeInjectTouchEvent(
AMOTION_EVENT_ACTION_DOWN,
pointerId,
x,
y,
screenWidth,
screenHeight,
1.0,
AMOTION_EVENT_BUTTON_PRIMARY,
AMOTION_EVENT_BUTTON_PRIMARY
);
}
export function encodeLongPressEnd(
x: number,
y: number,
screenWidth: number,
screenHeight: number,
pointerId: bigint = BigInt(-1)
): Buffer {
return encodeInjectTouchEvent(
AMOTION_EVENT_ACTION_UP,
pointerId,
x,
y,
screenWidth,
screenHeight,
0,
AMOTION_EVENT_BUTTON_PRIMARY,
0
);
}
/**
* Create key down + up messages for a key press.
* Returns array of buffers to send in sequence.
*/
export function encodeKeyPress(keycode: number, metastate: number = 0): Buffer[] {
return [
encodeInjectKeycode(AKEY_EVENT_ACTION_DOWN, keycode, 0, metastate),
encodeInjectKeycode(AKEY_EVENT_ACTION_UP, keycode, 0, metastate),
];
}