Skip to content

Commit cfd0ffc

Browse files
committed
fix(camera): harden source switching
1 parent e0cf3b1 commit cfd0ffc

3 files changed

Lines changed: 37 additions & 11 deletions

File tree

packages/client/src/features/simulators/CameraSimulationModal.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export function CameraSimulationModal({
7777
const activeBundleText = useMemo(() => {
7878
const bundleIds = status?.bundleIds ?? [];
7979
if (bundleIds.length === 0) {
80-
return status?.alive ? "helper running" : "not running";
80+
return status?.alive ? "daemon running" : "not running";
8181
}
8282
return bundleIds.join(", ");
8383
}, [status]);
@@ -95,7 +95,11 @@ export function CameraSimulationModal({
9595
try {
9696
const nextStatus = await fetchCameraStatus(udid);
9797
setStatus(nextStatus);
98-
if (nextStatus.mirror === "on" || nextStatus.mirror === "off") {
98+
if (
99+
nextStatus.mirror === "auto" ||
100+
nextStatus.mirror === "on" ||
101+
nextStatus.mirror === "off"
102+
) {
99103
setMirror(nextStatus.mirror);
100104
}
101105
if (

packages/server/native/camera/SimDeckCameraService.m

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,15 @@ static BOOL PublishImageAtPath(NSString *path, NSString **error) {
323323
return YES;
324324
}
325325

326+
static BOOL CanDecodeImageAtPath(NSString *path, NSString **error) {
327+
NSImage *image = [[NSImage alloc] initWithContentsOfFile:path];
328+
if ([image CGImageForProposedRect:NULL context:nil hints:nil]) {
329+
return YES;
330+
}
331+
if (error) *error = [NSString stringWithFormat:@"Unable to decode image at %@", path];
332+
return NO;
333+
}
334+
326335
@interface SimDeckCameraWebcamWriter : NSObject <AVCaptureVideoDataOutputSampleBufferDelegate>
327336
@property (nonatomic, copy) NSString *label;
328337
@end
@@ -404,6 +413,9 @@ static BOOL StartPlaceholder(NSString **error) {
404413
}
405414

406415
static BOOL StartImage(NSString *path, NSString **error) {
416+
if (!CanDecodeImageAtPath(path, error)) {
417+
return NO;
418+
}
407419
StopCurrentSource();
408420
if (!PublishImageAtPath(path, error)) {
409421
return NO;
@@ -475,7 +487,6 @@ static BOOL StartVideo(NSString *path, NSString **error) {
475487
}
476488

477489
static BOOL StartWebcam(NSString *requestedDevice, NSString **error) {
478-
StopCurrentSource();
479490
if (!EnsureCameraAccess(error)) {
480491
return NO;
481492
}
@@ -510,6 +521,7 @@ static BOOL StartWebcam(NSString *requestedDevice, NSString **error) {
510521
return NO;
511522
}
512523
[session addOutput:output];
524+
StopCurrentSource();
513525
gWebcamSession = session;
514526
gWebcamDelegate = writer;
515527
[session startRunning];
@@ -766,10 +778,15 @@ bool simdeck_camera_start(const char *udid,
766778
nativeError = @"Camera simulation is not running for this simulator.";
767779
return;
768780
}
781+
BOOL hasMirrorUpdate = mirror && mirror[0] && gHeader;
782+
uint32_t previousMirrorMode = hasMirrorUpdate ? gHeader->mirrorMode : SIMDECK_CAMERA_MIRROR_AUTO;
769783
if (mirror && mirror[0] && gHeader) {
770784
gHeader->mirrorMode = MirrorModeForName(StringFromCString(mirror));
771785
}
772786
if (source && source[0] && !SwitchSource(StringFromCString(source), StringFromCString(sourceArgument), &nativeError)) {
787+
if (hasMirrorUpdate && gHeader) {
788+
gHeader->mirrorMode = previousMirrorMode;
789+
}
773790
return;
774791
}
775792
result = JSONCString(StatusPayload(YES, nil));

packages/server/src/camera.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,19 +86,24 @@ pub fn start_camera(options: CameraStartOptions) -> Result<Value, AppError> {
8686
validate_udid(&options.udid)?;
8787
let source = normalize_source(options.source)?;
8888
let mirror = normalize_mirror(options.mirror.as_deref())?;
89+
let bundle_id = options
90+
.bundle_id
91+
.as_deref()
92+
.map(str::trim)
93+
.filter(|value| !value.is_empty());
94+
if let Some(bundle_id) = bundle_id {
95+
validate_bundle_id(bundle_id)?;
96+
}
8997
fs::create_dir_all(camera_state_dir()).map_err(app_internal)?;
9098

9199
let shm_name = shm_name_for_udid(&options.udid);
92100
native_start_camera(&options.udid, &shm_name, &source, &mirror)?;
93101

94-
if let Some(bundle_id) = options
95-
.bundle_id
96-
.as_deref()
97-
.map(str::trim)
98-
.filter(|value| !value.is_empty())
99-
{
100-
validate_bundle_id(bundle_id)?;
101-
launch_with_injector(&options.udid, bundle_id, &shm_name, &mirror)?;
102+
if let Some(bundle_id) = bundle_id {
103+
if let Err(error) = launch_with_injector(&options.udid, bundle_id, &shm_name, &mirror) {
104+
let _ = native_stop_camera(&options.udid);
105+
return Err(error);
106+
}
102107
record_injected_bundle(&options.udid, bundle_id)?;
103108
}
104109

0 commit comments

Comments
 (0)