appium-android-driver 5.14.7 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/build/lib/commands/actions.d.ts +6 -224
- package/build/lib/commands/actions.d.ts.map +1 -1
- package/build/lib/commands/actions.js +306 -405
- package/build/lib/commands/actions.js.map +1 -1
- package/build/lib/commands/alert.d.ts +7 -9
- package/build/lib/commands/alert.d.ts.map +1 -1
- package/build/lib/commands/alert.js +24 -18
- package/build/lib/commands/alert.js.map +1 -1
- package/build/lib/commands/app-management.d.ts +7 -313
- package/build/lib/commands/app-management.d.ts.map +1 -1
- package/build/lib/commands/app-management.js +135 -293
- package/build/lib/commands/app-management.js.map +1 -1
- package/build/lib/commands/context.d.ts +8 -92
- package/build/lib/commands/context.d.ts.map +1 -1
- package/build/lib/commands/context.js +381 -439
- package/build/lib/commands/context.js.map +1 -1
- package/build/lib/commands/element.d.ts +8 -35
- package/build/lib/commands/element.d.ts.map +1 -1
- package/build/lib/commands/element.js +153 -136
- package/build/lib/commands/element.js.map +1 -1
- package/build/lib/commands/emu-console.d.ts +6 -48
- package/build/lib/commands/emu-console.d.ts.map +1 -1
- package/build/lib/commands/emu-console.js +19 -34
- package/build/lib/commands/emu-console.js.map +1 -1
- package/build/lib/commands/execute.d.ts +6 -5
- package/build/lib/commands/execute.d.ts.map +1 -1
- package/build/lib/commands/execute.js +77 -66
- package/build/lib/commands/execute.js.map +1 -1
- package/build/lib/commands/file-actions.d.ts +7 -128
- package/build/lib/commands/file-actions.d.ts.map +1 -1
- package/build/lib/commands/file-actions.js +183 -219
- package/build/lib/commands/file-actions.js.map +1 -1
- package/build/lib/commands/find.d.ts +8 -12
- package/build/lib/commands/find.d.ts.map +1 -1
- package/build/lib/commands/find.js +19 -23
- package/build/lib/commands/find.js.map +1 -1
- package/build/lib/commands/general.d.ts +9 -132
- package/build/lib/commands/general.d.ts.map +1 -1
- package/build/lib/commands/general.js +281 -312
- package/build/lib/commands/general.js.map +1 -1
- package/build/lib/commands/ime.d.ts +7 -10
- package/build/lib/commands/ime.d.ts.map +1 -1
- package/build/lib/commands/ime.js +47 -35
- package/build/lib/commands/ime.js.map +1 -1
- package/build/lib/commands/index.d.ts +27 -2
- package/build/lib/commands/index.d.ts.map +1 -1
- package/build/lib/commands/index.js +41 -19
- package/build/lib/commands/index.js.map +1 -1
- package/build/lib/commands/intent.d.ts +7 -417
- package/build/lib/commands/intent.d.ts.map +1 -1
- package/build/lib/commands/intent.js +104 -216
- package/build/lib/commands/intent.js.map +1 -1
- package/build/lib/commands/keyboard.d.ts +6 -5
- package/build/lib/commands/keyboard.d.ts.map +1 -1
- package/build/lib/commands/keyboard.js +16 -8
- package/build/lib/commands/keyboard.js.map +1 -1
- package/build/lib/commands/log.d.ts +7 -44
- package/build/lib/commands/log.d.ts.map +1 -1
- package/build/lib/commands/log.js +146 -108
- package/build/lib/commands/log.js.map +1 -1
- package/build/lib/commands/media-projection.d.ts +7 -143
- package/build/lib/commands/media-projection.d.ts.map +1 -1
- package/build/lib/commands/media-projection.js +113 -140
- package/build/lib/commands/media-projection.js.map +1 -1
- package/build/lib/commands/mixins.d.ts +740 -0
- package/build/lib/commands/mixins.d.ts.map +1 -0
- package/build/lib/commands/mixins.js +19 -0
- package/build/lib/commands/mixins.js.map +1 -0
- package/build/lib/commands/network.d.ts +7 -138
- package/build/lib/commands/network.d.ts.map +1 -1
- package/build/lib/commands/network.js +212 -254
- package/build/lib/commands/network.js.map +1 -1
- package/build/lib/commands/performance.d.ts +24 -70
- package/build/lib/commands/performance.d.ts.map +1 -1
- package/build/lib/commands/performance.js +144 -100
- package/build/lib/commands/performance.js.map +1 -1
- package/build/lib/commands/permissions.d.ts +8 -92
- package/build/lib/commands/permissions.d.ts.map +1 -1
- package/build/lib/commands/permissions.js +75 -87
- package/build/lib/commands/permissions.js.map +1 -1
- package/build/lib/commands/recordscreen.d.ts +7 -193
- package/build/lib/commands/recordscreen.d.ts.map +1 -1
- package/build/lib/commands/recordscreen.js +151 -182
- package/build/lib/commands/recordscreen.js.map +1 -1
- package/build/lib/commands/shell.d.ts +7 -7
- package/build/lib/commands/shell.d.ts.map +1 -1
- package/build/lib/commands/shell.js +40 -33
- package/build/lib/commands/shell.js.map +1 -1
- package/build/lib/commands/streamscreen.d.ts +9 -103
- package/build/lib/commands/streamscreen.d.ts.map +1 -1
- package/build/lib/commands/streamscreen.js +261 -218
- package/build/lib/commands/streamscreen.js.map +1 -1
- package/build/lib/commands/system-bars.d.ts +22 -90
- package/build/lib/commands/system-bars.d.ts.map +1 -1
- package/build/lib/commands/system-bars.js +76 -74
- package/build/lib/commands/system-bars.js.map +1 -1
- package/build/lib/commands/touch.d.ts +10 -29
- package/build/lib/commands/touch.d.ts.map +1 -1
- package/build/lib/commands/touch.js +301 -285
- package/build/lib/commands/touch.js.map +1 -1
- package/build/lib/commands/types.d.ts +978 -0
- package/build/lib/commands/types.d.ts.map +1 -0
- package/build/lib/commands/types.js +3 -0
- package/build/lib/commands/types.js.map +1 -0
- package/build/lib/constraints.d.ts +291 -0
- package/build/lib/constraints.d.ts.map +1 -0
- package/build/lib/{desired-caps.js → constraints.js} +103 -102
- package/build/lib/constraints.js.map +1 -0
- package/build/lib/driver.d.ts +68 -37
- package/build/lib/driver.d.ts.map +1 -1
- package/build/lib/driver.js +123 -80
- package/build/lib/driver.js.map +1 -1
- package/build/lib/helpers/android.d.ts +164 -0
- package/build/lib/helpers/android.d.ts.map +1 -0
- package/build/lib/helpers/android.js +819 -0
- package/build/lib/helpers/android.js.map +1 -0
- package/build/lib/helpers/index.d.ts +7 -0
- package/build/lib/helpers/index.d.ts.map +1 -0
- package/build/lib/helpers/index.js +29 -0
- package/build/lib/helpers/index.js.map +1 -0
- package/build/lib/helpers/types.d.ts +121 -0
- package/build/lib/helpers/types.d.ts.map +1 -0
- package/build/lib/helpers/types.js +3 -0
- package/build/lib/helpers/types.js.map +1 -0
- package/build/lib/helpers/unlock.d.ts +32 -0
- package/build/lib/helpers/unlock.d.ts.map +1 -0
- package/build/lib/helpers/unlock.js +273 -0
- package/build/lib/helpers/unlock.js.map +1 -0
- package/build/lib/helpers/webview.d.ts +74 -0
- package/build/lib/helpers/webview.d.ts.map +1 -0
- package/build/lib/helpers/webview.js +421 -0
- package/build/lib/helpers/webview.js.map +1 -0
- package/build/lib/index.d.ts +9 -0
- package/build/lib/index.d.ts.map +1 -0
- package/build/lib/index.js +37 -0
- package/build/lib/index.js.map +1 -0
- package/build/lib/method-map.d.ts +0 -8
- package/build/lib/method-map.d.ts.map +1 -1
- package/build/lib/method-map.js +63 -74
- package/build/lib/method-map.js.map +1 -1
- package/build/lib/stubs.d.ts +0 -1
- package/build/lib/stubs.d.ts.map +1 -1
- package/build/lib/stubs.js +1 -0
- package/build/lib/stubs.js.map +1 -1
- package/build/lib/utils.d.ts +1 -1
- package/build/lib/utils.d.ts.map +1 -1
- package/lib/commands/actions.js +351 -464
- package/lib/commands/alert.js +27 -17
- package/lib/commands/app-management.js +156 -314
- package/lib/commands/context.js +457 -441
- package/lib/commands/element.js +201 -157
- package/lib/commands/emu-console.js +25 -45
- package/lib/commands/execute.js +106 -90
- package/lib/commands/file-actions.js +222 -240
- package/lib/commands/find.ts +103 -0
- package/lib/commands/general.js +327 -339
- package/lib/commands/ime.js +50 -34
- package/lib/commands/{index.js → index.ts} +20 -24
- package/lib/commands/intent.js +108 -249
- package/lib/commands/keyboard.js +20 -8
- package/lib/commands/log.js +172 -116
- package/lib/commands/media-projection.js +134 -161
- package/lib/commands/mixins.ts +966 -0
- package/lib/commands/network.js +252 -281
- package/lib/commands/performance.js +203 -132
- package/lib/commands/permissions.js +108 -109
- package/lib/commands/recordscreen.js +212 -209
- package/lib/commands/shell.js +51 -40
- package/lib/commands/streamscreen.js +355 -289
- package/lib/commands/system-bars.js +92 -83
- package/lib/commands/touch.js +357 -294
- package/lib/commands/types.ts +1097 -0
- package/lib/{desired-caps.js → constraints.ts} +106 -103
- package/lib/{driver.js → driver.ts} +278 -132
- package/lib/helpers/android.ts +1143 -0
- package/lib/helpers/index.ts +6 -0
- package/lib/helpers/types.ts +134 -0
- package/lib/helpers/unlock.ts +329 -0
- package/lib/helpers/webview.ts +582 -0
- package/lib/index.ts +18 -0
- package/lib/method-map.js +87 -98
- package/lib/stubs.ts +0 -1
- package/package.json +26 -19
- package/build/index.js +0 -51
- package/build/lib/android-helpers.d.ts +0 -136
- package/build/lib/android-helpers.d.ts.map +0 -1
- package/build/lib/android-helpers.js +0 -855
- package/build/lib/android-helpers.js.map +0 -1
- package/build/lib/commands/coverage.d.ts +0 -5
- package/build/lib/commands/coverage.d.ts.map +0 -1
- package/build/lib/commands/coverage.js +0 -19
- package/build/lib/commands/coverage.js.map +0 -1
- package/build/lib/desired-caps.d.ts +0 -353
- package/build/lib/desired-caps.d.ts.map +0 -1
- package/build/lib/desired-caps.js.map +0 -1
- package/build/lib/unlock-helpers.d.ts +0 -38
- package/build/lib/unlock-helpers.d.ts.map +0 -1
- package/build/lib/unlock-helpers.js +0 -266
- package/build/lib/unlock-helpers.js.map +0 -1
- package/build/lib/webview-helpers.d.ts +0 -224
- package/build/lib/webview-helpers.d.ts.map +0 -1
- package/build/lib/webview-helpers.js +0 -528
- package/build/lib/webview-helpers.js.map +0 -1
- package/index.js +0 -24
- package/lib/android-helpers.js +0 -983
- package/lib/commands/coverage.js +0 -18
- package/lib/commands/find.js +0 -82
- package/lib/unlock-helpers.js +0 -278
- package/lib/webview-helpers.js +0 -602
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
import { fs, system, logger, util } from '@appium/support';
|
|
3
|
-
import { exec, SubProcess } from 'teen_process';
|
|
4
|
-
import { checkPortStatus } from 'portscanner';
|
|
5
|
-
import http from 'http';
|
|
6
|
-
import net from 'net';
|
|
7
|
-
import B from 'bluebird';
|
|
8
|
-
import { waitForCondition } from 'asyncbox';
|
|
9
|
-
import { spawn } from 'child_process';
|
|
10
|
-
import url from 'url';
|
|
1
|
+
// @ts-check
|
|
11
2
|
|
|
12
|
-
|
|
3
|
+
import {fs, logger, system, util} from '@appium/support';
|
|
4
|
+
import {waitForCondition} from 'asyncbox';
|
|
5
|
+
import B from 'bluebird';
|
|
6
|
+
import _ from 'lodash';
|
|
7
|
+
import {spawn} from 'node:child_process';
|
|
8
|
+
import http from 'node:http';
|
|
9
|
+
import net from 'node:net';
|
|
10
|
+
import url from 'node:url';
|
|
11
|
+
import {checkPortStatus} from 'portscanner';
|
|
12
|
+
import {SubProcess, exec} from 'teen_process';
|
|
13
|
+
import {mixin} from './mixins';
|
|
13
14
|
|
|
14
15
|
const RECORDING_INTERVAL_SEC = 5;
|
|
15
16
|
const STREAMING_STARTUP_TIMEOUT_MS = 5000;
|
|
@@ -33,76 +34,114 @@ const BOUNDARY_STRING = '--2ae9746887f170b8cf7c271047ce314c';
|
|
|
33
34
|
|
|
34
35
|
const ADB_SCREEN_STREAMING_FEATURE = 'adb_screen_streaming';
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
/**
|
|
38
|
+
*
|
|
39
|
+
* @param {string} streamName
|
|
40
|
+
* @param {string} udid
|
|
41
|
+
* @returns {AppiumLogger}
|
|
42
|
+
*/
|
|
43
|
+
function createStreamingLogger(streamName, udid) {
|
|
44
|
+
return logger.getLogger(
|
|
45
|
+
`${streamName}@` +
|
|
46
|
+
_.truncate(udid, {
|
|
47
|
+
length: 8,
|
|
48
|
+
omission: '',
|
|
49
|
+
})
|
|
50
|
+
);
|
|
41
51
|
}
|
|
42
52
|
|
|
43
|
-
|
|
53
|
+
/**
|
|
54
|
+
*
|
|
55
|
+
* @param {ADB} adb
|
|
56
|
+
*/
|
|
57
|
+
async function verifyStreamingRequirements(adb) {
|
|
44
58
|
if (!_.trim(await adb.shell(['which', SCREENRECORD_BINARY]))) {
|
|
45
59
|
throw new Error(
|
|
46
|
-
`The required '${SCREENRECORD_BINARY}' binary is not available on the device under test`
|
|
60
|
+
`The required '${SCREENRECORD_BINARY}' binary is not available on the device under test`
|
|
61
|
+
);
|
|
47
62
|
}
|
|
48
63
|
|
|
49
64
|
const gstreamerCheckPromises = [];
|
|
50
65
|
for (const binaryName of [GSTREAMER_BINARY, GST_INSPECT_BINARY]) {
|
|
51
|
-
gstreamerCheckPromises.push(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
gstreamerCheckPromises.push(
|
|
67
|
+
(async () => {
|
|
68
|
+
try {
|
|
69
|
+
await fs.which(binaryName);
|
|
70
|
+
} catch (e) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`The '${binaryName}' binary is not available in the PATH on the host system. ` +
|
|
73
|
+
`See ${GST_TUTORIAL_URL} for more details on how to install it.`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
})()
|
|
77
|
+
);
|
|
59
78
|
}
|
|
60
79
|
await B.all(gstreamerCheckPromises);
|
|
61
80
|
|
|
62
81
|
const moduleCheckPromises = [];
|
|
63
82
|
for (const [name, modName] of _.toPairs(REQUIRED_GST_PLUGINS)) {
|
|
64
|
-
moduleCheckPromises.push(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
83
|
+
moduleCheckPromises.push(
|
|
84
|
+
(async () => {
|
|
85
|
+
const {stdout} = await exec(GST_INSPECT_BINARY, [name]);
|
|
86
|
+
if (!_.includes(stdout, modName)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`The required GStreamer plugin '${name}' from '${modName}' module is not installed. ` +
|
|
89
|
+
`See ${GST_TUTORIAL_URL} for more details on how to install it.`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
})()
|
|
93
|
+
);
|
|
72
94
|
}
|
|
73
95
|
await B.all(moduleCheckPromises);
|
|
74
96
|
}
|
|
75
97
|
|
|
76
|
-
|
|
98
|
+
const deviceInfoRegexes = /** @type {const} */ ([
|
|
99
|
+
['width', /\bdeviceWidth=(\d+)/],
|
|
100
|
+
['height', /\bdeviceHeight=(\d+)/],
|
|
101
|
+
['fps', /\bfps=(\d+)/],
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
*
|
|
106
|
+
* @param {ADB} adb
|
|
107
|
+
* @param {AppiumLogger} [log]
|
|
108
|
+
*/
|
|
109
|
+
async function getDeviceInfo(adb, log) {
|
|
77
110
|
const output = await adb.shell(['dumpsys', 'display']);
|
|
111
|
+
/**
|
|
112
|
+
* @type {DeviceInfo}
|
|
113
|
+
*/
|
|
78
114
|
const result = {};
|
|
79
|
-
for (const [key, pattern] of
|
|
80
|
-
['width', /\bdeviceWidth=(\d+)/],
|
|
81
|
-
['height', /\bdeviceHeight=(\d+)/],
|
|
82
|
-
['fps', /\bfps=(\d+)/],
|
|
83
|
-
]) {
|
|
115
|
+
for (const [key, pattern] of deviceInfoRegexes) {
|
|
84
116
|
const match = pattern.exec(output);
|
|
85
117
|
if (!match) {
|
|
86
118
|
log?.debug(output);
|
|
87
|
-
throw new Error(
|
|
88
|
-
`
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Cannot parse the device ${key} from the adb command output. ` +
|
|
121
|
+
`Check the server log for more details.`
|
|
122
|
+
);
|
|
89
123
|
}
|
|
90
124
|
result[key] = parseInt(match[1], 10);
|
|
91
125
|
}
|
|
92
|
-
result.udid = adb.curDeviceId;
|
|
126
|
+
result.udid = String(adb.curDeviceId);
|
|
93
127
|
return result;
|
|
94
128
|
}
|
|
95
129
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
130
|
+
/**
|
|
131
|
+
*
|
|
132
|
+
* @param {ADB} adb
|
|
133
|
+
* @param {AppiumLogger} log
|
|
134
|
+
* @param {DeviceInfo} deviceInfo
|
|
135
|
+
* @param {{width?: string|number, height?: string|number, bitRate?: string|number}} opts
|
|
136
|
+
* @returns
|
|
137
|
+
*/
|
|
138
|
+
async function initDeviceStreamingProc(adb, log, deviceInfo, opts = {}) {
|
|
139
|
+
const {width, height, bitRate} = opts;
|
|
140
|
+
const adjustedWidth = _.isUndefined(width) ? deviceInfo.width : parseInt(String(width), 10);
|
|
141
|
+
const adjustedHeight = _.isUndefined(height) ? deviceInfo.height : parseInt(String(height), 10);
|
|
142
|
+
const adjustedBitrate = _.isUndefined(bitRate) ? DEFAULT_BITRATE : parseInt(String(bitRate), 10);
|
|
143
|
+
let screenRecordCmd =
|
|
144
|
+
SCREENRECORD_BINARY +
|
|
106
145
|
` --output-format=h264` +
|
|
107
146
|
// 5 seconds is fine to detect rotation changes
|
|
108
147
|
` --time-limit=${RECORDING_INTERVAL_SEC}`;
|
|
@@ -126,6 +165,10 @@ async function initDeviceStreamingProc (adb, log, deviceInfo, opts = {}) {
|
|
|
126
165
|
|
|
127
166
|
let isStarted = false;
|
|
128
167
|
const deviceStreamingLogger = createStreamingLogger(SCREENRECORD_BINARY, deviceInfo.udid);
|
|
168
|
+
/**
|
|
169
|
+
*
|
|
170
|
+
* @param {Buffer|string} chunk
|
|
171
|
+
*/
|
|
129
172
|
const errorsListener = (chunk) => {
|
|
130
173
|
const stderr = chunk.toString();
|
|
131
174
|
if (_.trim(stderr)) {
|
|
@@ -134,6 +177,10 @@ async function initDeviceStreamingProc (adb, log, deviceInfo, opts = {}) {
|
|
|
134
177
|
};
|
|
135
178
|
deviceStreaming.stderr.on('data', errorsListener);
|
|
136
179
|
|
|
180
|
+
/**
|
|
181
|
+
*
|
|
182
|
+
* @param {Buffer|string} chunk
|
|
183
|
+
*/
|
|
137
184
|
const startupListener = (chunk) => {
|
|
138
185
|
if (!isStarted) {
|
|
139
186
|
isStarted = !_.isEmpty(chunk);
|
|
@@ -149,7 +196,10 @@ async function initDeviceStreamingProc (adb, log, deviceInfo, opts = {}) {
|
|
|
149
196
|
});
|
|
150
197
|
} catch (e) {
|
|
151
198
|
log.errorAndThrow(
|
|
152
|
-
`Cannot start the screen streaming process. Original error: ${
|
|
199
|
+
`Cannot start the screen streaming process. Original error: ${
|
|
200
|
+
/** @type {Error} */ (e).message
|
|
201
|
+
}`
|
|
202
|
+
);
|
|
153
203
|
} finally {
|
|
154
204
|
deviceStreaming.stderr.removeListener('data', errorsListener);
|
|
155
205
|
deviceStreaming.stdout.removeListener('data', startupListener);
|
|
@@ -157,39 +207,63 @@ async function initDeviceStreamingProc (adb, log, deviceInfo, opts = {}) {
|
|
|
157
207
|
return deviceStreaming;
|
|
158
208
|
}
|
|
159
209
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
} = opts;
|
|
169
|
-
const adjustedWidth = parseInt(width, 10) || deviceInfo.width;
|
|
170
|
-
const adjustedHeight = parseInt(height, 10) || deviceInfo.height;
|
|
171
|
-
const gstreamerPipeline = new SubProcess(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
'
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
210
|
+
/**
|
|
211
|
+
*
|
|
212
|
+
* @param {import('node:child_process').ChildProcess} deviceStreamingProc
|
|
213
|
+
* @param {DeviceInfo} deviceInfo
|
|
214
|
+
* @param {AppiumLogger} log
|
|
215
|
+
* @param {import('./types').InitGStreamerPipelineOpts} opts
|
|
216
|
+
*/
|
|
217
|
+
async function initGstreamerPipeline(deviceStreamingProc, deviceInfo, log, opts) {
|
|
218
|
+
const {width, height, quality, tcpPort, considerRotation, logPipelineDetails} = opts;
|
|
219
|
+
const adjustedWidth = parseInt(String(width), 10) || deviceInfo.width;
|
|
220
|
+
const adjustedHeight = parseInt(String(height), 10) || deviceInfo.height;
|
|
221
|
+
const gstreamerPipeline = new SubProcess(
|
|
222
|
+
GSTREAMER_BINARY,
|
|
223
|
+
[
|
|
224
|
+
'-v',
|
|
225
|
+
'fdsrc',
|
|
226
|
+
'fd=0',
|
|
227
|
+
'!',
|
|
228
|
+
'video/x-h264,' +
|
|
229
|
+
`width=${considerRotation ? Math.max(adjustedWidth, adjustedHeight) : adjustedWidth},` +
|
|
230
|
+
`height=${considerRotation ? Math.max(adjustedWidth, adjustedHeight) : adjustedHeight},` +
|
|
231
|
+
`framerate=${deviceInfo.fps}/1,` +
|
|
232
|
+
'byte-stream=true',
|
|
233
|
+
'!',
|
|
234
|
+
'h264parse',
|
|
235
|
+
'!',
|
|
236
|
+
'queue',
|
|
237
|
+
'leaky=downstream',
|
|
238
|
+
'!',
|
|
239
|
+
'avdec_h264',
|
|
240
|
+
'!',
|
|
241
|
+
'queue',
|
|
242
|
+
'leaky=downstream',
|
|
243
|
+
'!',
|
|
244
|
+
'jpegenc',
|
|
245
|
+
`quality=${quality}`,
|
|
246
|
+
'!',
|
|
247
|
+
'multipartmux',
|
|
248
|
+
`boundary=${BOUNDARY_STRING}`,
|
|
249
|
+
'!',
|
|
250
|
+
'tcpserversink',
|
|
251
|
+
`host=${TCP_HOST}`,
|
|
252
|
+
`port=${tcpPort}`,
|
|
253
|
+
],
|
|
254
|
+
{
|
|
255
|
+
stdio: [deviceStreamingProc.stdout, 'pipe', 'pipe'],
|
|
256
|
+
}
|
|
257
|
+
);
|
|
189
258
|
gstreamerPipeline.on('exit', (code, signal) => {
|
|
190
259
|
log.debug(`Pipeline streaming process exited with code ${code}, signal ${signal}`);
|
|
191
260
|
});
|
|
192
261
|
const gstreamerLogger = createStreamingLogger('gst', deviceInfo.udid);
|
|
262
|
+
/**
|
|
263
|
+
*
|
|
264
|
+
* @param {string} stdout
|
|
265
|
+
* @param {string} stderr
|
|
266
|
+
*/
|
|
193
267
|
const gstOutputListener = (stdout, stderr) => {
|
|
194
268
|
if (_.trim(stderr || stdout)) {
|
|
195
269
|
gstreamerLogger.debug(stderr || stdout);
|
|
@@ -200,20 +274,26 @@ async function initGstreamerPipeline (deviceStreamingProc, deviceInfo, log, opts
|
|
|
200
274
|
try {
|
|
201
275
|
log.info(`Starting GStreamer pipeline: ${gstreamerPipeline.rep}`);
|
|
202
276
|
await gstreamerPipeline.start(0);
|
|
203
|
-
await waitForCondition(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
277
|
+
await waitForCondition(
|
|
278
|
+
async () => {
|
|
279
|
+
try {
|
|
280
|
+
return (await checkPortStatus(tcpPort, TCP_HOST)) === 'open';
|
|
281
|
+
} catch (ign) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
waitMs: STREAMING_STARTUP_TIMEOUT_MS,
|
|
287
|
+
intervalMs: 300,
|
|
208
288
|
}
|
|
209
|
-
|
|
210
|
-
waitMs: STREAMING_STARTUP_TIMEOUT_MS,
|
|
211
|
-
intervalMs: 300,
|
|
212
|
-
});
|
|
289
|
+
);
|
|
213
290
|
} catch (e) {
|
|
214
291
|
didFail = true;
|
|
215
292
|
log.errorAndThrow(
|
|
216
|
-
`Cannot start the screen streaming pipeline. Original error: ${
|
|
293
|
+
`Cannot start the screen streaming pipeline. Original error: ${
|
|
294
|
+
/** @type {Error} */ (e).message
|
|
295
|
+
}`
|
|
296
|
+
);
|
|
217
297
|
} finally {
|
|
218
298
|
if (!logPipelineDetails || didFail) {
|
|
219
299
|
gstreamerPipeline.removeListener('output', gstOutputListener);
|
|
@@ -222,232 +302,218 @@ async function initGstreamerPipeline (deviceStreamingProc, deviceInfo, log, opts
|
|
|
222
302
|
return gstreamerPipeline;
|
|
223
303
|
}
|
|
224
304
|
|
|
225
|
-
function extractRemoteAddress (req) {
|
|
226
|
-
return req.headers['x-forwarded-for']
|
|
227
|
-
|| req.socket.remoteAddress
|
|
228
|
-
|| req.connection.remoteAddress
|
|
229
|
-
|| req.connection.socket.remoteAddress;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
305
|
/**
|
|
234
|
-
* @
|
|
235
|
-
*
|
|
236
|
-
* @property {?number} width - The scaled width of the device's screen. If unset then the script will assign it
|
|
237
|
-
* to the actual screen width measured in pixels.
|
|
238
|
-
* @property {?number} height - The scaled height of the device's screen. If unset then the script will assign it
|
|
239
|
-
* to the actual screen height measured in pixels.
|
|
240
|
-
* @property {?number} bitRate - The video bit rate for the video, in bits per second.
|
|
241
|
-
* The default value is 4000000 (4 Mb/s). You can increase the bit rate to improve video quality,
|
|
242
|
-
* but doing so results in larger movie files.
|
|
243
|
-
* @property {?string} host [127.0.0.1] - The IP address/host name to start the MJPEG server on.
|
|
244
|
-
* You can set it to `0.0.0.0` to trigger the broadcast on all available network interfaces.
|
|
245
|
-
* @property {?string} pathname - The HTTP request path the MJPEG server should be available on.
|
|
246
|
-
* If unset then any pathname on the given `host`/`port` combination will work. Note that the value
|
|
247
|
-
* should always start with a single slash: `/`
|
|
248
|
-
* @property {?number} tcpPort [8094] - The port number to start the internal TCP MJPEG broadcast on.
|
|
249
|
-
* This type of broadcast always starts on the loopback interface (`127.0.0.1`).
|
|
250
|
-
* @property {?number} port [8093] - The port number to start the MJPEG server on.
|
|
251
|
-
* @property {?number} quality [70] - The quality value for the streamed JPEG images.
|
|
252
|
-
* This number should be in range [1, 100], where 100 is the best quality.
|
|
253
|
-
* @property {?boolean} considerRotation [false] - If set to `true` then GStreamer pipeline will
|
|
254
|
-
* increase the dimensions of the resulting images to properly fit images in both landscape and
|
|
255
|
-
* portrait orientations. Set it to `true` if the device rotation is not going to be the same during the
|
|
256
|
-
* broadcasting session.
|
|
257
|
-
* @property {?boolean} logPipelineDetails [false] - Whether to log GStreamer pipeline events into
|
|
258
|
-
* the standard log output. Might be useful for debugging purposes.
|
|
306
|
+
* @param {import('node:http').IncomingMessage} req
|
|
307
|
+
* @privateRemarks This may need to be future-proofed, as `IncomingMessage.connection` is deprecated and its `socket` prop is likely private
|
|
259
308
|
*/
|
|
309
|
+
function extractRemoteAddress(req) {
|
|
310
|
+
return (
|
|
311
|
+
req.headers['x-forwarded-for'] ||
|
|
312
|
+
req.socket.remoteAddress ||
|
|
313
|
+
req.connection.remoteAddress ||
|
|
314
|
+
// @ts-expect-error socket may be a private API??
|
|
315
|
+
req.connection.socket.remoteAddress
|
|
316
|
+
);
|
|
317
|
+
}
|
|
260
318
|
|
|
261
319
|
/**
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
* session is stopped.
|
|
265
|
-
* This method only works if the `adb_screen_streaming` feature is
|
|
266
|
-
* enabled on the server side.
|
|
267
|
-
*
|
|
268
|
-
* @param {?StartScreenStreamingOptions} options - The available options.
|
|
269
|
-
* @throws {Error} If screen streaming has failed to start or
|
|
270
|
-
* is not supported on the host system or
|
|
271
|
-
* the corresponding server feature is not enabled.
|
|
320
|
+
* @type {import('./mixins').StreamScreenMixin & ThisType<import('../driver').AndroidDriver>}
|
|
321
|
+
* @satisfies {import('@appium/types').ExternalDriver}
|
|
272
322
|
*/
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const {
|
|
277
|
-
width,
|
|
278
|
-
height,
|
|
279
|
-
bitRate,
|
|
280
|
-
host = DEFAULT_HOST,
|
|
281
|
-
port = DEFAULT_PORT,
|
|
282
|
-
pathname,
|
|
283
|
-
tcpPort = DEFAULT_PORT + 1,
|
|
284
|
-
quality = DEFAULT_QUALITY,
|
|
285
|
-
considerRotation = false,
|
|
286
|
-
logPipelineDetails = false,
|
|
287
|
-
} = options;
|
|
323
|
+
const StreamScreenMixin = {
|
|
324
|
+
async mobileStartScreenStreaming(options = {}) {
|
|
325
|
+
this.ensureFeatureEnabled(ADB_SCREEN_STREAMING_FEATURE);
|
|
288
326
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
327
|
+
const {
|
|
328
|
+
width,
|
|
329
|
+
height,
|
|
330
|
+
bitRate,
|
|
331
|
+
host = DEFAULT_HOST,
|
|
332
|
+
port = DEFAULT_PORT,
|
|
333
|
+
pathname,
|
|
334
|
+
tcpPort = DEFAULT_PORT + 1,
|
|
335
|
+
quality = DEFAULT_QUALITY,
|
|
336
|
+
considerRotation = false,
|
|
337
|
+
logPipelineDetails = false,
|
|
338
|
+
} = options;
|
|
339
|
+
const adb = /** @type {ADB} */ (this.adb);
|
|
340
|
+
if (_.isUndefined(this._screenStreamingProps)) {
|
|
341
|
+
await verifyStreamingRequirements(adb);
|
|
342
|
+
}
|
|
343
|
+
if (!_.isEmpty(this._screenStreamingProps)) {
|
|
344
|
+
this.log.info(
|
|
345
|
+
`The screen streaming session is already running. ` +
|
|
346
|
+
`Stop it first in order to start a new one.`
|
|
347
|
+
);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if ((await checkPortStatus(port, host)) === 'open') {
|
|
351
|
+
this.log.info(
|
|
352
|
+
`The port #${port} at ${host} is busy. ` +
|
|
353
|
+
`Assuming the screen streaming is already running`
|
|
354
|
+
);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if ((await checkPortStatus(tcpPort, TCP_HOST)) === 'open') {
|
|
358
|
+
this.log.errorAndThrow(
|
|
359
|
+
`The port #${tcpPort} at ${TCP_HOST} is busy. ` +
|
|
360
|
+
`Make sure there are no leftovers from previous sessions.`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
this._screenStreamingProps = undefined;
|
|
307
364
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
width,
|
|
311
|
-
height,
|
|
312
|
-
bitRate,
|
|
313
|
-
});
|
|
314
|
-
let gstreamerPipeline;
|
|
315
|
-
try {
|
|
316
|
-
gstreamerPipeline = await initGstreamerPipeline(deviceStreamingProc, deviceInfo, this.log, {
|
|
365
|
+
const deviceInfo = await getDeviceInfo(adb, this.log);
|
|
366
|
+
const deviceStreamingProc = await initDeviceStreamingProc(adb, this.log, deviceInfo, {
|
|
317
367
|
width,
|
|
318
368
|
height,
|
|
319
|
-
|
|
320
|
-
tcpPort,
|
|
321
|
-
considerRotation,
|
|
322
|
-
logPipelineDetails,
|
|
369
|
+
bitRate,
|
|
323
370
|
});
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
deviceStreamingProc.
|
|
371
|
+
let gstreamerPipeline;
|
|
372
|
+
try {
|
|
373
|
+
gstreamerPipeline = await initGstreamerPipeline(deviceStreamingProc, deviceInfo, this.log, {
|
|
374
|
+
width,
|
|
375
|
+
height,
|
|
376
|
+
quality,
|
|
377
|
+
tcpPort,
|
|
378
|
+
considerRotation,
|
|
379
|
+
logPipelineDetails,
|
|
380
|
+
});
|
|
381
|
+
} catch (e) {
|
|
382
|
+
if (deviceStreamingProc.kill(0)) {
|
|
383
|
+
deviceStreamingProc.kill();
|
|
384
|
+
}
|
|
385
|
+
throw e;
|
|
327
386
|
}
|
|
328
|
-
throw e;
|
|
329
|
-
}
|
|
330
387
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
388
|
+
/** @type {import('node:net').Socket|undefined} */
|
|
389
|
+
let mjpegSocket;
|
|
390
|
+
/** @type {import('node:http').Server|undefined} */
|
|
391
|
+
let mjpegServer;
|
|
392
|
+
try {
|
|
393
|
+
await new B((resolve, reject) => {
|
|
394
|
+
mjpegSocket = net.createConnection(tcpPort, TCP_HOST, () => {
|
|
395
|
+
this.log.info(`Successfully connected to MJPEG stream at tcp://${TCP_HOST}:${tcpPort}`);
|
|
396
|
+
mjpegServer = http.createServer((req, res) => {
|
|
397
|
+
const remoteAddress = extractRemoteAddress(req);
|
|
398
|
+
const currentPathname = url.parse(String(req.url)).pathname;
|
|
399
|
+
this.log.info(
|
|
400
|
+
`Got an incoming screen broadcasting request from ${remoteAddress} ` +
|
|
401
|
+
`(${req.headers['user-agent'] || 'User Agent unknown'}) at ${currentPathname}`
|
|
402
|
+
);
|
|
342
403
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
404
|
+
if (pathname && currentPathname !== pathname) {
|
|
405
|
+
this.log.info(
|
|
406
|
+
'Rejecting the broadcast request since it does not match the given pathname'
|
|
407
|
+
);
|
|
408
|
+
res.writeHead(404, {
|
|
409
|
+
Connection: 'close',
|
|
410
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
411
|
+
});
|
|
412
|
+
res.write(`'${currentPathname}' did not match any known endpoints`);
|
|
413
|
+
res.end();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
this.log.info('Starting MJPEG broadcast');
|
|
418
|
+
res.writeHead(200, {
|
|
419
|
+
'Cache-Control':
|
|
420
|
+
'no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0',
|
|
421
|
+
Pragma: 'no-cache',
|
|
346
422
|
Connection: 'close',
|
|
347
|
-
'Content-Type':
|
|
423
|
+
'Content-Type': `multipart/x-mixed-replace; boundary=${BOUNDARY_STRING}`,
|
|
348
424
|
});
|
|
349
|
-
res.write(`'${currentPathname}' did not match any known endpoints`);
|
|
350
|
-
res.end();
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
425
|
|
|
354
|
-
|
|
355
|
-
res.writeHead(200, {
|
|
356
|
-
'Cache-Control': 'no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0',
|
|
357
|
-
Pragma: 'no-cache',
|
|
358
|
-
Connection: 'close',
|
|
359
|
-
'Content-Type': `multipart/x-mixed-replace; boundary=${BOUNDARY_STRING}`
|
|
426
|
+
/** @type {import('node:net').Socket} */ (mjpegSocket).pipe(res);
|
|
360
427
|
});
|
|
361
|
-
|
|
362
|
-
|
|
428
|
+
mjpegServer.on('error', (e) => {
|
|
429
|
+
this.log.warn(e);
|
|
430
|
+
reject(e);
|
|
431
|
+
});
|
|
432
|
+
mjpegServer.on('close', () => {
|
|
433
|
+
this.log.debug(`MJPEG server at http://${host}:${port} has been closed`);
|
|
434
|
+
});
|
|
435
|
+
mjpegServer.on('listening', () => {
|
|
436
|
+
this.log.info(`Successfully started MJPEG server at http://${host}:${port}`);
|
|
437
|
+
resolve();
|
|
438
|
+
});
|
|
439
|
+
mjpegServer.listen(port, host);
|
|
363
440
|
});
|
|
364
|
-
|
|
365
|
-
this.log.
|
|
441
|
+
mjpegSocket.on('error', (e) => {
|
|
442
|
+
this.log.error(e);
|
|
366
443
|
reject(e);
|
|
367
444
|
});
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
if (gstreamerPipeline.isRunning) {
|
|
388
|
-
await gstreamerPipeline.stop();
|
|
389
|
-
}
|
|
390
|
-
if (mjpegSocket) {
|
|
391
|
-
mjpegSocket.destroy();
|
|
392
|
-
}
|
|
393
|
-
if (mjpegServer && mjpegServer.listening) {
|
|
394
|
-
mjpegServer.close();
|
|
445
|
+
}).timeout(
|
|
446
|
+
STREAMING_STARTUP_TIMEOUT_MS,
|
|
447
|
+
`Cannot connect to the streaming server within ${STREAMING_STARTUP_TIMEOUT_MS}ms`
|
|
448
|
+
);
|
|
449
|
+
} catch (e) {
|
|
450
|
+
if (deviceStreamingProc.kill(0)) {
|
|
451
|
+
deviceStreamingProc.kill();
|
|
452
|
+
}
|
|
453
|
+
if (gstreamerPipeline.isRunning) {
|
|
454
|
+
await gstreamerPipeline.stop();
|
|
455
|
+
}
|
|
456
|
+
if (mjpegSocket) {
|
|
457
|
+
mjpegSocket.destroy();
|
|
458
|
+
}
|
|
459
|
+
if (mjpegServer && mjpegServer.listening) {
|
|
460
|
+
mjpegServer.close();
|
|
461
|
+
}
|
|
462
|
+
throw e;
|
|
395
463
|
}
|
|
396
|
-
throw e;
|
|
397
|
-
}
|
|
398
464
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
465
|
+
this._screenStreamingProps = {
|
|
466
|
+
deviceStreamingProc,
|
|
467
|
+
gstreamerPipeline,
|
|
468
|
+
mjpegSocket,
|
|
469
|
+
mjpegServer,
|
|
470
|
+
};
|
|
471
|
+
},
|
|
406
472
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
if (!_.isUndefined(this._screenStreamingProps)) {
|
|
414
|
-
this.log.debug(`Screen streaming is not running. There is nothing to stop`);
|
|
473
|
+
async mobileStopScreenStreaming() {
|
|
474
|
+
if (_.isEmpty(this._screenStreamingProps)) {
|
|
475
|
+
if (!_.isUndefined(this._screenStreamingProps)) {
|
|
476
|
+
this.log.debug(`Screen streaming is not running. There is nothing to stop`);
|
|
477
|
+
}
|
|
478
|
+
return;
|
|
415
479
|
}
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
480
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
gstreamerPipeline,
|
|
422
|
-
mjpegSocket,
|
|
423
|
-
mjpegServer,
|
|
424
|
-
} = this._screenStreamingProps;
|
|
481
|
+
const {deviceStreamingProc, gstreamerPipeline, mjpegSocket, mjpegServer} =
|
|
482
|
+
this._screenStreamingProps;
|
|
425
483
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
try {
|
|
436
|
-
await gstreamerPipeline.stop('SIGINT');
|
|
437
|
-
} catch (e) {
|
|
438
|
-
this.log.warn(e);
|
|
484
|
+
try {
|
|
485
|
+
mjpegSocket.end();
|
|
486
|
+
if (mjpegServer.listening) {
|
|
487
|
+
mjpegServer.close();
|
|
488
|
+
}
|
|
489
|
+
if (deviceStreamingProc.kill(0)) {
|
|
490
|
+
deviceStreamingProc.kill('SIGINT');
|
|
491
|
+
}
|
|
492
|
+
if (gstreamerPipeline.isRunning) {
|
|
439
493
|
try {
|
|
440
|
-
await gstreamerPipeline.stop('
|
|
441
|
-
} catch (
|
|
442
|
-
this.log.
|
|
494
|
+
await gstreamerPipeline.stop('SIGINT');
|
|
495
|
+
} catch (e) {
|
|
496
|
+
this.log.warn(e);
|
|
497
|
+
try {
|
|
498
|
+
await gstreamerPipeline.stop('SIGKILL');
|
|
499
|
+
} catch (e1) {
|
|
500
|
+
this.log.error(e1);
|
|
501
|
+
}
|
|
443
502
|
}
|
|
444
503
|
}
|
|
504
|
+
this.log.info(`Successfully terminated the screen streaming MJPEG server`);
|
|
505
|
+
} finally {
|
|
506
|
+
this._screenStreamingProps = undefined;
|
|
445
507
|
}
|
|
446
|
-
|
|
447
|
-
} finally {
|
|
448
|
-
this._screenStreamingProps = null;
|
|
449
|
-
}
|
|
508
|
+
},
|
|
450
509
|
};
|
|
451
510
|
|
|
511
|
+
mixin(StreamScreenMixin);
|
|
512
|
+
|
|
513
|
+
export default StreamScreenMixin;
|
|
452
514
|
|
|
453
|
-
|
|
515
|
+
/**
|
|
516
|
+
* @typedef {import('appium-adb').ADB} ADB
|
|
517
|
+
* @typedef {import('@appium/types').AppiumLogger} AppiumLogger
|
|
518
|
+
* @typedef {import('./types').DeviceInfo} DeviceInfo
|
|
519
|
+
*/
|