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.
Files changed (210) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/build/lib/commands/actions.d.ts +6 -224
  3. package/build/lib/commands/actions.d.ts.map +1 -1
  4. package/build/lib/commands/actions.js +306 -405
  5. package/build/lib/commands/actions.js.map +1 -1
  6. package/build/lib/commands/alert.d.ts +7 -9
  7. package/build/lib/commands/alert.d.ts.map +1 -1
  8. package/build/lib/commands/alert.js +24 -18
  9. package/build/lib/commands/alert.js.map +1 -1
  10. package/build/lib/commands/app-management.d.ts +7 -313
  11. package/build/lib/commands/app-management.d.ts.map +1 -1
  12. package/build/lib/commands/app-management.js +135 -293
  13. package/build/lib/commands/app-management.js.map +1 -1
  14. package/build/lib/commands/context.d.ts +8 -92
  15. package/build/lib/commands/context.d.ts.map +1 -1
  16. package/build/lib/commands/context.js +381 -439
  17. package/build/lib/commands/context.js.map +1 -1
  18. package/build/lib/commands/element.d.ts +8 -35
  19. package/build/lib/commands/element.d.ts.map +1 -1
  20. package/build/lib/commands/element.js +153 -136
  21. package/build/lib/commands/element.js.map +1 -1
  22. package/build/lib/commands/emu-console.d.ts +6 -48
  23. package/build/lib/commands/emu-console.d.ts.map +1 -1
  24. package/build/lib/commands/emu-console.js +19 -34
  25. package/build/lib/commands/emu-console.js.map +1 -1
  26. package/build/lib/commands/execute.d.ts +6 -5
  27. package/build/lib/commands/execute.d.ts.map +1 -1
  28. package/build/lib/commands/execute.js +77 -66
  29. package/build/lib/commands/execute.js.map +1 -1
  30. package/build/lib/commands/file-actions.d.ts +7 -128
  31. package/build/lib/commands/file-actions.d.ts.map +1 -1
  32. package/build/lib/commands/file-actions.js +183 -219
  33. package/build/lib/commands/file-actions.js.map +1 -1
  34. package/build/lib/commands/find.d.ts +8 -12
  35. package/build/lib/commands/find.d.ts.map +1 -1
  36. package/build/lib/commands/find.js +19 -23
  37. package/build/lib/commands/find.js.map +1 -1
  38. package/build/lib/commands/general.d.ts +9 -132
  39. package/build/lib/commands/general.d.ts.map +1 -1
  40. package/build/lib/commands/general.js +281 -312
  41. package/build/lib/commands/general.js.map +1 -1
  42. package/build/lib/commands/ime.d.ts +7 -10
  43. package/build/lib/commands/ime.d.ts.map +1 -1
  44. package/build/lib/commands/ime.js +47 -35
  45. package/build/lib/commands/ime.js.map +1 -1
  46. package/build/lib/commands/index.d.ts +27 -2
  47. package/build/lib/commands/index.d.ts.map +1 -1
  48. package/build/lib/commands/index.js +41 -19
  49. package/build/lib/commands/index.js.map +1 -1
  50. package/build/lib/commands/intent.d.ts +7 -417
  51. package/build/lib/commands/intent.d.ts.map +1 -1
  52. package/build/lib/commands/intent.js +104 -216
  53. package/build/lib/commands/intent.js.map +1 -1
  54. package/build/lib/commands/keyboard.d.ts +6 -5
  55. package/build/lib/commands/keyboard.d.ts.map +1 -1
  56. package/build/lib/commands/keyboard.js +16 -8
  57. package/build/lib/commands/keyboard.js.map +1 -1
  58. package/build/lib/commands/log.d.ts +7 -44
  59. package/build/lib/commands/log.d.ts.map +1 -1
  60. package/build/lib/commands/log.js +146 -108
  61. package/build/lib/commands/log.js.map +1 -1
  62. package/build/lib/commands/media-projection.d.ts +7 -143
  63. package/build/lib/commands/media-projection.d.ts.map +1 -1
  64. package/build/lib/commands/media-projection.js +113 -140
  65. package/build/lib/commands/media-projection.js.map +1 -1
  66. package/build/lib/commands/mixins.d.ts +740 -0
  67. package/build/lib/commands/mixins.d.ts.map +1 -0
  68. package/build/lib/commands/mixins.js +19 -0
  69. package/build/lib/commands/mixins.js.map +1 -0
  70. package/build/lib/commands/network.d.ts +7 -138
  71. package/build/lib/commands/network.d.ts.map +1 -1
  72. package/build/lib/commands/network.js +212 -254
  73. package/build/lib/commands/network.js.map +1 -1
  74. package/build/lib/commands/performance.d.ts +24 -70
  75. package/build/lib/commands/performance.d.ts.map +1 -1
  76. package/build/lib/commands/performance.js +144 -100
  77. package/build/lib/commands/performance.js.map +1 -1
  78. package/build/lib/commands/permissions.d.ts +8 -92
  79. package/build/lib/commands/permissions.d.ts.map +1 -1
  80. package/build/lib/commands/permissions.js +75 -87
  81. package/build/lib/commands/permissions.js.map +1 -1
  82. package/build/lib/commands/recordscreen.d.ts +7 -193
  83. package/build/lib/commands/recordscreen.d.ts.map +1 -1
  84. package/build/lib/commands/recordscreen.js +151 -182
  85. package/build/lib/commands/recordscreen.js.map +1 -1
  86. package/build/lib/commands/shell.d.ts +7 -7
  87. package/build/lib/commands/shell.d.ts.map +1 -1
  88. package/build/lib/commands/shell.js +40 -33
  89. package/build/lib/commands/shell.js.map +1 -1
  90. package/build/lib/commands/streamscreen.d.ts +9 -103
  91. package/build/lib/commands/streamscreen.d.ts.map +1 -1
  92. package/build/lib/commands/streamscreen.js +261 -218
  93. package/build/lib/commands/streamscreen.js.map +1 -1
  94. package/build/lib/commands/system-bars.d.ts +22 -90
  95. package/build/lib/commands/system-bars.d.ts.map +1 -1
  96. package/build/lib/commands/system-bars.js +76 -74
  97. package/build/lib/commands/system-bars.js.map +1 -1
  98. package/build/lib/commands/touch.d.ts +10 -29
  99. package/build/lib/commands/touch.d.ts.map +1 -1
  100. package/build/lib/commands/touch.js +301 -285
  101. package/build/lib/commands/touch.js.map +1 -1
  102. package/build/lib/commands/types.d.ts +978 -0
  103. package/build/lib/commands/types.d.ts.map +1 -0
  104. package/build/lib/commands/types.js +3 -0
  105. package/build/lib/commands/types.js.map +1 -0
  106. package/build/lib/constraints.d.ts +291 -0
  107. package/build/lib/constraints.d.ts.map +1 -0
  108. package/build/lib/{desired-caps.js → constraints.js} +103 -102
  109. package/build/lib/constraints.js.map +1 -0
  110. package/build/lib/driver.d.ts +68 -37
  111. package/build/lib/driver.d.ts.map +1 -1
  112. package/build/lib/driver.js +123 -80
  113. package/build/lib/driver.js.map +1 -1
  114. package/build/lib/helpers/android.d.ts +164 -0
  115. package/build/lib/helpers/android.d.ts.map +1 -0
  116. package/build/lib/helpers/android.js +819 -0
  117. package/build/lib/helpers/android.js.map +1 -0
  118. package/build/lib/helpers/index.d.ts +7 -0
  119. package/build/lib/helpers/index.d.ts.map +1 -0
  120. package/build/lib/helpers/index.js +29 -0
  121. package/build/lib/helpers/index.js.map +1 -0
  122. package/build/lib/helpers/types.d.ts +121 -0
  123. package/build/lib/helpers/types.d.ts.map +1 -0
  124. package/build/lib/helpers/types.js +3 -0
  125. package/build/lib/helpers/types.js.map +1 -0
  126. package/build/lib/helpers/unlock.d.ts +32 -0
  127. package/build/lib/helpers/unlock.d.ts.map +1 -0
  128. package/build/lib/helpers/unlock.js +273 -0
  129. package/build/lib/helpers/unlock.js.map +1 -0
  130. package/build/lib/helpers/webview.d.ts +74 -0
  131. package/build/lib/helpers/webview.d.ts.map +1 -0
  132. package/build/lib/helpers/webview.js +421 -0
  133. package/build/lib/helpers/webview.js.map +1 -0
  134. package/build/lib/index.d.ts +9 -0
  135. package/build/lib/index.d.ts.map +1 -0
  136. package/build/lib/index.js +37 -0
  137. package/build/lib/index.js.map +1 -0
  138. package/build/lib/method-map.d.ts +0 -8
  139. package/build/lib/method-map.d.ts.map +1 -1
  140. package/build/lib/method-map.js +63 -74
  141. package/build/lib/method-map.js.map +1 -1
  142. package/build/lib/stubs.d.ts +0 -1
  143. package/build/lib/stubs.d.ts.map +1 -1
  144. package/build/lib/stubs.js +1 -0
  145. package/build/lib/stubs.js.map +1 -1
  146. package/build/lib/utils.d.ts +1 -1
  147. package/build/lib/utils.d.ts.map +1 -1
  148. package/lib/commands/actions.js +351 -464
  149. package/lib/commands/alert.js +27 -17
  150. package/lib/commands/app-management.js +156 -314
  151. package/lib/commands/context.js +457 -441
  152. package/lib/commands/element.js +201 -157
  153. package/lib/commands/emu-console.js +25 -45
  154. package/lib/commands/execute.js +106 -90
  155. package/lib/commands/file-actions.js +222 -240
  156. package/lib/commands/find.ts +103 -0
  157. package/lib/commands/general.js +327 -339
  158. package/lib/commands/ime.js +50 -34
  159. package/lib/commands/{index.js → index.ts} +20 -24
  160. package/lib/commands/intent.js +108 -249
  161. package/lib/commands/keyboard.js +20 -8
  162. package/lib/commands/log.js +172 -116
  163. package/lib/commands/media-projection.js +134 -161
  164. package/lib/commands/mixins.ts +966 -0
  165. package/lib/commands/network.js +252 -281
  166. package/lib/commands/performance.js +203 -132
  167. package/lib/commands/permissions.js +108 -109
  168. package/lib/commands/recordscreen.js +212 -209
  169. package/lib/commands/shell.js +51 -40
  170. package/lib/commands/streamscreen.js +355 -289
  171. package/lib/commands/system-bars.js +92 -83
  172. package/lib/commands/touch.js +357 -294
  173. package/lib/commands/types.ts +1097 -0
  174. package/lib/{desired-caps.js → constraints.ts} +106 -103
  175. package/lib/{driver.js → driver.ts} +278 -132
  176. package/lib/helpers/android.ts +1143 -0
  177. package/lib/helpers/index.ts +6 -0
  178. package/lib/helpers/types.ts +134 -0
  179. package/lib/helpers/unlock.ts +329 -0
  180. package/lib/helpers/webview.ts +582 -0
  181. package/lib/index.ts +18 -0
  182. package/lib/method-map.js +87 -98
  183. package/lib/stubs.ts +0 -1
  184. package/package.json +26 -19
  185. package/build/index.js +0 -51
  186. package/build/lib/android-helpers.d.ts +0 -136
  187. package/build/lib/android-helpers.d.ts.map +0 -1
  188. package/build/lib/android-helpers.js +0 -855
  189. package/build/lib/android-helpers.js.map +0 -1
  190. package/build/lib/commands/coverage.d.ts +0 -5
  191. package/build/lib/commands/coverage.d.ts.map +0 -1
  192. package/build/lib/commands/coverage.js +0 -19
  193. package/build/lib/commands/coverage.js.map +0 -1
  194. package/build/lib/desired-caps.d.ts +0 -353
  195. package/build/lib/desired-caps.d.ts.map +0 -1
  196. package/build/lib/desired-caps.js.map +0 -1
  197. package/build/lib/unlock-helpers.d.ts +0 -38
  198. package/build/lib/unlock-helpers.d.ts.map +0 -1
  199. package/build/lib/unlock-helpers.js +0 -266
  200. package/build/lib/unlock-helpers.js.map +0 -1
  201. package/build/lib/webview-helpers.d.ts +0 -224
  202. package/build/lib/webview-helpers.d.ts.map +0 -1
  203. package/build/lib/webview-helpers.js +0 -528
  204. package/build/lib/webview-helpers.js.map +0 -1
  205. package/index.js +0 -24
  206. package/lib/android-helpers.js +0 -983
  207. package/lib/commands/coverage.js +0 -18
  208. package/lib/commands/find.js +0 -82
  209. package/lib/unlock-helpers.js +0 -278
  210. package/lib/webview-helpers.js +0 -602
