appium-android-driver
Version:
Android UiAutomator and Chrome support for Appium
202 lines (185 loc) • 7.06 kB
text/typescript
import {fs, net, util} from '@appium/support';
import type {NetOptions, HttpUploadOptions} from '@appium/support';
import _ from 'lodash';
import moment from 'moment';
import path from 'node:path';
import type {HTTPMethod, StringRecord} from '@appium/types';
import type {AndroidDriver} from '../driver';
import type {ADB} from 'appium-adb';
import type {FormFields} from './types';
// https://github.com/appium/io.appium.settings#internal-audio--video-recording
const DEFAULT_EXT = '.mp4';
const MIN_API_LEVEL = 29;
const DEFAULT_FILENAME_FORMAT = 'YYYY-MM-DDTHH-mm-ss';
/**
* Starts media projection-based screen recording on the Android device.
*
* @param resolution Maximum supported resolution on-device (Detected automatically by the app
* itself), which usually equals to Full HD 1920x1080 on most phones however
* you can change it to following supported resolutions as well: "1920x1080",
* "1280x720", "720x480", "320x240", "176x144".
* @param priority Recording thread priority.
* If you face performance drops during testing with recording enabled, you
* can reduce recording priority. 'high' by default.
* @param maxDurationSec Maximum allowed duration is 15 minutes; you can increase it if your test
* takes longer than that. 900s by default.
* @param filename You can type recording video file name as you want, but recording currently
* supports only "mp4" format so your filename must end with ".mp4". An
* invalid file name will fail to start the recording. If not provided then
* the current timestamp will be used as file name.
* @returns Promise that resolves to `true` if recording was started, `false` if another recording is already in progress.
*/
export async function mobileStartMediaProjectionRecording(
this: AndroidDriver,
resolution?: string,
priority?: 'high' | 'normal' | 'low',
maxDurationSec?: number,
filename?: string,
): Promise<boolean> {
await verifyMediaProjectionRecordingIsSupported(this.adb);
const recorder = this.settingsApp.makeMediaProjectionRecorder();
const fname = adjustMediaExtension(filename || moment().format(DEFAULT_FILENAME_FORMAT));
const didStart = await recorder.start({
resolution,
priority,
maxDurationSec,
filename: fname,
});
if (didStart) {
this.log.info(`A new media projection recording '${fname}' has been successfully started`);
} else {
this.log.info(
'Another media projection recording is already in progress. There is nothing to start',
);
}
return didStart;
}
/**
* Checks if media projection recording is currently running.
*
* @returns Promise that resolves to `true` if recording is running, `false` otherwise.
*/
export async function mobileIsMediaProjectionRecordingRunning(
this: AndroidDriver,
): Promise<boolean> {
await verifyMediaProjectionRecordingIsSupported(this.adb);
const recorder = this.settingsApp.makeMediaProjectionRecorder();
return await recorder.isRunning();
}
/**
* Stops the media projection recording and returns the recorded video.
*
* @param remotePath The path to the remote location, where the resulting video should be
* uploaded. The following protocols are supported: http/https, ftp. Null or
* empty string value (the default setting) means the content of resulting
* file should be encoded as Base64 and passed as the endpoint response value.
* An exception will be thrown if the generated media file is too big to fit
* into the available process memory.
* @param user The name of the user for the remote authentication.
* @param pass The password for the remote authentication.
* @param method The http multipart upload method name. 'PUT' by default.
* @param headers Additional headers mapping for multipart http(s) uploads.
* @param fileFieldName The name of the form field, where the file content BLOB should be stored
* for http(s) uploads. 'file' by default.
* @param formFields Additional form fields for multipart http(s) uploads.
* @param uploadTimeout The actual media upload request timeout in milliseconds.
* Defaults to `@appium/support.net.DEFAULT_TIMEOUT_MS`.
* @returns Promise that resolves to the recorded video as a base64-encoded string
* if `remotePath` is not provided, or an empty string if the video was uploaded to a remote location.
* @throws {Error} If no recent recording was found.
*/
export async function mobileStopMediaProjectionRecording(
this: AndroidDriver,
remotePath?: string,
user?: string,
pass?: string,
method?: HTTPMethod,
headers?: StringRecord,
fileFieldName?: string,
formFields?: FormFields,
uploadTimeout?: number,
): Promise<string> {
await verifyMediaProjectionRecordingIsSupported(this.adb);
const recorder = this.settingsApp.makeMediaProjectionRecorder();
if (await recorder.stop()) {
this.log.info('Successfully stopped a media projection recording. Pulling the recorded media');
} else {
this.log.info('Media projection recording is not running. There is nothing to stop');
}
const recentRecordingPath = await recorder.pullRecent();
if (!recentRecordingPath) {
throw new Error(`No recent media projection recording have been found. Did you start any?`);
}
if (_.isEmpty(remotePath)) {
const {size} = await fs.stat(recentRecordingPath);
this.log.debug(
`The size of the resulting media projection recording is ${util.toReadableSizeString(size)}`,
);
}
try {
return await uploadRecordedMedia(recentRecordingPath, remotePath, {
user,
pass,
method,
headers,
fileFieldName,
formFields,
uploadTimeout,
});
} finally {
await fs.rimraf(path.dirname(recentRecordingPath));
}
}
// #region Internal helpers
async function uploadRecordedMedia(
localFile: string,
remotePath?: string,
uploadOptions: UploadOptions = {},
): Promise<string> {
if (_.isEmpty(remotePath)) {
return (await util.toInMemoryBase64(localFile)).toString();
}
const {
user,
pass,
method,
headers,
fileFieldName,
formFields,
uploadTimeout: timeout,
} = uploadOptions;
const options: NetOptions & HttpUploadOptions = {
method: method || 'PUT',
headers,
fileFieldName,
formFields,
timeout,
};
if (user && pass) {
options.auth = {user, pass};
}
await net.uploadFile(localFile, remotePath as string, options);
return '';
}
function adjustMediaExtension(name: string): string {
return _.toLower(name).endsWith(DEFAULT_EXT) ? name : `${name}${DEFAULT_EXT}`;
}
async function verifyMediaProjectionRecordingIsSupported(adb: ADB): Promise<void> {
const apiLevel = await adb.getApiLevel();
if (apiLevel < MIN_API_LEVEL) {
throw new Error(
`Media projection-based recording is not available on API Level ${apiLevel}. ` +
`Minimum required API Level is ${MIN_API_LEVEL}.`,
);
}
}
// #endregion
interface UploadOptions {
user?: string;
pass?: string;
method?: HTTPMethod;
headers?: StringRecord;
fileFieldName?: string;
formFields?: FormFields;
uploadTimeout?: number;
}