io.appium.settings 8.0.9 → 8.1.0
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.md +12 -0
- package/README.md +126 -0
- package/apks/settings_apk-debug.apk +0 -0
- package/build/lib/client.d.ts +4 -0
- package/build/lib/client.d.ts.map +1 -1
- package/build/lib/client.js +4 -0
- package/build/lib/client.js.map +1 -1
- package/build/lib/commands/clipboard.d.ts.map +1 -1
- package/build/lib/commands/clipboard.js +1 -1
- package/build/lib/commands/clipboard.js.map +1 -1
- package/build/lib/commands/jpeg-stream.d.ts +51 -0
- package/build/lib/commands/jpeg-stream.d.ts.map +1 -0
- package/build/lib/commands/jpeg-stream.js +152 -0
- package/build/lib/commands/jpeg-stream.js.map +1 -0
- package/build/lib/commands/media-projection.d.ts.map +1 -1
- package/build/lib/commands/media-projection.js.map +1 -1
- package/build/lib/commands/media.js +1 -1
- package/build/lib/commands/media.js.map +1 -1
- package/build/lib/commands/network.js +1 -1
- package/build/lib/commands/network.js.map +1 -1
- package/build/lib/commands/types.d.ts +65 -0
- package/build/lib/commands/types.d.ts.map +1 -1
- package/build/lib/commands/video-stream.d.ts +50 -0
- package/build/lib/commands/video-stream.d.ts.map +1 -0
- package/build/lib/commands/video-stream.js +160 -0
- package/build/lib/commands/video-stream.js.map +1 -0
- package/build/lib/constants.d.ts +7 -0
- package/build/lib/constants.d.ts.map +1 -1
- package/build/lib/constants.js +7 -0
- package/build/lib/constants.js.map +1 -1
- package/build/lib/index.js +1 -1
- package/build/lib/index.js.map +1 -1
- package/build/lib/{utils.d.ts → utils/apk-path.d.ts} +1 -1
- package/build/lib/utils/apk-path.d.ts.map +1 -0
- package/build/lib/{utils.js → utils/apk-path.js} +1 -1
- package/build/lib/utils/apk-path.js.map +1 -0
- package/build/lib/utils/index.d.ts +5 -0
- package/build/lib/utils/index.d.ts.map +1 -0
- package/build/lib/utils/index.js +4 -0
- package/build/lib/utils/index.js.map +1 -0
- package/build/lib/utils/streaming-protocol.d.ts +47 -0
- package/build/lib/utils/streaming-protocol.d.ts.map +1 -0
- package/build/lib/utils/streaming-protocol.js +67 -0
- package/build/lib/utils/streaming-protocol.js.map +1 -0
- package/build/lib/utils/streaming-transport.d.ts +142 -0
- package/build/lib/utils/streaming-transport.d.ts.map +1 -0
- package/build/lib/utils/streaming-transport.js +416 -0
- package/build/lib/utils/streaming-transport.js.map +1 -0
- package/lib/client.ts +5 -0
- package/lib/commands/clipboard.ts +1 -0
- package/lib/commands/jpeg-stream.ts +171 -0
- package/lib/commands/media-projection.ts +1 -5
- package/lib/commands/media.ts +1 -1
- package/lib/commands/network.ts +1 -1
- package/lib/commands/types.ts +70 -0
- package/lib/commands/video-stream.ts +179 -0
- package/lib/constants.ts +10 -0
- package/lib/index.ts +1 -1
- package/lib/utils/index.ts +4 -0
- package/lib/utils/streaming-protocol.ts +93 -0
- package/lib/utils/streaming-transport.ts +450 -0
- package/package.json +1 -1
- package/build/lib/utils.d.ts.map +0 -1
- package/build/lib/utils.js.map +0 -1
- /package/lib/{utils.ts → utils/apk-path.ts} +0 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import { retryInterval } from 'asyncbox';
|
|
3
|
+
import { createParserState, parseFrames, StreamTrack } from './streaming-protocol.js';
|
|
4
|
+
const DEFAULT_QUEUE_CAPACITY = 60;
|
|
5
|
+
const CONNECT_TIMEOUT_MS = 5000;
|
|
6
|
+
const CONNECT_RETRY_INTERVAL_MS = 200;
|
|
7
|
+
// adb forward's host-side TCP listener accepts a connection immediately, independent of
|
|
8
|
+
// whether the on-device local socket is bound yet; if it isn't, adb tears the connection
|
|
9
|
+
// down right away with no data. Wait this long past 'connect' before trusting the socket.
|
|
10
|
+
const CONNECT_VERIFY_GRACE_MS = 150;
|
|
11
|
+
/**
|
|
12
|
+
* A bounded async-iterable queue that drops the oldest buffered item once its
|
|
13
|
+
* capacity is exceeded, so a slow consumer never causes unbounded memory growth.
|
|
14
|
+
* An optional `isProtected` predicate can shield items (e.g. video CONFIG frames)
|
|
15
|
+
* from eviction as long as any non-protected item remains to drop instead. An
|
|
16
|
+
* optional `dependencyChain` additionally makes eviction dependency-aware: dropping
|
|
17
|
+
* any chain member cascades to every following chain member up to (not including)
|
|
18
|
+
* the next sync point, and further chain-member pushes are discarded until a fresh
|
|
19
|
+
* sync point arrives - since anything in between is undecodable, buffering it only
|
|
20
|
+
* to have the consumer choke on it later isn't useful.
|
|
21
|
+
*/
|
|
22
|
+
export class BoundedFrameQueue {
|
|
23
|
+
capacity;
|
|
24
|
+
isProtected;
|
|
25
|
+
dependencyChain;
|
|
26
|
+
items = [];
|
|
27
|
+
waiters = [];
|
|
28
|
+
errorWaiters = [];
|
|
29
|
+
ended = false;
|
|
30
|
+
endError;
|
|
31
|
+
awaitingSyncPoint = false;
|
|
32
|
+
constructor(capacity = DEFAULT_QUEUE_CAPACITY, isProtected, dependencyChain) {
|
|
33
|
+
this.capacity = capacity;
|
|
34
|
+
this.isProtected = isProtected;
|
|
35
|
+
this.dependencyChain = dependencyChain;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Enqueues an item. If a consumer is already awaiting the next item, it is
|
|
39
|
+
* delivered directly; otherwise it is buffered, dropping the oldest droppable
|
|
40
|
+
* (non-protected) buffered item first if `capacity` would be exceeded. If a
|
|
41
|
+
* `dependencyChain` was given and a prior eviction broke it without a later sync
|
|
42
|
+
* point already buffered, chain-member items are silently discarded here until a
|
|
43
|
+
* fresh sync point item is pushed.
|
|
44
|
+
*
|
|
45
|
+
* @param item - The item to enqueue
|
|
46
|
+
*/
|
|
47
|
+
push(item) {
|
|
48
|
+
if (this.ended) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const chain = this.dependencyChain;
|
|
52
|
+
if (this.awaitingSyncPoint && chain?.isChainMember(item)) {
|
|
53
|
+
if (!chain.isSyncPoint(item)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this.awaitingSyncPoint = false;
|
|
57
|
+
}
|
|
58
|
+
const waiter = this.waiters.shift();
|
|
59
|
+
if (waiter) {
|
|
60
|
+
this.errorWaiters.shift();
|
|
61
|
+
waiter({ value: item, done: false });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.items.push(item);
|
|
65
|
+
while (this.items.length > this.capacity) {
|
|
66
|
+
this.evictOnce();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Marks the dependency chain (if configured) as broken by something outside this
|
|
71
|
+
* queue's own eviction - e.g. a producer detecting a sequence gap in items it never
|
|
72
|
+
* even offered here. The next chain-member item pushed is discarded unless it is
|
|
73
|
+
* itself a sync point, exactly as if a local eviction had cascaded off the end of
|
|
74
|
+
* the buffer. A no-op if no `dependencyChain` was configured.
|
|
75
|
+
*/
|
|
76
|
+
notifyGap() {
|
|
77
|
+
if (this.dependencyChain) {
|
|
78
|
+
this.awaitingSyncPoint = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Evicts one item to bring the queue back under capacity: the oldest non-protected
|
|
83
|
+
* item, or (if every buffered item is protected, which should not happen in
|
|
84
|
+
* practice) the true oldest one regardless. If the evicted item was a
|
|
85
|
+
* `dependencyChain` member, cascades to drop every following chain member up to
|
|
86
|
+
* the next sync point, setting {@link awaitingSyncPoint} if none is found.
|
|
87
|
+
*/
|
|
88
|
+
evictOnce() {
|
|
89
|
+
const isProtected = this.isProtected;
|
|
90
|
+
const index = isProtected ? this.items.findIndex((item) => !isProtected(item)) : 0;
|
|
91
|
+
if (index === -1) {
|
|
92
|
+
this.items.shift();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const [evicted] = this.items.splice(index, 1);
|
|
96
|
+
const chain = this.dependencyChain;
|
|
97
|
+
if (chain?.isChainMember(evicted) && !this.dropDependentsFrom(index, chain)) {
|
|
98
|
+
this.awaitingSyncPoint = true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Starting at `index`, removes chain-member items up to (not including) the next
|
|
103
|
+
* sync point, skipping over non-chain-member items (e.g. an interleaved audio
|
|
104
|
+
* track) in place without disturbing them.
|
|
105
|
+
*
|
|
106
|
+
* @returns True if a sync point was found (and kept), false if the scan reached
|
|
107
|
+
* the end of the buffer first
|
|
108
|
+
*/
|
|
109
|
+
dropDependentsFrom(index, chain) {
|
|
110
|
+
while (index < this.items.length) {
|
|
111
|
+
const item = this.items[index];
|
|
112
|
+
if (!chain.isChainMember(item)) {
|
|
113
|
+
index++;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (chain.isSyncPoint(item)) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
this.items.splice(index, 1);
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Ends the queue. Any already-buffered items are still delivered first; once
|
|
125
|
+
* drained, the async iterator completes, rejecting with `error` if one is given.
|
|
126
|
+
* A no-op if the queue has already ended.
|
|
127
|
+
*
|
|
128
|
+
* @param error - Optional error to surface to the consumer once buffered items are drained
|
|
129
|
+
*/
|
|
130
|
+
end(error) {
|
|
131
|
+
if (this.ended) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
this.ended = true;
|
|
135
|
+
this.endError = error;
|
|
136
|
+
for (;;) {
|
|
137
|
+
const waiter = this.waiters.shift();
|
|
138
|
+
const errorWaiter = this.errorWaiters.shift();
|
|
139
|
+
if (!waiter || !errorWaiter) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (error) {
|
|
143
|
+
errorWaiter(error);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
waiter({ value: undefined, done: true });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
next() {
|
|
151
|
+
if (this.items.length > 0) {
|
|
152
|
+
return Promise.resolve({ value: this.items.shift(), done: false });
|
|
153
|
+
}
|
|
154
|
+
if (this.ended) {
|
|
155
|
+
return this.endError ? Promise.reject(this.endError) : Promise.resolve({ value: undefined, done: true });
|
|
156
|
+
}
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
this.waiters.push(resolve);
|
|
159
|
+
this.errorWaiters.push(reject);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Async-iterates the queue, yielding items as they are pushed until {@link end} is called.
|
|
164
|
+
*/
|
|
165
|
+
async *[Symbol.asyncIterator]() {
|
|
166
|
+
for (;;) {
|
|
167
|
+
const result = await this.next();
|
|
168
|
+
if (result.done) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
yield result.value;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Validates local-port selection options for a live stream session.
|
|
177
|
+
*
|
|
178
|
+
* @param opts - Options containing an optional localPort and/or localPortRange
|
|
179
|
+
* @throws {TypeError} If both localPort and localPortRange are provided, or either is malformed
|
|
180
|
+
*/
|
|
181
|
+
export function validateLocalPortOpts(opts) {
|
|
182
|
+
const { localPort, localPortRange } = opts;
|
|
183
|
+
if (localPort !== undefined && localPortRange !== undefined) {
|
|
184
|
+
throw new TypeError('Only one of localPort or localPortRange may be provided');
|
|
185
|
+
}
|
|
186
|
+
if (localPort !== undefined && (!Number.isInteger(localPort) || localPort < 1 || localPort > 65535)) {
|
|
187
|
+
throw new TypeError(`localPort must be an integer between 1 and 65535, got ${localPort}`);
|
|
188
|
+
}
|
|
189
|
+
if (localPortRange !== undefined) {
|
|
190
|
+
const [min, max] = localPortRange;
|
|
191
|
+
if (!Number.isInteger(min) || !Number.isInteger(max) || min < 1 || max > 65535 || min > max) {
|
|
192
|
+
throw new TypeError(`localPortRange must be a [min, max] tuple with 1 <= min <= max <= 65535, got [${min}, ${max}]`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Tries to bind a local TCP server to `port` (0 lets the OS pick an ephemeral one),
|
|
198
|
+
* immediately closes it, and returns the bound port - or `null` if `port` is already
|
|
199
|
+
* in use (EADDRINUSE/EACCES), so the caller can try another candidate.
|
|
200
|
+
*/
|
|
201
|
+
async function tryBindPort(port) {
|
|
202
|
+
return new Promise((resolve, reject) => {
|
|
203
|
+
const server = net.createServer();
|
|
204
|
+
server.unref();
|
|
205
|
+
server.once('error', (err) => {
|
|
206
|
+
if (err.code === 'EADDRINUSE' || err.code === 'EACCES') {
|
|
207
|
+
resolve(null);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
reject(err);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
server.listen(port, '127.0.0.1', () => {
|
|
214
|
+
const address = server.address();
|
|
215
|
+
const boundPort = address && typeof address === 'object' ? address.port : null;
|
|
216
|
+
server.close(() => {
|
|
217
|
+
if (boundPort) {
|
|
218
|
+
resolve(boundPort);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
reject(new Error('Unable to determine a free local port'));
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Resolves the local TCP port to use for the `adb forward` bridge: the exact
|
|
229
|
+
* `localPort` if given, the first free port in `localPortRange` if given, or an
|
|
230
|
+
* OS-assigned ephemeral port otherwise.
|
|
231
|
+
*
|
|
232
|
+
* @param opts - Port selection options; see {@link validateLocalPortOpts}
|
|
233
|
+
* @returns The bound, free local TCP port
|
|
234
|
+
* @throws {Error} If the requested port is busy, or no free port exists in the requested range
|
|
235
|
+
*/
|
|
236
|
+
export async function resolveLocalPort(opts) {
|
|
237
|
+
const { localPort, localPortRange } = opts;
|
|
238
|
+
if (localPort !== undefined) {
|
|
239
|
+
const bound = await tryBindPort(localPort);
|
|
240
|
+
if (bound === null) {
|
|
241
|
+
throw new Error(`Local port ${localPort} is already in use`);
|
|
242
|
+
}
|
|
243
|
+
return bound;
|
|
244
|
+
}
|
|
245
|
+
if (localPortRange) {
|
|
246
|
+
const [min, max] = localPortRange;
|
|
247
|
+
for (let port = min; port <= max; port++) {
|
|
248
|
+
const bound = await tryBindPort(port);
|
|
249
|
+
if (bound !== null) {
|
|
250
|
+
return bound;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
throw new Error(`No free local port found in range ${min}-${max}`);
|
|
254
|
+
}
|
|
255
|
+
// No preference given - let the OS assign an ephemeral port.
|
|
256
|
+
const bound = await tryBindPort(0);
|
|
257
|
+
if (bound === null) {
|
|
258
|
+
throw new Error('Unable to determine a free local port');
|
|
259
|
+
}
|
|
260
|
+
return bound;
|
|
261
|
+
}
|
|
262
|
+
async function connectOnce(port) {
|
|
263
|
+
return new Promise((resolve, reject) => {
|
|
264
|
+
const socket = net.connect(port, '127.0.0.1');
|
|
265
|
+
let settled = false;
|
|
266
|
+
let verifyTimer;
|
|
267
|
+
const cleanup = () => {
|
|
268
|
+
socket.removeListener('error', onError);
|
|
269
|
+
socket.removeListener('close', onClose);
|
|
270
|
+
clearTimeout(verifyTimer);
|
|
271
|
+
};
|
|
272
|
+
const onError = (err) => {
|
|
273
|
+
if (settled) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
settled = true;
|
|
277
|
+
cleanup();
|
|
278
|
+
socket.destroy();
|
|
279
|
+
reject(err);
|
|
280
|
+
};
|
|
281
|
+
const onClose = () => {
|
|
282
|
+
if (settled) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
settled = true;
|
|
286
|
+
cleanup();
|
|
287
|
+
reject(new Error('Connection was closed before the on-device stream became ready'));
|
|
288
|
+
};
|
|
289
|
+
socket.once('error', onError);
|
|
290
|
+
socket.once('close', onClose);
|
|
291
|
+
socket.once('connect', () => {
|
|
292
|
+
verifyTimer = setTimeout(() => {
|
|
293
|
+
if (settled) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
settled = true;
|
|
297
|
+
cleanup();
|
|
298
|
+
resolve(socket);
|
|
299
|
+
}, CONNECT_VERIFY_GRACE_MS);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
async function connectWithRetry(port, timeoutMs, intervalMs) {
|
|
304
|
+
const times = Math.max(1, Math.ceil(timeoutMs / intervalMs));
|
|
305
|
+
let socket;
|
|
306
|
+
try {
|
|
307
|
+
socket = await retryInterval(times, intervalMs, connectOnce, port);
|
|
308
|
+
}
|
|
309
|
+
catch (e) {
|
|
310
|
+
throw new Error(`Could not connect to the local streaming socket on port ${port} after ${times} attempts. ` +
|
|
311
|
+
`Last error: ${e instanceof Error ? e.message : e}`, { cause: e });
|
|
312
|
+
}
|
|
313
|
+
if (!socket) {
|
|
314
|
+
throw new Error(`Could not connect to the local streaming socket on port ${port}`);
|
|
315
|
+
}
|
|
316
|
+
return socket;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Bridges the on-device local abstract socket to the host over `adb forward`,
|
|
320
|
+
* parses the streaming wire protocol, and exposes the result as a bounded
|
|
321
|
+
* async-iterable frame queue.
|
|
322
|
+
*/
|
|
323
|
+
export class StreamTransport {
|
|
324
|
+
adb;
|
|
325
|
+
localPort;
|
|
326
|
+
socket;
|
|
327
|
+
// Config frames (e.g. video SPS/PPS) carry decoder state a consumer needs to make sense
|
|
328
|
+
// of everything that follows, so protect them from the bounded queue's drop-oldest policy.
|
|
329
|
+
// Non-config video frames form a decode-dependency chain (a delta frame is undecodable
|
|
330
|
+
// without every frame back to its last keyframe), so eviction there must cascade up to
|
|
331
|
+
// the next keyframe rather than leaving orphaned, undecodable frames behind - audio
|
|
332
|
+
// frames have no such dependency and are left alone by this either way.
|
|
333
|
+
queue = new BoundedFrameQueue(DEFAULT_QUEUE_CAPACITY, (frame) => frame.isConfig, {
|
|
334
|
+
isChainMember: (frame) => frame.track === StreamTrack.Video && !frame.isConfig,
|
|
335
|
+
isSyncPoint: (frame) => frame.isKeyFrame,
|
|
336
|
+
});
|
|
337
|
+
parserState = createParserState();
|
|
338
|
+
closed = false;
|
|
339
|
+
// Tracks the video track's shared sequence counter (CONFIG and data frames both draw
|
|
340
|
+
// from it) so a gap - meaning the on-device FrameQueue dropped a frame under
|
|
341
|
+
// backpressure before we ever saw it - can be detected on receipt, not just when our
|
|
342
|
+
// own queue evicts something locally.
|
|
343
|
+
lastVideoSequence;
|
|
344
|
+
constructor(adb, localPort, socket) {
|
|
345
|
+
this.adb = adb;
|
|
346
|
+
this.localPort = localPort;
|
|
347
|
+
this.socket = socket;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Resolves a local port, forwards it to the on-device local abstract socket via
|
|
351
|
+
* `adb forward`, and connects to it (retrying until the on-device session accepts).
|
|
352
|
+
* On failure, any port forward that was already set up is best-effort removed.
|
|
353
|
+
*
|
|
354
|
+
* @param adb - ADB instance for device communication
|
|
355
|
+
* @param socketName - Name of the on-device local abstract socket to forward
|
|
356
|
+
* @param portOpts - Optional local port selection options
|
|
357
|
+
* @returns A connected StreamTransport instance
|
|
358
|
+
* @throws {Error} If the local port cannot be resolved, or the connection fails
|
|
359
|
+
*/
|
|
360
|
+
static async connect(adb, socketName, portOpts = {}) {
|
|
361
|
+
const localPort = await resolveLocalPort(portOpts);
|
|
362
|
+
await adb.forwardAbstractPort(localPort, socketName);
|
|
363
|
+
let socket;
|
|
364
|
+
try {
|
|
365
|
+
socket = await connectWithRetry(localPort, CONNECT_TIMEOUT_MS, CONNECT_RETRY_INTERVAL_MS);
|
|
366
|
+
}
|
|
367
|
+
catch (e) {
|
|
368
|
+
await adb.removePortForward(localPort).catch(() => { });
|
|
369
|
+
throw e;
|
|
370
|
+
}
|
|
371
|
+
const transport = new StreamTransport(adb, localPort, socket);
|
|
372
|
+
transport.wire();
|
|
373
|
+
return transport;
|
|
374
|
+
}
|
|
375
|
+
wire() {
|
|
376
|
+
this.socket.on('data', (chunk) => {
|
|
377
|
+
try {
|
|
378
|
+
for (const frame of parseFrames(this.parserState, chunk)) {
|
|
379
|
+
if (frame.track === StreamTrack.Video) {
|
|
380
|
+
if (this.lastVideoSequence !== undefined && frame.sequence !== this.lastVideoSequence + 1n) {
|
|
381
|
+
this.queue.notifyGap();
|
|
382
|
+
}
|
|
383
|
+
this.lastVideoSequence = frame.sequence;
|
|
384
|
+
}
|
|
385
|
+
this.queue.push(frame);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
catch (e) {
|
|
389
|
+
this.queue.end(e instanceof Error ? e : new Error(String(e)));
|
|
390
|
+
this.socket.destroy();
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
this.socket.on('close', () => this.queue.end());
|
|
394
|
+
this.socket.on('error', (err) => this.queue.end(err));
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Returns an async generator yielding parsed frames as they arrive on the socket,
|
|
398
|
+
* until the connection closes or errors.
|
|
399
|
+
*/
|
|
400
|
+
frames() {
|
|
401
|
+
return this.queue[Symbol.asyncIterator]();
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Destroys the client socket and best-effort removes the `adb forward` port
|
|
405
|
+
* mapping. Safe to call more than once.
|
|
406
|
+
*/
|
|
407
|
+
async close() {
|
|
408
|
+
if (this.closed) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
this.closed = true;
|
|
412
|
+
this.socket.destroy();
|
|
413
|
+
await this.adb.removePortForward(this.localPort).catch(() => { });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
//# sourceMappingURL=streaming-transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streaming-transport.js","sourceRoot":"","sources":["../../../lib/utils/streaming-transport.ts"],"names":[],"mappings":"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"}
|
package/lib/client.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {setAnimationState} from './commands/animation.js';
|
|
|
6
6
|
import {setBluetoothState, unpairAllBluetoothDevices} from './commands/bluetooth.js';
|
|
7
7
|
import {getClipboard} from './commands/clipboard.js';
|
|
8
8
|
import {setGeoLocation, getGeoLocation, refreshGeoLocationCache} from './commands/geolocation.js';
|
|
9
|
+
import {makeJpegStreamSession} from './commands/jpeg-stream.js';
|
|
9
10
|
import {setDeviceLocale, listSupportedLocales} from './commands/locale.js';
|
|
10
11
|
import {makeMediaProjectionRecorder, adjustMediaProjectionServicePermissions} from './commands/media-projection.js';
|
|
11
12
|
import {scanMedia} from './commands/media.js';
|
|
@@ -13,6 +14,7 @@ import {setDataState, setWifiState} from './commands/network.js';
|
|
|
13
14
|
import {getNotifications, adjustNotificationsPermissions} from './commands/notifications.js';
|
|
14
15
|
import {getSmsList} from './commands/sms.js';
|
|
15
16
|
import {performEditorAction, typeUnicode} from './commands/typing.js';
|
|
17
|
+
import {makeVideoStreamSession} from './commands/video-stream.js';
|
|
16
18
|
import {SETTINGS_HELPER_ID, SETTINGS_HELPER_MAIN_ACTIVITY} from './constants.js';
|
|
17
19
|
import {log, LOG_PREFIX} from './logger.js';
|
|
18
20
|
|
|
@@ -62,6 +64,9 @@ export class SettingsApp {
|
|
|
62
64
|
makeMediaProjectionRecorder = makeMediaProjectionRecorder;
|
|
63
65
|
adjustMediaProjectionServicePermissions = adjustMediaProjectionServicePermissions;
|
|
64
66
|
|
|
67
|
+
makeJpegStreamSession = makeJpegStreamSession;
|
|
68
|
+
makeVideoStreamSession = makeVideoStreamSession;
|
|
69
|
+
|
|
65
70
|
constructor(opts: SettingsAppOpts) {
|
|
66
71
|
this.adb = opts.adb;
|
|
67
72
|
this.log = log;
|
|
@@ -33,6 +33,7 @@ export async function getClipboard(this: SettingsApp): Promise<string> {
|
|
|
33
33
|
`Cannot retrieve the current clipboard content from the device. ` +
|
|
34
34
|
`Make sure the Appium Settings application is up to date. ` +
|
|
35
35
|
`Original error: ${err.message}`,
|
|
36
|
+
{cause: err},
|
|
36
37
|
);
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import {randomUUID} from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import type {ADB} from 'appium-adb';
|
|
4
|
+
import {waitForCondition} from 'asyncbox';
|
|
5
|
+
|
|
6
|
+
import type {SettingsApp} from '../client.js';
|
|
7
|
+
import {
|
|
8
|
+
JPEG_STREAM_ACTION_START,
|
|
9
|
+
JPEG_STREAM_ACTION_STOP,
|
|
10
|
+
JPEG_STREAM_SERVICE_NAME,
|
|
11
|
+
SETTINGS_HELPER_ID,
|
|
12
|
+
STREAMING_ACTIVITY_NAME,
|
|
13
|
+
} from '../constants.js';
|
|
14
|
+
import {StreamTransport, validateLocalPortOpts} from '../utils/index.js';
|
|
15
|
+
import type {JpegFrame, StartJpegStreamOpts} from './types.js';
|
|
16
|
+
|
|
17
|
+
const STREAM_STARTUP_TIMEOUT_MS = 3 * 1000;
|
|
18
|
+
const STREAM_STOP_TIMEOUT_MS = 3 * 1000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates a new instance of the live JPEG frame streaming session.
|
|
22
|
+
* The stream only works since Android API 29+
|
|
23
|
+
*
|
|
24
|
+
* @returns The streaming session instance
|
|
25
|
+
*/
|
|
26
|
+
export function makeJpegStreamSession(this: SettingsApp): JpegStreamSession {
|
|
27
|
+
return new JpegStreamSession(this.adb);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function validateJpegStreamOpts(opts: StartJpegStreamOpts): void {
|
|
31
|
+
const {fps, quality, scale} = opts;
|
|
32
|
+
if (fps !== undefined && (!Number.isInteger(fps) || fps <= 0)) {
|
|
33
|
+
throw new TypeError(`fps must be a positive integer, got ${fps}`);
|
|
34
|
+
}
|
|
35
|
+
if (quality !== undefined && (!Number.isInteger(quality) || quality < 1 || quality > 100)) {
|
|
36
|
+
throw new TypeError(`quality must be an integer between 1 and 100, got ${quality}`);
|
|
37
|
+
}
|
|
38
|
+
if (scale !== undefined && (!Number.isInteger(scale) || scale < 1 || scale > 100)) {
|
|
39
|
+
throw new TypeError(`scale must be an integer between 1 and 100, got ${scale}`);
|
|
40
|
+
}
|
|
41
|
+
validateLocalPortOpts(opts);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Live JPEG frame streaming session for the device screen.
|
|
46
|
+
* This class provides methods to start, stop, and consume a continuous
|
|
47
|
+
* sequence of JPEG-encoded frames over a local socket (API 29+).
|
|
48
|
+
*/
|
|
49
|
+
export class JpegStreamSession {
|
|
50
|
+
private transport: StreamTransport | null = null;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Creates a new JpegStreamSession instance.
|
|
54
|
+
*
|
|
55
|
+
* @param adb - ADB instance for device communication
|
|
56
|
+
*/
|
|
57
|
+
constructor(private readonly adb: ADB) {}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Checks if the JPEG stream is currently running.
|
|
61
|
+
*/
|
|
62
|
+
async isRunning(): Promise<boolean> {
|
|
63
|
+
const stdout = await this.adb.shell(['dumpsys', 'activity', 'services', JPEG_STREAM_SERVICE_NAME]);
|
|
64
|
+
return stdout.includes(JPEG_STREAM_SERVICE_NAME);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Starts the live JPEG frame stream.
|
|
69
|
+
* If a stream is already running, this method returns false without starting a new one.
|
|
70
|
+
*
|
|
71
|
+
* @param opts Streaming options including fps, quality, scale, and the local port to use
|
|
72
|
+
* @returns True if the stream was started successfully, false if already running
|
|
73
|
+
* @throws {Error} If the stream fails to start within the timeout period
|
|
74
|
+
*/
|
|
75
|
+
async start(opts: StartJpegStreamOpts = {}): Promise<boolean> {
|
|
76
|
+
validateJpegStreamOpts(opts);
|
|
77
|
+
if (await this.isRunning()) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const {fps, quality, scale, localPort, localPortRange} = opts;
|
|
82
|
+
// Socket names are validated on-device against ^[a-zA-Z0-9._-]{1,200}$, so this
|
|
83
|
+
// must not contain a "/" - unlike JPEG_STREAM_SERVICE_NAME.
|
|
84
|
+
const socketName = `${SETTINGS_HELPER_ID}.jpegstream.${randomUUID()}`;
|
|
85
|
+
const args = [
|
|
86
|
+
'am',
|
|
87
|
+
'start',
|
|
88
|
+
'-n',
|
|
89
|
+
STREAMING_ACTIVITY_NAME,
|
|
90
|
+
'-a',
|
|
91
|
+
JPEG_STREAM_ACTION_START,
|
|
92
|
+
'--es',
|
|
93
|
+
'socket_name',
|
|
94
|
+
socketName,
|
|
95
|
+
];
|
|
96
|
+
if (fps) {
|
|
97
|
+
args.push('--es', 'fps', `${fps}`);
|
|
98
|
+
}
|
|
99
|
+
if (quality) {
|
|
100
|
+
args.push('--es', 'quality', `${quality}`);
|
|
101
|
+
}
|
|
102
|
+
if (scale) {
|
|
103
|
+
args.push('--es', 'scale', `${scale}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
await this.adb.shell(args);
|
|
107
|
+
try {
|
|
108
|
+
await waitForCondition(async () => await this.isRunning(), {
|
|
109
|
+
waitMs: STREAM_STARTUP_TIMEOUT_MS,
|
|
110
|
+
intervalMs: 300,
|
|
111
|
+
});
|
|
112
|
+
} catch {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`The JPEG stream is not running after ${STREAM_STARTUP_TIMEOUT_MS}ms. ` +
|
|
115
|
+
`Please check the logcat output for more details.`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.transport = await StreamTransport.connect(this.adb, socketName, {localPort, localPortRange});
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Yields JPEG frames as they arrive from the device.
|
|
125
|
+
* `start()` must be called before iterating.
|
|
126
|
+
*/
|
|
127
|
+
async *frames(): AsyncGenerator<JpegFrame> {
|
|
128
|
+
if (!this.transport) {
|
|
129
|
+
throw new Error('The JPEG stream has not been started. Call start() first.');
|
|
130
|
+
}
|
|
131
|
+
for await (const frame of this.transport.frames()) {
|
|
132
|
+
yield {
|
|
133
|
+
data: frame.data,
|
|
134
|
+
sequence: Number(frame.sequence),
|
|
135
|
+
timestampMicros: Number(frame.timestampMicros),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Stops the currently running JPEG stream.
|
|
142
|
+
*
|
|
143
|
+
* @returns True if the stream was stopped successfully, false if it was not running
|
|
144
|
+
* @throws {Error} If the stream fails to stop within the timeout period
|
|
145
|
+
*/
|
|
146
|
+
async stop(): Promise<boolean> {
|
|
147
|
+
try {
|
|
148
|
+
if (!(await this.isRunning())) {
|
|
149
|
+
// The on-device service may have already stopped itself (e.g. the client
|
|
150
|
+
// disconnected), leaving a local transport/adb-forward that still needs closing.
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await this.adb.shell(['am', 'start', '-n', STREAMING_ACTIVITY_NAME, '-a', JPEG_STREAM_ACTION_STOP]);
|
|
155
|
+
try {
|
|
156
|
+
await waitForCondition(async () => !(await this.isRunning()), {
|
|
157
|
+
waitMs: STREAM_STOP_TIMEOUT_MS,
|
|
158
|
+
intervalMs: 300,
|
|
159
|
+
});
|
|
160
|
+
} catch {
|
|
161
|
+
throw new Error(`The attempt to stop the current JPEG stream timed out after ${STREAM_STOP_TIMEOUT_MS}ms`);
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
} finally {
|
|
165
|
+
if (this.transport) {
|
|
166
|
+
await this.transport.close();
|
|
167
|
+
this.transport = null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -48,16 +48,12 @@ const RECORDINGS_ROOT = `/storage/emulated/0/Android/data/${SETTINGS_HELPER_ID}/
|
|
|
48
48
|
* using Android's MediaProjection API (API 29+).
|
|
49
49
|
*/
|
|
50
50
|
export class MediaProjectionRecorder {
|
|
51
|
-
private readonly adb: ADB;
|
|
52
|
-
|
|
53
51
|
/**
|
|
54
52
|
* Creates a new MediaProjectionRecorder instance.
|
|
55
53
|
*
|
|
56
54
|
* @param adb - ADB instance for device communication
|
|
57
55
|
*/
|
|
58
|
-
constructor(adb: ADB) {
|
|
59
|
-
this.adb = adb;
|
|
60
|
-
}
|
|
56
|
+
constructor(private readonly adb: ADB) {}
|
|
61
57
|
|
|
62
58
|
/**
|
|
63
59
|
* Checks if the recording is currently running.
|
package/lib/commands/media.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {LOG_PREFIX} from '../logger.js';
|
|
|
13
13
|
export async function scanMedia(this: SettingsApp, destination: string): Promise<void> {
|
|
14
14
|
this.log.debug(LOG_PREFIX, `Scanning '${destination}' for media files`);
|
|
15
15
|
await this.checkBroadcast(
|
|
16
|
-
['-n', MEDIA_SCAN_RECEIVER, '-a', MEDIA_SCAN_ACTION, '--es', 'path', destination],
|
|
16
|
+
['-n', MEDIA_SCAN_RECEIVER, '-a', MEDIA_SCAN_ACTION, '--es', 'path', `'${destination.replace(/'/g, `'"'"'`)}'`],
|
|
17
17
|
'scan media',
|
|
18
18
|
);
|
|
19
19
|
}
|
package/lib/commands/network.ts
CHANGED
|
@@ -51,7 +51,7 @@ export async function setDataState(this: SettingsApp, on: boolean, isEmulator =
|
|
|
51
51
|
try {
|
|
52
52
|
await this.adb.shell(['cmd', 'phone', 'data', on ? 'enable' : 'disable']);
|
|
53
53
|
} catch (e: any) {
|
|
54
|
-
throw new Error(`Cannot change the data state. Original error: ${e.stderr || e.message}
|
|
54
|
+
throw new Error(`Cannot change the data state. Original error: ${e.stderr || e.message}`, {cause: e});
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
}
|