Skip to content

Commit 9da1025

Browse files
committed
fix(recorder): fix screen recording
1 parent 6cce956 commit 9da1025

File tree

4 files changed

+404
-218
lines changed

4 files changed

+404
-218
lines changed

lib/commands/extension.ts

Lines changed: 69 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,10 @@
1-
import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process';
2-
import { readFile, unlink } from 'node:fs/promises';
3-
import { tmpdir } from 'node:os';
4-
import { join, normalize } from 'node:path';
51
import { W3C_ELEMENT_KEY, errors } from '@appium/base-driver';
62
import { Element, Rect } from '@appium/types';
7-
import { NovaWindowsDriver } from '../driver';
8-
import { $, getBundledFfmpegPath, sleep } from '../util';
3+
import { tmpdir } from 'node:os';
4+
import { join, normalize } from 'node:path';
95
import { POWER_SHELL_FEATURE } from '../constants';
10-
import { keyDown,
11-
keyUp,
12-
mouseDown,
13-
mouseMoveAbsolute,
14-
mouseScroll,
15-
mouseUp,
16-
sendKeyboardEvents
17-
} from '../winapi/user32';
18-
import { KeyEventFlags, VirtualKey } from '../winapi/types';
6+
import { NovaWindowsDriver } from '../driver';
7+
import { ClickType, Enum, Key } from '../enums';
198
import {
209
AutomationElement,
2110
AutomationElementMode,
@@ -29,7 +18,18 @@ import {
2918
convertStringToCondition,
3019
pwsh
3120
} from '../powershell';
32-
import { ClickType, Enum, Key } from '../enums';
21+
import { $, sleep } from '../util';
22+
import { DEFAULT_EXT, ScreenRecorder, UploadOptions, uploadRecordedMedia } from './screen-recorder';
23+
import { KeyEventFlags, VirtualKey } from '../winapi/types';
24+
import {
25+
keyDown,
26+
keyUp,
27+
mouseDown,
28+
mouseMoveAbsolute,
29+
mouseScroll,
30+
mouseUp,
31+
sendKeyboardEvents
32+
} from '../winapi/user32';
3333

3434
const PLATFORM_COMMAND_PREFIX = 'windows:';
3535

@@ -715,110 +715,73 @@ export async function executeScroll(this: NovaWindowsDriver, scrollArgs: {
715715
export async function startRecordingScreen(this: NovaWindowsDriver, args?: {
716716
outputPath?: string,
717717
timeLimit?: number,
718-
videoSize?: string,
719718
videoFps?: number,
719+
videoFilter?: string,
720+
preset?: string,
721+
captureCursor?: boolean,
722+
captureClicks?: boolean,
723+
audioInput?: string,
720724
forceRestart?: boolean,
721725
}): Promise<void> {
722726
const {
723-
outputPath = join(tmpdir(), `novawindows-recording-${Date.now()}.mp4`),
724-
timeLimit = 180,
725-
videoSize,
726-
videoFps = 15,
727-
forceRestart = false,
728-
} = args ?? {};
729-
730-
if (this.recordingProcess && !forceRestart) {
731-
throw new errors.InvalidArgumentError('Screen recording is already in progress. Use forceRestart to start a new recording.');
732-
}
733-
734-
if (this.recordingProcess && forceRestart) {
735-
const oldProc = this.recordingProcess;
736-
this.recordingProcess = undefined;
737-
this.recordingOutputPath = undefined;
738-
oldProc.stdin?.write('q');
739-
try {
740-
await new Promise<void>((resolve) => {
741-
oldProc.on('exit', () => resolve());
742-
setTimeout(() => {
743-
oldProc.kill('SIGKILL');
744-
resolve();
745-
}, 3000);
746-
});
747-
} catch {
748-
oldProc.kill('SIGKILL');
749-
}
750-
}
751-
752-
const ffmpegPath = getBundledFfmpegPath();
753-
if (!ffmpegPath) {
754-
throw new errors.UnknownError(
755-
'Screen recording is not available: the bundled ffmpeg is missing. Reinstall the driver.'
756-
);
757-
}
758-
759-
const ffmpegArgs = [
760-
'-f', 'gdigrab',
761-
'-framerate', String(videoFps),
762-
'-i', 'desktop',
763-
'-t', String(timeLimit),
764-
'-c:v', 'libx264',
765-
'-preset', 'ultrafast',
766-
'-y',
767727
outputPath,
768-
];
769-
if (videoSize) {
770-
const sizeIdx = ffmpegArgs.indexOf('-i');
771-
ffmpegArgs.splice(sizeIdx, 0, '-video_size', videoSize);
772-
}
728+
timeLimit,
729+
videoFps: fps,
730+
videoFilter,
731+
preset,
732+
captureCursor,
733+
captureClicks,
734+
audioInput,
735+
forceRestart = true,
736+
} = args ?? {};
773737

774-
const proc = spawn(ffmpegPath, ffmpegArgs, { stdio: ['pipe', 'pipe', 'pipe'] });
775-
proc.on('error', (err) => {
776-
this.log.error(
777-
`Screen recording failed: ${err.message}. The bundled ffmpeg may be missing or invalid; try reinstalling the driver.`
778-
);
738+
if (this._screenRecorder?.isRunning()) {
739+
this.log.debug('The screen recording is already running');
740+
if (!forceRestart) {
741+
this.log.debug('Doing nothing');
742+
return;
743+
}
744+
this.log.debug('Forcing the active screen recording to stop');
745+
await this._screenRecorder.stop(true);
746+
} else if (this._screenRecorder) {
747+
this.log.debug('Clearing the recent screen recording');
748+
await this._screenRecorder.stop(true);
749+
}
750+
this._screenRecorder = null;
751+
752+
const videoPath = outputPath ?? join(tmpdir(), `novawindows-recording-${Date.now()}.${DEFAULT_EXT}`);
753+
this._screenRecorder = new ScreenRecorder(videoPath, this.log, {
754+
fps: fps !== undefined ? parseInt(String(fps), 10) : undefined,
755+
timeLimit: timeLimit !== undefined ? parseInt(String(timeLimit), 10) : undefined,
756+
preset,
757+
captureCursor,
758+
captureClicks,
759+
videoFilter,
760+
audioInput,
779761
});
780-
proc.stderr?.on('data', () => { /* suppress ffmpeg progress output */ });
781-
782-
this.recordingProcess = proc as ChildProcessWithoutNullStreams;
783-
this.recordingOutputPath = outputPath;
762+
try {
763+
await this._screenRecorder.start();
764+
} catch (e) {
765+
this._screenRecorder = null;
766+
throw e;
767+
}
784768
}
785769

786-
export async function stopRecordingScreen(this: NovaWindowsDriver, args?: { remotePath?: string }): Promise<string> {
787-
const { remotePath } = args ?? {};
788-
789-
if (!this.recordingProcess || !this.recordingOutputPath) {
790-
throw new errors.InvalidArgumentError('No screen recording in progress.');
770+
export async function stopRecordingScreen(this: NovaWindowsDriver, args?: UploadOptions): Promise<string> {
771+
if (!this._screenRecorder) {
772+
this.log.debug('No screen recording has been started. Doing nothing');
773+
return '';
791774
}
792775

793-
const proc = this.recordingProcess;
794-
const outputPath = this.recordingOutputPath;
795-
this.recordingProcess = undefined;
796-
this.recordingOutputPath = undefined;
797-
798-
proc.stdin?.write('q');
799-
800-
await new Promise<void>((resolve) => {
801-
proc.on('exit', () => resolve());
802-
setTimeout(() => resolve(), 5000);
803-
});
804-
805-
if (remotePath) {
806-
// TODO: upload to remotePath; for now return empty per Appium convention
807-
try {
808-
await unlink(outputPath);
809-
} catch {
810-
/* ignore */
811-
}
776+
this.log.debug('Retrieving the resulting video data');
777+
const videoPath = await this._screenRecorder.stop();
778+
if (!videoPath) {
779+
this.log.debug('No video data is found. Returning an empty string');
812780
return '';
813781
}
814782

815-
try {
816-
const buffer = await readFile(outputPath);
817-
await unlink(outputPath);
818-
return buffer.toString('base64');
819-
} catch (err) {
820-
throw new errors.UnknownError(`Failed to read recording: ${(err as Error).message}`);
821-
}
783+
const { remotePath, ...uploadOpts } = args ?? {};
784+
return await uploadRecordedMedia(videoPath, remotePath, uploadOpts);
822785
}
823786

824787
export async function deleteFile(this: NovaWindowsDriver, args: { path: string }): Promise<void> {

0 commit comments

Comments
 (0)