appium-android-driver 12.4.7 → 12.4.8

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.
@@ -1,8 +1,12 @@
1
1
  import {fs, net, system, tempDir, timing, util} from '@appium/support';
2
+ import type {NetOptions, HttpUploadOptions} from '@appium/support';
2
3
  import {waitForCondition} from 'asyncbox';
3
4
  import _ from 'lodash';
4
5
  import path from 'path';
5
6
  import {exec} from 'teen_process';
7
+ import type {AndroidDriver} from '../driver';
8
+ import type {ADB} from 'appium-adb';
9
+ import type {StartScreenRecordingOpts, StopScreenRecordingOpts, ScreenRecordingProperties} from './types';
6
10
 
7
11
  const RETRY_PAUSE = 300;
8
12
  const RETRY_TIMEOUT = 5000;
@@ -17,12 +21,22 @@ const FFMPEG_BINARY = `ffmpeg${system.isWindows() ? '.exe' : ''}`;
17
21
  const ADB_PULL_TIMEOUT = 5 * 60 * 1000;
18
22
 
19
23
  /**
24
+ * Starts screen recording on the Android device.
20
25
  *
21
- * @this {import('../driver').AndroidDriver}
22
- * @param {import('./types').StartScreenRecordingOpts} [options={}]
23
- * @returns {Promise<string>}
26
+ * This method uses Android's `screenrecord` command to capture the screen.
27
+ * The recording can be configured with various options such as video size,
28
+ * bit rate, time limit, and more.
29
+ *
30
+ * @param options Recording options. See {@link StartScreenRecordingOpts} for details.
31
+ * @returns Promise that resolves to the result of stopping any previous recording,
32
+ * or an empty string if no previous recording was active.
33
+ * @throws {Error} If screen recording is not supported on the device or emulator,
34
+ * or if the time limit is invalid.
24
35
  */
