@u4/adbkit
Version:
A Typescript client for the Android Debug Bridge.
763 lines • 32.8 kB
JavaScript
import EventEmitter from 'node:events';
import { Buffer } from 'node:buffer';
import fs from 'node:fs';
import assert from 'node:assert';
import PromiseDuplex from 'promise-duplex';
import Utils from '../../utils.js';
import { MotionEventMap, OrientationMap, ControlMessageMap, codexMap } from './ScrcpyConst.js';
import { BufWrite } from '../minicap/BufWrite.js';
import { parse_sequence_parameter_set } from './sps.js';
import prebuilds from "@u4/minicap-prebuilt";
const SC_SOCKET_NAME_PREFIX = "scrcpy_";
const debug = Utils.debug('adb:scrcpy');
// const KEYFRAME_PTS = BigInt(1) << BigInt(62);
// from https://github.com/Genymobile/scrcpy/blob/master/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
const PACKET_FLAG_CONFIG = BigInt(1) << BigInt(63);
const PACKET_FLAG_KEY_FRAME = BigInt(1) << BigInt(62);
/**
* How scrcpy works?
*
* Its a jar file that runs on an adb shell. It records the screen in h264 and offers it in a given tcp port.
*
* scrcpy params
* maxSize (integer, multiple of 8) 0
* bitRate (integer)
* tunnelForward (optional, bool) use "adb forward" instead of "adb tunnel"
* crop (optional, string) "width:height:x:y"
* sendFrameMeta (optional, bool)
*
* The video stream contains raw packets, without time information. If sendFrameMeta is enabled a meta header is added
* before each packet.
* The "meta" header length is 12 bytes
* [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ...
* <-------------> <-----> <-----------------------------...
* PTS size raw packet (of size len)
*
* WARNING:
* Need USB Debug checked in developper option for MIUI
*/
export default class Scrcpy extends EventEmitter {
constructor(client, config = {}) {
super();
this.client = client;
/**
* closed had been call stop all new activity
*/
this.closed = false;
this.on = (event, listener) => super.on(event, listener);
this.off = (event, listener) => super.off(event, listener);
this.once = (event, listener) => super.once(event, listener);
this.emit = (event, ...args) => super.emit(event, ...args);
this.config = {
scid: '0' + Math.random().toString(16).substring(2, 9),
noAudio: true, // disable audio TMP
version: "2.7",
// port: 8099,
maxSize: 600,
maxFps: 0,
flip: false,
bitrate: 999999999,
lockedVideoOrientation: OrientationMap.LOCK_VIDEO_ORIENTATION_UNLOCKED,
tunnelForward: true,
tunnelDelay: 1000,
crop: '', //'9999:9999:0:0',
sendFrameMeta: true, // send PTS so that the client may record properly
control: true,
displayId: 0,
showTouches: false,
stayAwake: true,
codecOptions: '',
encoderName: '',
powerOffScreenOnClose: false,
// clipboardAutosync: true,
...config
};
this._name = new Promise((resolve) => this.setName = resolve);
this._width = new Promise((resolve) => this.setWidth = resolve);
this._height = new Promise((resolve) => this.setHeight = resolve);
this._codec = new Promise((resolve) => this.setCodec = resolve);
this._onTermination = new Promise((resolve) => this.setFatalError = resolve);
this._firstFrame = new Promise((resolve) => this.setFirstFrame = resolve);
let versionSplit = this.config.version.split(".").map(Number);
if (versionSplit.length === 2) {
versionSplit = [...versionSplit, 0];
}
this.major = versionSplit[0];
this.minor = versionSplit[1];
this.patch = versionSplit[2];
this.strVersion = `${this.major.toString().padStart(2, '0')}.${this.minor.toString().padStart(2, '0')}.${this.patch.toString().padStart(2, '0')}`;
}
get name() { return this._name; }
get width() { return this._width; }
get height() { return this._height; }
/**
* Clever way to detect Termination.
* return the Ending message.
*/
get onTermination() { return this._onTermination; }
/**
* Promise to the first emited frame
* can be used to unsure that scrcpy propery start
*/
get firstFrame() { return this._firstFrame; }
/**
* return the used codex can be "H264", "H265", "AV1", "AAC" or "OPUS"
*/
get codec() { return this._codec; }
/**
* emit scrcpyServer output as Error
* @param duplex
* @returns
*/
async ListenErrors(duplex) {
try {
const errors = [];
for (;;) {
if (!duplex.readable) // the server is stoped
break;
await Utils.waitforReadable(duplex, 0, 'wait for error from ScrcpyServer');
const data = await duplex.read();
if (data) {
const msg = data.toString().trim();
errors.push(msg);
try {
this.emit('error', Error(msg));
// eslint-disable-next-line @typescript-eslint/no-unused-vars
}
catch (e) {
// emit Error but to not want to Quit Yet
}
}
else {
if (errors.length > 0)
this._setFatalError(errors.join('\n'));
// else no error
break;
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
}
catch (e) {
// must never throw
//this.emit('error', e as Error);
//this.setError((e as Error).message);
}
}
_setFatalError(msg) {
// console.error(`_setFatalError. Scrcpy fatal error:`, msg);
if (this.setFatalError) {
if (msg instanceof Error) {
this.setFatalError(msg.message + '\n' + msg.stack);
}
else {
this.setFatalError(msg);
}
this.setFatalError = undefined;
}
}
/**
* get last current video config
*/
get videoConfig() {
return this.lastConf;
}
/**
* Read a message from the contoler Duplex
*
* @param duplex only supoport clipboard
* @returns
*/
async readOneMessage(duplex) {
if (!duplex)
return '';
// const waitforReadable = () => new Promise<void>((resolve) => duplex.readable.stream.once('readable', resolve));
await Utils.waitforReadable(duplex);
let chunk = (await duplex.read(1));
const type = chunk.readUInt8();
switch (type) {
case 0: // clipboard
{
await Utils.waitforReadable(duplex);
chunk = (await duplex.read(4));
await Utils.waitforReadable(duplex);
const len = chunk.readUint32BE();
await Utils.waitforReadable(duplex);
chunk = (await duplex.read(len));
const text = chunk.toString('utf8');
return text;
}
default:
throw Error(`Unsupported message type:${type}`);
}
}
_getStartupLine(jarDest) {
const args = [];
const { maxSize, bitrate, maxFps, lockedVideoOrientation, tunnelForward, crop, sendFrameMeta, control, displayId, showTouches, stayAwake, codecOptions, encoderName, powerOffScreenOnClose, clipboardAutosync } = this.config;
args.push(`CLASSPATH=${jarDest}`);
args.push('app_process');
args.push('/');
args.push('com.genymobile.scrcpy.Server');
const versionStr = this.strVersion;
// first args is the expected version number
if (versionStr === "02.02.00") {
// V2.2 is the only version that expect a v prefix
args.push("v" + this.config.version);
}
else {
args.push(this.config.version);
}
// args.push(this.config.version); // arg 0 Scrcpy server version
//if (this.config.version <= 20) {
if (this.major < 2) {
// Version 11 => 20
args.push("info"); // Log level: info, verbose...
args.push(maxSize); // Max screen width (long side)
args.push(bitrate); // Bitrate of video
args.push(maxFps); // Max frame per second
args.push(lockedVideoOrientation); // Lock screen orientation: LOCK_SCREEN_ORIENTATION
args.push(tunnelForward); // Tunnel forward
args.push(crop || '-'); // Crop screen
args.push(sendFrameMeta); // Send frame rate to client
args.push(control); // Control enabled
args.push(displayId); // Display id
args.push(showTouches); // Show touches
args.push(stayAwake); // if self.stay_awake else "false", Stay awake
args.push(codecOptions || '-'); // Codec (video encoding) options
args.push(encoderName || '-'); // Encoder name
args.push(powerOffScreenOnClose); // Power off screen after server closed
}
else {
if (this.major >= 2) {
args.push(`scid=${this.config.scid}`);
if (this.config.noAudio)
args.push(`audio=false`);
// if (this.config.noControl)
// args.push(`no_control=${this.config.noControl}`);
}
if (versionStr >= "02.04.00") {
args.push(`video_source=display`);
}
args.push("log_level=info");
args.push(`max_size=${maxSize}`);
args.push("clipboard_autosync=false"); // cause crash on some newer phone and we do not use that feature.
if (this.major >= 2) {
args.push(`video_bit_rate=${bitrate}`);
}
else {
args.push(`bit_rate=${bitrate}`);
}
args.push(`max_fps=${maxFps}`);
args.push(`lock_video_orientation=${lockedVideoOrientation}`);
args.push(`tunnel_forward=${tunnelForward}`); // Tunnel forward
if (crop && crop !== '-')
args.push(`crop=${crop}`); // Crop screen
args.push(`send_frame_meta=${sendFrameMeta}`); // Send frame rate to client
args.push(`control=${control}`); // Control enabled
args.push(`display_id=${displayId}`); // Display id
args.push(`show_touches=${showTouches}`); // Show touches
args.push(`stay_awake=${stayAwake}`); // if self.stay_awake else "false", Stay awake
if (codecOptions && codecOptions !== '-')
args.push(`codec_options=${codecOptions}`); // Codec (video encoding) options
if (encoderName && encoderName !== '-')
args.push(`encoder_name=${encoderName}`); // Encoder name
args.push(`power_off_on_close=${powerOffScreenOnClose}`); // Power off screen after server closed
// args.push(`clipboard_autosync=${clipboardAutosync}`); // default is True
if (clipboardAutosync !== undefined)
args.push(`clipboard_autosync=${clipboardAutosync}`); // default is True
//if (this.config.version >= 22) {
if (versionStr >= "01.22.00") {
const { downsizeOnError, sendDeviceMeta, sendDummyByte, rawVideoStream } = this.config;
if (downsizeOnError !== undefined)
args.push(`downsize_on_error=${downsizeOnError}`);
if (sendDeviceMeta !== undefined)
args.push(`send_device_meta=${sendDeviceMeta}`);
if (sendDummyByte !== undefined)
args.push(`send_dummy_byte=${sendDummyByte}`);
if (rawVideoStream !== undefined)
args.push(`raw_video_stream=${rawVideoStream}`);
}
if (versionStr >= "01.22.00") {
const { cleanup } = this.config;
if (cleanup !== undefined)
args.push(`raw_video_stream=${cleanup}`);
}
// check Server.java
}
return args.map(a => a.toString()).join(' ');
}
/**
* Will connect to the android device, send & run the server and return deviceName, width and height.
* After that data will be offered as a 'data' event.
*/
async start() {
if (this.closed) // can not start once stop called
return this;
let dstFolder = '/data/local/tmp';
let dstFolderStat = await this.client.stat(dstFolder).catch((e) => { console.log(e); return null; });
if (!dstFolderStat) {
dstFolder = '/tmp';
dstFolderStat = await this.client.stat(dstFolder).catch(() => null);
}
if (!dstFolderStat) {
throw Error("can not find a writable tmp dest folder in device");
}
const jarDest = `${dstFolder}/scrcpy-server-v${this.config.version}.jar`;
// Transfer server jar to device...
const jar = prebuilds.getScrcpyJar(this.config.version);
const srcStat = await fs.promises.stat(jar).catch(() => null);
if (!srcStat)
throw Error(`fail to get ressource ${jar}`);
const dstStat = await this.client.stat(jarDest).catch(() => null);
if (!dstStat || srcStat.size !== dstStat.size) {
try {
debug(`pushing scrcpy-server.jar to ${this.client.serial}`);
const transfer = await this.client.push(jar, jarDest);
await transfer.waitForEnd();
}
catch (e) {
debug(`Impossible to transfer server scrcpy-server.jar to ${this.client.serial}`, e);
throw e;
}
}
else {
debug(`scrcpy-server.jar already present in ${this.client.serial}, keep it`);
}
///////
// Build the commandline to start the server
try {
const cmdLine = this._getStartupLine(jarDest);
// console.log("starting scrcpy server with cmdLine:");
// console.log(cmdLine);
// console.log("");
if (this.closed) // can not start once stop called
return this;
const duplex = await this.client.shell(cmdLine);
this.scrcpyServer = new PromiseDuplex(duplex);
this.scrcpyServer.once("finish").then(() => {
debug(`scrcpyServer finished on device ${this.client.serial}`);
this.stop();
});
// debug only
// extraUtils.dumpReadable(this.scrcpyServer, 'scrcpyServer');
}
catch (e) {
debug('Impossible to run server:', e);
throw e;
}
let stdoutContent = '';
for (;;) {
if (!await Utils.waitforReadable(this.scrcpyServer, this.config.tunnelDelay, 'scrcpyServer stdout loading')) {
// const msg = `First line should be '[server] // INFO: Device: Name (Version), reveived:\n\n${info}`
if (!stdoutContent)
stdoutContent = "no stdout content";
const error = `Starting scrcpyServer failed, scrcpy stdout:${stdoutContent}`;
this._setFatalError(error);
this.stop();
throw Error(error);
}
const srvOut = await this.scrcpyServer.read();
stdoutContent += (srvOut) ? srvOut.toString() : '';
// the server may crash within the first message
const errorIndex = stdoutContent.indexOf("[server] ERROR:");
if (errorIndex >= 0) {
const error = stdoutContent.substring(errorIndex);
this._setFatalError(error);
this.stop();
throw Error(error);
}
if (stdoutContent.includes('[server] INFO: Device: '))
break;
// console.log('stdoutContent:', stdoutContent);
}
this.ListenErrors(this.scrcpyServer).then(() => { }, () => { });
// from V2.0 SC_SOCKET_NAME name can be change
let SC_SOCKET_NAME = 'scrcpy';
if (this.major >= 2) {
SC_SOCKET_NAME = SC_SOCKET_NAME_PREFIX + this.config.scid;
assert(this.config.scid.length == 8, `scid length should be 8`);
}
// Wait 1 sec to forward to work
// await Util.delay(this.config.tunnelDelay);
if (this.closed) // can not start once stop called
return this;
// Connect videoSocket
await Utils.delay(100);
this.videoSocket = await this.client.openLocal2(`localabstract:${SC_SOCKET_NAME}`, 'first connection to scrcpy for video');
this.videoSocket.stream.on('error', (e) => {
console.error('videoSocket error', e);
});
if (this.closed) {
this.stop();
return this;
}
if (this.major >= 2 && !this.config.noAudio) {
// Connect audioSocket
this.audioSocket = await this.client.openLocal2(`localabstract:${SC_SOCKET_NAME}`, 'first connection to scrcpy for audio');
// Connect controlSocket
if (this.closed) {
this.stop();
return this;
}
}
this.controlSocket = await this.client.openLocal2(`localabstract:${SC_SOCKET_NAME}`, 'second connection to scrcpy for control');
if (this.closed) {
this.stop();
return this;
}
// First chunk is 69 bytes length -> 1 dummy byte, 64 bytes for deviceName, 2 bytes for width & 2 bytes for height
try {
await Utils.waitforReadable(this.videoSocket, 0, 'videoSocket 1st 1 bit chunk');
const firstChunk = await this.videoSocket.read(1);
if (!firstChunk) {
throw Error('fail to read firstChunk, inclease tunnelDelay for this device.');
}
// old protocol
const control = firstChunk.at(0);
if (firstChunk.at(0) !== 0) {
if (control)
throw Error(`Control code should be 0x00, receves: 0x${control.toString(16).padStart(2, '0')}`);
throw Error(`Control code should be 0x00, receves nothing.`);
}
}
catch (e) {
debug('Impossible to read first chunk:', e);
throw e;
}
if (this.config.sendFrameMeta) {
void this.startStreamWithMeta().catch((e) => {
this._setFatalError(e);
this.stop();
});
}
else {
this.startStreamRaw();
}
// wait the first chunk
await this.height;
return this;
}
stop() {
this.closed = true;
let close = false;
if (this.videoSocket) {
this.videoSocket.destroy();
this.videoSocket = undefined;
close = true;
}
if (this.audioSocket) {
this.audioSocket.destroy();
this.audioSocket = undefined;
close = true;
}
if (this.controlSocket) {
this.controlSocket.destroy();
this.controlSocket = undefined;
close = true;
}
if (this.scrcpyServer)
this.scrcpyServer.destroy();
if (close) {
this.emit('disconnect');
this._setFatalError('stoped');
}
return close;
}
isRunning() {
return this.videoSocket !== null;
}
startStreamRaw() {
assert(this.videoSocket);
this.videoSocket.stream.on('data', d => this.emit('raw', d));
}
/**
* capture all video trafique in a loop
* get resolve once capture stop
*/
async startStreamWithMeta() {
const strVersion = this.strVersion;
assert(this.videoSocket);
this.videoSocket.stream.pause();
await Utils.waitforReadable(this.videoSocket, 0, 'videoSocket header');
if (this.major >= 2) {
const chunk = this.videoSocket.stream.read(64);
if (!chunk)
throw Error('fail to read firstChunk, inclease tunnelDelay for this device.');
const name = chunk.toString('utf8', 0, 64).trim();
this.setName(name);
// const width = chunk.readUint16BE(64);
// this.setWidth(width);
// const height = chunk.readUint16BE(66);
// this.setHeight(height);
}
else {
const chunk = this.videoSocket.stream.read(68);
if (!chunk)
throw Error('fail to read firstChunk, inclease tunnelDelay for this device.');
const name = chunk.toString('utf8', 0, 64).trim();
this.setName(name);
const width = chunk.readUint16BE(64);
this.setWidth(width);
const height = chunk.readUint16BE(66);
this.setHeight(height);
}
let codec = "H264";
// let header: Uint8Array | undefined;
if (this.major >= 2) {
await Utils.waitforReadable(this.videoSocket, 0, 'videoSocket Codec header');
const frameMeta = this.videoSocket.stream.read(12);
const codecId = frameMeta.readUInt32BE(0);
// Read width (4 bytes)
const width = frameMeta.readUInt32BE(4);
// Read height (4 bytes)
const height = frameMeta.readUInt32BE(8);
switch (codecId) {
case codexMap.H264:
codec = "H264";
break;
case codexMap.H265:
codec = "H265";
break;
case codexMap.AV1:
codec = "AV1";
break;
case codexMap.OPUS:
codec = "OPUS";
break;
case codexMap.AAC:
codec = "AAC";
break;
case codexMap.RAW:
codec = "RAW";
break;
default:
codec = "UNKNOWN";
}
this.setCodec(codec);
this.setWidth(width);
this.setHeight(height);
}
let pts = BigInt(0); // Buffer.alloc(0);
for (;;) {
if (!this.videoSocket)
break;
await Utils.waitforReadable(this.videoSocket, 0, 'videoSocket packet size');
let len = undefined;
if (this.config.sendFrameMeta) {
if (!this.videoSocket)
break;
const frameMeta = this.videoSocket.stream.read(12);
if (!frameMeta) {
// regular end condition
return;
}
pts = frameMeta.readBigUint64BE();
len = frameMeta.readUInt32BE(8);
// debug(`\tHeader:PTS =`, pts);
// debug(`\tHeader:len =`, len);
}
const config = !!(pts & PACKET_FLAG_CONFIG);
let streamChunk = null; // Buffer
while (streamChunk === null) {
if (!this.videoSocket) // the server is stoped
break;
if (!this.videoSocket.stream.readable) // the server is stoped
break;
await Utils.waitforReadable(this.videoSocket, 0, 'videoSocket streamChunk');
streamChunk = this.videoSocket.stream.read(len);
if (streamChunk) {
// const chunk_Uint8Array = streamChunk as unknown as Uint8Array;
if (config) { // non-media data packet len: 30 .. 33
/**
* is a config package pts have PACKET_FLAG_CONFIG flag
*/
const sequenceParameterSet = parse_sequence_parameter_set(streamChunk, codec);
const { profile_idc: profileIndex, constraint_set: constraintSet, level_idc: levelIndex, pic_width_in_mbs_minus1, pic_height_in_map_units_minus1, frame_mbs_only_flag, frame_crop_left_offset, frame_crop_right_offset, frame_crop_top_offset, frame_crop_bottom_offset, } = sequenceParameterSet;
const encodedWidth = (pic_width_in_mbs_minus1 + 1) * 16;
const encodedHeight = (pic_height_in_map_units_minus1 + 1) * (2 - frame_mbs_only_flag) * 16;
const cropLeft = frame_crop_left_offset * 2;
const cropRight = frame_crop_right_offset * 2;
const cropTop = frame_crop_top_offset * 2;
const cropBottom = frame_crop_bottom_offset * 2;
const croppedWidth = encodedWidth - cropLeft - cropRight;
const croppedHeight = encodedHeight - cropTop - cropBottom;
const videoConf = {
profileIndex, constraintSet, levelIndex, encodedWidth, encodedHeight,
cropLeft, cropRight, cropTop, cropBottom, croppedWidth, croppedHeight,
data: streamChunk,
};
this.lastConf = videoConf;
this.emit('config', videoConf);
}
else {
/**
* if pts have PACKET_FLAG_KEY_FRAME, this is a keyframe
*/
const keyframe = !!(pts & PACKET_FLAG_KEY_FRAME);
if (keyframe) {
pts &= ~PACKET_FLAG_KEY_FRAME;
}
const frame = { keyframe, pts, data: streamChunk, config: this.lastConf };
if (this.setFirstFrame) {
this.setFirstFrame();
this.setFirstFrame = undefined;
}
this.emit('frame', frame);
}
}
else {
// large chunk.
// console.log('fail to streamChunk len:', len);
await Utils.delay(0);
}
}
}
}
// ControlMessages
// TYPE_INJECT_KEYCODE
/**
* // will be convert in a android.view.KeyEvent
* https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/KeyEvent.java
* @param action
* @param keyCode
* @param repeatCount
* @param metaState combinaison of KeyEventMeta
*/
async injectKeycodeEvent(action, keyCode, repeatCount, metaState) {
const chunk = new BufWrite(14);
chunk.writeUint8(ControlMessageMap.TYPE_INJECT_KEYCODE);
chunk.writeUint8(action);
chunk.writeUint32BE(keyCode);
chunk.writeUint32BE(repeatCount);
chunk.writeUint32BE(metaState);
assert(this.controlSocket);
await this.controlSocket.write(chunk.buffer);
}
// TYPE_INJECT_TEXT
async injectText(text) {
const chunk = new BufWrite(5);
chunk.writeUint8(ControlMessageMap.TYPE_INJECT_TEXT);
chunk.writeString(text);
assert(this.controlSocket);
await this.controlSocket.write(chunk.buffer);
}
/**
* android.view.MotionEvent;
* https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/MotionEvent.java
* @param action
* @param pointerId
* @param position
* @param screenSize
* @param pressure
*
* see parseInjectTouchEvent()
*/
// usb.data_len == 28
async injectTouchEvent(action, pointerId, position, screenSize, pressure) {
let size = 28;
if (this.major >= 2) {
size += 4;
}
const chunk = new BufWrite(size);
chunk.writeUint8(ControlMessageMap.TYPE_INJECT_TOUCH_EVENT);
chunk.writeUint8(action); // action readUnsignedByte
if (pressure === undefined) {
if (action == MotionEventMap.ACTION_UP)
pressure = 0x0;
else if (action == MotionEventMap.ACTION_DOWN)
pressure = 0xffff;
else
pressure = 0xffff;
}
// Writes a long to the underlying output stream as eight bytes, high byte first.
chunk.writeBigUint64BE(pointerId); // long pointerId = dis.readLong();
// Position position = parsePosition();
chunk.writeUint32BE(position.x | 0); // int x = dis.readInt();
chunk.writeUint32BE(position.y | 0); // int y = dis.readInt();
chunk.writeUint16BE(screenSize.x | 0); // int screenWidth = dis.readUnsignedShort();
chunk.writeUint16BE(screenSize.y | 0); // int screenHeight = dis.readUnsignedShort();
chunk.writeUint16BE(pressure); // Binary.u16FixedPointToFloat(dis.readShort());
chunk.writeUint32BE(MotionEventMap.BUTTON_PRIMARY); // int actionButton = dis.readInt();
if (this.major >= 2) {
chunk.writeUint32BE(MotionEventMap.BUTTON_PRIMARY); // int buttons = dis.readInt();
}
assert(this.controlSocket);
try {
await this.controlSocket.write(chunk.buffer);
return true;
}
catch (e) {
debug(`injectTouchEvent failed:`, e);
return false;
// if the device is not connected anymore, we can not write to the controlSocket
}
// console.log(chunk.buffer.toString('hex'))
}
async injectScrollEvent(position, screenSize, HScroll, VScroll) {
const chunk = new BufWrite(20);
chunk.writeUint8(ControlMessageMap.TYPE_INJECT_SCROLL_EVENT);
// Writes a long to the underlying output stream as eight bytes, high byte first.
chunk.writeUint32BE(position.x | 0);
chunk.writeUint32BE(position.y | 0);
chunk.writeUint16BE(screenSize.x | 0);
chunk.writeUint16BE(screenSize.y | 0);
chunk.writeUint16BE(HScroll);
chunk.writeInt32BE(VScroll);
chunk.writeInt32BE(MotionEventMap.BUTTON_PRIMARY);
assert(this.controlSocket);
await this.controlSocket.write(chunk.buffer);
}
// TYPE_BACK_OR_SCREEN_ON
async injectBackOrScreenOn() {
const chunk = new BufWrite(2);
chunk.writeUint8(ControlMessageMap.TYPE_BACK_OR_SCREEN_ON);
chunk.writeUint8(MotionEventMap.ACTION_UP);
assert(this.controlSocket);
await this.controlSocket.write(chunk.buffer);
}
// TYPE_EXPAND_NOTIFICATION_PANEL
async expandNotificationPanel() {
const chunk = Buffer.allocUnsafe(1);
chunk.writeUInt8(ControlMessageMap.TYPE_EXPAND_NOTIFICATION_PANEL);
assert(this.controlSocket);
await this.controlSocket.write(chunk);
}
// TYPE_COLLAPSE_PANELS
async collapsePannels() {
const chunk = Buffer.allocUnsafe(1);
chunk.writeUInt8(ControlMessageMap.TYPE_EXPAND_SETTINGS_PANEL);
assert(this.controlSocket);
await this.controlSocket.write(chunk);
}
// TYPE_GET_CLIPBOARD
async getClipboard() {
const chunk = Buffer.allocUnsafe(1);
chunk.writeUInt8(ControlMessageMap.TYPE_GET_CLIPBOARD);
assert(this.controlSocket);
await this.controlSocket.write(chunk);
return this.readOneMessage(this.controlSocket);
}
// TYPE_SET_CLIPBOARD
async setClipboard(text) {
const chunk = new BufWrite(6);
chunk.writeUint8(ControlMessageMap.TYPE_SET_CLIPBOARD);
chunk.writeUint8(1); // past
chunk.writeString(text);
assert(this.controlSocket);
await this.controlSocket.write(chunk.buffer);
}
// TYPE_SET_SCREEN_POWER_MODE
async setScreenPowerMode() {
const chunk = Buffer.allocUnsafe(1);
chunk.writeUInt8(ControlMessageMap.TYPE_SET_SCREEN_POWER_MODE);
assert(this.controlSocket);
await this.controlSocket.write(chunk);
}
// TYPE_ROTATE_DEVICE
async rotateDevice() {
const chunk = Buffer.allocUnsafe(1);
chunk.writeUInt8(ControlMessageMap.TYPE_ROTATE_DEVICE);
assert(this.controlSocket);
await this.controlSocket.write(chunk);
}
}
//# sourceMappingURL=Scrcpy.js.map