@yume-chan/scrcpy 0.0.13 → 0.0.16

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 (80) hide show
  1. package/CHANGELOG.json +45 -0
  2. package/CHANGELOG.md +25 -1
  3. package/LICENSE +21 -21
  4. package/README.md +90 -89
  5. package/bin/scrcpy-server +0 -0
  6. package/bin/version.d.ts +1 -1
  7. package/bin/version.js +1 -1
  8. package/esm/client.d.ts +14 -2
  9. package/esm/client.d.ts.map +1 -1
  10. package/esm/client.js +275 -143
  11. package/esm/client.js.map +1 -1
  12. package/esm/connection.d.ts.map +1 -1
  13. package/esm/connection.js +74 -86
  14. package/esm/connection.js.map +1 -1
  15. package/esm/decoder/tinyh264/index.js +48 -48
  16. package/esm/decoder/tinyh264/index.js.map +1 -1
  17. package/esm/decoder/tinyh264/wrapper.js +3 -2
  18. package/esm/decoder/tinyh264/wrapper.js.map +1 -1
  19. package/esm/decoder/web-codecs/index.js +26 -21
  20. package/esm/decoder/web-codecs/index.js.map +1 -1
  21. package/esm/message.d.ts +13 -8
  22. package/esm/message.d.ts.map +1 -1
  23. package/esm/message.js +14 -8
  24. package/esm/message.js.map +1 -1
  25. package/esm/options/1_16/index.d.ts +8 -4
  26. package/esm/options/1_16/index.d.ts.map +1 -1
  27. package/esm/options/1_16/index.js +44 -18
  28. package/esm/options/1_16/index.js.map +1 -1
  29. package/esm/options/1_16/sps.js +3 -2
  30. package/esm/options/1_16/sps.js.map +1 -1
  31. package/esm/options/1_18.d.ts +3 -2
  32. package/esm/options/1_18.d.ts.map +1 -1
  33. package/esm/options/1_18.js +24 -1
  34. package/esm/options/1_18.js.map +1 -1
  35. package/esm/options/1_21.js +4 -1
  36. package/esm/options/1_21.js.map +1 -1
  37. package/esm/options/1_22.d.ts +3 -3
  38. package/esm/options/1_22.d.ts.map +1 -1
  39. package/esm/options/1_22.js +9 -6
  40. package/esm/options/1_22.js.map +1 -1
  41. package/esm/options/1_23.js +4 -1
  42. package/esm/options/1_23.js.map +1 -1
  43. package/esm/options/1_24.d.ts +9 -0
  44. package/esm/options/1_24.d.ts.map +1 -0
  45. package/esm/options/1_24.js +13 -0
  46. package/esm/options/1_24.js.map +1 -0
  47. package/esm/options/common.d.ts +2 -0
  48. package/esm/options/common.d.ts.map +1 -1
  49. package/esm/options/common.js.map +1 -1
  50. package/esm/options/index.d.ts +1 -0
  51. package/esm/options/index.d.ts.map +1 -1
  52. package/esm/options/index.js +1 -0
  53. package/esm/options/index.js.map +1 -1
  54. package/esm/push-server.js +5 -10
  55. package/esm/push-server.js.map +1 -1
  56. package/package.json +8 -8
  57. package/scrcpy.LICENSE +203 -203
  58. package/scripts/fetch-server.cjs +28 -28
  59. package/src/client.ts +435 -275
  60. package/src/codec.ts +35 -35
  61. package/src/connection.ts +148 -145
  62. package/src/decoder/index.ts +3 -3
  63. package/src/decoder/tinyh264/index.ts +112 -112
  64. package/src/decoder/tinyh264/types.d.ts +137 -137
  65. package/src/decoder/tinyh264/worker.ts +2 -2
  66. package/src/decoder/tinyh264/wrapper.ts +89 -89
  67. package/src/decoder/types.ts +35 -35
  68. package/src/decoder/web-codecs/index.ts +99 -99
  69. package/src/index.ts +8 -8
  70. package/src/message.ts +113 -105
  71. package/src/options/1_16/index.ts +345 -315
  72. package/src/options/1_16/sps.ts +300 -300
  73. package/src/options/1_18.ts +61 -41
  74. package/src/options/1_21.ts +34 -34
  75. package/src/options/1_22.ts +78 -80
  76. package/src/options/1_24.ts +18 -0
  77. package/src/options/common.ts +66 -63
  78. package/src/options/index.ts +7 -6
  79. package/src/push-server.ts +24 -24
  80. package/src/utils.ts +5 -5