@@ -1,15 +1,16 @@
1
- import _ from 'lodash';
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
- const commands = {};
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
- function createStreamingLogger (streamName, udid) {
37
- return logger.getLogger(`${streamName}@` + _.truncate(udid, {
38
- length: 8,
39
- omission: '',
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
- async function verifyStreamingRequirements (adb) {
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((async () => {
52
- try {
53
- await fs.which(binaryName);
54
- } catch (e) {
55
- throw new Error(`The '${binaryName}' binary is not available in the PATH on the host system. ` +
56
- `See ${GST_TUTORIAL_URL} for more details on how to install it.`);
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((async () => {
65
- const {stdout} = await exec(GST_INSPECT_BINARY, [name]);
66
- if (!_.includes(stdout, modName)) {
67
- throw new Error(
68
- `The required GStreamer plugin '${name}' from '${modName}' module is not installed. ` +
69
- `See ${GST_TUTORIAL_URL} for more details on how to install it.`);
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
- async function getDeviceInfo (adb, log = null) {
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(`Cannot parse the device ${key} from the adb command output. ` +
88
- `Check the server log for more details.`);
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
- async function initDeviceStreamingProc (adb, log, deviceInfo, opts = {}) {
97
- const {
98
- width,
99
- height,
100
- bitRate,
101
- } = opts;
102
- const adjustedWidth = parseInt(width, 10) || deviceInfo.width;
103
- const adjustedHeight = parseInt(height, 10) || deviceInfo.height;
104
- const adjustedBitrate = parseInt(bitRate, 10) || DEFAULT_BITRATE;
105
- let screenRecordCmd = SCREENRECORD_BINARY +
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: ${e.message}`);
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
- async function initGstreamerPipeline (deviceStreamingProc, deviceInfo, log, opts = {}) {
161
- const {
162
- width,
163
- height,
164
- quality,
165
- tcpPort,
166
- considerRotation,
167
- logPipelineDetails,
168
- } = opts;
169
- const adjustedWidth = parseInt(width, 10) || deviceInfo.width;
170
- const adjustedHeight = parseInt(height, 10) || deviceInfo.height;
171
- const gstreamerPipeline = new SubProcess(GSTREAMER_BINARY, [
172
- '-v',
173
- 'fdsrc', 'fd=0',
174
- '!', 'video/x-h264,' +
175
- `width=${considerRotation ? Math.max(adjustedWidth, adjustedHeight) : adjustedWidth},` +
176
- `height=${considerRotation ? Math.max(adjustedWidth, adjustedHeight) : adjustedHeight},` +
177
- `framerate=${deviceInfo.fps}/1,` +
178
- 'byte-stream=true',
179
- '!', 'h264parse',
180
- '!', 'queue', 'leaky=downstream',
181
- '!', 'avdec_h264',
182
- '!', 'queue', 'leaky=downstream',
183
- '!', 'jpegenc', `quality=${quality}`,
184
- '!', 'multipartmux', `boundary=${BOUNDARY_STRING}`,
185
- '!', 'tcpserversink', `host=${TCP_HOST}`, `port=${tcpPort}`,
186
- ], {
187
- stdio: [deviceStreamingProc.stdout, 'pipe', 'pipe']
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(async () => {
204
- try {
205
- return (await checkPortStatus(tcpPort, TCP_HOST)) === 'open';
206
- } catch (ign) {
207
- return false;
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: ${e.message}`);
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
- * @typedef {Object} StartScreenStreamingOptions
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
- * Starts device screen broadcast by creating MJPEG server.
263
- * Multiple calls to this method have no effect unless the previous streaming
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
- commands.mobileStartScreenStreaming = async function mobileStartScreenStreaming (options = {}) {
274
- this.ensureFeatureEnabled(ADB_SCREEN_STREAMING_FEATURE);
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
- if (_.isUndefined(this._screenStreamingProps)) {
290
- await verifyStreamingRequirements(this.adb);
291
- }
292
- if (!_.isEmpty(this._screenStreamingProps)) {
293
- this.log.info(`The screen streaming session is already running. ` +
294
- `Stop it first in order to start a new one.`);
295
- return;
296
- }
297
- if ((await checkPortStatus(port, host)) === 'open') {
298
- this.log.info(`The port #${port} at ${host} is busy. ` +
299
- `Assuming the screen streaming is already running`);
300
- return;
301
- }
302
- if ((await checkPortStatus(tcpPort, TCP_HOST)) === 'open') {
303
- this.log.errorAndThrow(`The port #${tcpPort} at ${TCP_HOST} is busy. ` +
304
- `Make sure there are no leftovers from previous sessions.`);
305
- }
306
- this._screenStreamingProps = null;
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
- const deviceInfo = await getDeviceInfo(this.adb, this.log);
309
- const deviceStreamingProc = await initDeviceStreamingProc(this.adb, this.log, deviceInfo, {
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
- quality,
320
- tcpPort,
321
- considerRotation,
322
- logPipelineDetails,
369
+ bitRate,
323
370
  });
324
- } catch (e) {
325
- if (deviceStreamingProc.kill(0)) {
326
- deviceStreamingProc.kill();
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
- let mjpegSocket;
332
- let mjpegServer;
333
- try {
334
- await new B((resolve, reject) => {
335
- mjpegSocket = net.createConnection(tcpPort, TCP_HOST, () => {
336
- this.log.info(`Successfully connected to MJPEG stream at tcp://${TCP_HOST}:${tcpPort}`);
337
- mjpegServer = http.createServer((req, res) => {
338
- const remoteAddress = extractRemoteAddress(req);
339
- const currentPathname = url.parse(req.url).pathname;
340
- this.log.info(`Got an incoming screen bradcasting request from ${remoteAddress} ` +
341
- `(${req.headers['user-agent'] || 'User Agent unknown'}) at ${currentPathname}`);
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
- if (pathname && currentPathname !== pathname) {
344
- this.log.info('Rejecting the broadcast request since it does not match the given pathname');
345
- res.writeHead(404, {
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': 'text/plain; charset=utf-8',
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
- this.log.info('Starting MJPEG broadcast');
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
- mjpegSocket.pipe(res);
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
- mjpegServer.on('error', (e) => {
365
- this.log.warn(e);
441
+ mjpegSocket.on('error', (e) => {
442
+ this.log.error(e);
366
443
  reject(e);
367
444
  });
368
- mjpegServer.on('close', () => {
369
- this.log.debug(`MJPEG server at http://${host}:${port} has been closed`);
370
- });
371
- mjpegServer.on('listening', () => {
372
- this.log.info(`Successfully started MJPEG server at http://${host}:${port}`);
373
- resolve();
374
- });
375
- mjpegServer.listen(port, host);
376
- });
377
- mjpegSocket.on('error', (e) => {
378
- this.log.error(e);
379
- reject(e);
380
- });
381
- }).timeout(STREAMING_STARTUP_TIMEOUT_MS,
382
- `Cannot connect to the streaming server within ${STREAMING_STARTUP_TIMEOUT_MS}ms`);
383
- } catch (e) {
384
- if (deviceStreamingProc.kill(0)) {
385
- deviceStreamingProc.kill();
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
- this._screenStreamingProps = {
400
- deviceStreamingProc,
401
- gstreamerPipeline,
402
- mjpegSocket,
403
- mjpegServer,
404
- };
405
- };
465
+ this._screenStreamingProps = {
466
+ deviceStreamingProc,
467
+ gstreamerPipeline,
468
+ mjpegSocket,
469
+ mjpegServer,
470
+ };
471
+ },
406
472
 
407
- /**
408
- * Stop screen streaming.
409
- * If no screen streaming server has been started then nothing is done.
410
- */
411
- commands.mobileStopScreenStreaming = async function mobileStopScreenStreaming (/* options = {} */) {
412
- if (_.isEmpty(this._screenStreamingProps)) {
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
- const {
420
- deviceStreamingProc,
421
- gstreamerPipeline,
422
- mjpegSocket,
423
- mjpegServer,
424
- } = this._screenStreamingProps;
481
+ const {deviceStreamingProc, gstreamerPipeline, mjpegSocket, mjpegServer} =
482
+ this._screenStreamingProps;
425
483
 
426
- try {
427
- mjpegSocket.end();
428
- if (mjpegServer.listening) {
429
- mjpegServer.close();
430
- }
431
- if (deviceStreamingProc.kill(0)) {
432
- deviceStreamingProc.kill('SIGINT');
433
- }
434
- if (gstreamerPipeline.isRunning) {
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('SIGKILL');
441
- } catch (e1) {
442
- this.log.error(e1);
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
- this.log.info(`Successfully terminated the screen streaming MJPEG server`);
447
- } finally {
448
- this._screenStreamingProps = null;
449
- }
508
+ },
450
509
  };
451
510
 
511
+ mixin(StreamScreenMixin);
512
+
513
+ export default StreamScreenMixin;
452
514
 
453
- export default commands;
515
+ /**
516
+ * @typedef {import('appium-adb').ADB} ADB
517
+ * @typedef {import('@appium/types').AppiumLogger} AppiumLogger
518
+ * @typedef {import('./types').DeviceInfo} DeviceInfo
519
+ */