appium-xcuitest-driver
Version:
Appium driver for iOS using XCUITest for backend
226 lines • 9.37 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AudioRecorder = void 0;
exports.startAudioRecording = startAudioRecording;
exports.stopAudioRecording = stopAudioRecording;
const support_1 = require("appium/support");
const teen_process_1 = require("teen_process");
const utils_1 = require("../utils");
const asyncbox_1 = require("asyncbox");
const MAX_RECORDING_TIME_SEC = 43200;
const AUDIO_RECORD_FEAT_NAME = 'audio_record';
const DEFAULT_SOURCE = 'avfoundation';
const PROCESS_STARTUP_TIMEOUT_MS = 5000;
const DEFAULT_EXT = '.mp4';
const FFMPEG_BINARY = 'ffmpeg';
const ffmpegLogger = support_1.logger.getLogger(FFMPEG_BINARY);
class AudioRecorder {
input;
log;
audioPath;
opts;
mainProcess;
constructor(input, log, audioPath, opts = {}) {
this.input = input;
this.log = log;
this.audioPath = audioPath;
this.opts = opts;
this.mainProcess = null;
}
async start(timeoutSeconds) {
try {
await support_1.fs.which(FFMPEG_BINARY);
}
catch {
throw new Error(`'${FFMPEG_BINARY}' binary is not found in PATH. Install it using 'brew install ffmpeg'. ` +
`Check https://www.ffmpeg.org/download.html for more details.`);
}
const { audioSource, audioCodec, audioBitrate, audioChannels, audioRate } = this.opts;
const args = [
'-t',
`${timeoutSeconds}`,
'-f',
audioSource,
'-i',
String(this.input),
'-c:a',
audioCodec,
'-b:a',
audioBitrate,
'-ac',
`${audioChannels}`,
'-ar',
`${audioRate}`,
this.audioPath,
];
this.mainProcess = new teen_process_1.SubProcess(FFMPEG_BINARY, args);
let isCaptureStarted = false;
this.mainProcess.on('output', (stdout, stderr) => {
if (stderr) {
if (stderr.trim().startsWith('size=')) {
if (!isCaptureStarted) {
isCaptureStarted = true;
}
}
else {
ffmpegLogger.info(`${stderr}`);
}
}
});
await this.mainProcess.start(0);
try {
await (0, asyncbox_1.waitForCondition)(() => isCaptureStarted, {
waitMs: PROCESS_STARTUP_TIMEOUT_MS,
intervalMs: 300,
});
}
catch {
this.log.warn(`Audio recording process did not start within ${PROCESS_STARTUP_TIMEOUT_MS}ms. Continuing anyway`);
}
if (!this.mainProcess.isRunning) {
this.mainProcess = null;
throw new Error(`The audio recording process '${FFMPEG_BINARY}' died unexpectedly. ` +
`Check server logs for more details`);
}
this.log.info(`Starting capture on audio input '${this.input}' with command: '${support_1.util.quote([
FFMPEG_BINARY,
...args,
])}'. ` + `Will timeout in ${timeoutSeconds}s`);
this.mainProcess.once('exit', (code, signal) => {
// ffmpeg returns code 255 if SIGINT arrives
if ([0, 255].includes(code ?? 0)) {
this.log.info(`The recording session on audio input '${this.input}' has been finished`);
}
else {
this.log.debug(`The recording session on audio input '${this.input}' has exited ` +
`with code ${code}, signal ${signal}`);
}
});
}
isRecording() {
return !!this.mainProcess?.isRunning;
}
async interrupt(force = false) {
if (this.isRecording()) {
const interruptPromise = this.mainProcess?.stop(force ? 'SIGTERM' : 'SIGINT');
this.mainProcess = null;
try {
await interruptPromise;
}
catch (e) {
this.log.warn(`Cannot ${force ? 'terminate' : 'interrupt'} ${FFMPEG_BINARY}. ` +
`Original error: ${e.message}`);
return false;
}
}
return true;
}
async finish() {
await this.interrupt();
return this.audioPath;
}
async cleanup() {
if (await support_1.fs.exists(this.audioPath)) {
await support_1.fs.rimraf(this.audioPath);
}
}
}
exports.AudioRecorder = AudioRecorder;
/**
* Records the given hardware audio input and saves it into an `.mp4` file.
*
* **To use this command, the `audio_record` security feature must be enabled _and_ [FFMpeg](https://ffmpeg.org/) must be installed on the Appium server.**
*
* @param audioInput - The name of the corresponding audio input device to use for the capture. The full list of capture devices could be shown by executing `ffmpeg -f avfoundation -list_devices true -i ""`
* @param timeLimit - The maximum recording time, in seconds.
* @param audioCodec - The name of the audio codec.
* @param audioBitrate - The bitrate of the resulting audio stream.
* @param audioChannels - The count of audio channels in the resulting stream. Setting it to `1` will create a single channel (mono) audio stream.
* @param audioRate - The sampling rate of the resulting audio stream (in Hz).
* @param forceRestart - Whether to restart audio capture process forcefully when `mobile: startRecordingAudio` is called (`true`) or ignore the call until the current audio recording is completed (`false`).
* @group Real Device Only
*/
async function startAudioRecording(audioInput, timeLimit = 180, audioCodec = 'aac', audioBitrate = '128k', audioChannels = 2, audioRate = 44100, forceRestart = false) {
if (!this.isFeatureEnabled(AUDIO_RECORD_FEAT_NAME)) {
throw this.log.errorWithException(`Audio capture feature must be enabled on the server side. ` +
`Please set '--relaxed-security' or '--allow-insecure' with '${AUDIO_RECORD_FEAT_NAME}' option. ` +
`Read https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/security.md for more details.`);
}
if (!audioInput) {
throw this.log.errorWithException(`The mandatory audioInput option is not provided. Please set it ` +
`to a correct value (e. g. ':1'). Use 'ffmpeg -f avfoundation -list_devices true -i ""' ` +
`command to list available input sources`);
}
if (this._audioRecorder?.isRecording()) {
this.log.info(`There is an active audio recording process`);
if (forceRestart) {
this.log.info(`Stopping it because 'forceRestart' option is set to true`);
await this._audioRecorder.interrupt(true);
}
else {
this.log.info(`Doing nothing. ` +
`Set 'forceRestart' option to true if you'd like to start a new audio recording session`);
return;
}
}
if (this._audioRecorder) {
await this._audioRecorder.cleanup();
this._audioRecorder = null;
}
const audioPath = await support_1.tempDir.path({
prefix: `appium_${support_1.util.uuidV4().substring(0, 8)}`,
suffix: DEFAULT_EXT,
});
const audioRecorder = new AudioRecorder(audioInput, this.log, audioPath, {
audioSource: DEFAULT_SOURCE,
audioCodec,
audioBitrate,
audioChannels: Number(audioChannels),
audioRate: Number(audioRate),
});
const timeoutSeconds = parseInt(String(timeLimit), 10);
if (isNaN(timeoutSeconds) || timeoutSeconds > MAX_RECORDING_TIME_SEC || timeoutSeconds <= 0) {
throw this.log.errorWithException(`The timeLimit value must be in range [1, ${MAX_RECORDING_TIME_SEC}] seconds. ` +
`The value of '${timeLimit}' has been passed instead.`);
}
try {
await audioRecorder.start(timeoutSeconds);
}
catch (e) {
await audioRecorder.interrupt(true);
await audioRecorder.cleanup();
throw e;
}
this._audioRecorder = audioRecorder;
}
/**
* Stop recording of the audio input. If no audio 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 audio recording
* processes are running then the method returns an empty string.
*
* @returns Base64-encoded content of the recorded media file or an
* empty string if no audio recording has been started before.
* @throws {Error} If there was an error while getting the recorded file.
*/
async function stopAudioRecording() {
if (!this._audioRecorder) {
this.log.info('Audio recording has not been started. There is nothing to stop');
return '';
}
let resultPath;
try {
resultPath = await this._audioRecorder.finish();
if (!(await support_1.fs.exists(resultPath))) {
throw this.log.errorWithException(`${FFMPEG_BINARY} has failed ` + `to store the actual audio recording at '${resultPath}'`);
}
}
catch (e) {
await this._audioRecorder.interrupt(true);
await this._audioRecorder.cleanup();
this._audioRecorder = null;
throw e;
}
return await (0, utils_1.encodeBase64OrUpload)(resultPath);
}
//# sourceMappingURL=record-audio.js.map