Skip to content

Commit af5eeb6

Browse files
committed
fix: address camera video PR feedback
1 parent 0267fe2 commit af5eeb6

5 files changed

Lines changed: 88 additions & 20 deletions

File tree

src/__tests__/client.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,35 @@ test('apps.open resolves session device identifiers from open response', async (
8080
assert.equal(result.device?.ios?.simulatorSetPath, '/tmp/sim-set');
8181
});
8282

83+
test('devices.boot forwards Android emulator camera options', async () => {
84+
const setup = createTransport(async () => ({
85+
ok: true,
86+
data: {
87+
platform: 'android',
88+
target: 'mobile',
89+
device: 'Pixel_9_Pro_XL',
90+
id: 'emulator-5554',
91+
kind: 'emulator',
92+
booted: true,
93+
},
94+
}));
95+
const client = createAgentDeviceClient(setup.config, { transport: setup.transport });
96+
97+
await client.devices.boot({
98+
platform: 'android',
99+
device: 'Pixel_9_Pro_XL',
100+
cameraFront: '/tmp/front.mp4',
101+
cameraBack: 'virtualscene',
102+
});
103+
104+
assert.equal(setup.calls.length, 1);
105+
assert.equal(setup.calls[0]?.command, 'boot');
106+
assert.equal(setup.calls[0]?.flags?.platform, 'android');
107+
assert.equal(setup.calls[0]?.flags?.device, 'Pixel_9_Pro_XL');
108+
assert.equal(setup.calls[0]?.flags?.cameraFront, '/tmp/front.mp4');
109+
assert.equal(setup.calls[0]?.flags?.cameraBack, 'virtualscene');
110+
});
111+
83112
test('apps.open forwards explicit runtime hints through the daemon request', async () => {
84113
const setup = createTransport(async () => ({
85114
ok: true,

src/client-normalizers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,8 @@ export function buildFlags(options: InternalRequestOptions): CommandFlags {
311311
pauseMs: options.pauseMs,
312312
pattern: options.pattern,
313313
headless: options.headless,
314+
cameraFront: options.cameraFront,
315+
cameraBack: options.cameraBack,
314316
restart: options.restart,
315317
replayUpdate: options.replayUpdate,
316318
replayBackend: options.replayBackend,

src/platforms/android/devices.ts

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -438,27 +438,9 @@ export async function ensureAndroidEmulatorBooted(params: {
438438
resolvedAvdName,
439439
params.serial,
440440
);
441-
if (existing && (params.cameraFront || params.cameraBack)) {
442-
throw new AppError(
443-
'INVALID_STATE',
444-
'Android emulator camera inputs can only be applied when starting an emulator.',
445-
{
446-
avdName: resolvedAvdName,
447-
serial: existing.id,
448-
hint: 'Shut down the emulator first, then run boot again with --camera-front or --camera-back.',
449-
},
450-
);
451-
}
441+
assertCameraInputsCanApplyToEmulator(existing, resolvedAvdName, params);
452442
if (!existing) {
453-
const launchArgs = ['-avd', resolvedAvdName];
454-
if (params.headless) {
455-
launchArgs.push('-no-window', '-no-audio');
456-
}
457-
const cameraFront = resolveAndroidEmulatorCameraMode(params.cameraFront, 'front');
458-
if (cameraFront) launchArgs.push('-camera-front', cameraFront);
459-
const cameraBack = resolveAndroidEmulatorCameraMode(params.cameraBack, 'back');
460-
if (cameraBack) launchArgs.push('-camera-back', cameraBack);
461-
runCmdDetached('emulator', launchArgs);
443+
runCmdDetached('emulator', buildEmulatorLaunchArgs(resolvedAvdName, params));
462444
}
463445

464446
const discovered =
@@ -487,6 +469,45 @@ export async function ensureAndroidEmulatorBooted(params: {
487469
};
488470
}
489471

472+
function assertCameraInputsCanApplyToEmulator(
473+
existing: DeviceInfo | undefined,
474+
resolvedAvdName: string,
475+
params: {
476+
cameraFront?: string;
477+
cameraBack?: string;
478+
},
479+
): void {
480+
if (!existing || (!params.cameraFront && !params.cameraBack)) return;
481+
throw new AppError(
482+
'INVALID_STATE',
483+
'Android emulator camera inputs can only be applied when starting an emulator.',
484+
{
485+
avdName: resolvedAvdName,
486+
serial: existing.id,
487+
hint: 'Shut down the emulator first, then run boot again with --camera-front or --camera-back.',
488+
},
489+
);
490+
}
491+
492+
function buildEmulatorLaunchArgs(
493+
resolvedAvdName: string,
494+
params: {
495+
headless?: boolean;
496+
cameraFront?: string;
497+
cameraBack?: string;
498+
},
499+
): string[] {
500+
const launchArgs = ['-avd', resolvedAvdName];
501+
if (params.headless) {
502+
launchArgs.push('-no-window', '-no-audio');
503+
}
504+
const cameraFront = resolveAndroidEmulatorCameraMode(params.cameraFront, 'front');
505+
if (cameraFront) launchArgs.push('-camera-front', cameraFront);
506+
const cameraBack = resolveAndroidEmulatorCameraMode(params.cameraBack, 'back');
507+
if (cameraBack) launchArgs.push('-camera-back', cameraBack);
508+
return launchArgs;
509+
}
510+
490511
function resolveAndroidEmulatorCameraMode(
491512
value: string | undefined,
492513
camera: 'front' | 'back',

website/docs/docs/client-api.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,19 @@ Additional CLI-backed methods are exposed on their domain groups with typed opti
257257
- `client.recording.record()` and `client.recording.trace()`
258258
- `client.settings.update()`
259259

260+
`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:
261+
262+
```ts
263+
await client.devices.boot({
264+
platform: 'android',
265+
device: 'Pixel_9_Pro_XL',
266+
cameraFront: './front.mp4',
267+
cameraBack: 'virtualscene',
268+
});
269+
```
270+
271+
Camera inputs are Android-emulator-only and apply only when starting the emulator; shut down a running emulator before changing them.
272+
260273
`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.
261274

262275
`client.recording.record({ action: 'start', path, quality: 5 })` starts a smaller 50% resolution video; omit `quality` to keep native/current resolution.

website/docs/docs/commands.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ agent-device boot
3838
agent-device boot --platform ios
3939
agent-device boot --platform android
4040
agent-device boot --platform android --device Pixel_9_Pro_XL --headless
41+
agent-device boot --platform android --device Pixel_9_Pro_XL --camera-back ./back.mp4 --camera-front none
4142
agent-device shutdown --platform ios
4243
agent-device shutdown --platform android --device Pixel_9_Pro_XL
4344
agent-device open [app|url] [url]
@@ -62,6 +63,8 @@ agent-device app-switcher
6263
- `boot` is mainly needed when starting a new session and `open` fails because no booted simulator/emulator is available.
6364
- Android: `boot --platform android --device <avd-name>` launches that emulator in GUI mode when needed.
6465
- Android: add `--headless` to launch without opening a GUI window.
66+
- 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.
67+
- Android camera inputs apply only when starting an emulator. Shut down an already-running emulator first, then run `boot` again with the camera flags.
6568
- Android: `shutdown --platform android --device <avd-name>` stops a running emulator.
6669
- `open [app|url] [url]` already boots/activates the selected target when needed.
6770
- `open <url>` deep links are supported on Android and iOS.

0 commit comments

Comments
 (0)