package/src/client.ts CHANGED
@@ -1,275 +1,435 @@
1
- import { AdbBufferedStream, AdbSubprocessNoneProtocol, DecodeUtf8Stream, InspectStream, TransformStream, WritableStream, type Adb, type AdbSocket, type AdbSubprocessProtocol, type ReadableStream, type WritableStreamDefaultWriter } from '@yume-chan/adb';
2
- import { EventEmitter } from '@yume-chan/event';
3
- import Struct from '@yume-chan/struct';
4
- import { AndroidMotionEventAction, ScrcpyControlMessageType, ScrcpyInjectKeyCodeControlMessage, ScrcpyInjectTextControlMessage, ScrcpyInjectTouchControlMessage, type AndroidKeyEventAction } from './message.js';
5
- import type { ScrcpyInjectScrollControlMessage1_22, ScrcpyOptions, VideoStreamPacket } from "./options/index.js";
6
-
7
- function* splitLines(text: string): Generator<string, void, void> {
8
- let start = 0;
9
-
10
- while (true) {
11
- const index = text.indexOf('\n', start);
12
- if (index === -1) {
13
- return;
14
- }
15
-
16
- const line = text.substring(start, index);
17
- yield line;
18
-
19
- start = index + 1;
20
- }
21
- }
22
-
23
- const ClipboardMessage =
24
- new Struct()
25
- .uint32('length')
26
- .string('content', { lengthField: 'length' });
27
-
28
- export class ScrcpyClient {
29
- public static async getEncoders(
30
- adb: Adb,
31
- path: string,
32
- version: string,
33
- options: ScrcpyOptions<any>
34
- ): Promise<string[]> {
35
- // Provide an invalid encoder name
36
- // So the server will return all available encoders
37
- options.value.encoderName = '_';
38
- // Disable control for faster connection in 1.22+
39
- options.value.control = false;
40
-
41
- // Scrcpy server will open connections, before initializing encoder
42
- // Thus although an invalid encoder name is given, the start process will success
43
- const client = await ScrcpyClient.start(adb, path, version, options);
44
-
45
- const encoderNameRegex = options.getOutputEncoderNameRegex();
46
- const encoders: string[] = [];
47
- await client.stdout.pipeTo(new WritableStream({
48
- write(line) {
49
- const match = line.match(encoderNameRegex);
50
- if (match) {
51
- encoders.push(match[1]!);
52
- }
53
- },
54
- }));
55
-
56
- return encoders;
57
- }
58
-
59
- public static async start(
60
- adb: Adb,
61
- path: string,
62
- version: string,
63
- options: ScrcpyOptions<any>
64
- ) {
65
- const connection = options.createConnection(adb);
66
- let process: AdbSubprocessProtocol | undefined;
67
-
68
- try {
69
- await connection.initialize();
70
-
71
- process = await adb.subprocess.spawn(
72
- [
73
- // cspell: disable-next-line
74
- `CLASSPATH=${path}`,
75
- 'app_process',
76
- /* unused */ '/',
77
- 'com.genymobile.scrcpy.Server',
78
- version,
79
- ...options.formatServerArguments(),
80
- ],
81
- {
82
- // Scrcpy server doesn't split stdout and stderr,
83
- // so disable Shell Protocol to simplify processing
84
- protocols: [AdbSubprocessNoneProtocol],
85
- }
86
- );
87
-
88
- const result = await Promise.race([
89
- process.exit,
90
- connection.getStreams(),
91
- ]);
92
-
93
- if (typeof result === 'number') {
94
- throw new Error('scrcpy server exited prematurely');
95
- }
96
-
97
- const [videoStream, controlStream] = result;
98
- return new ScrcpyClient(adb, options, process, videoStream, controlStream);
99
- } catch (e) {
100
- await process?.kill();
101
- throw e;
102
- } finally {
103
- connection.dispose();
104
- }
105
- }
106
-
107
- private _adb: Adb;
108
- public get adb() { return this._adb; }
109
-
110
- private options: ScrcpyOptions<any>;
111
- private process: AdbSubprocessProtocol;
112
-
113
- private _stdout: ReadableStream<string>;
114
- public get stdout() { return this._stdout; }
115
-
116
- public get exit() { return this.process.exit; }
117
-
118
- private _screenWidth: number | undefined;
119
- public get screenWidth() { return this._screenWidth; }
120
-
121
- private _screenHeight: number | undefined;
122
- public get screenHeight() { return this._screenHeight; }
123
-
124
- private _videoStream: ReadableStream<VideoStreamPacket>;
125
- public get videoStream() { return this._videoStream; }
126
-
127
- private _controlStreamWriter: WritableStreamDefaultWriter<Uint8Array> | undefined;
128
-
129
- private readonly clipboardChangeEvent = new EventEmitter<string>();
130
- public get onClipboardChange() { return this.clipboardChangeEvent.event; }
131
-
132
- private lastTouchMessage = 0;
133
-
134
- public constructor(
135
- adb: Adb,
136
- options: ScrcpyOptions<any>,
137
- process: AdbSubprocessProtocol,
138
- videoStream: AdbSocket,
139
- controlStream: AdbSocket | undefined,
140
- ) {
141
- this._adb = adb;
142
- this.options = options;
143
- this.process = process;
144
-
145
- this._stdout = process.stdout
146
- .pipeThrough(new DecodeUtf8Stream())
147
- .pipeThrough(new TransformStream({
148
- transform(chunk, controller) {
149
- for (const line of splitLines(chunk)) {
150
- if (line === '') {
151
- continue;
152
- }
153
- controller.enqueue(line);
154
- }
155
- },
156
- }));
157
-
158
- this._videoStream = videoStream.readable
159
- .pipeThrough(options.createVideoStreamTransformer())
160
- .pipeThrough(new InspectStream(packet => {
161
- if (packet.type === 'configuration') {
162
- this._screenWidth = packet.data.croppedWidth;
163
- this._screenHeight = packet.data.croppedHeight;
164
- }
165
- }));
166
-
167
- if (controlStream) {
168
- const buffered = new AdbBufferedStream(controlStream);
169
- this._controlStreamWriter = controlStream.writable.getWriter();
170
- (async () => {
171
- try {
172
- while (true) {
173
- const type = await buffered.read(1);
174
- switch (type[0]) {
175
- case 0:
176
- const { content } = await ClipboardMessage.deserialize(buffered);
177
- this.clipboardChangeEvent.fire(content!);
178
- break;
179
- default:
180
- throw new Error('unknown control message type');
181
- }
182
- }
183
- } catch {
184
- // TODO: Scrcpy: handle error
185
- }
186
- })();
187
- }
188
- }
189
-
190
- private checkControlStream(caller: string) {
191
- if (!this._controlStreamWriter) {
192
- throw new Error(`${caller} called with control disabled`);
193
- }
194
-
195
- return this._controlStreamWriter;
196
- }
197
-
198
- public async injectKeyCode(message: Omit<ScrcpyInjectKeyCodeControlMessage, 'type'>) {
199
- const controlStream = this.checkControlStream('injectKeyCode');
200
-
201
- await controlStream.write(ScrcpyInjectKeyCodeControlMessage.serialize({
202
- ...message,
203
- type: ScrcpyControlMessageType.InjectKeycode,
204
- }));
205
- }
206
-
207
- public async injectText(text: string) {
208
- const controlStream = this.checkControlStream('injectText');
209
-
210
- await controlStream.write(ScrcpyInjectTextControlMessage.serialize({
211
- type: ScrcpyControlMessageType.InjectText,
212
- text,
213
- }));
214
- }
215
-
216
- public async injectTouch(message: Omit<ScrcpyInjectTouchControlMessage, 'type' | 'screenWidth' | 'screenHeight'>) {
217
- const controlStream = this.checkControlStream('injectTouch');
218
-
219
- if (!this.screenWidth || !this.screenHeight) {
220
- return;
221
- }
222
-
223
- // ADB streams are actually pretty low-bandwidth and laggy
224
- // Re-sample move events to avoid flooding the connection
225
-
226
- // TODO: Scrcpy: investigate how to throttle touch events
227
- // because 60FPS may still be too high
228
- const now = Date.now();
229
- if (now - this.lastTouchMessage < 16 &&
230
- [AndroidMotionEventAction.Move, AndroidMotionEventAction.HoverMove].includes(message.action)) {
231
- return;
232
- }
233
-
234
- this.lastTouchMessage = now;
235
- await controlStream.write(ScrcpyInjectTouchControlMessage.serialize({
236
- ...message,
237
- type: ScrcpyControlMessageType.InjectTouch,
238
- screenWidth: this.screenWidth,
239
- screenHeight: this.screenHeight,
240
- }));
241
- }
242
-
243
- public async injectScroll(message: Omit<ScrcpyInjectScrollControlMessage1_22, 'type' | 'screenWidth' | 'screenHeight'>) {
244
- const controlStream = this.checkControlStream('injectScroll');
245
-
246
- if (!this.screenWidth || !this.screenHeight) {
247
- return;
248
- }
249
-
250
- const buffer = this.options!.serializeInjectScrollControlMessage({
251
- ...message,
252
- type: ScrcpyControlMessageType.InjectScroll,
253
- screenWidth: this.screenWidth,
254
- screenHeight: this.screenHeight,
255
- });
256
- await controlStream.write(buffer);
257
- }
258
-
259
- public async pressBackOrTurnOnScreen(action: AndroidKeyEventAction) {
260
- const controlStream = this.checkControlStream('pressBackOrTurnOnScreen');
261
-
262
- const buffer = this.options!.serializeBackOrScreenOnControlMessage({
263
- type: ScrcpyControlMessageType.BackOrScreenOn,
264
- action,
265
- });
266
- if (buffer) {
267
- await controlStream.write(buffer);
268
- }
269
- }
270
-
271
- public async close() {
272
- // No need to close streams. Kill the process will destroy them from the other side.
273
- await this.process?.kill();
274
- }
275
- }
1
+ import { AbortController, AdbBufferedStream, AdbSubprocessNoneProtocol, DecodeUtf8Stream, InspectStream, ReadableStream, TransformStream, WritableStream, type Adb, type AdbSocket, type AdbSubprocessProtocol, type WritableStreamDefaultWriter } from '@yume-chan/adb';
2
+ import { EventEmitter } from '@yume-chan/event';
3
+ import Struct from '@yume-chan/struct';
4
+ import { AndroidMotionEventAction, ScrcpyControlMessageType, ScrcpyInjectKeyCodeControlMessage, ScrcpyInjectTextControlMessage, ScrcpyInjectTouchControlMessage, ScrcpySimpleControlMessage, type AndroidKeyEventAction } from './message.js';
5
+ import type { ScrcpyInjectScrollControlMessage1_22, ScrcpyOptions, VideoStreamPacket } from "./options/index.js";
6
+
7
+ function* splitLines(text: string): Generator<string, void, void> {
8
+ let start = 0;
9
+
10
+ while (true) {
11
+ const index = text.indexOf('\n', start);
12
+ if (index === -1) {
13
+ return;
14
+ }
15
+
16
+ const line = text.substring(start, index);
17
+ yield line;
18
+
19
+ start = index + 1;
20
+ }
21
+ }
22
+
23
+ class SplitLinesStream extends TransformStream<string, string>{
24
+ constructor() {
25
+ super({
26
+ transform(chunk, controller) {
27
+ for (const line of splitLines(chunk)) {
28
+ if (line === '') {
29
+ continue;
30
+ }
31
+ controller.enqueue(line);
32
+ }
33
+ },
34
+ });
35
+ }
36
+ }
37
+
38
+ class ArrayToStream<T> extends ReadableStream<T>{
39
+ private array!: T[];
40
+ private index = 0;
41
+
42
+ constructor(array: T[]) {
43
+ super({
44
+ start: async () => {
45
+ await Promise.resolve();
46
+ this.array = array;
47
+ },
48
+ pull: (controller) => {
49
+ if (this.index < this.array.length) {
50
+ controller.enqueue(this.array[this.index]!);
51
+ this.index += 1;
52
+ } else {
53
+ controller.close();
54
+ }
55
+ },
56
+ });
57
+ }
58
+ }
59
+
60
+ class ConcatStream<T> extends ReadableStream<T>{
61
+ private streams!: ReadableStream<T>[];
62
+ private index = 0;
63
+ private reader!: ReadableStreamDefaultReader<T>;
64
+
65
+ constructor(...streams: ReadableStream<T>[]) {
66
+ super({
67
+ start: async (controller) => {
68
+ await Promise.resolve();
69
+
70
+ this.streams = streams;
71
+ this.advance(controller);
72
+ },
73
+ pull: async (controller) => {
74
+ const result = await this.reader.read();
75
+ if (!result.done) {
76
+ controller.enqueue(result.value);
77
+ return;
78
+ }
79
+ this.advance(controller);
80
+ }
81
+ });
82
+ }
83
+
84
+ private advance(controller: ReadableStreamDefaultController<T>) {
85
+ if (this.index < this.streams.length) {
86
+ this.reader = this.streams[this.index]!.getReader();
87
+ this.index += 1;
88
+ } else {
89
+ controller.close();
90
+ }
91
+ }
92
+ }
93
+
94
+ const ClipboardMessage =
95
+ new Struct()
96
+ .uint32('length')
97
+ .string('content', { lengthField: 'length' });
98
+
99
+ export class ScrcpyClient {
100
+ /**
101
+ * This method will modify the given `options`,
102
+ * so don't reuse it elsewhere.
103
+ */
104
+ public static async getEncoders(
105
+ adb: Adb,
106
+ path: string,
107
+ version: string,
108
+ options: ScrcpyOptions<any>
109
+ ): Promise<string[]> {
110
+ // Provide an invalid encoder name
111
+ // So the server will return all available encoders
112
+ options.value.encoderName = '_';
113
+ // Disable control for faster connection in 1.22+
114
+ options.value.control = false;
115
+ options.value.sendDeviceMeta = false;
116
+ options.value.sendDummyByte = false;
117
+
118
+ // Scrcpy server will open connections, before initializing encoder
119
+ // Thus although an invalid encoder name is given, the start process will success
120
+ const client = await ScrcpyClient.start(adb, path, version, options);
121
+
122
+ const encoderNameRegex = options.getOutputEncoderNameRegex();
123
+ const encoders: string[] = [];
124
+ await client.stdout.pipeTo(new WritableStream({
125
+ write(line) {
126
+ const match = line.match(encoderNameRegex);
127
+ if (match) {
128
+ encoders.push(match[1]!);
129
+ }
130
+ },
131
+ }));
132
+
133
+ return encoders;
134
+ }
135
+
136
+ /**
137
+ * This method will modify the given `options`,
138
+ * so don't reuse it elsewhere.
139
+ */
140
+ public static async getDisplays(
141
+ adb: Adb,
142
+ path: string,
143
+ version: string,
144
+ options: ScrcpyOptions<any>
145
+ ): Promise<number[]> {
146
+ // Similar to `getEncoders`, pass an invalid option and parse the output
147
+ options.value.displayId = -1;
148
+
149
+ options.value.control = false;
150
+ options.value.sendDeviceMeta = false;
151
+ options.value.sendDummyByte = false;
152
+
153
+ try {
154
+ // Server will exit before opening connections when an invalid display id was given.
155
+ await ScrcpyClient.start(adb, path, version, options);
156
+ } catch (e) {
157
+ if (e instanceof Error) {
158
+ const output = (e as any).output as string[];
159
+
160
+ const displayIdRegex = /\s+scrcpy --display (\d+)/;
161
+ const displays: number[] = [];
162
+ for (const line of output) {
163
+ const match = line.match(displayIdRegex);
164
+ if (match) {
165
+ displays.push(Number.parseInt(match[1]!, 10));
166
+ }
167
+ }
168
+ return displays;
169
+ }
170
+ }
171
+
172
+ throw new Error('failed to get displays');
173
+ }
174
+
175
+ public static async start(
176
+ adb: Adb,
177
+ path: string,
178
+ version: string,
179
+ options: ScrcpyOptions<any>
180
+ ) {
181
+ const connection = options.createConnection(adb);
182
+ let process: AdbSubprocessProtocol | undefined;
183
+
184
+ try {
185
+ await connection.initialize();
186
+
187
+ process = await adb.subprocess.spawn(
188
+ [
189
+ // cspell: disable-next-line
190
+ `CLASSPATH=${path}`,
191
+ 'app_process',
192
+ /* unused */ '/',
193
+ 'com.genymobile.scrcpy.Server',
194
+ version,
195
+ ...options.formatServerArguments(),
196
+ ],
197
+ {
198
+ // Scrcpy server doesn't split stdout and stderr,
199
+ // so disable Shell Protocol to simplify processing
200
+ protocols: [AdbSubprocessNoneProtocol],
201
+ }
202
+ );
203
+
204
+ const stdout = process.stdout
205
+ .pipeThrough(new DecodeUtf8Stream())
206
+ .pipeThrough(new SplitLinesStream());
207
+
208
+ // Read stdout, otherwise `process.exit` won't resolve.
209
+ const output: string[] = [];
210
+ const abortController = new AbortController();
211
+ const pipe = stdout
212
+ .pipeTo(new WritableStream({
213
+ write(chunk) {
214
+ output.push(chunk);
215
+ }
216
+ }), {
217
+ signal: abortController.signal,
218
+ preventCancel: true,
219
+ })
220
+ .catch(() => { });
221
+
222
+ const result = await Promise.race([
223
+ process.exit,
224
+ connection.getStreams(),
225
+ ]);
226
+
227
+ if (typeof result === 'number') {
228
+ const error = new Error('scrcpy server exited prematurely');
229
+ (error as any).output = output;
230
+ throw error;
231
+ }
232
+
233
+ abortController.abort();
234
+ await pipe;
235
+
236
+ const [videoStream, controlStream] = result;
237
+ return new ScrcpyClient(
238
+ adb,
239
+ options,
240
+ process,
241
+ new ConcatStream(
242
+ new ArrayToStream(output),
243
+ stdout,
244
+ ),
245
+ videoStream,
246
+ controlStream
247
+ );
248
+ } catch (e) {
249
+ await process?.kill();
250
+ throw e;
251
+ } finally {
252
+ connection.dispose();
253
+ }
254
+ }
255
+
256
+ private _adb: Adb;
257
+ public get adb() { return this._adb; }
258
+
259
+ private options: ScrcpyOptions<any>;
260
+ private process: AdbSubprocessProtocol;
261
+
262
+ private _stdout: ReadableStream<string>;
263
+ public get stdout() { return this._stdout; }
264
+
265
+ public get exit() { return this.process.exit; }
266
+
267
+ private _screenWidth: number | undefined;
268
+ public get screenWidth() { return this._screenWidth; }
269
+
270
+ private _screenHeight: number | undefined;
271
+ public get screenHeight() { return this._screenHeight; }
272
+
273
+ private _videoStream: ReadableStream<VideoStreamPacket>;
274
+ public get videoStream() { return this._videoStream; }
275
+
276
+ private _controlStreamWriter: WritableStreamDefaultWriter<Uint8Array> | undefined;
277
+
278
+ private readonly clipboardChangeEvent = new EventEmitter<string>();
279
+ public get onClipboardChange() { return this.clipboardChangeEvent.event; }
280
+
281
+ private lastTouchMessage = 0;
282
+
283
+ public constructor(
284
+ adb: Adb,
285
+ options: ScrcpyOptions<any>,
286
+ process: AdbSubprocessProtocol,
287
+ stdout: ReadableStream<string>,
288
+ videoStream: AdbSocket,
289
+ controlStream: AdbSocket | undefined,
290
+ ) {
291
+ this._adb = adb;
292
+ this.options = options;
293
+ this.process = process;
294
+
295
+ this._stdout = stdout;
296
+
297
+ this._videoStream = videoStream.readable
298
+ .pipeThrough(options.createVideoStreamTransformer())
299
+ .pipeThrough(new InspectStream(packet => {
300
+ if (packet.type === 'configuration') {
301
+ this._screenWidth = packet.data.croppedWidth;
302
+ this._screenHeight = packet.data.croppedHeight;
303
+ }
304
+ }));
305
+
306
+ if (controlStream) {
307
+ const buffered = new AdbBufferedStream(controlStream);
308
+ this._controlStreamWriter = controlStream.writable.getWriter();
309
+ (async () => {
310
+ try {
311
+ while (true) {
312
+ const type = await buffered.read(1);
313
+ switch (type[0]) {
314
+ case 0:
315
+ const { content } = await ClipboardMessage.deserialize(buffered);
316
+ this.clipboardChangeEvent.fire(content!);
317
+ break;
318
+ default:
319
+ throw new Error('unknown control message type');
320
+ }
321
+ }
322
+ } catch {
323
+ // TODO: Scrcpy: handle error
324
+ }
325
+ })();
326
+ }
327
+ }
328
+
329
+ private checkControlStream(caller: string) {
330
+ if (!this._controlStreamWriter) {
331
+ throw new Error(`${caller} called with control disabled`);
332
+ }
333
+
334
+ return this._controlStreamWriter;
335
+ }
336
+
337
+ private getControlMessageTypeValue(type: ScrcpyControlMessageType) {
338
+ const list = this.options.getControlMessageTypes();
339
+ const index = list.indexOf(type);
340
+ if (index === -1) {
341
+ throw new Error('Not supported');
342
+ }
343
+ return index;
344
+ }
345
+
346
+ public async injectKeyCode(message: Omit<ScrcpyInjectKeyCodeControlMessage, 'type'>) {
347
+ const controlStream = this.checkControlStream('injectKeyCode');
348
+
349
+ await controlStream.write(ScrcpyInjectKeyCodeControlMessage.serialize({
350
+ ...message,
351
+ type: this.getControlMessageTypeValue(ScrcpyControlMessageType.InjectKeycode),
352
+ }));
353
+ }
354
+
355
+ public async injectText(text: string) {
356
+ const controlStream = this.checkControlStream('injectText');
357
+
358
+ await controlStream.write(ScrcpyInjectTextControlMessage.serialize({
359
+ type: this.getControlMessageTypeValue(ScrcpyControlMessageType.InjectText),
360
+ text,
361
+ }));
362
+ }
363
+
364
+ public async injectTouch(message: Omit<ScrcpyInjectTouchControlMessage, 'type' | 'screenWidth' | 'screenHeight'>) {
365
+ const controlStream = this.checkControlStream('injectTouch');
366
+
367
+ if (!this.screenWidth || !this.screenHeight) {
368
+ return;
369
+ }
370
+
371
+ // ADB streams are actually pretty low-bandwidth and laggy
372
+ // Re-sample move events to avoid flooding the connection
373
+
374
+ // TODO: Scrcpy: investigate how to throttle touch events
375
+ // because 60FPS may still be too high
376
+ const now = Date.now();
377
+ if (now - this.lastTouchMessage < 16 &&
378
+ [AndroidMotionEventAction.Move, AndroidMotionEventAction.HoverMove].includes(message.action)) {
379
+ return;
380
+ }
381
+
382
+ this.lastTouchMessage = now;
383
+ await controlStream.write(ScrcpyInjectTouchControlMessage.serialize({
384
+ ...message,
385
+ type: this.getControlMessageTypeValue(ScrcpyControlMessageType.InjectTouch),
386
+ screenWidth: this.screenWidth,
387
+ screenHeight: this.screenHeight,
388
+ }));
389
+ }
390
+
391
+ public async injectScroll(message: Omit<ScrcpyInjectScrollControlMessage1_22, 'type' | 'screenWidth' | 'screenHeight'>) {
392
+ const controlStream = this.checkControlStream('injectScroll');
393
+
394
+ if (!this.screenWidth || !this.screenHeight) {
395
+ return;
396
+ }
397
+
398
+ const buffer = this.options!.serializeInjectScrollControlMessage({
399
+ ...message,
400
+ type: this.getControlMessageTypeValue(ScrcpyControlMessageType.InjectScroll),
401
+ screenWidth: this.screenWidth,
402
+ screenHeight: this.screenHeight,
403
+ });
404
+ await controlStream.write(buffer);
405
+ }
406
+
407
+ public async pressBackOrTurnOnScreen(action: AndroidKeyEventAction) {
408
+ const controlStream = this.checkControlStream('pressBackOrTurnOnScreen');
409
+
410
+ const buffer = this.options!.serializeBackOrScreenOnControlMessage({
411
+ type: this.getControlMessageTypeValue(ScrcpyControlMessageType.BackOrScreenOn),
412
+ action,
413
+ });
414
+ if (buffer) {
415
+ await controlStream.write(buffer);
416
+ }
417
+ }
418
+
419
+ private async sendSimpleControlMessage(type: ScrcpyControlMessageType, name: string) {
420
+ const controlStream = this.checkControlStream(name);
421
+ const buffer = ScrcpySimpleControlMessage.serialize({
422
+ type: this.getControlMessageTypeValue(type),
423
+ });
424
+ await controlStream.write(buffer);
425
+ }
426
+
427
+ public async rotateDevice() {
428
+ await this.sendSimpleControlMessage(ScrcpyControlMessageType.RotateDevice, 'rotateDevice');
429
+ }
430
+
431
+ public async close() {
432
+ // No need to close streams. Kill the process will destroy them from the other side.
433
+ await this.process?.kill();
434
+ }
435
+ }