fix: address camera video PR feedback

This commit is contained in:
Michał Pierzchała
2026-06-10 20:05:17 +02:00
parent 0267fe2103
commit af5eeb6835
5 changed files with 88 additions and 20 deletions
+29
View File
@@ -80,6 +80,35 @@ test('apps.open resolves session device identifiers from open response', async (
assert.equal(result.device?.ios?.simulatorSetPath, '/tmp/sim-set');
});
test('devices.boot forwards Android emulator camera options', async () => {
const setup = createTransport(async () => ({
ok: true,
data: {
platform: 'android',
target: 'mobile',
device: 'Pixel_9_Pro_XL',
id: 'emulator-5554',
kind: 'emulator',
booted: true,
},
}));
const client = createAgentDeviceClient(setup.config, { transport: setup.transport });
await client.devices.boot({
platform: 'android',
device: 'Pixel_9_Pro_XL',
cameraFront: '/tmp/front.mp4',
cameraBack: 'virtualscene',
});
assert.equal(setup.calls.length, 1);
assert.equal(setup.calls[0]?.command, 'boot');
assert.equal(setup.calls[0]?.flags?.platform, 'android');
assert.equal(setup.calls[0]?.flags?.device, 'Pixel_9_Pro_XL');
assert.equal(setup.calls[0]?.flags?.cameraFront, '/tmp/front.mp4');
assert.equal(setup.calls[0]?.flags?.cameraBack, 'virtualscene');
});
test('apps.open forwards explicit runtime hints through the daemon request', async () => {
const setup = createTransport(async () => ({
ok: true,
+2
View File
@@ -311,6 +311,8 @@ export function buildFlags(options: InternalRequestOptions): CommandFlags {
pauseMs: options.pauseMs,
pattern: options.pattern,
headless: options.headless,
cameraFront: options.cameraFront,
cameraBack: options.cameraBack,
restart: options.restart,
replayUpdate: options.replayUpdate,
replayBackend: options.replayBackend,
+41 -20
View File
@@ -438,27 +438,9 @@ export async function ensureAndroidEmulatorBooted(params: {
resolvedAvdName,
params.serial,
);
if (existing && (params.cameraFront || params.cameraBack)) {
throw new AppError(
'INVALID_STATE',
'Android emulator camera inputs can only be applied when starting an emulator.',
{
avdName: resolvedAvdName,
serial: existing.id,
hint: 'Shut down the emulator first, then run boot again with --camera-front or --camera-back.',
},
);
}
assertCameraInputsCanApplyToEmulator(existing, resolvedAvdName, params);
if (!existing) {
const launchArgs = ['-avd', resolvedAvdName];
if (params.headless) {
launchArgs.push('-no-window', '-no-audio');
}
const cameraFront = resolveAndroidEmulatorCameraMode(params.cameraFront, 'front');
if (cameraFront) launchArgs.push('-camera-front', cameraFront);
const cameraBack = resolveAndroidEmulatorCameraMode(params.cameraBack, 'back');
if (cameraBack) launchArgs.push('-camera-back', cameraBack);
runCmdDetached('emulator', launchArgs);
runCmdDetached('emulator', buildEmulatorLaunchArgs(resolvedAvdName, params));
}
const discovered =
@@ -487,6 +469,45 @@ export async function ensureAndroidEmulatorBooted(params: {
};
}
function assertCameraInputsCanApplyToEmulator(
existing: DeviceInfo | undefined,
resolvedAvdName: string,
params: {
cameraFront?: string;
cameraBack?: string;
},
): void {
if (!existing || (!params.cameraFront && !params.cameraBack)) return;
throw new AppError(
'INVALID_STATE',
'Android emulator camera inputs can only be applied when starting an emulator.',
{
avdName: resolvedAvdName,
serial: existing.id,
hint: 'Shut down the emulator first, then run boot again with --camera-front or --camera-back.',
},
);
}
function buildEmulatorLaunchArgs(
resolvedAvdName: string,
params: {
headless?: boolean;
cameraFront?: string;
cameraBack?: string;
},
): string[] {
const launchArgs = ['-avd', resolvedAvdName];
if (params.headless) {
launchArgs.push('-no-window', '-no-audio');
}
const cameraFront = resolveAndroidEmulatorCameraMode(params.cameraFront, 'front');
if (cameraFront) launchArgs.push('-camera-front', cameraFront);
const cameraBack = resolveAndroidEmulatorCameraMode(params.cameraBack, 'back');
if (cameraBack) launchArgs.push('-camera-back', cameraBack);
return launchArgs;
}
function resolveAndroidEmulatorCameraMode(
value: string | undefined,
camera: 'front' | 'back',
+13
View File
@@ -257,6 +257,19 @@ Additional CLI-backed methods are exposed on their domain groups with typed opti
- `client.recording.record()` and `client.recording.trace()`
- `client.settings.update()`
`client.devices.boot({ platform: 'android', device: 'Pixel_9_Pro_XL', headless: true })` starts an Android emulator without a GUI when it is not already running. To launch with emulator camera inputs, pass `cameraFront` and/or `cameraBack` with `emulated`, `none`, `webcam<N>`, `virtualscene` for the back camera, or a video file path:
```ts
await client.devices.boot({
platform: 'android',
device: 'Pixel_9_Pro_XL',
cameraFront: './front.mp4',
cameraBack: 'virtualscene',
});
```
Camera inputs are Android-emulator-only and apply only when starting the emulator; shut down a running emulator before changing them.
`client.observability.perf()` returns daemon-shaped JSON so local and remote transports expose the same metrics payload. Pass `{ area: 'metrics' }` for the broad startup/CPU/memory/frame first pass, or `{ area: 'frames' }` for a focused frame/jank-health payload. On Android and supported Apple targets, `data.metrics.fps.droppedFramePercent` is the primary frame-smoothness value. Android derives it from the current `adb shell dumpsys gfxinfo <package> framestats` window; connected iOS devices derive it from `xcrun xctrace` Animation Hitches for the active app process. Frame samples include `windowStartedAt`, `windowEndedAt`, and `worstWindows` so agents can correlate dropped-frame clusters with logs, network entries, and their own session actions. A successful Android read resets Android frame stats; `open <app>` resets the Android frame window too, so agents can call `perf({ area: 'frames' })`, perform a transition or gesture, then call it again to inspect that focused window. iOS simulator and macOS app sessions report frame health as unavailable rather than inventing FPS or dropped-frame values.
`client.recording.record({ action: 'start', path, quality: 5 })` starts a smaller 50% resolution video; omit `quality` to keep native/current resolution.
+3
View File
@@ -38,6 +38,7 @@ agent-device boot
agent-device boot --platform ios
agent-device boot --platform android
agent-device boot --platform android --device Pixel_9_Pro_XL --headless
agent-device boot --platform android --device Pixel_9_Pro_XL --camera-back ./back.mp4 --camera-front none
agent-device shutdown --platform ios
agent-device shutdown --platform android --device Pixel_9_Pro_XL
agent-device open [app|url] [url]
@@ -62,6 +63,8 @@ agent-device app-switcher
- `boot` is mainly needed when starting a new session and `open` fails because no booted simulator/emulator is available.
- Android: `boot --platform android --device <avd-name>` launches that emulator in GUI mode when needed.
- Android: add `--headless` to launch without opening a GUI window.
- Android: add `--camera-front <mode|videoPath>` and/or `--camera-back <mode|videoPath>` when launching an emulator with camera inputs. Supported modes are `emulated`, `none`, `webcam<N>`, and `virtualscene` for the back camera only. File paths are converted to `videofile:<absolute-path>` for the emulator.
- Android camera inputs apply only when starting an emulator. Shut down an already-running emulator first, then run `boot` again with the camera flags.
- Android: `shutdown --platform android --device <avd-name>` stops a running emulator.
- `open [app|url] [url]` already boots/activates the selected target when needed.
- `open <url>` deep links are supported on Android and iOS.