@tapflowio/android-agent 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +34 -0
  3. package/bin/.gitkeep +0 -0
  4. package/bin/scrcpy-server.jar +0 -0
  5. package/dist/AdbWrapper.d.ts +28 -0
  6. package/dist/AdbWrapper.d.ts.map +1 -0
  7. package/dist/AdbWrapper.js +138 -0
  8. package/dist/AdbWrapper.js.map +1 -0
  9. package/dist/AndroidAgent.d.ts +44 -0
  10. package/dist/AndroidAgent.d.ts.map +1 -0
  11. package/dist/AndroidAgent.js +695 -0
  12. package/dist/AndroidAgent.js.map +1 -0
  13. package/dist/AndroidTouchHelper.d.ts +23 -0
  14. package/dist/AndroidTouchHelper.d.ts.map +1 -0
  15. package/dist/AndroidTouchHelper.js +73 -0
  16. package/dist/AndroidTouchHelper.js.map +1 -0
  17. package/dist/EmulatorLauncher.d.ts +6 -0
  18. package/dist/EmulatorLauncher.d.ts.map +1 -0
  19. package/dist/EmulatorLauncher.js +75 -0
  20. package/dist/EmulatorLauncher.js.map +1 -0
  21. package/dist/adb.d.ts +7 -0
  22. package/dist/adb.d.ts.map +1 -0
  23. package/dist/adb.js +41 -0
  24. package/dist/adb.js.map +1 -0
  25. package/dist/index.d.ts +9 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +8 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/scrcpy/ScrcpyControl.d.ts +17 -0
  30. package/dist/scrcpy/ScrcpyControl.d.ts.map +1 -0
  31. package/dist/scrcpy/ScrcpyControl.js +99 -0
  32. package/dist/scrcpy/ScrcpyControl.js.map +1 -0
  33. package/dist/scrcpy/ScrcpySession.d.ts +20 -0
  34. package/dist/scrcpy/ScrcpySession.d.ts.map +1 -0
  35. package/dist/scrcpy/ScrcpySession.js +127 -0
  36. package/dist/scrcpy/ScrcpySession.js.map +1 -0
  37. package/dist/scrcpy/ScrcpyVideo.d.ts +24 -0
  38. package/dist/scrcpy/ScrcpyVideo.d.ts.map +1 -0
  39. package/dist/scrcpy/ScrcpyVideo.js +145 -0
  40. package/dist/scrcpy/ScrcpyVideo.js.map +1 -0
  41. package/package.json +64 -0