25
- export async function startRecordingScreen(options = {}) {
36
+ export async function startRecordingScreen(
37
+ this: AndroidDriver,
38
+ options: StartScreenRecordingOpts = {},
39
+ ): Promise<string> {
26
40
  await verifyScreenRecordIsSupported(this.adb, this.isEmulator());
27
41
 
28
42
  let result = '';
@@ -47,7 +61,8 @@ export async function startRecordingScreen(options = {}) {
47
61
 
48
62
  if (!_.isEmpty(this._screenRecordingProperties)) {
49
63
  // XXX: this doesn't need to be done in serial, does it?
50
- for (const record of this._screenRecordingProperties.records || []) {
64
+ const props = this._screenRecordingProperties;
65
+ for (const record of props.records || []) {
51
66
  await this.adb.rimraf(record);
52
67
  }
53
68
  this._screenRecordingProperties = undefined;
@@ -61,7 +76,7 @@ export async function startRecordingScreen(options = {}) {
61
76
  );
62
77
  }
63
78
 
64
- this._screenRecordingProperties = {
79
+ const recordingProps: ScreenRecordingProperties = {
65
80
  timer: new timing.Timer().start(),
66
81
  videoSize,
67
82
  timeLimit,
@@ -72,42 +87,55 @@ export async function startRecordingScreen(options = {}) {
72
87
  recordingProcess: null,
73
88
  stopped: false,
74
89
  };
75
- await scheduleScreenRecord.bind(this)(this._screenRecordingProperties);
90
+ this._screenRecordingProperties = recordingProps;
91
+ await scheduleScreenRecord.bind(this)(recordingProps);
76
92
  return result;
77
93
  }
78
94
 
79
95
  /**
96
+ * Stops screen recording and returns the recorded video.
97
+ *
98
+ * This method stops any active screen recording session and returns the recorded
99
+ * video as a base64-encoded string or uploads it to a remote location if specified.
100
+ * If multiple recording chunks were created (for long recordings), they will be
101
+ * merged using ffmpeg if available.
80
102
  *
81
- * @this {import('../driver').AndroidDriver}
82
- * @param {import('./types').StopScreenRecordingOpts} [options={}]
83
- * @returns {Promise<string>}
103
+ * @param options Stop recording options. See {@link StopScreenRecordingOpts} for details.
104
+ * @returns Promise that resolves to the recorded video as a base64-encoded string
105
+ * if `remotePath` is not provided, or an empty string if the video was uploaded to a remote location.
106
+ * @throws {Error} If screen recording is not supported, no recording was active,
107
+ * or if the recording process cannot be stopped.
84
108
  */
85
- export async function stopRecordingScreen(options = {}) {
109
+ export async function stopRecordingScreen(
110
+ this: AndroidDriver,
111
+ options: StopScreenRecordingOpts = {},
112
+ ): Promise<string> {
86
113
  await verifyScreenRecordIsSupported(this.adb, this.isEmulator());
87
114
 
88
- if (!_.isEmpty(this._screenRecordingProperties)) {
89
- this._screenRecordingProperties.stopped = true;
115
+ const props = this._screenRecordingProperties;
116
+ if (!_.isEmpty(props)) {
117
+ props.stopped = true;
90
118
  }
91
119
 
92
120
  try {
93
121
  await terminateBackgroundScreenRecording(this.adb, false);
94
122
  } catch (err) {
95
- this.log.warn(/** @type {Error} */ (err).message);
96
- if (!_.isEmpty(this._screenRecordingProperties)) {
123
+ this.log.warn((err as Error).message);
124
+ if (!_.isEmpty(props)) {
97
125
  this.log.warn('The resulting video might be corrupted');
98
126
  }
99
127
  }
100
128
 
101
- if (_.isEmpty(this._screenRecordingProperties)) {
129
+ if (_.isEmpty(props)) {
102
130
  this.log.info(
103
131
  `Screen recording has not been previously started by Appium. There is nothing to stop`,
104
132
  );
105
133
  return '';
106
134
  }
107
135
 
108
- if (this._screenRecordingProperties.recordingProcess?.isRunning) {
136
+ if (props.recordingProcess?.isRunning) {
109
137
  try {
110
- await this._screenRecordingProperties.recordingProcess.stop(
138
+ await props.recordingProcess.stop(
111
139
  'SIGINT',
112
140
  PROCESS_SHUTDOWN_TIMEOUT,
113
141
  );
@@ -116,10 +144,10 @@ export async function stopRecordingScreen(options = {}) {
116
144
  `Unable to stop screen recording within ${PROCESS_SHUTDOWN_TIMEOUT}ms`,
117
145
  );
118
146
  }
119
- this._screenRecordingProperties.recordingProcess = null;
147
+ props.recordingProcess = null;
120
148
  }
121
149
 
122
- if (_.isEmpty(this._screenRecordingProperties.records)) {
150
+ if (_.isEmpty(props.records)) {
123
151
  throw this.log.errorWithException(
124
152
  `No screen recordings have been stored on the device so far. ` +
125
153
  `Are you sure the ${SCREENRECORD_BINARY} utility works as expected?`,
@@ -128,14 +156,14 @@ export async function stopRecordingScreen(options = {}) {
128
156
 
129
157
  const tmpRoot = await tempDir.openDir();
130
158
  try {
131
- const localRecords = [];
132
- for (const pathOnDevice of this._screenRecordingProperties.records) {
159
+ const localRecords: string[] = [];
160
+ for (const pathOnDevice of props.records) {
133
161
  const relativePath = path.resolve(tmpRoot, path.posix.basename(pathOnDevice));
134
162
  localRecords.push(relativePath);
135
163
  await this.adb.pull(pathOnDevice, relativePath, { timeout: ADB_PULL_TIMEOUT });
136
164
  await this.adb.rimraf(pathOnDevice);
137
165
  }
138
- let resultFilePath = /** @type {string} */ (_.last(localRecords));
166
+ let resultFilePath = _.last(localRecords) as string;
139
167
  if (localRecords.length > 1) {
140
168
  this.log.info(`Got ${localRecords.length} screen recordings. Trying to merge them`);
141
169
  try {
@@ -143,7 +171,7 @@ export async function stopRecordingScreen(options = {}) {
143
171
  } catch (e) {
144
172
  this.log.warn(
145
173
  `Cannot merge the recorded files. The most recent screen recording is going to be returned as the result. ` +
146
- `Original error: ${/** @type {Error} */ (e).message}`,
174
+ `Original error: ${(e as Error).message}`,
147
175
  );
148
176
  }
149
177
  }
@@ -162,23 +190,17 @@ export async function stopRecordingScreen(options = {}) {
162
190
 
163
191
  // #region Internal helpers
164
192
 
165
- /**
166
- *
167
- * @param {string} localFile
168
- * @param {string} [remotePath]
169
- * @param {import('./types').StopScreenRecordingOpts} uploadOptions
170
- * @returns {Promise<string>}
171
- */
172
- async function uploadRecordedMedia(localFile, remotePath, uploadOptions = {}) {
193
+ async function uploadRecordedMedia(
194
+ localFile: string,
195
+ remotePath?: string,
196
+ uploadOptions: StopScreenRecordingOpts = {},
197
+ ): Promise<string> {
173
198
  if (_.isEmpty(remotePath)) {
174
199
  return (await util.toInMemoryBase64(localFile)).toString();
175
200
  }
176
201
 
177
202
  const {user, pass, method, headers, fileFieldName, formFields} = uploadOptions;
178
- /**
179
- * @type {import('@appium/support').NetOptions & import('@appium/support').HttpUploadOptions}
180
- */
181
- const options = {
203
+ const options: NetOptions & HttpUploadOptions = {
182
204
  method: method || 'PUT',
183
205
  headers,
184
206
  fileFieldName,
@@ -187,16 +209,11 @@ async function uploadRecordedMedia(localFile, remotePath, uploadOptions = {}) {
187
209
  if (user && pass) {
188
210
  options.auth = {user, pass};
189
211
  }
190
- await net.uploadFile(localFile, /** @type {string} */ (remotePath), options);
212
+ await net.uploadFile(localFile, remotePath as string, options);
191
213
  return '';
192
214
  }
193
215
 
194
- /**
195
- *
196
- * @param {ADB} adb
197
- * @param {boolean} isEmulator
198
- */
199
- async function verifyScreenRecordIsSupported(adb, isEmulator) {
216
+ async function verifyScreenRecordIsSupported(adb: ADB, isEmulator: boolean): Promise<void> {
200
217
  const apiLevel = await adb.getApiLevel();
201
218
  if (isEmulator && apiLevel < MIN_EMULATOR_API_LEVEL) {
202
219
  throw new Error(
@@ -205,12 +222,10 @@ async function verifyScreenRecordIsSupported(adb, isEmulator) {
205
222
  }
206
223
  }
207
224
 
208
- /**
209
- * @this {import('../driver').AndroidDriver}
210
- * @param {import('@appium/types').StringRecord} recordingProperties
211
- * @returns {Promise<void>}
212
- */
213
- async function scheduleScreenRecord(recordingProperties) {
225
+ async function scheduleScreenRecord(
226
+ this: AndroidDriver,
227
+ recordingProperties: ScreenRecordingProperties,
228
+ ): Promise<void> {
214
229
  if (recordingProperties.stopped) {
215
230
  return;
216
231
  }
@@ -219,7 +234,7 @@ async function scheduleScreenRecord(recordingProperties) {
219
234
 
220
235
  let currentTimeLimit = MAX_RECORDING_TIME_SEC;
221
236
  if (util.hasValue(recordingProperties.currentTimeLimit)) {
222
- const currentTimeLimitInt = parseInt(recordingProperties.currentTimeLimit, 10);
237
+ const currentTimeLimitInt = parseInt(String(recordingProperties.currentTimeLimit), 10);
223
238
  if (!isNaN(currentTimeLimitInt) && currentTimeLimitInt < MAX_RECORDING_TIME_SEC) {
224
239
  currentTimeLimit = currentTimeLimitInt;
225
240
  }
@@ -238,13 +253,13 @@ async function scheduleScreenRecord(recordingProperties) {
238
253
  }
239
254
  const currentDuration = timer.getDuration().asSeconds.toFixed(0);
240
255
  this.log.debug(`The overall screen recording duration is ${currentDuration}s so far`);
241
- const timeLimitInt = parseInt(timeLimit, 10);
242
- if (isNaN(timeLimitInt) || currentDuration >= timeLimitInt) {
256
+ const timeLimitInt = parseInt(String(timeLimit), 10);
257
+ if (isNaN(timeLimitInt) || Number(currentDuration) >= timeLimitInt) {
243
258
  this.log.debug('There is no need to start the next recording chunk');
244
259
  return;
245
260
  }
246
261
 
247
- recordingProperties.currentTimeLimit = timeLimitInt - currentDuration;
262
+ recordingProperties.currentTimeLimit = timeLimitInt - Number(currentDuration);
248
263
  const chunkDuration =
249
264
  recordingProperties.currentTimeLimit < MAX_RECORDING_TIME_SEC
250
265
  ? recordingProperties.currentTimeLimit
@@ -257,7 +272,7 @@ async function scheduleScreenRecord(recordingProperties) {
257
272
  try {
258
273
  await scheduleScreenRecord.bind(this)(recordingProperties);
259
274
  } catch (e) {
260
- this.log.error(/** @type {Error} */ (e).stack);
275
+ this.log.error((e as Error).stack);
261
276
  recordingProperties.stopped = true;
262
277
  }
263
278
  })();
@@ -280,13 +295,10 @@ async function scheduleScreenRecord(recordingProperties) {
280
295
  recordingProperties.recordingProcess = recordingProc;
281
296
  }
282
297
 
283
- /**
284
- *
285
- * @this {import('../driver').AndroidDriver}
286
- * @param {string[]} mediaFiles
287
- * @returns {Promise<string>}
288
- */
289
- async function mergeScreenRecords(mediaFiles) {
298
+ async function mergeScreenRecords(
299
+ this: AndroidDriver,
300
+ mediaFiles: string[],
301
+ ): Promise<string> {
290
302
  try {
291
303
  await fs.which(FFMPEG_BINARY);
292
304
  } catch {
@@ -310,14 +322,9 @@ async function mergeScreenRecords(mediaFiles) {
310
322
  return result;
311
323
  }
312
324
 
313
- /**
314
- *
315
- * @param {ADB} adb
316
- * @param {boolean} force
317
- * @returns {Promise<boolean>}
318
- */
319
- async function terminateBackgroundScreenRecording(adb, force = true) {
320
- const isScreenrecordRunning = async () => _.includes(await adb.listProcessStatus(), SCREENRECORD_BINARY);
325
+ async function terminateBackgroundScreenRecording(adb: ADB, force = true): Promise<boolean> {
326
+ const isScreenrecordRunning = async (): Promise<boolean> =>
327
+ _.includes(await adb.listProcessStatus(), SCREENRECORD_BINARY);
321
328
  if (!await isScreenrecordRunning()) {
322
329
  return false;
323
330
  }
@@ -331,13 +338,10 @@ async function terminateBackgroundScreenRecording(adb, force = true) {
331
338
  return true;
332
339
  } catch (err) {
333
340
  throw new Error(
334
- `Unable to stop the background screen recording: ${/** @type {Error} */ (err).message}`,
341
+ `Unable to stop the background screen recording: ${(err as Error).message}`,
335
342
  );
336
343
  }
337
344
  }
338
345
 
339
346
  // #endregion
340
347
 
341
- /**
342
- * @typedef {import('appium-adb').ADB} ADB
343
- */
@@ -1,5 +1,7 @@
1
1
  import type {HTTPMethod, StringRecord} from '@appium/types';
2
2
  import type {AndroidDriverCaps} from '../driver';
3
+ import type {SubProcess} from 'teen_process';
4
+ import {timing} from '@appium/support';
3
5
 
4
6
  /**
5
7
  * @privateRemarks probably better defined in `appium-adb`
@@ -618,3 +620,18 @@ export interface InjectedImageProperties {
618
620
  position?: InjectedImagePosition;
619
621
  rotation?: InjectedImageRotation;
620
622
  }
623
+
624
+ /**
625
+ * @internal
626
+ */
627
+ export interface ScreenRecordingProperties {
628
+ timer: timing.Timer;
629
+ videoSize?: string;
630
+ timeLimit: string | number;
631
+ currentTimeLimit?: string | number;
632
+ bitRate?: string | number;
633
+ bugReport?: boolean;
634
+ records: string[];
635
+ recordingProcess: SubProcess | null;
636
+ stopped: boolean;
637
+ }
package/lib/driver.ts CHANGED
@@ -210,6 +210,7 @@ import {getSystemBars, mobilePerformStatusBarCommand} from './commands/system-ba
210
210
  import {getDeviceTime, mobileGetDeviceTime} from './commands/time';
211
211
  import { executeMethodMap } from './execute-method-map';
212
212
  import { LRUCache } from 'lru-cache';
213
+ import type {ScreenRecordingProperties} from './commands/types';
213
214
 
214
215
  export type AndroidDriverCaps = DriverCaps<AndroidDriverConstraints>;
215
216
  export type W3CAndroidDriverCaps = W3CDriverCaps<AndroidDriverConstraints>;
@@ -240,7 +241,7 @@ class AndroidDriver
240
241
  _wasWindowAnimationDisabled?: boolean;
241
242
  _cachedActivityArgs: StringRecord;
242
243
  _screenStreamingProps?: StringRecord;
243
- _screenRecordingProperties?: StringRecord;
244
+ _screenRecordingProperties?: ScreenRecordingProperties;
244
245
  _logcatWebsocketListener?: LogcatListener;
245
246
  _bidiServerLogListener?: (...args: any[]) => void;
246
247
  _bidiProxyUrl: string | null = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-android-driver",
3
- "version": "12.4.7",
3
+ "version": "12.4.8",
4
4
  "description": "Android UiAutomator and Chrome support for Appium",
5
5
  "keywords": [
6
6
  "appium",