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,450 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
|
|
3
|
+
import type {ADB} from 'appium-adb';
|
|
4
|
+
import {retryInterval} from 'asyncbox';
|
|
5
|
+
|
|
6
|
+
import type {LocalPortOpts} from '../commands/types.js';
|
|
7
|
+
import {createParserState, parseFrames, StreamTrack, type StreamFrame} from './streaming-protocol.js';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_QUEUE_CAPACITY = 60;
|
|
10
|
+
const CONNECT_TIMEOUT_MS = 5000;
|
|
11
|
+
const CONNECT_RETRY_INTERVAL_MS = 200;
|
|
12
|
+
// adb forward's host-side TCP listener accepts a connection immediately, independent of
|
|
13
|
+
// whether the on-device local socket is bound yet; if it isn't, adb tears the connection
|
|
14
|
+
// down right away with no data. Wait this long past 'connect' before trusting the socket.
|
|
15
|
+
const CONNECT_VERIFY_GRACE_MS = 150;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Describes a decode-dependency chain within the queued items (e.g. H.264/HEVC video
|
|
19
|
+
* frames, where a delta frame is undecodable without every frame back to its last
|
|
20
|
+
* keyframe). Items outside the chain (e.g. an interleaved audio track) are unaffected.
|
|
21
|
+
*/
|
|
22
|
+
export interface DependencyChain<T> {
|
|
23
|
+
/** True for items that participate in the chain at all (e.g. non-CONFIG video frames). */
|
|
24
|
+
isChainMember: (item: T) => boolean;
|
|
25
|
+
/** True for a chain member that doesn't depend on earlier ones (e.g. a keyframe). */
|
|
26
|
+
isSyncPoint: (item: T) => boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A bounded async-iterable queue that drops the oldest buffered item once its
|
|
31
|
+
* capacity is exceeded, so a slow consumer never causes unbounded memory growth.
|
|
32
|
+
* An optional `isProtected` predicate can shield items (e.g. video CONFIG frames)
|
|
33
|
+
* from eviction as long as any non-protected item remains to drop instead. An
|
|
34
|
+
* optional `dependencyChain` additionally makes eviction dependency-aware: dropping
|
|
35
|
+
* any chain member cascades to every following chain member up to (not including)
|
|
36
|
+
* the next sync point, and further chain-member pushes are discarded until a fresh
|
|
37
|
+
* sync point arrives - since anything in between is undecodable, buffering it only
|
|
38
|
+
* to have the consumer choke on it later isn't useful.
|
|
39
|
+
*/
|
|
40
|
+
export class BoundedFrameQueue<T> {
|
|
41
|
+
private readonly items: T[] = [];
|
|
42
|
+
private readonly waiters: Array<(result: IteratorResult<T>) => void> = [];
|
|
43
|
+
private readonly errorWaiters: Array<(err: Error) => void> = [];
|
|
44
|
+
private ended = false;
|
|
45
|
+
private endError: Error | undefined;
|
|
46
|
+
private awaitingSyncPoint = false;
|
|
47
|
+
|
|
48
|
+
constructor(
|
|
49
|
+
private readonly capacity: number = DEFAULT_QUEUE_CAPACITY,
|
|
50
|
+
private readonly isProtected?: (item: T) => boolean,
|
|
51
|
+
private readonly dependencyChain?: DependencyChain<T>,
|
|
52
|
+
) {}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Enqueues an item. If a consumer is already awaiting the next item, it is
|
|
56
|
+
* delivered directly; otherwise it is buffered, dropping the oldest droppable
|
|
57
|
+
* (non-protected) buffered item first if `capacity` would be exceeded. If a
|
|
58
|
+
* `dependencyChain` was given and a prior eviction broke it without a later sync
|
|
59
|
+
* point already buffered, chain-member items are silently discarded here until a
|
|
60
|
+
* fresh sync point item is pushed.
|
|
61
|
+
*
|
|
62
|
+
* @param item - The item to enqueue
|
|
63
|
+
*/
|
|
64
|
+
push(item: T): void {
|
|
65
|
+
if (this.ended) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const chain = this.dependencyChain;
|
|
69
|
+
if (this.awaitingSyncPoint && chain?.isChainMember(item)) {
|
|
70
|
+
if (!chain.isSyncPoint(item)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.awaitingSyncPoint = false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const waiter = this.waiters.shift();
|
|
77
|
+
if (waiter) {
|
|
78
|
+
this.errorWaiters.shift();
|
|
79
|
+
waiter({value: item, done: false});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
this.items.push(item);
|
|
83
|
+
while (this.items.length > this.capacity) {
|
|
84
|
+
this.evictOnce();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Marks the dependency chain (if configured) as broken by something outside this
|
|
90
|
+
* queue's own eviction - e.g. a producer detecting a sequence gap in items it never
|
|
91
|
+
* even offered here. The next chain-member item pushed is discarded unless it is
|
|
92
|
+
* itself a sync point, exactly as if a local eviction had cascaded off the end of
|
|
93
|
+
* the buffer. A no-op if no `dependencyChain` was configured.
|
|
94
|
+
*/
|
|
95
|
+
notifyGap(): void {
|
|
96
|
+
if (this.dependencyChain) {
|
|
97
|
+
this.awaitingSyncPoint = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Evicts one item to bring the queue back under capacity: the oldest non-protected
|
|
103
|
+
* item, or (if every buffered item is protected, which should not happen in
|
|
104
|
+
* practice) the true oldest one regardless. If the evicted item was a
|
|
105
|
+
* `dependencyChain` member, cascades to drop every following chain member up to
|
|
106
|
+
* the next sync point, setting {@link awaitingSyncPoint} if none is found.
|
|
107
|
+
*/
|
|
108
|
+
private evictOnce(): void {
|
|
109
|
+
const isProtected = this.isProtected;
|
|
110
|
+
const index = isProtected ? this.items.findIndex((item) => !isProtected(item)) : 0;
|
|
111
|
+
if (index === -1) {
|
|
112
|
+
this.items.shift();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const [evicted] = this.items.splice(index, 1);
|
|
116
|
+
const chain = this.dependencyChain;
|
|
117
|
+
if (chain?.isChainMember(evicted) && !this.dropDependentsFrom(index, chain)) {
|
|
118
|
+
this.awaitingSyncPoint = true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Starting at `index`, removes chain-member items up to (not including) the next
|
|
124
|
+
* sync point, skipping over non-chain-member items (e.g. an interleaved audio
|
|
125
|
+
* track) in place without disturbing them.
|
|
126
|
+
*
|
|
127
|
+
* @returns True if a sync point was found (and kept), false if the scan reached
|
|
128
|
+
* the end of the buffer first
|
|
129
|
+
*/
|
|
130
|
+
private dropDependentsFrom(index: number, chain: DependencyChain<T>): boolean {
|
|
131
|
+
while (index < this.items.length) {
|
|
132
|
+
const item = this.items[index];
|
|
133
|
+
if (!chain.isChainMember(item)) {
|
|
134
|
+
index++;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (chain.isSyncPoint(item)) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
this.items.splice(index, 1);
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Ends the queue. Any already-buffered items are still delivered first; once
|
|
147
|
+
* drained, the async iterator completes, rejecting with `error` if one is given.
|
|
148
|
+
* A no-op if the queue has already ended.
|
|
149
|
+
*
|
|
150
|
+
* @param error - Optional error to surface to the consumer once buffered items are drained
|
|
151
|
+
*/
|
|
152
|
+
end(error?: Error): void {
|
|
153
|
+
if (this.ended) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
this.ended = true;
|
|
157
|
+
this.endError = error;
|
|
158
|
+
for (;;) {
|
|
159
|
+
const waiter = this.waiters.shift();
|
|
160
|
+
const errorWaiter = this.errorWaiters.shift();
|
|
161
|
+
if (!waiter || !errorWaiter) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (error) {
|
|
165
|
+
errorWaiter(error);
|
|
166
|
+
} else {
|
|
167
|
+
waiter({value: undefined, done: true});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private next(): Promise<IteratorResult<T>> {
|
|
173
|
+
if (this.items.length > 0) {
|
|
174
|
+
return Promise.resolve({value: this.items.shift() as T, done: false});
|
|
175
|
+
}
|
|
176
|
+
if (this.ended) {
|
|
177
|
+
return this.endError ? Promise.reject(this.endError) : Promise.resolve({value: undefined, done: true});
|
|
178
|
+
}
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
this.waiters.push(resolve);
|
|
181
|
+
this.errorWaiters.push(reject);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Async-iterates the queue, yielding items as they are pushed until {@link end} is called.
|
|
187
|
+
*/
|
|
188
|
+
async *[Symbol.asyncIterator](): AsyncGenerator<T> {
|
|
189
|
+
for (;;) {
|
|
190
|
+
const result = await this.next();
|
|
191
|
+
if (result.done) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
yield result.value;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Validates local-port selection options for a live stream session.
|
|
201
|
+
*
|
|
202
|
+
* @param opts - Options containing an optional localPort and/or localPortRange
|
|
203
|
+
* @throws {TypeError} If both localPort and localPortRange are provided, or either is malformed
|
|
204
|
+
*/
|
|
205
|
+
export function validateLocalPortOpts(opts: LocalPortOpts): void {
|
|
206
|
+
const {localPort, localPortRange} = opts;
|
|
207
|
+
if (localPort !== undefined && localPortRange !== undefined) {
|
|
208
|
+
throw new TypeError('Only one of localPort or localPortRange may be provided');
|
|
209
|
+
}
|
|
210
|
+
if (localPort !== undefined && (!Number.isInteger(localPort) || localPort < 1 || localPort > 65535)) {
|
|
211
|
+
throw new TypeError(`localPort must be an integer between 1 and 65535, got ${localPort}`);
|
|
212
|
+
}
|
|
213
|
+
if (localPortRange !== undefined) {
|
|
214
|
+
const [min, max] = localPortRange;
|
|
215
|
+
if (!Number.isInteger(min) || !Number.isInteger(max) || min < 1 || max > 65535 || min > max) {
|
|
216
|
+
throw new TypeError(
|
|
217
|
+
`localPortRange must be a [min, max] tuple with 1 <= min <= max <= 65535, got [${min}, ${max}]`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Tries to bind a local TCP server to `port` (0 lets the OS pick an ephemeral one),
|
|
225
|
+
* immediately closes it, and returns the bound port - or `null` if `port` is already
|
|
226
|
+
* in use (EADDRINUSE/EACCES), so the caller can try another candidate.
|
|
227
|
+
*/
|
|
228
|
+
async function tryBindPort(port: number): Promise<number | null> {
|
|
229
|
+
return new Promise((resolve, reject) => {
|
|
230
|
+
const server = net.createServer();
|
|
231
|
+
server.unref();
|
|
232
|
+
server.once('error', (err: NodeJS.ErrnoException) => {
|
|
233
|
+
if (err.code === 'EADDRINUSE' || err.code === 'EACCES') {
|
|
234
|
+
resolve(null);
|
|
235
|
+
} else {
|
|
236
|
+
reject(err);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
server.listen(port, '127.0.0.1', () => {
|
|
240
|
+
const address = server.address();
|
|
241
|
+
const boundPort = address && typeof address === 'object' ? address.port : null;
|
|
242
|
+
server.close(() => {
|
|
243
|
+
if (boundPort) {
|
|
244
|
+
resolve(boundPort);
|
|
245
|
+
} else {
|
|
246
|
+
reject(new Error('Unable to determine a free local port'));
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Resolves the local TCP port to use for the `adb forward` bridge: the exact
|
|
255
|
+
* `localPort` if given, the first free port in `localPortRange` if given, or an
|
|
256
|
+
* OS-assigned ephemeral port otherwise.
|
|
257
|
+
*
|
|
258
|
+
* @param opts - Port selection options; see {@link validateLocalPortOpts}
|
|
259
|
+
* @returns The bound, free local TCP port
|
|
260
|
+
* @throws {Error} If the requested port is busy, or no free port exists in the requested range
|
|
261
|
+
*/
|
|
262
|
+
export async function resolveLocalPort(opts: LocalPortOpts): Promise<number> {
|
|
263
|
+
const {localPort, localPortRange} = opts;
|
|
264
|
+
|
|
265
|
+
if (localPort !== undefined) {
|
|
266
|
+
const bound = await tryBindPort(localPort);
|
|
267
|
+
if (bound === null) {
|
|
268
|
+
throw new Error(`Local port ${localPort} is already in use`);
|
|
269
|
+
}
|
|
270
|
+
return bound;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (localPortRange) {
|
|
274
|
+
const [min, max] = localPortRange;
|
|
275
|
+
for (let port = min; port <= max; port++) {
|
|
276
|
+
const bound = await tryBindPort(port);
|
|
277
|
+
if (bound !== null) {
|
|
278
|
+
return bound;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
throw new Error(`No free local port found in range ${min}-${max}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// No preference given - let the OS assign an ephemeral port.
|
|
285
|
+
const bound = await tryBindPort(0);
|
|
286
|
+
if (bound === null) {
|
|
287
|
+
throw new Error('Unable to determine a free local port');
|
|
288
|
+
}
|
|
289
|
+
return bound;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function connectOnce(port: number): Promise<net.Socket> {
|
|
293
|
+
return new Promise<net.Socket>((resolve, reject) => {
|
|
294
|
+
const socket = net.connect(port, '127.0.0.1');
|
|
295
|
+
let settled = false;
|
|
296
|
+
let verifyTimer: NodeJS.Timeout | undefined;
|
|
297
|
+
|
|
298
|
+
const cleanup = () => {
|
|
299
|
+
socket.removeListener('error', onError);
|
|
300
|
+
socket.removeListener('close', onClose);
|
|
301
|
+
clearTimeout(verifyTimer);
|
|
302
|
+
};
|
|
303
|
+
const onError = (err: Error) => {
|
|
304
|
+
if (settled) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
settled = true;
|
|
308
|
+
cleanup();
|
|
309
|
+
socket.destroy();
|
|
310
|
+
reject(err);
|
|
311
|
+
};
|
|
312
|
+
const onClose = () => {
|
|
313
|
+
if (settled) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
settled = true;
|
|
317
|
+
cleanup();
|
|
318
|
+
reject(new Error('Connection was closed before the on-device stream became ready'));
|
|
319
|
+
};
|
|
320
|
+
socket.once('error', onError);
|
|
321
|
+
socket.once('close', onClose);
|
|
322
|
+
socket.once('connect', () => {
|
|
323
|
+
verifyTimer = setTimeout(() => {
|
|
324
|
+
if (settled) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
settled = true;
|
|
328
|
+
cleanup();
|
|
329
|
+
resolve(socket);
|
|
330
|
+
}, CONNECT_VERIFY_GRACE_MS);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function connectWithRetry(port: number, timeoutMs: number, intervalMs: number): Promise<net.Socket> {
|
|
336
|
+
const times = Math.max(1, Math.ceil(timeoutMs / intervalMs));
|
|
337
|
+
let socket: net.Socket | null;
|
|
338
|
+
try {
|
|
339
|
+
socket = await retryInterval(times, intervalMs, connectOnce, port);
|
|
340
|
+
} catch (e) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
`Could not connect to the local streaming socket on port ${port} after ${times} attempts. ` +
|
|
343
|
+
`Last error: ${e instanceof Error ? e.message : e}`,
|
|
344
|
+
{cause: e},
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
if (!socket) {
|
|
348
|
+
throw new Error(`Could not connect to the local streaming socket on port ${port}`);
|
|
349
|
+
}
|
|
350
|
+
return socket;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Bridges the on-device local abstract socket to the host over `adb forward`,
|
|
355
|
+
* parses the streaming wire protocol, and exposes the result as a bounded
|
|
356
|
+
* async-iterable frame queue.
|
|
357
|
+
*/
|
|
358
|
+
export class StreamTransport {
|
|
359
|
+
// Config frames (e.g. video SPS/PPS) carry decoder state a consumer needs to make sense
|
|
360
|
+
// of everything that follows, so protect them from the bounded queue's drop-oldest policy.
|
|
361
|
+
// Non-config video frames form a decode-dependency chain (a delta frame is undecodable
|
|
362
|
+
// without every frame back to its last keyframe), so eviction there must cascade up to
|
|
363
|
+
// the next keyframe rather than leaving orphaned, undecodable frames behind - audio
|
|
364
|
+
// frames have no such dependency and are left alone by this either way.
|
|
365
|
+
private readonly queue = new BoundedFrameQueue<StreamFrame>(DEFAULT_QUEUE_CAPACITY, (frame) => frame.isConfig, {
|
|
366
|
+
isChainMember: (frame) => frame.track === StreamTrack.Video && !frame.isConfig,
|
|
367
|
+
isSyncPoint: (frame) => frame.isKeyFrame,
|
|
368
|
+
});
|
|
369
|
+
private readonly parserState = createParserState();
|
|
370
|
+
private closed = false;
|
|
371
|
+
// Tracks the video track's shared sequence counter (CONFIG and data frames both draw
|
|
372
|
+
// from it) so a gap - meaning the on-device FrameQueue dropped a frame under
|
|
373
|
+
// backpressure before we ever saw it - can be detected on receipt, not just when our
|
|
374
|
+
// own queue evicts something locally.
|
|
375
|
+
private lastVideoSequence: bigint | undefined;
|
|
376
|
+
|
|
377
|
+
private constructor(
|
|
378
|
+
private readonly adb: ADB,
|
|
379
|
+
private readonly localPort: number,
|
|
380
|
+
private readonly socket: net.Socket,
|
|
381
|
+
) {}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Resolves a local port, forwards it to the on-device local abstract socket via
|
|
385
|
+
* `adb forward`, and connects to it (retrying until the on-device session accepts).
|
|
386
|
+
* On failure, any port forward that was already set up is best-effort removed.
|
|
387
|
+
*
|
|
388
|
+
* @param adb - ADB instance for device communication
|
|
389
|
+
* @param socketName - Name of the on-device local abstract socket to forward
|
|
390
|
+
* @param portOpts - Optional local port selection options
|
|
391
|
+
* @returns A connected StreamTransport instance
|
|
392
|
+
* @throws {Error} If the local port cannot be resolved, or the connection fails
|
|
393
|
+
*/
|
|
394
|
+
static async connect(adb: ADB, socketName: string, portOpts: LocalPortOpts = {}): Promise<StreamTransport> {
|
|
395
|
+
const localPort = await resolveLocalPort(portOpts);
|
|
396
|
+
await adb.forwardAbstractPort(localPort, socketName);
|
|
397
|
+
let socket: net.Socket;
|
|
398
|
+
try {
|
|
399
|
+
socket = await connectWithRetry(localPort, CONNECT_TIMEOUT_MS, CONNECT_RETRY_INTERVAL_MS);
|
|
400
|
+
} catch (e) {
|
|
401
|
+
await adb.removePortForward(localPort).catch(() => {});
|
|
402
|
+
throw e;
|
|
403
|
+
}
|
|
404
|
+
const transport = new StreamTransport(adb, localPort, socket);
|
|
405
|
+
transport.wire();
|
|
406
|
+
return transport;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private wire(): void {
|
|
410
|
+
this.socket.on('data', (chunk: Buffer) => {
|
|
411
|
+
try {
|
|
412
|
+
for (const frame of parseFrames(this.parserState, chunk)) {
|
|
413
|
+
if (frame.track === StreamTrack.Video) {
|
|
414
|
+
if (this.lastVideoSequence !== undefined && frame.sequence !== this.lastVideoSequence + 1n) {
|
|
415
|
+
this.queue.notifyGap();
|
|
416
|
+
}
|
|
417
|
+
this.lastVideoSequence = frame.sequence;
|
|
418
|
+
}
|
|
419
|
+
this.queue.push(frame);
|
|
420
|
+
}
|
|
421
|
+
} catch (e) {
|
|
422
|
+
this.queue.end(e instanceof Error ? e : new Error(String(e)));
|
|
423
|
+
this.socket.destroy();
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
this.socket.on('close', () => this.queue.end());
|
|
427
|
+
this.socket.on('error', (err) => this.queue.end(err));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Returns an async generator yielding parsed frames as they arrive on the socket,
|
|
432
|
+
* until the connection closes or errors.
|
|
433
|
+
*/
|
|
434
|
+
frames(): AsyncGenerator<StreamFrame> {
|
|
435
|
+
return this.queue[Symbol.asyncIterator]();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Destroys the client socket and best-effort removes the `adb forward` port
|
|
440
|
+
* mapping. Safe to call more than once.
|
|
441
|
+
*/
|
|
442
|
+
async close(): Promise<void> {
|
|
443
|
+
if (this.closed) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
this.closed = true;
|
|
447
|
+
this.socket.destroy();
|
|
448
|
+
await this.adb.removePortForward(this.localPort).catch(() => {});
|
|
449
|
+
}
|
|
450
|
+
}
|
package/package.json
CHANGED
package/build/lib/utils.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../lib/utils.ts"],"names":[],"mappings":"AAMA;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C"}
|
package/build/lib/utils.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../lib/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAC,aAAa,EAAC,MAAM,UAAU,CAAC;AAEvC,MAAM,WAAW,GAAG,oBAAoB,CAAC;AAEzC;;;;GAIG;AACH,MAAM,UAAU,kBAAkB;IAChC,OAAO,IAAI,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,MAAM,EAAE,wBAAwB,CAAC,CAAC;AAC7E,CAAC;AAED;;;;;GAKG;AACH,SAAS,iBAAiB;IACxB,IAAI,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9D,IAAI,UAAU,GAAG,KAAK,CAAC;IACvB,OAAO,CAAC,UAAU,EAAE,CAAC;QACnB,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;QAC3D,IAAI,CAAC;YACH,IAAI,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;gBAC1G,OAAO,UAAU,CAAC;YACpB,CAAC;QACH,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;QACV,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACtC,UAAU,GAAG,UAAU,CAAC,MAAM,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC;IACpE,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,+BAA+B,WAAW,iBAAiB,CAAC,CAAC;AAC/E,CAAC"}
|
|
File without changes
|