@@ -0,0 +1,127 @@
1
+ import net from 'net';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { execFile, spawn } from 'child_process';
5
+ import { randomBytes } from 'crypto';
6
+ import { promisify } from 'util';
7
+ import { ScrcpyVideo } from './ScrcpyVideo.js';
8
+ import { ScrcpyControl } from './ScrcpyControl.js';
9
+ import { createLogger } from '@tapflowio/agent-core';
10
+ const logger = createLogger('android-agent:scrcpy');
11
+ const execFileAsync = promisify(execFile);
12
+ const SCRCPY_SERVER_VERSION = '3.1';
13
+ const DEVICE_PATH = '/data/local/tmp/scrcpy-server.jar';
14
+ function getAdbPath() {
15
+ if (process.env['ADB_PATH'])
16
+ return process.env['ADB_PATH'];
17
+ const androidHome = process.env['ANDROID_HOME'];
18
+ if (!androidHome)
19
+ throw new Error('ANDROID_HOME not set');
20
+ return `${androidHome}/platform-tools/adb`;
21
+ }
22
+ function getServerJarPath() {
23
+ const dir = path.dirname(fileURLToPath(import.meta.url));
24
+ // src/scrcpy/ → ../../bin/ (or dist/scrcpy/ → ../../bin/)
25
+ return path.resolve(dir, '../../bin/scrcpy-server.jar');
26
+ }
27
+ // Each session gets its own port; control connects to the same forwarded port
28
+ let nextPort = 27183;
29
+ function allocatePort() {
30
+ return nextPort++;
31
+ }
32
+ export class ScrcpySession {
33
+ scid;
34
+ socketName;
35
+ serverProc = null;
36
+ videoSocket = null;
37
+ controlSocket = null;
38
+ _video = null;
39
+ _control = null;
40
+ port = 0;
41
+ constructor() {
42
+ const buf = randomBytes(4);
43
+ buf[0] &= 0x7f; // Java Integer.parseInt rejects values > 0x7FFFFFFF
44
+ this.scid = buf.toString('hex');
45
+ this.socketName = `scrcpy_${this.scid}`;
46
+ }
47
+ get video() {
48
+ if (!this._video)
49
+ throw new Error('ScrcpySession not started');
50
+ return this._video;
51
+ }
52
+ get control() {
53
+ if (!this._control)
54
+ throw new Error('ScrcpySession not started');
55
+ return this._control;
56
+ }
57
+ async start(serial, onRotation) {
58
+ const adb = getAdbPath();
59
+ const port = allocatePort();
60
+ this.port = port;
61
+ const jarPath = getServerJarPath();
62
+ await execFileAsync(adb, ['-s', serial, 'push', jarPath, DEVICE_PATH]);
63
+ const serverProc = spawn(adb, [
64
+ '-s', serial, 'shell',
65
+ `CLASSPATH=${DEVICE_PATH} app_process / com.genymobile.scrcpy.Server`,
66
+ SCRCPY_SERVER_VERSION,
67
+ `scid=${this.scid}`,
68
+ 'tunnel_forward=true',
69
+ 'video_codec=h264',
70
+ 'video_encoder=OMX.google.h264.encoder', // software encoder — avoids c2.android.avc.encoder stalling under GPU load
71
+ 'video_bit_rate=8000000',
72
+ 'max_fps=30',
73
+ 'audio=false',
74
+ 'stay_awake=true', // prevent device display from sleeping during capture
75
+ 'send_device_meta=true', // 64-byte name + codec-id + width + height header
76
+ 'send_frame_meta=false', // raw Annex B stream (no length prefix per frame)
77
+ 'send_dummy_byte=false', // skip the 1-byte connection-check byte
78
+ 'video_codec_options=i-frame-interval:int=1', // 1s IDR interval for faster keyframe recovery after restart
79
+ ], { stdio: ['ignore', 'pipe', 'ignore'] });
80
+ this.serverProc = serverProc;
81
+ // adb shell mixes server stdout+stderr into the local process stdout
82
+ serverProc.stdout?.on('data', (d) => {
83
+ const msg = d.toString().trim();
84
+ if (msg)
85
+ logger.debug(msg);
86
+ });
87
+ serverProc.unref();
88
+ try {
89
+ // Wait for server to bind the abstract socket
90
+ await new Promise((r) => setTimeout(r, 1500));
91
+ await execFileAsync(adb, ['-s', serial, 'forward', `tcp:${port}`, `localabstract:${this.socketName}`]);
92
+ // scrcpy accepts connections sequentially on the same socket:
93
+ // 1st accept() → video stream, 2nd accept() → control stream
94
+ this.videoSocket = await this.connectTcp(port);
95
+ this.controlSocket = await this.connectTcp(port);
96
+ this._video = new ScrcpyVideo(this.videoSocket);
97
+ const info = await this._video.deviceInfo();
98
+ this._control = new ScrcpyControl(this.controlSocket, info.width, info.height, onRotation);
99
+ logger.info(`ready — ${info.deviceName} ${info.width}×${info.height}`);
100
+ return info;
101
+ }
102
+ catch (e) {
103
+ serverProc.kill();
104
+ this.serverProc = null;
105
+ throw e;
106
+ }
107
+ }
108
+ stop(serial) {
109
+ this.serverProc?.kill();
110
+ this.serverProc = null;
111
+ this.videoSocket?.destroy();
112
+ this.controlSocket?.destroy();
113
+ this.videoSocket = null;
114
+ this.controlSocket = null;
115
+ this._video = null;
116
+ this._control = null;
117
+ const adb = getAdbPath();
118
+ execFile(adb, ['-s', serial, 'forward', '--remove', `tcp:${this.port}`], () => { });
119
+ }
120
+ connectTcp(port) {
121
+ return new Promise((resolve, reject) => {
122
+ const socket = net.connect(port, '127.0.0.1', () => resolve(socket));
123
+ socket.once('error', reject);
124
+ });
125
+ }
126
+ }
127
+ //# sourceMappingURL=ScrcpySession.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScrcpySession.js","sourceRoot":"","sources":["../../src/scrcpy/ScrcpySession.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import type { Socket } from 'net';
2
+ export interface ScrcpyDeviceInfo {
3
+ deviceName: string;
4
+ width: number;
5
+ height: number;
6
+ }
7
+ export declare class ScrcpyVideo {
8
+ private readonly socket;
9
+ private headerBuf;
10
+ private readonly dataChunks;
11
+ private headerResolve;
12
+ private headerReject;
13
+ private streamController;
14
+ private pending;
15
+ private headerConsumed;
16
+ private endReceived;
17
+ constructor(socket: Socket);
18
+ deviceInfo(): Promise<ScrcpyDeviceInfo>;
19
+ start(): ReadableStream<Buffer>;
20
+ private onData;
21
+ private drainNalUnits;
22
+ private parseHeader;
23
+ }
24
+ //# sourceMappingURL=ScrcpyVideo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScrcpyVideo.d.ts","sourceRoot":"","sources":["../../src/scrcpy/ScrcpyVideo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,KAAK,CAAA;AAUjC,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,qBAAa,WAAW;IAUV,OAAO,CAAC,QAAQ,CAAC,MAAM;IATnC,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAe;IAC1C,OAAO,CAAC,aAAa,CAAkD;IACvE,OAAO,CAAC,YAAY,CAAoC;IACxD,OAAO,CAAC,gBAAgB,CAAuD;IAC/E,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,WAAW,CAAQ;gBAEE,MAAM,EAAE,MAAM;IAkC3C,UAAU,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAQvC,KAAK,IAAI,cAAc,CAAC,MAAM,CAAC;IAe/B,OAAO,CAAC,MAAM;IAkBd,OAAO,CAAC,aAAa;IAarB,OAAO,CAAC,WAAW;CAUpB"}
@@ -0,0 +1,145 @@
1
+ // Header layout (send_device_meta=true, send_stream_meta=true default):
2
+ // [0..63] device name, null-padded (64 bytes)
3
+ // [64..67] codec ID as ASCII ("h264") (4 bytes)
4
+ // [68..71] width uint32 BE (4 bytes)
5
+ // [72..75] height uint32 BE (4 bytes)
6
+ const HEADER_SIZE = 76;
7
+ const DEVICE_NAME_SIZE = 64;
8
+ export class ScrcpyVideo {
9
+ socket;
10
+ headerBuf = null;
11
+ dataChunks = [];
12
+ headerResolve = null;
13
+ headerReject = null;
14
+ streamController = null;
15
+ pending = Buffer.alloc(0);
16
+ headerConsumed = false;
17
+ endReceived = false;
18
+ constructor(socket) {
19
+ this.socket = socket;
20
+ socket.on('data', (chunk) => this.onData(chunk));
21
+ socket.on('end', () => {
22
+ this.endReceived = true;
23
+ if (!this.headerConsumed) {
24
+ this.headerReject?.(new Error('scrcpy server closed before sending device info'));
25
+ this.headerReject = null;
26
+ return;
27
+ }
28
+ // Flush the last NAL unit (Annex B: last unit has no following start code)
29
+ if (this.pending.length > 0) {
30
+ const last = Buffer.from(this.pending);
31
+ this.pending = Buffer.alloc(0);
32
+ if (this.streamController) {
33
+ this.streamController.enqueue(last);
34
+ }
35
+ else {
36
+ this.dataChunks.push(last);
37
+ }
38
+ }
39
+ this.streamController?.close();
40
+ });
41
+ socket.on('close', () => {
42
+ if (!this.headerConsumed) {
43
+ this.headerReject?.(new Error('scrcpy server connection closed'));
44
+ this.headerReject = null;
45
+ }
46
+ });
47
+ socket.on('error', (e) => {
48
+ this.headerReject?.(e);
49
+ this.headerReject = null;
50
+ this.streamController?.error(e);
51
+ });
52
+ }
53
+ deviceInfo() {
54
+ if (this.headerBuf)
55
+ return Promise.resolve(this.parseHeader(this.headerBuf));
56
+ return new Promise((resolve, reject) => {
57
+ this.headerResolve = resolve;
58
+ this.headerReject = reject;
59
+ });
60
+ }
61
+ start() {
62
+ return new ReadableStream({
63
+ start: (controller) => {
64
+ this.streamController = controller;
65
+ // Flush any NAL units that arrived before start() was called
66
+ for (const chunk of this.dataChunks)
67
+ controller.enqueue(chunk);
68
+ this.dataChunks.length = 0;
69
+ if (this.endReceived)
70
+ controller.close();
71
+ },
72
+ cancel: () => {
73
+ this.socket.destroy();
74
+ },
75
+ });
76
+ }
77
+ onData(chunk) {
78
+ this.pending = Buffer.concat([this.pending, chunk]);
79
+ if (!this.headerConsumed) {
80
+ if (this.pending.length < HEADER_SIZE)
81
+ return;
82
+ const header = this.pending.subarray(0, HEADER_SIZE);
83
+ this.pending = this.pending.subarray(HEADER_SIZE);
84
+ this.headerBuf = header;
85
+ this.headerConsumed = true;
86
+ const info = this.parseHeader(header);
87
+ this.headerResolve?.(info);
88
+ this.headerResolve = null;
89
+ this.headerReject = null;
90
+ }
91
+ this.drainNalUnits();
92
+ }
93
+ drainNalUnits() {
94
+ // send_frame_meta=false → raw H.264 Annex B stream (no length prefix)
95
+ const { units, remaining } = extractAnnexBNals(this.pending);
96
+ this.pending = Buffer.from(remaining);
97
+ for (const nal of units) {
98
+ if (this.streamController) {
99
+ this.streamController.enqueue(nal);
100
+ }
101
+ else {
102
+ this.dataChunks.push(nal);
103
+ }
104
+ }
105
+ }
106
+ parseHeader(buf) {
107
+ const deviceName = buf
108
+ .subarray(0, DEVICE_NAME_SIZE)
109
+ .toString('utf8')
110
+ .replace(/\0.*/, '');
111
+ // bytes [64..67] = codec ID ("h264"), skip
112
+ const width = buf.readUInt32BE(68);
113
+ const height = buf.readUInt32BE(72);
114
+ return { deviceName, width, height };
115
+ }
116
+ }
117
+ function findStartCode(buf, from) {
118
+ for (let i = from; i + 2 < buf.length; i++) {
119
+ if (buf[i] === 0 && buf[i + 1] === 0) {
120
+ if (buf[i + 2] === 1)
121
+ return { pos: i, len: 3 };
122
+ if (i + 3 < buf.length && buf[i + 2] === 0 && buf[i + 3] === 1)
123
+ return { pos: i, len: 4 };
124
+ }
125
+ }
126
+ return null;
127
+ }
128
+ function extractAnnexBNals(buf) {
129
+ const units = [];
130
+ const first = findStartCode(buf, 0);
131
+ if (!first)
132
+ return { units, remaining: buf };
133
+ let nalStart = first.pos;
134
+ let searchFrom = first.pos + first.len;
135
+ while (true) {
136
+ const next = findStartCode(buf, searchFrom);
137
+ if (!next)
138
+ break;
139
+ units.push(Buffer.from(buf.subarray(nalStart, next.pos)));
140
+ nalStart = next.pos;
141
+ searchFrom = next.pos + next.len;
142
+ }
143
+ return { units, remaining: buf.subarray(nalStart) };
144
+ }
145
+ //# sourceMappingURL=ScrcpyVideo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScrcpyVideo.js","sourceRoot":"","sources":["../../src/scrcpy/ScrcpyVideo.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@tapflowio/android-agent",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Android emulator agent for tapflow — ADB control and scrcpy H.264 streaming",
5
+ "keywords": [
6
+ "tapflow",
7
+ "android",
8
+ "emulator",
9
+ "adb",
10
+ "scrcpy",
11
+ "streaming",
12
+ "qa"
13
+ ],
14
+ "homepage": "https://github.com/jo-duchan/tapflow",
15
+ "bugs": "https://github.com/jo-duchan/tapflow/issues",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/jo-duchan/tapflow.git",
19
+ "directory": "packages/android-agent"
20
+ },
21
+ "license": "MIT",
22
+ "author": "tapflow contributors",
23
+ "type": "module",
24
+ "main": "dist/index.js",
25
+ "types": "dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "source": "./src/index.ts",
29
+ "tsx": "./src/index.ts",
30
+ "import": "./dist/index.js",
31
+ "default": "./dist/index.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "bin",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "engines": {
41
+ "node": ">=20"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "dependencies": {
47
+ "ws": "^8.0.0",
48
+ "@tapflowio/agent-core": "0.1.0-alpha.1"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^20.0.0",
52
+ "@types/ws": "^8.0.0",
53
+ "typescript": "^5.0.0",
54
+ "vitest": "^2.0.0",
55
+ "@tapflowio/relay": "0.1.0-alpha.1"
56
+ },
57
+ "scripts": {
58
+ "build": "tsc",
59
+ "typecheck": "tsc --noEmit",
60
+ "test": "vitest run",
61
+ "lint": "eslint src",
62
+ "download-scrcpy-server": "node scripts/download-scrcpy-server.mjs"
63
+ }
64
+ }