From 31a17891aa53514651df4af80c3449fce565c675 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Fri, 27 Jul 2018 12:24:40 +0200 Subject: [PATCH 1/8] Add a possibility to record longer videos in Android --- lib/commands/recordscreen.js | 385 ++++++++++++----------- test/unit/commands/recordscreen-specs.js | 165 +--------- 2 files changed, 203 insertions(+), 347 deletions(-) diff --git a/lib/commands/recordscreen.js b/lib/commands/recordscreen.js index 1351aa98f..cda8a24b2 100644 --- a/lib/commands/recordscreen.js +++ b/lib/commands/recordscreen.js @@ -1,130 +1,162 @@ import _ from 'lodash'; import _fs from 'fs'; import url from 'url'; -import { retryInterval, waitForCondition } from 'asyncbox'; -import B from 'bluebird'; -import { util, fs, net } from 'appium-support'; +import { retryInterval } from 'asyncbox'; +import { util, fs, net, tempDir, system } from 'appium-support'; import log from '../logger'; -import temp from 'temp'; +import { SubProcess, exec } from 'teen_process'; +import path from 'path'; let commands = {}, extensions = {}; const RETRY_PAUSE = 1000; const MAX_RECORDING_TIME_SEC = 60 * 3; +const MAX_TIME_SEC = 60 * 30; const DEFAULT_RECORDING_TIME_SEC = MAX_RECORDING_TIME_SEC; const PROCESS_SHUTDOWN_TIMEOUT_SEC = 5; const SCREENRECORD_BINARY = 'screenrecord'; const DEFAULT_EXT = '.mp4'; const MIN_EMULATOR_API_LEVEL = 27; +const FFMPEG_BINARY = `ffmpeg${system.isWindows() ? '.exe' : ''}`; +async function uploadRecordedMedia (adb, localFile, remotePath = null, uploadOptions = {}) { + const {size} = await fs.stat(localFile); + log.debug(`The size of the recent screen recording is ${util.toReadableSizeString(size)}`); + if (_.isEmpty(remotePath)) { + const memoryUsage = process.memoryUsage(); + const maxMemoryLimit = (memoryUsage.heapTotal - memoryUsage.heapUsed) / 2; + if (size >= maxMemoryLimit) { + throw new Error(`Cannot read the recorded media '${localFile}' to the memory, ` + + `because the file is too large ` + + `(${util.toReadableSizeString(size)} >= ${util.toReadableSizeString(maxMemoryLimit)}). ` + + `Try to provide a link to a remote writable location instead.`); + } + return (await fs.readFile(localFile)).toString('base64'); + } -async function extractCurrentRecordingPath (adb, pids) { - let lsofOutput = ''; - try { - const {output} = await adb.shell(['lsof', '-p', pids.join(',')]); - lsofOutput = output; - } catch (err) { - log.warn(`Cannot extract the path to the current screen capture. ` + - `Original error: ${err.message}`); - return null; + const remoteUrl = url.parse(remotePath); + let options = {}; + const {user, pass, method} = uploadOptions; + if (remoteUrl.protocol.startsWith('http')) { + options = { + url: remoteUrl.href, + method: method || 'PUT', + multipart: [{ body: _fs.createReadStream(localFile) }], + }; + if (user && pass) { + options.auth = {user, pass}; + } + } else if (remoteUrl.protocol.startsWith('ftp')) { + options = { + host: remoteUrl.hostname, + port: remoteUrl.port || 21, + }; + if (user && pass) { + options.user = user; + options.pass = pass; + } } - log.debug(`Got the following output from lsof: ${lsofOutput}`); - const pattern = new RegExp(/\d+\s+(\/.*\.mp4)/); - const matches = pattern.exec(lsofOutput); - return _.isEmpty(matches) ? null : _.last(matches); + await net.uploadFile(localFile, remotePath, options); + return ''; } -async function finishScreenCapture (adb, pids) { - try { - await adb.shell(['kill', '-2', ...pids]); - } catch (e) { - return true; +async function verifyScreenRecordIsSupported (adb, isEmulator) { + const apiLevel = await adb.getApiLevel(); + if (isEmulator && apiLevel < MIN_EMULATOR_API_LEVEL) { + throw new Error(`Screen recording does not work on emulators running Android API level less than ${MIN_EMULATOR_API_LEVEL}`); } - try { - // Wait until the process is terminated - await waitForCondition(async () => { - try { - const output = await adb.shell(['ps']); - for (const pid of pids) { - if (new RegExp(`\\b${pid}\\b[^\\n]+\\b${SCREENRECORD_BINARY}$`, 'm').test(output)) { - return false; - } - } - return true; - } catch (err) { - log.warn(err.message); - return false; - } - }, {waitMs: PROCESS_SHUTDOWN_TIMEOUT_SEC * 1000, intervalMs: 500}); - } catch (e) { - return false; + if (apiLevel < 19) { + throw new Error(`Screen recording not available on API Level ${apiLevel}. Minimum API Level is 19.`); } - return true; } -async function uploadRecordedMedia (adb, pathOnDevice, remotePath = null, uploadOptions = {}) { - const localFile = temp.path({prefix: 'appium', suffix: DEFAULT_EXT}); - try { - await adb.pull(pathOnDevice, localFile); +async function scheduleScreenRecord (adb, recordingProperties) { + if (recordingProperties.stopped) { + return; + } - const {size} = await fs.stat(localFile); - log.debug(`The size of the recent screen recording is ${util.toReadableSizeString(size)}`); - if (_.isEmpty(remotePath)) { - const memoryUsage = process.memoryUsage(); - const maxMemoryLimit = (memoryUsage.heapTotal - memoryUsage.heapUsed) / 2; - if (size >= maxMemoryLimit) { - throw new Error(`Cannot read the recorded media '${pathOnDevice}' to the memory, ` + - `because the file is too large ` + - `(${util.toReadableSizeString(size)} >= ${util.toReadableSizeString(maxMemoryLimit)}). ` + - `Try to provide a link to a remote writable location instead.`); - } - const content = await fs.readFile(localFile); - return content.toString('base64'); + const { + startTimestamp, + videoSize, + bitRate, + timeLimit, + bugReport, + } = recordingProperties; + //make adb command + const cmd = [SCREENRECORD_BINARY]; + if (util.hasValue(videoSize)) { + cmd.push('--size', videoSize); + } + if (util.hasValue(timeLimit)) { + let timeLimitArg = parseInt(timeLimit, 10); + if (!isNaN(timeLimitArg)) { + cmd.push('--time-limit', + `${timeLimitArg <= MAX_RECORDING_TIME_SEC ? timeLimitArg : MAX_RECORDING_TIME_SEC}`); } + } + if (util.hasValue(bitRate)) { + cmd.push('--bit-rate', `${bitRate}`); + } + if (bugReport) { + cmd.push('--bugreport'); + } + const pathOnDevice = `/sdcard/${Math.floor(new Date())}${DEFAULT_EXT}`; + cmd.push(pathOnDevice); - const remoteUrl = url.parse(remotePath); - let options = {}; - const {user, pass, method} = uploadOptions; - if (remoteUrl.protocol.startsWith('http')) { - options = { - url: remoteUrl.href, - method: method || 'PUT', - multipart: [{ body: _fs.createReadStream(localFile) }], - }; - if (user && pass) { - options.auth = {user, pass}; - } - } else if (remoteUrl.protocol.startsWith('ftp')) { - options = { - host: remoteUrl.hostname, - port: remoteUrl.port || 21, - }; - if (user && pass) { - options.user = user; - options.pass = pass; - } + const recordingProc = new SubProcess(adb.executable.path, + [...adb.executable.defaultArgs, 'shell', ...cmd]); + + recordingProc.on('end', () => { + if (recordingProperties.stopped || !util.hasValue(timeLimit)) { + return; } - await net.uploadFile(localFile, remotePath, options); - return ''; - } finally { - await fs.rimraf(localFile); - try { - await adb.rimraf(pathOnDevice); - } catch (e) { - log.warn(`Cannot delete the recorded screen media '${pathOnDevice}' from the device. Continuing anyway`); + const totalDuration = process.hrtime(startTimestamp)[0]; + log.debug(`The overall screen recording duration is ${totalDuration}s so far`); + const timeLimitInt = parseInt(timeLimit, 10); + if (isNaN(timeLimitInt) || timeLimitInt >= totalDuration) { + log.debug('There is no need to start the next recording chunk'); + return; + } + + log.debug(`Starting the next chunk of screen recording in order to achieve ${timeLimitInt}s duration`); + scheduleScreenRecord(adb, recordingProperties) + .catch((e) => { + log.error(e.stack); + recordingProperties.stopped = true; + }); + }); + + await recordingProc.start(0); + await retryInterval(10, RETRY_PAUSE, async () => { + if (!await adb.fileExists(pathOnDevice)) { + throw new Error(`The remote file '${pathOnDevice}' does not exist`); } + }); + + if (_.isArray(recordingProperties.records)) { + recordingProperties.records.push(pathOnDevice); } + recordingProperties.recordingProcess = recordingProc; } -async function verifyScreenRecordIsSupported (adb, isEmulator) { - const apiLevel = await adb.getApiLevel(); - if (isEmulator && apiLevel < MIN_EMULATOR_API_LEVEL) { - throw new Error(`Screen recording does not work on emulators running Android API level less than ${MIN_EMULATOR_API_LEVEL}`); - } - if (apiLevel < 19) { - throw new Error(`Screen recording not available on API Level ${apiLevel}. Minimum API Level is 19.`); +async function mergeScreenRecords (mediaFiles) { + try { + await fs.which(FFMPEG_BINARY); + } catch (e) { + throw new Error(`${FFMPEG_BINARY} utility is not available in PATH. Please install it from https://www.ffmpeg.org/`); } + const configContent = mediaFiles + .map((x) => `file '${x}'`) + .join('\n'); + const configFile = path.resolve(path.dirname(mediaFiles[0]), 'config.txt'); + await fs.writeFile(configFile, configContent, 'utf8'); + log.debug(`Generated merging config '${configFile}' with the content:\n${configContent}`); + const result = path.resolve(path.dirname(mediaFiles[0]), `merge_${Math.floor(new Date())}${DEFAULT_EXT}`); + const args = ['-safe', '0', '-f', 'concat', '-i', configFile, '-c', 'copy', result]; + log.info(`Initiating screen records merge with command '${FFMPEG_BINARY} ${args.join(' ')}'`); + await exec(FFMPEG_BINARY, args); + return result; } @@ -151,7 +183,12 @@ async function verifyScreenRecordIsSupported (adb, isEmulator) { * @property {?boolean} bugReport - Set it to `true` in order to display additional information on the video overlay, * such as a timestamp, that is helpful in videos captured to illustrate bugs. * This option is only supported since API level 27 (Android P). - * @property {?string|number} timeLimit - The maximum recording time, in seconds. The default and maximum value is 180 (3 minutes). + * @property {?string|number} timeLimit - The maximum recording time, in seconds. The default value is 180 (3 minutes). + * The maximum value is 1800 (30 minutes). If the passed value is greater than 180 then + * the algorithm will try to schedule multiple screen recording chunks and merge the + * resulting videos into a single media file using `ffmpeg` utility. + * If the utility is not available in PATH then the most recent screen recording chunk is + * going to be returned. * @property {?string|number} bitRate - The video bit rate for the video, in megabits per second. * The default value is 4. You can increase the bit rate to improve video quality, * but doing so results in larger movie files. @@ -180,6 +217,13 @@ commands.startRecordingScreen = async function (options = {}) { let result = ''; if (!forceRestart) { result = await this.stopRecordingScreen(options); + } else if (!_.isEmpty(this._screenRecordingProperties)) { + if (this._screenRecordingProperties.recordingProcess && this._screenRecordingProperties.recordingProcess.isRunning) { + try { + await this._screenRecordingProperties.recordingProcess.stop('SIGTERM', PROCESS_SHUTDOWN_TIMEOUT_SEC * 1000); + this._screenRecordingProperties.recordingProcess = null; + } catch (ign) {} + } } try { const pids = (await this.adb.getPIDsByName(SCREENRECORD_BINARY)).map((p) => `${p}`); @@ -189,73 +233,31 @@ commands.startRecordingScreen = async function (options = {}) { } catch (err) { log.errorAndThrow(`Unable to stop screen recording: ${err.message}`); } - if (!_.isEmpty(this._recentScreenRecordingPath)) { - try { - await this.adb.rimraf(this._recentScreenRecordingPath); - } catch (ign) {} - this._recentScreenRecordingPath = null; + if (!_.isEmpty(this._screenRecordingProperties)) { + for (const record of (this._screenRecordingProperties.records || [])) { + await this.adb.rimraf(record); + } + this._screenRecordingProperties = null; } - const pathOnDevice = `/sdcard/${Math.floor(new Date())}${DEFAULT_EXT}`; - - //make adb command - const cmd = [SCREENRECORD_BINARY]; - if (util.hasValue(videoSize)) { - cmd.push('--size', videoSize); - } - if (util.hasValue(timeLimit)) { - cmd.push('--time-limit', `${timeLimit}`); - } - if (util.hasValue(bitRate)) { - cmd.push('--bit-rate', `${bitRate}`); - } - if (bugReport) { - cmd.push('--bugreport'); + const timeout = parseFloat(timeLimit); + if (isNaN(timeout) || timeout > MAX_TIME_SEC || timeout <= 0) { + throw new Error(`The timeLimit value must be in range (0, ${MAX_TIME_SEC}] seconds. ` + + `The value of ${timeLimit} has been passed instead.`); } - cmd.push(pathOnDevice); - - // wrap in a manual Promise so we can handle errors in adb shell operation - return await new B(async (resolve, reject) => { - let err = null; - let timeout = Math.floor(parseFloat(timeLimit) * 1000); - if (timeout > MAX_RECORDING_TIME_SEC * 1000 || timeout <= 0) { - return reject(new Error(`The timeLimit value must be in range (0, ${MAX_RECORDING_TIME_SEC}] seconds. ` + - `The value of ${timeLimit} has been passed instead.`)); - } - log.debug(`Beginning screen recording with command: 'adb shell ${cmd.join(' ')}'. ` + - `Will timeout in ${timeout / 1000} s`); - // screenrecord has its owen timer, so we only use this one as a safety precaution - timeout += PROCESS_SHUTDOWN_TIMEOUT_SEC * 1000 * 2; - // do not await here, as the call runs in the background and we check for its product - this.adb.shell(cmd, {timeout, killSignal: 'SIGINT'}).catch((e) => { - err = e; - }); - - // there is the delay time to start recording the screen, so, wait until it is ready. - // the ready condition is - // 1. check the movie file is created - // 2. check it is started to capture the screen - try { - await retryInterval(10, RETRY_PAUSE, async () => { - if (err) { - return; - } - if (!await this.adb.fileExists(pathOnDevice)) { - throw new Error(`The remote file '${pathOnDevice}' does not exist`); - } - }); - } catch (e) { - err = e; - } - - if (err) { - log.error(`Error recording screen: ${err.message}`); - return reject(err); - } - this._recentScreenRecordingPath = pathOnDevice; - resolve(result); - }); + this._screenRecordingProperties = { + startTimestamp: process.hrtime(), + videoSize, + timeLimit, + bitRate, + bugReport, + records: [], + recordingProcess: null, + stopped: false, + }; + await scheduleScreenRecord(this.adb, this._screenRecordingProperties); + return result; }; /** @@ -273,14 +275,12 @@ commands.startRecordingScreen = async function (options = {}) { */ /** - * Stop recording the screen. If no screen recording process is running then - * the endpoint will try to get the recently recorded file. - * If no previously recorded file is found and no active screen recording - * processes are running then the method returns an empty string. + * Stop recording the screen. + * If no screen recording has been started before then the method returns an empty string. * * @param {?StopRecordingOptions} options - The available options. * @returns {string} Base64-encoded content of the recorded media file if 'remotePath' - * parameter is empty or null or an empty string. + * parameter is falsy or an empty string. * @throws {Error} If there was an error while getting the name of a media file * or the file content cannot be uploaded to the remote location * or screen recording is not supported on the device under test. @@ -290,32 +290,51 @@ commands.stopRecordingScreen = async function (options = {}) { await verifyScreenRecordIsSupported(this.adb, this.isEmulator()); - const pids = (await this.adb.getPIDsByName(SCREENRECORD_BINARY)).map((p) => `${p}`); - let pathOnDevice = this._recentScreenRecordingPath; - if (_.isEmpty(pids)) { - log.info(`Screen recording is not running. There is nothing to stop.`); - } else { - pathOnDevice = pathOnDevice || await extractCurrentRecordingPath(this.adb, pids); - try { - if (_.isEmpty(pathOnDevice)) { - log.errorAndThrow(`Cannot parse the path to the file created by ` + - `screen recorder process from 'ps' output. ` + - `Did you start screen recording before?`); - } - } finally { - if (!await finishScreenCapture(this.adb, pids)) { - log.warn(`Unable to stop screen recording. Continuing anyway`); + if (!_.isEmpty(this._screenRecordingProperties)) { + this._screenRecordingProperties.stopped = true; + if (this._screenRecordingProperties.recordingProcess && this._screenRecordingProperties.recordingProcess.isRunning) { + try { + await this._screenRecordingProperties.recordingProcess.stop('SIGINT', PROCESS_SHUTDOWN_TIMEOUT_SEC * 1000); + this._screenRecordingProperties.recordingProcess = null; + } catch (e) { + log.warn(`Unable to stop screen recording. Continuing anyway. Original error: ${e.message}`); } } } + if (_.isEmpty(this._screenRecordingProperties)) { + log.info(`Screen recording has not been started by Appium. There is nothing to retrieve`); + return ''; + } + + if (_.isEmpty(this._screenRecordingProperties.records)) { + log.errorAndThrow(`No screen recordings have been stored on the device so far. ` + + `Are you sure the ${SCREENRECORD_BINARY} utility works as expected?`); + } + let result = ''; - if (!_.isEmpty(pathOnDevice)) { - try { - result = await uploadRecordedMedia(this.adb, pathOnDevice, remotePath, {user, pass, method}); - } finally { - this._recentScreenRecordingPath = null; + const tmpRoot = await tempDir.openDir(); + try { + const localRecords = []; + for (const pathOnDevice of this._screenRecordingProperties.records) { + localRecords.push(path.resolve(tmpRoot, path.posix.basename(pathOnDevice))); + await this.adb.pull(pathOnDevice, _.last(localRecords)); + await this.adb.rimraf(pathOnDevice); + } + let resultFilePath = _.last(localRecords); + if (localRecords.length > 1) { + log.info(`Got ${localRecords.length} screen recordings. Trying to merge them`); + try { + resultFilePath = await mergeScreenRecords(localRecords); + } catch (e) { + log.warn(`Cannot merge the recorded files. The most recent screen recording is going to be sent as the result. ` + + `Original error: ${e.message}`); + } } + result = await uploadRecordedMedia(this.adb, resultFilePath, remotePath, {user, pass, method}); + } finally { + await fs.rimraf(tmpRoot); + this._screenRecordingProperties = null; } return result; }; diff --git a/test/unit/commands/recordscreen-specs.js b/test/unit/commands/recordscreen-specs.js index ef37e2981..b5bfc47b3 100644 --- a/test/unit/commands/recordscreen-specs.js +++ b/test/unit/commands/recordscreen-specs.js @@ -5,8 +5,6 @@ import { withMocks } from 'appium-test-support'; import { fs } from 'appium-support'; import temp from 'temp'; import ADB from 'appium-adb'; -import sinon from 'sinon'; -import B from 'bluebird'; chai.should(); @@ -19,9 +17,6 @@ describe('recording the screen', function () { this.timeout(60000); describe('basic', withMocks({adb, driver, fs, temp}, (mocks) => { - const localFile = '/path/to/local.mp4'; - const mediaContent = Buffer.from('appium'); - it('should fail to recording the screen on an older emulator', async function () { mocks.driver.expects('isEmulator').returns(true); mocks.adb.expects('getApiLevel').returns(26); @@ -36,167 +31,9 @@ describe('recording the screen', function () { await driver.startRecordingScreen().should.eventually.be.rejectedWith(/Screen recording not available on API Level 18. Minimum API Level is 19/); }); - describe('beginning the recording', function () { - beforeEach(function () { - driver._recentScreenRecordingPath = null; - mocks.driver.expects('isEmulator').atLeast(1).returns(false); - mocks.adb.expects('getApiLevel').atLeast(1).returns(19); - mocks.adb.expects('getPIDsByName') - .atLeast(1).withExactArgs('screenrecord').returns([]); - }); - afterEach(function () { - mocks.driver.verify(); - mocks.adb.verify(); - mocks.fs.verify(); - mocks.temp.verify(); - }); - - it('should call adb to start screen recording', async function () { - mocks.adb.expects('shell').once().returns(new B(() => {})); - mocks.adb.expects('fileExists').once().returns(true); - - await driver.startRecordingScreen(); - driver._recentScreenRecordingPath.should.not.be.empty; - }); - - it('should return previous capture before starting a new recording', async function () { - const remotePath = '/sdcard/video.mp4'; - - mocks.adb.expects('shell').returns(new B(() => {})); - mocks.adb.expects('fileExists').once().returns(true); - mocks.adb.expects('pull').once().withExactArgs(remotePath, localFile); - mocks.fs.expects('readFile').once().withExactArgs(localFile).returns(mediaContent); - mocks.adb.expects('rimraf').once().withExactArgs(remotePath); - mocks.fs.expects('rimraf').withExactArgs(localFile).once(); - mocks.fs.expects('stat').once().withExactArgs(localFile).returns({size: 100}); - mocks.temp.expects('path').once().returns(localFile); - - driver._recentScreenRecordingPath = remotePath; - (await driver.startRecordingScreen()) - .should.be.eql(mediaContent.toString('base64')); - driver._recentScreenRecordingPath.should.not.be.empty; - driver._recentScreenRecordingPath.should.not.be.eql(localFile); - }); - - it('should fail if adb screen recording errors out', async function () { - let shellStub = sinon.stub(adb, 'shell'); - try { - shellStub - .returns(B.reject(new Error('shell command failed'))); - - await driver.startRecordingScreen().should.eventually.be.rejectedWith(/shell command failed/); - } finally { - shellStub.restore(); - } - }); - - it('should call ls multiple times and fail if the file never appears', async function () { - mocks.adb.expects('shell').once().returns(new B(() => {})); - let fileExistsStub = sinon.stub(adb, 'fileExists'); - try { - fileExistsStub.withArgs().returns(false); - - await driver.startRecordingScreen().should.eventually.be.rejectedWith(/does not exist/); - } finally { - fileExistsStub.restore(); - } - }); - }); - describe('stopRecordingScreen', function () { - const psOutput = ` - USER PID PPID VSZ RSS WCHAN ADDR S NAME - root 8384 2 0 0 worker_thread 0 S [kworker/0:1] - u0_a43 8400 1510 1449772 90992 ep_poll 0 S com.google.android.apps.messaging:rcs - root 8423 2 0 0 worker_thread 0 S [kworker/u4:2] - u0_a43 8435 1510 1452544 93576 ep_poll 0 S com.google.android.apps.messaging - u0_a7 8471 1510 1427536 79804 ep_poll 0 S android.process.acore - root 8669 2 0 0 worker_thread 0 S [kworker/u5:1] - u0_a35 8805 1510 1426428 61540 ep_poll 0 S com.google.android.apps.wallpaper - u0_a10 8864 1510 1427412 69752 ep_poll 0 S android.process.media - root 8879 2 0 0 worker_thread 0 S [kworker/1:1] - u0_a60 8897 1510 1490420 108852 ep_poll 0 S com.google.android.apps.photos - shell 9136 1422 7808 2784 0 ebddfaf0 R ps - `; - - beforeEach(function () { - mocks.driver.expects('isEmulator').atLeast(1).returns(false); - mocks.adb.expects('getApiLevel').atLeast(1).returns(19); - }); - afterEach(function () { - mocks.driver.verify(); - mocks.adb.verify(); - mocks.fs.verify(); - mocks.temp.verify(); - }); - - it('should kill the process and get the content of the created mp4 file using lsof', async function () { - const pids = ['1']; - driver._recentScreenRecordingPath = null; - const remotePath = '/sdcard/file.mp4'; - mocks.adb.expects('getPIDsByName').withExactArgs('screenrecord') - .atLeast(1).returns(pids); - mocks.adb.expects('shell').withExactArgs(['lsof', '-p', pids.join(',')]).returns({output: ` - screenrec 11328 shell mem REG 253,0 1330160 554 /system/bin/linker64 - screenrec 11328 shell 0u unix 0t0 99935 socket - screenrec 11328 shell 1u unix 0t0 99935 socket - screenrec 11328 shell 2u unix 0t0 99937 socket - screenrec 11328 shell 3u CHR 10,64 0t0 12300 /dev/binder - screenrec 11328 shell 4u unix 0t0 101825 socket - screenrec 11328 shell 5w CHR 254,0 0t0 2923 /dev/pmsg0 - screenrec 11328 shell 6u CHR 10,62 0t0 11690 /dev/ashmem - screenrec 11328 shell 7u CHR 10,62 0t0 11690 /dev/ashmem - screenrec 11328 shell 8w REG 0,5 0 6706 /sys/kernel/debug/tracing/trace_marker - screenrec 11328 shell 9u REG 0,19 11521 294673 ${remotePath} - `}); - mocks.adb.expects('shell').withExactArgs(['kill', '-2', ...pids]); - mocks.adb.expects('shell').withExactArgs(['ps']).returns(psOutput); - mocks.adb.expects('pull').once().withExactArgs(remotePath, localFile); - mocks.fs.expects('readFile').once().withExactArgs(localFile).returns(mediaContent); - mocks.adb.expects('rimraf').once().withExactArgs(remotePath); - mocks.fs.expects('rimraf').once().withExactArgs(localFile); - mocks.fs.expects('stat').once().withExactArgs(localFile).returns({size: 100}); - mocks.temp.expects('path').once().returns(localFile); - - (await driver.stopRecordingScreen()).should.eql(mediaContent.toString('base64')); - }); - - it('should use the remembered file path if present', async function () { - const pids = ['1']; - driver._recentScreenRecordingPath = '/sdcard/file.mp4'; - mocks.adb.expects('getPIDsByName').withExactArgs('screenrecord') - .atLeast(1).returns(pids); - mocks.adb.expects('shell').withExactArgs(['kill', '-2', ...pids]); - mocks.adb.expects('shell').withExactArgs(['ps']).returns(psOutput); - mocks.adb.expects('pull').once().withExactArgs(driver._recentScreenRecordingPath, localFile); - mocks.fs.expects('readFile').once().withExactArgs(localFile).returns(mediaContent); - mocks.adb.expects('rimraf').once().withExactArgs(driver._recentScreenRecordingPath); - mocks.fs.expects('rimraf').withExactArgs(localFile).once(); - mocks.fs.expects('stat').once().withExactArgs(localFile).returns({size: 100}); - mocks.temp.expects('path').once().returns(localFile); - - (await driver.stopRecordingScreen()).should.eql(mediaContent.toString('base64')); - }); - - it('should fail if the recorded file is too large', async function () { - const pids = ['1']; - driver._recentScreenRecordingPath = '/sdcard/file.mp4'; - mocks.adb.expects('getPIDsByName').withExactArgs('screenrecord') - .atLeast(1).returns(pids); - mocks.adb.expects('shell').withExactArgs(['kill', '-2', ...pids]); - mocks.adb.expects('shell').withExactArgs(['ps']).returns(psOutput); - mocks.adb.expects('pull').once().withExactArgs(driver._recentScreenRecordingPath, localFile); - mocks.adb.expects('rimraf').once().withExactArgs(driver._recentScreenRecordingPath); - mocks.fs.expects('rimraf').withExactArgs(localFile).once(); - mocks.fs.expects('stat').once().withExactArgs(localFile) - .returns({size: process.memoryUsage().heapTotal}); - mocks.temp.expects('path').once().returns(localFile); - - await driver.stopRecordingScreen().should.eventually.be.rejectedWith(/is too large/); - }); - it('should return empty string if no recording processes are running', async function () { - driver._recentScreenRecordingPath = null; + driver._screenRecordingProperties = null; mocks.adb.expects('getPIDsByName') .atLeast(1).withExactArgs('screenrecord').returns([]); From 81e7131197f09698ebe0b1dce6f955f5f254b97a Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Fri, 27 Jul 2018 12:36:49 +0200 Subject: [PATCH 2/8] Throw an error if screen recording cannot be stopped --- lib/commands/recordscreen.js | 2 +- test/unit/commands/recordscreen-specs.js | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/lib/commands/recordscreen.js b/lib/commands/recordscreen.js index cda8a24b2..7057f43d9 100644 --- a/lib/commands/recordscreen.js +++ b/lib/commands/recordscreen.js @@ -297,7 +297,7 @@ commands.stopRecordingScreen = async function (options = {}) { await this._screenRecordingProperties.recordingProcess.stop('SIGINT', PROCESS_SHUTDOWN_TIMEOUT_SEC * 1000); this._screenRecordingProperties.recordingProcess = null; } catch (e) { - log.warn(`Unable to stop screen recording. Continuing anyway. Original error: ${e.message}`); + log.errorAndThrow(`Unable to stop screen recording. Original error: ${e.message}`); } } } diff --git a/test/unit/commands/recordscreen-specs.js b/test/unit/commands/recordscreen-specs.js index b5bfc47b3..a2ac3a919 100644 --- a/test/unit/commands/recordscreen-specs.js +++ b/test/unit/commands/recordscreen-specs.js @@ -30,15 +30,5 @@ describe('recording the screen', function () { await driver.startRecordingScreen().should.eventually.be.rejectedWith(/Screen recording not available on API Level 18. Minimum API Level is 19/); }); - - describe('stopRecordingScreen', function () { - it('should return empty string if no recording processes are running', async function () { - driver._screenRecordingProperties = null; - mocks.adb.expects('getPIDsByName') - .atLeast(1).withExactArgs('screenrecord').returns([]); - - (await driver.stopRecordingScreen()).should.eql(''); - }); - }); })); }); From 9b9aec4cf8a39a6e39fc9c864ca7fbae7dbb3b63 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Fri, 27 Jul 2018 12:39:23 +0200 Subject: [PATCH 3/8] Simplify some conditions --- lib/commands/recordscreen.js | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/lib/commands/recordscreen.js b/lib/commands/recordscreen.js index 7057f43d9..dc7fcd20e 100644 --- a/lib/commands/recordscreen.js +++ b/lib/commands/recordscreen.js @@ -286,27 +286,23 @@ commands.startRecordingScreen = async function (options = {}) { * or screen recording is not supported on the device under test. */ commands.stopRecordingScreen = async function (options = {}) { - const {remotePath, user, pass, method} = options; - await verifyScreenRecordIsSupported(this.adb, this.isEmulator()); - if (!_.isEmpty(this._screenRecordingProperties)) { - this._screenRecordingProperties.stopped = true; - if (this._screenRecordingProperties.recordingProcess && this._screenRecordingProperties.recordingProcess.isRunning) { - try { - await this._screenRecordingProperties.recordingProcess.stop('SIGINT', PROCESS_SHUTDOWN_TIMEOUT_SEC * 1000); - this._screenRecordingProperties.recordingProcess = null; - } catch (e) { - log.errorAndThrow(`Unable to stop screen recording. Original error: ${e.message}`); - } - } - } - if (_.isEmpty(this._screenRecordingProperties)) { log.info(`Screen recording has not been started by Appium. There is nothing to retrieve`); return ''; } + this._screenRecordingProperties.stopped = true; + if (this._screenRecordingProperties.recordingProcess && this._screenRecordingProperties.recordingProcess.isRunning) { + try { + await this._screenRecordingProperties.recordingProcess.stop('SIGINT', PROCESS_SHUTDOWN_TIMEOUT_SEC * 1000); + this._screenRecordingProperties.recordingProcess = null; + } catch (e) { + log.errorAndThrow(`Unable to stop screen recording. Original error: ${e.message}`); + } + } + if (_.isEmpty(this._screenRecordingProperties.records)) { log.errorAndThrow(`No screen recordings have been stored on the device so far. ` + `Are you sure the ${SCREENRECORD_BINARY} utility works as expected?`); @@ -331,6 +327,7 @@ commands.stopRecordingScreen = async function (options = {}) { `Original error: ${e.message}`); } } + const {remotePath, user, pass, method} = options; result = await uploadRecordedMedia(this.adb, resultFilePath, remotePath, {user, pass, method}); } finally { await fs.rimraf(tmpRoot); From f8402e3fe76404423235b2a2776ff7a020c016ce Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Fri, 27 Jul 2018 13:55:30 +0200 Subject: [PATCH 4/8] Adjust duration calculation --- lib/commands/recordscreen.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/commands/recordscreen.js b/lib/commands/recordscreen.js index dc7fcd20e..8d5bea35b 100644 --- a/lib/commands/recordscreen.js +++ b/lib/commands/recordscreen.js @@ -88,15 +88,14 @@ async function scheduleScreenRecord (adb, recordingProperties) { if (util.hasValue(videoSize)) { cmd.push('--size', videoSize); } - if (util.hasValue(timeLimit)) { - let timeLimitArg = parseInt(timeLimit, 10); - if (!isNaN(timeLimitArg)) { - cmd.push('--time-limit', - `${timeLimitArg <= MAX_RECORDING_TIME_SEC ? timeLimitArg : MAX_RECORDING_TIME_SEC}`); + if (util.hasValue(recordingProperties.currentTimeLimit)) { + const currentTimeLimitInt = parseInt(recordingProperties.currentTimeLimit, 10); + if (!isNaN(currentTimeLimitInt)) { + cmd.push('--time-limit', currentTimeLimitInt % MAX_RECORDING_TIME_SEC); } } if (util.hasValue(bitRate)) { - cmd.push('--bit-rate', `${bitRate}`); + cmd.push('--bit-rate', bitRate); } if (bugReport) { cmd.push('--bugreport'); @@ -111,15 +110,17 @@ async function scheduleScreenRecord (adb, recordingProperties) { if (recordingProperties.stopped || !util.hasValue(timeLimit)) { return; } - const totalDuration = process.hrtime(startTimestamp)[0]; - log.debug(`The overall screen recording duration is ${totalDuration}s so far`); + const currentDuration = process.hrtime(startTimestamp)[0]; + log.debug(`The overall screen recording duration is ${currentDuration}s so far`); const timeLimitInt = parseInt(timeLimit, 10); - if (isNaN(timeLimitInt) || timeLimitInt >= totalDuration) { + if (isNaN(timeLimitInt) || timeLimitInt >= currentDuration) { log.debug('There is no need to start the next recording chunk'); return; } - log.debug(`Starting the next chunk of screen recording in order to achieve ${timeLimitInt}s duration`); + recordingProperties.currentTimeLimit = timeLimitInt - currentDuration; + log.debug(`Starting the next ${recordingProperties.currentTimeLimit % MAX_RECORDING_TIME_SEC}s-chunk ` + + `of screen recording in order to achieve ${timeLimitInt}s duration`); scheduleScreenRecord(adb, recordingProperties) .catch((e) => { log.error(e.stack); @@ -250,6 +251,7 @@ commands.startRecordingScreen = async function (options = {}) { startTimestamp: process.hrtime(), videoSize, timeLimit, + currentTimeLimit: timeLimit, bitRate, bugReport, records: [], From 1121b45bb991e4cbdc83902ba51b3a2dcda9627b Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Fri, 27 Jul 2018 14:00:20 +0200 Subject: [PATCH 5/8] Fix chunk duration calcutation --- lib/commands/recordscreen.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/commands/recordscreen.js b/lib/commands/recordscreen.js index 8d5bea35b..0f594d30f 100644 --- a/lib/commands/recordscreen.js +++ b/lib/commands/recordscreen.js @@ -91,7 +91,7 @@ async function scheduleScreenRecord (adb, recordingProperties) { if (util.hasValue(recordingProperties.currentTimeLimit)) { const currentTimeLimitInt = parseInt(recordingProperties.currentTimeLimit, 10); if (!isNaN(currentTimeLimitInt)) { - cmd.push('--time-limit', currentTimeLimitInt % MAX_RECORDING_TIME_SEC); + cmd.push('--time-limit', currentTimeLimitInt < MAX_RECORDING_TIME_SEC ? currentTimeLimitInt : MAX_RECORDING_TIME_SEC); } } if (util.hasValue(bitRate)) { @@ -119,7 +119,10 @@ async function scheduleScreenRecord (adb, recordingProperties) { } recordingProperties.currentTimeLimit = timeLimitInt - currentDuration; - log.debug(`Starting the next ${recordingProperties.currentTimeLimit % MAX_RECORDING_TIME_SEC}s-chunk ` + + const chunkDuration = recordingProperties.currentTimeLimit < MAX_RECORDING_TIME_SEC + ? recordingProperties.currentTimeLimit + : MAX_RECORDING_TIME_SEC; + log.debug(`Starting the next ${chunkDuration}s-chunk ` + `of screen recording in order to achieve ${timeLimitInt}s duration`); scheduleScreenRecord(adb, recordingProperties) .catch((e) => { From afe28c64d052ef113833975b1232624fc51b8f92 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Fri, 27 Jul 2018 17:09:00 +0200 Subject: [PATCH 6/8] Update some logic and error messages --- lib/commands/recordscreen.js | 77 +++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/lib/commands/recordscreen.js b/lib/commands/recordscreen.js index 0f594d30f..f81935c88 100644 --- a/lib/commands/recordscreen.js +++ b/lib/commands/recordscreen.js @@ -1,7 +1,7 @@ import _ from 'lodash'; import _fs from 'fs'; import url from 'url'; -import { retryInterval } from 'asyncbox'; +import { waitForCondition } from 'asyncbox'; import { util, fs, net, tempDir, system } from 'appium-support'; import log from '../logger'; import { SubProcess, exec } from 'teen_process'; @@ -10,7 +10,8 @@ import path from 'path'; let commands = {}, extensions = {}; -const RETRY_PAUSE = 1000; +const RETRY_PAUSE = 300; +const RETRY_TIMEOUT = 5000; const MAX_RECORDING_TIME_SEC = 60 * 3; const MAX_TIME_SEC = 60 * 30; const DEFAULT_RECORDING_TIME_SEC = MAX_RECORDING_TIME_SEC; @@ -22,7 +23,7 @@ const FFMPEG_BINARY = `ffmpeg${system.isWindows() ? '.exe' : ''}`; async function uploadRecordedMedia (adb, localFile, remotePath = null, uploadOptions = {}) { const {size} = await fs.stat(localFile); - log.debug(`The size of the recent screen recording is ${util.toReadableSizeString(size)}`); + log.debug(`The size of the resulting screen recording is ${util.toReadableSizeString(size)}`); if (_.isEmpty(remotePath)) { const memoryUsage = process.memoryUsage(); const maxMemoryLimit = (memoryUsage.heapTotal - memoryUsage.heapUsed) / 2; @@ -103,8 +104,8 @@ async function scheduleScreenRecord (adb, recordingProperties) { const pathOnDevice = `/sdcard/${Math.floor(new Date())}${DEFAULT_EXT}`; cmd.push(pathOnDevice); - const recordingProc = new SubProcess(adb.executable.path, - [...adb.executable.defaultArgs, 'shell', ...cmd]); + const fullCmd = [...adb.executable.defaultArgs, 'shell', ...cmd]; + const recordingProc = new SubProcess(adb.executable.path, fullCmd); recordingProc.on('end', () => { if (recordingProperties.stopped || !util.hasValue(timeLimit)) { @@ -113,7 +114,7 @@ async function scheduleScreenRecord (adb, recordingProperties) { const currentDuration = process.hrtime(startTimestamp)[0]; log.debug(`The overall screen recording duration is ${currentDuration}s so far`); const timeLimitInt = parseInt(timeLimit, 10); - if (isNaN(timeLimitInt) || timeLimitInt >= currentDuration) { + if (isNaN(timeLimitInt) || currentDuration >= timeLimitInt) { log.debug('There is no need to start the next recording chunk'); return; } @@ -123,7 +124,7 @@ async function scheduleScreenRecord (adb, recordingProperties) { ? recordingProperties.currentTimeLimit : MAX_RECORDING_TIME_SEC; log.debug(`Starting the next ${chunkDuration}s-chunk ` + - `of screen recording in order to achieve ${timeLimitInt}s duration`); + `of screen recording in order to achieve ${timeLimitInt}s total duration`); scheduleScreenRecord(adb, recordingProperties) .catch((e) => { log.error(e.stack); @@ -131,16 +132,16 @@ async function scheduleScreenRecord (adb, recordingProperties) { }); }); + log.debug(`Invoking '${fullCmd.join(' ')}'`); await recordingProc.start(0); - await retryInterval(10, RETRY_PAUSE, async () => { - if (!await adb.fileExists(pathOnDevice)) { - throw new Error(`The remote file '${pathOnDevice}' does not exist`); - } - }); - - if (_.isArray(recordingProperties.records)) { - recordingProperties.records.push(pathOnDevice); + try { + await waitForCondition(async () => await adb.fileExists(pathOnDevice), + {waitMs: RETRY_TIMEOUT, intervalMs: RETRY_PAUSE}); + } catch (e) { + throw new Error(`The expected screen record file '${pathOnDevice}' does not exist after ${RETRY_TIMEOUT}ms`); } + + recordingProperties.records.push(pathOnDevice); recordingProperties.recordingProcess = recordingProc; } @@ -155,14 +156,29 @@ async function mergeScreenRecords (mediaFiles) { .join('\n'); const configFile = path.resolve(path.dirname(mediaFiles[0]), 'config.txt'); await fs.writeFile(configFile, configContent, 'utf8'); - log.debug(`Generated merging config '${configFile}' with the content:\n${configContent}`); + log.debug(`Generated ffmpeg merging config '${configFile}' with items:\n${configContent}`); const result = path.resolve(path.dirname(mediaFiles[0]), `merge_${Math.floor(new Date())}${DEFAULT_EXT}`); const args = ['-safe', '0', '-f', 'concat', '-i', configFile, '-c', 'copy', result]; - log.info(`Initiating screen records merge with command '${FFMPEG_BINARY} ${args.join(' ')}'`); + log.info(`Initiating screen records merging using the command '${FFMPEG_BINARY} ${args.join(' ')}'`); await exec(FFMPEG_BINARY, args); return result; } +async function terminateBackgroundScreenRecording (adb) { + const pids = (await adb.getPIDsByName(SCREENRECORD_BINARY)) + .map((p) => `${p}`); + if (_.isEmpty(pids)) { + return false; + } + + try { + await adb.shell(['kill', ...pids]); + return true; + } catch (err) { + throw new Error(`Unable to stop the background screen recording: ${err.message}`); + } +} + /** * @typedef {Object} StartRecordingOptions @@ -214,11 +230,10 @@ async function mergeScreenRecords (mediaFiles) { * @throws {Error} If screen recording has failed to start or is not supported on the device under test. */ commands.startRecordingScreen = async function (options = {}) { - const {videoSize, timeLimit=DEFAULT_RECORDING_TIME_SEC, bugReport, bitRate, forceRestart} = options; - await verifyScreenRecordIsSupported(this.adb, this.isEmulator()); let result = ''; + const {videoSize, timeLimit=DEFAULT_RECORDING_TIME_SEC, bugReport, bitRate, forceRestart} = options; if (!forceRestart) { result = await this.stopRecordingScreen(options); } else if (!_.isEmpty(this._screenRecordingProperties)) { @@ -229,14 +244,13 @@ commands.startRecordingScreen = async function (options = {}) { } catch (ign) {} } } - try { - const pids = (await this.adb.getPIDsByName(SCREENRECORD_BINARY)).map((p) => `${p}`); - if (!_.isEmpty(pids)) { - await this.adb.shell(['kill', ...pids]); - } - } catch (err) { - log.errorAndThrow(`Unable to stop screen recording: ${err.message}`); + + if (await terminateBackgroundScreenRecording(this.adb)) { + log.warn(`There were some ${SCREENRECORD_BINARY} process leftovers running ` + + `in the background. Make sure you stop screen recording each time after it is started, ` + + `otherwise the recorded media might quickly exceed all the free space on the device under test.`); } + if (!_.isEmpty(this._screenRecordingProperties)) { for (const record of (this._screenRecordingProperties.records || [])) { await this.adb.rimraf(record); @@ -294,7 +308,10 @@ commands.stopRecordingScreen = async function (options = {}) { await verifyScreenRecordIsSupported(this.adb, this.isEmulator()); if (_.isEmpty(this._screenRecordingProperties)) { - log.info(`Screen recording has not been started by Appium. There is nothing to retrieve`); + log.info(`Screen recording has not been previously started by Appium. There is nothing to stop`); + try { + await terminateBackgroundScreenRecording(this.adb); + } catch (ign) {} return ''; } @@ -308,6 +325,10 @@ commands.stopRecordingScreen = async function (options = {}) { } } + try { + await terminateBackgroundScreenRecording(this.adb); + } catch (ign) {} + if (_.isEmpty(this._screenRecordingProperties.records)) { log.errorAndThrow(`No screen recordings have been stored on the device so far. ` + `Are you sure the ${SCREENRECORD_BINARY} utility works as expected?`); @@ -328,7 +349,7 @@ commands.stopRecordingScreen = async function (options = {}) { try { resultFilePath = await mergeScreenRecords(localRecords); } catch (e) { - log.warn(`Cannot merge the recorded files. The most recent screen recording is going to be sent as the result. ` + + log.warn(`Cannot merge the recorded files. The most recent screen recording is going to be returned as the result. ` + `Original error: ${e.message}`); } } From 4b674558c1adba0dac72698430d67d90723618a0 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Fri, 27 Jul 2018 19:25:49 +0200 Subject: [PATCH 7/8] Change the OOM behaviour --- lib/commands/recordscreen.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/commands/recordscreen.js b/lib/commands/recordscreen.js index f81935c88..e882f2270 100644 --- a/lib/commands/recordscreen.js +++ b/lib/commands/recordscreen.js @@ -28,10 +28,10 @@ async function uploadRecordedMedia (adb, localFile, remotePath = null, uploadOpt const memoryUsage = process.memoryUsage(); const maxMemoryLimit = (memoryUsage.heapTotal - memoryUsage.heapUsed) / 2; if (size >= maxMemoryLimit) { - throw new Error(`Cannot read the recorded media '${localFile}' to the memory, ` + - `because the file is too large ` + - `(${util.toReadableSizeString(size)} >= ${util.toReadableSizeString(maxMemoryLimit)}). ` + - `Try to provide a link to a remote writable location instead.`); + log.info(`The file might be too large to fit into the process memory ` + + `(${util.toReadableSizeString(size)} >= ${util.toReadableSizeString(maxMemoryLimit)}). ` + + `Provide a link to a remote writable location for video upload ` + + `(http(s) and ftp protocols are supported) if you experience Out Of Memory errors`); } return (await fs.readFile(localFile)).toString('base64'); } From c1609036e24c5deb83193a3808678e0e0ef976bc Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Tue, 31 Jul 2018 15:46:00 +0200 Subject: [PATCH 8/8] Switch to use adb call --- lib/commands/recordscreen.js | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/lib/commands/recordscreen.js b/lib/commands/recordscreen.js index e882f2270..99f4cf8e2 100644 --- a/lib/commands/recordscreen.js +++ b/lib/commands/recordscreen.js @@ -4,7 +4,7 @@ import url from 'url'; import { waitForCondition } from 'asyncbox'; import { util, fs, net, tempDir, system } from 'appium-support'; import log from '../logger'; -import { SubProcess, exec } from 'teen_process'; +import { exec } from 'teen_process'; import path from 'path'; @@ -84,28 +84,21 @@ async function scheduleScreenRecord (adb, recordingProperties) { timeLimit, bugReport, } = recordingProperties; - //make adb command - const cmd = [SCREENRECORD_BINARY]; - if (util.hasValue(videoSize)) { - cmd.push('--size', videoSize); - } + + let currentTimeLimit = MAX_RECORDING_TIME_SEC; if (util.hasValue(recordingProperties.currentTimeLimit)) { const currentTimeLimitInt = parseInt(recordingProperties.currentTimeLimit, 10); - if (!isNaN(currentTimeLimitInt)) { - cmd.push('--time-limit', currentTimeLimitInt < MAX_RECORDING_TIME_SEC ? currentTimeLimitInt : MAX_RECORDING_TIME_SEC); + if (!isNaN(currentTimeLimitInt) && currentTimeLimitInt < MAX_RECORDING_TIME_SEC) { + currentTimeLimit = currentTimeLimitInt; } } - if (util.hasValue(bitRate)) { - cmd.push('--bit-rate', bitRate); - } - if (bugReport) { - cmd.push('--bugreport'); - } const pathOnDevice = `/sdcard/${Math.floor(new Date())}${DEFAULT_EXT}`; - cmd.push(pathOnDevice); - - const fullCmd = [...adb.executable.defaultArgs, 'shell', ...cmd]; - const recordingProc = new SubProcess(adb.executable.path, fullCmd); + const recordingProc = adb.screenrecord(pathOnDevice, { + videoSize, + bitRate, + timeLimit: currentTimeLimit, + bugReport, + }); recordingProc.on('end', () => { if (recordingProperties.stopped || !util.hasValue(timeLimit)) { @@ -132,7 +125,6 @@ async function scheduleScreenRecord (adb, recordingProperties) { }); }); - log.debug(`Invoking '${fullCmd.join(' ')}'`); await recordingProc.start(0); try { await waitForCondition(async () => await adb.fileExists(pathOnDevice),