@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.
- package/CHANGELOG.json +45 -0
- package/CHANGELOG.md +25 -1
- package/LICENSE +21 -21
- package/README.md +90 -89
- package/bin/scrcpy-server +0 -0
- package/bin/version.d.ts +1 -1
- package/bin/version.js +1 -1
- package/esm/client.d.ts +14 -2
- package/esm/client.d.ts.map +1 -1
- package/esm/client.js +275 -143
- package/esm/client.js.map +1 -1
- package/esm/connection.d.ts.map +1 -1
- package/esm/connection.js +74 -86
- package/esm/connection.js.map +1 -1
- package/esm/decoder/tinyh264/index.js +48 -48
- package/esm/decoder/tinyh264/index.js.map +1 -1
- package/esm/decoder/tinyh264/wrapper.js +3 -2
- package/esm/decoder/tinyh264/wrapper.js.map +1 -1
- package/esm/decoder/web-codecs/index.js +26 -21
- package/esm/decoder/web-codecs/index.js.map +1 -1
- package/esm/message.d.ts +13 -8
- package/esm/message.d.ts.map +1 -1
- package/esm/message.js +14 -8
- package/esm/message.js.map +1 -1
- package/esm/options/1_16/index.d.ts +8 -4
- package/esm/options/1_16/index.d.ts.map +1 -1
- package/esm/options/1_16/index.js +44 -18
- package/esm/options/1_16/index.js.map +1 -1
- package/esm/options/1_16/sps.js +3 -2
- package/esm/options/1_16/sps.js.map +1 -1
- package/esm/options/1_18.d.ts +3 -2
- package/esm/options/1_18.d.ts.map +1 -1
- package/esm/options/1_18.js +24 -1
- package/esm/options/1_18.js.map +1 -1
- package/esm/options/1_21.js +4 -1
- package/esm/options/1_21.js.map +1 -1
- package/esm/options/1_22.d.ts +3 -3
- package/esm/options/1_22.d.ts.map +1 -1
- package/esm/options/1_22.js +9 -6
- package/esm/options/1_22.js.map +1 -1
- package/esm/options/1_23.js +4 -1
- package/esm/options/1_23.js.map +1 -1
- package/esm/options/1_24.d.ts +9 -0
- package/esm/options/1_24.d.ts.map +1 -0
- package/esm/options/1_24.js +13 -0
- package/esm/options/1_24.js.map +1 -0
- package/esm/options/common.d.ts +2 -0
- package/esm/options/common.d.ts.map +1 -1
- package/esm/options/common.js.map +1 -1
- package/esm/options/index.d.ts +1 -0
- package/esm/options/index.d.ts.map +1 -1
- package/esm/options/index.js +1 -0
- package/esm/options/index.js.map +1 -1
- package/esm/push-server.js +5 -10
- package/esm/push-server.js.map +1 -1
- package/package.json +8 -8
- package/scrcpy.LICENSE +203 -203
- package/scripts/fetch-server.cjs +28 -28
- package/src/client.ts +435 -275
- package/src/codec.ts +35 -35
- package/src/connection.ts +148 -145
- package/src/decoder/index.ts +3 -3
- package/src/decoder/tinyh264/index.ts +112 -112
- package/src/decoder/tinyh264/types.d.ts +137 -137
- package/src/decoder/tinyh264/worker.ts +2 -2
- package/src/decoder/tinyh264/wrapper.ts +89 -89
- package/src/decoder/types.ts +35 -35
- package/src/decoder/web-codecs/index.ts +99 -99
- package/src/index.ts +8 -8
- package/src/message.ts +113 -105
- package/src/options/1_16/index.ts +345 -315
- package/src/options/1_16/sps.ts +300 -300
- package/src/options/1_18.ts +61 -41
- package/src/options/1_21.ts +34 -34
- package/src/options/1_22.ts +78 -80
- package/src/options/1_24.ts +18 -0
- package/src/options/common.ts +66 -63
- package/src/options/index.ts +7 -6
- package/src/push-server.ts +24 -24
- 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
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
public
|
|
272
|
-
|
|
273
|
-
|
|
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
|
+
}
|