Skip to content

Commit

Permalink
feat: Add mobile wrappers for media-projection-based screen recording (
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach authored Jun 22, 2022
1 parent a0fec8e commit 30e361f
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 5 deletions.
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,53 @@ type | string | yes | The unlock type. See the documentation on [appium:unlockTy
strategy | string | no | Unlock strategy. See the documentation on [appium:unlockStrategy](#device-locking) capability for more details | uiautomator
timeoutMs | number | no | Unlock timeout. See the documentation on [appium:unlockSuccessTimeout](#device-locking) capability for more details | 5000

### mobile: startMediaProjectionRecording

Starts a new recording of the device activity using [Media Projection](https://developer.android.com/reference/android/media/projection/MediaProjection) API. This API is available since Android 10 (API level 29) and allows to record device screen and audio in high quality. Video and audio encoding is done by Android itself.
The recording is done by [Appium Settings helper](https://github.com/appium/io.appium.settings#internal-audio--video-recording).

#### Arguments

Name | Type | Required | Description | Example
--- | --- | --- | --- | ---
resolution | string | no | The resolution of the resulting video, which usually equals to Full HD 1920x1080 on most phones, however you could change it to one of the following supported resolutions: "1920x1080", "1280x720", "720x480", "320x240", "176x144" | 1280x720
maxDurationSec | number | no | The maximum number of seconds allowed for the recording to run. 900 seconds by default (15 minutes) | 300
priority | string | no | Recording thread priority is set to maximum (`high`) by default. However if you face performance drops during testing with recording enabled, you could reduce the recording priority to `normal` or `low`. | low
filename | string | no | 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. | screen.mp4

#### Returned Result

`true` if a new recording has successfully started. `false` if another recording is currently running.

### mobile: isMediaProjectionRecordingRunning

Check if a media projection recording is currently running

#### Returned Result

`true` if a recording is running.

### mobile: stopMediaProjectionRecording

Stops a recording and retrieves the recently recorded media. If no recording has been started before then an error is thrown. If the recording has been already finished before this API has been called then the most recent recorded media is returned.

#### Arguments

Name | Type | Required | Description | Example
--- | --- | --- | --- | ---
remotePath | string | no | 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 endpoont response value. An exception will be thrown if the generated media file is too big to fit into the available process memory. | https://myserver.com/upload
user | string | no | The name of the user for the remote authentication. | admin
pass | string | no | The password for the remote authentication. | pa$$w0rd
method | string | no | The http multipart upload method name. The 'PUT' one is used by default. | POST
headers | Map<string, string> | no | Additional headers mapping for multipart http(s) uploads | {'Agent': '007'}
fileFieldName | string | no | The name of the form field, where the file content BLOB should be stored for http(s) uploads. `file` by default | blob
formFields | Map<string, string> or Array<Pair> | no | Additional form fields for multipart http(s) uploads. | {'name': 'yolo.mp4'}

#### Returned Result

Base64-encoded content of the recorded media file if `remotePath` argument is falsy or an empty string.


## Backdoor Extension Usage

Expand Down
4 changes: 4 additions & 0 deletions lib/commands/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ extensions.executeMobile = async function executeMobile (mobileCommand, opts = {
unlock: 'mobileUnlock',

refreshGpsCache: 'mobileRefreshGpsCache',

startMediaProjectionRecording: 'mobileStartMediaProjectionRecording',
isMediaProjectionRecordingRunning: 'mobileIsMediaProjectionRecordingRunning',
stopMediaProjectionRecording: 'mobileStopMediaProjectionRecording',
};

if (!_.has(mobileCommandsMapping, mobileCommand)) {
Expand Down
23 changes: 18 additions & 5 deletions lib/driver.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import _ from 'lodash';
import path from 'path';
import B from 'bluebird';
import { BaseDriver, errors, isErrorType, DeviceSettings} from 'appium/driver';
import { EspressoRunner, TEST_APK_PKG } from './espresso-runner';
import { fs, tempDir, zip } from 'appium/support';
Expand Down Expand Up @@ -561,16 +562,22 @@ class EspressoDriver extends BaseDriver {
async deleteSession () {
this.log.debug('Deleting espresso session');

try {
const screenRecordingStopTasks = [async () => {
if (!_.isEmpty(this._screenRecordingProperties)) {
await this.stopRecordingScreen();
}
} catch (ign) {}
}, async () => {
if (await this.mobileIsMediaProjectionRecordingRunning()) {
await this.mobileStopMediaProjectionRecording();
}
}, async () => {
if (!_.isEmpty(this._screenStreamingProps)) {
await this.mobileStopScreenStreaming();
}
}];

await androidHelpers.removeAllSessionWebSocketHandlers(this.server, this.sessionId);

await this.mobileStopScreenStreaming();

if (this.espresso) {
if (this.jwpProxyActive) {
await this.espresso.deleteSession();
Expand All @@ -579,8 +586,14 @@ class EspressoDriver extends BaseDriver {
}
this.jwpProxyActive = false;

// TODO below logic is duplicated from uiautomator2
if (this.adb) {
await B.all(screenRecordingStopTasks.map((task) => {
(async () => {
try {
await task();
} catch (ign) {}
})();
}));
if (this.wasAnimationEnabled) {
try {
await this.adb.setAnimationState(true);
Expand Down

0 comments on commit 30e361f

Please sign in to comment.