@expo/serve-sim 0.1.35-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +279 -0
  3. package/Sources/SimAXSettings/build.sh +21 -0
  4. package/Sources/SimAXSettings/sim-ax-settings.m +273 -0
  5. package/Sources/SimCameraHelper/build.sh +33 -0
  6. package/Sources/SimCameraHelper/main.m +955 -0
  7. package/Sources/SimCameraInjector/SimCamFakes.h +88 -0
  8. package/Sources/SimCameraInjector/SimCamFakes.m +704 -0
  9. package/Sources/SimCameraInjector/SimCamFrameSource.h +26 -0
  10. package/Sources/SimCameraInjector/SimCamFrameSource.m +577 -0
  11. package/Sources/SimCameraInjector/SimCamLog.h +5 -0
  12. package/Sources/SimCameraInjector/SimCamLog.m +9 -0
  13. package/Sources/SimCameraInjector/SimCamSwizzles.h +3 -0
  14. package/Sources/SimCameraInjector/SimCamSwizzles.m +1338 -0
  15. package/Sources/SimCameraInjector/SimCameraInjector.m +19 -0
  16. package/Sources/SimCameraInjector/build.sh +39 -0
  17. package/Sources/SimCameraInjector/include/SimCamShared.h +79 -0
  18. package/dist/bin/LiveKitWebRTC.framework/LiveKitWebRTC +0 -0
  19. package/dist/bin/LiveKitWebRTC.framework/Resources/Info.plist +36 -0
  20. package/dist/bin/LiveKitWebRTC.framework/Resources/LICENSE.webrtc +29 -0
  21. package/dist/bin/LiveKitWebRTC.framework/Resources/PrivacyInfo.xcprivacy +32 -0
  22. package/dist/bin/LiveKitWebRTC.framework/_CodeSignature/CodeResources +150 -0
  23. package/dist/middleware.cjs +2 -0
  24. package/dist/middleware.js +123 -0
  25. package/dist/native/serve-sim-native.node +0 -0
  26. package/dist/serve-sim.js +218 -0
  27. package/dist/simax/serve-sim-ax-settings +0 -0
  28. package/dist/simcam/libSimCameraInjector.dylib +0 -0
  29. package/dist/simcam/serve-sim-camera-helper +0 -0
  30. package/dist/state.js +1 -0
  31. package/package.json +99 -0
  32. package/src/ax-shared.ts +25 -0
  33. package/src/ax.ts +258 -0
  34. package/src/camera-helper.ts +150 -0
  35. package/src/connect-to-fetch.ts +239 -0
  36. package/src/middleware.ts +2208 -0
  37. package/src/native.ts +294 -0
  38. package/src/state.ts +86 -0
  39. package/src/stream-settings.ts +202 -0
@@ -0,0 +1,1338 @@
1
+ #import "SimCamSwizzles.h"
2
+ #import "SimCamFakes.h"
3
+ #import "SimCamFrameSource.h"
4
+ #import "SimCamLog.h"
5
+
6
+ #import <AVFoundation/AVFoundation.h>
7
+ #import <CoreImage/CoreImage.h>
8
+ #import <CoreMedia/CoreMedia.h>
9
+ #import <CoreMotion/CoreMotion.h>
10
+ #import <CoreVideo/CoreVideo.h>
11
+ #import <UIKit/UIKit.h>
12
+ #import <objc/runtime.h>
13
+ #import <objc/message.h>
14
+ #include <stdatomic.h>
15
+ #include <execinfo.h>
16
+ #include <dlfcn.h>
17
+ #include <string.h>
18
+
19
+ #pragma mark - Swizzling helpers
20
+
21
+ static BOOL SwizzleClassMethod(Class cls, SEL orig, SEL swiz) {
22
+ Method o = class_getClassMethod(cls, orig);
23
+ Method s = class_getClassMethod(cls, swiz);
24
+ if (!o) {
25
+ simcam_log(@"swizzle FAILED: +[%@ %@] (orig method not found)",
26
+ NSStringFromClass(cls), NSStringFromSelector(orig));
27
+ return NO;
28
+ }
29
+ if (!s) {
30
+ simcam_log(@"swizzle FAILED: +[%@ %@] (replacement %@ not found)",
31
+ NSStringFromClass(cls), NSStringFromSelector(orig),
32
+ NSStringFromSelector(swiz));
33
+ return NO;
34
+ }
35
+ method_exchangeImplementations(o, s);
36
+ return YES;
37
+ }
38
+ static BOOL SwizzleInstanceMethod(Class cls, SEL orig, SEL swiz) {
39
+ Method o = class_getInstanceMethod(cls, orig);
40
+ Method s = class_getInstanceMethod(cls, swiz);
41
+ if (!o) {
42
+ simcam_log(@"swizzle FAILED: -[%@ %@] (orig method not found)",
43
+ NSStringFromClass(cls), NSStringFromSelector(orig));
44
+ return NO;
45
+ }
46
+ if (!s) {
47
+ simcam_log(@"swizzle FAILED: -[%@ %@] (replacement %@ not found)",
48
+ NSStringFromClass(cls), NSStringFromSelector(orig),
49
+ NSStringFromSelector(swiz));
50
+ return NO;
51
+ }
52
+ // `orig` may be inherited rather than implemented directly on `cls` (e.g.
53
+ // on iOS 26 UIImagePickerController no longer overrides viewDidAppear:).
54
+ // A plain method_exchangeImplementations would then mutate the *superclass*
55
+ // Method, clobbering that selector for every subclass — and our `swiz`
56
+ // selector only exists on `cls`, so unrelated controllers crash with
57
+ // "unrecognized selector simcam_…". Install the override directly on `cls`
58
+ // instead: add `orig` pointing at the swizzled IMP, and if that succeeds
59
+ // (no direct impl existed) repoint `swiz` at the inherited original so the
60
+ // [self simcam_…] call still reaches it.
61
+ IMP origIMP = method_getImplementation(o);
62
+ IMP swizIMP = method_getImplementation(s);
63
+ if (class_addMethod(cls, orig, swizIMP, method_getTypeEncoding(s))) {
64
+ class_replaceMethod(cls, swiz, origIMP, method_getTypeEncoding(o));
65
+ } else {
66
+ method_exchangeImplementations(o, s);
67
+ }
68
+ return YES;
69
+ }
70
+
71
+ #pragma mark - NSNotificationCenter swizzle (suppress AVF runtime error)
72
+
73
+ @interface NSNotificationCenter (SimCam)
74
+ @end
75
+ @implementation NSNotificationCenter (SimCam)
76
+
77
+ - (void)simcam_postNotificationName:(NSNotificationName)name
78
+ object:(id)object
79
+ userInfo:(NSDictionary *)userInfo {
80
+ if (SimCamShouldSwallowAVFRuntimeError(name, object)) {
81
+ SimCamLogSwallowedRuntimeError(@"postNotificationName:object:userInfo:", object, userInfo);
82
+ return;
83
+ }
84
+ [self simcam_postNotificationName:name object:object userInfo:userInfo];
85
+ }
86
+
87
+ - (void)simcam_postNotificationName:(NSNotificationName)name object:(id)object {
88
+ if (SimCamShouldSwallowAVFRuntimeError(name, object)) {
89
+ SimCamLogSwallowedRuntimeError(@"postNotificationName:object:", object, nil);
90
+ return;
91
+ }
92
+ [self simcam_postNotificationName:name object:object];
93
+ }
94
+
95
+ - (void)simcam_postNotification:(NSNotification *)note {
96
+ if (SimCamShouldSwallowAVFRuntimeError(note.name, note.object)) {
97
+ SimCamLogSwallowedRuntimeError(@"postNotification:", note.object, note.userInfo);
98
+ return;
99
+ }
100
+ [self simcam_postNotification:note];
101
+ }
102
+
103
+ @end
104
+
105
+ #pragma mark - AVCaptureDevice swizzles
106
+
107
+ @interface AVCaptureDevice (SimCam)
108
+ @end
109
+ @implementation AVCaptureDevice (SimCam)
110
+ + (AVCaptureDevice *)simcam_defaultDeviceWithDeviceType:(AVCaptureDeviceType)t
111
+ mediaType:(AVMediaType)m
112
+ position:(AVCaptureDevicePosition)p {
113
+ if ([m isEqualToString:AVMediaTypeVideo] || m == nil) {
114
+ AVCaptureDevicePosition resolved =
115
+ (p == AVCaptureDevicePositionBack) ? AVCaptureDevicePositionBack
116
+ : AVCaptureDevicePositionFront;
117
+ simcam_log(@"defaultDeviceWithDeviceType: %@ position: %d → fake",
118
+ t, (int)resolved);
119
+ return SimCamFakeDeviceForPosition(resolved);
120
+ }
121
+ return [self simcam_defaultDeviceWithDeviceType:t mediaType:m position:p];
122
+ }
123
+ + (NSArray<AVCaptureDevice *> *)simcam_devicesWithMediaType:(AVMediaType)m {
124
+ if ([m isEqualToString:AVMediaTypeVideo]) {
125
+ return @[
126
+ SimCamFakeDeviceForPosition(AVCaptureDevicePositionFront),
127
+ SimCamFakeDeviceForPosition(AVCaptureDevicePositionBack),
128
+ ];
129
+ }
130
+ return [self simcam_devicesWithMediaType:m];
131
+ }
132
+ + (NSArray<AVCaptureDevice *> *)simcam_devices {
133
+ NSArray *real = [self simcam_devices];
134
+ NSArray *fakes = @[
135
+ SimCamFakeDeviceForPosition(AVCaptureDevicePositionFront),
136
+ SimCamFakeDeviceForPosition(AVCaptureDevicePositionBack),
137
+ ];
138
+ return [fakes arrayByAddingObjectsFromArray:real ?: @[]];
139
+ }
140
+ @end
141
+
142
+ #pragma mark - AVCaptureDeviceDiscoverySession swizzles
143
+
144
+ @interface AVCaptureDeviceDiscoverySession (SimCam)
145
+ @end
146
+ @implementation AVCaptureDeviceDiscoverySession (SimCam)
147
+ + (AVCaptureDeviceDiscoverySession *)simcam_discoverySessionWithDeviceTypes:(NSArray<AVCaptureDeviceType> *)types
148
+ mediaType:(AVMediaType)m
149
+ position:(AVCaptureDevicePosition)p {
150
+ AVCaptureDeviceDiscoverySession *real =
151
+ [self simcam_discoverySessionWithDeviceTypes:types mediaType:m position:p];
152
+ if ([m isEqualToString:AVMediaTypeVideo] || m == nil) {
153
+ NSMutableArray *list = [NSMutableArray new];
154
+ if (p == AVCaptureDevicePositionUnspecified || p == AVCaptureDevicePositionFront)
155
+ [list addObject:SimCamFakeDeviceForPosition(AVCaptureDevicePositionFront)];
156
+ if (p == AVCaptureDevicePositionUnspecified || p == AVCaptureDevicePositionBack)
157
+ [list addObject:SimCamFakeDeviceForPosition(AVCaptureDevicePositionBack)];
158
+ @try {
159
+ [real setValue:list forKey:@"devices"];
160
+ } @catch (__unused id e) {
161
+ simcam_log(@"could not override discovery session devices");
162
+ }
163
+ }
164
+ return real;
165
+ }
166
+ @end
167
+
168
+ #pragma mark - AVCaptureDeviceInput swizzle
169
+
170
+ @interface AVCaptureDeviceInput (SimCam)
171
+ @end
172
+ @implementation AVCaptureDeviceInput (SimCam)
173
+ - (instancetype)simcam_initWithDevice:(AVCaptureDevice *)device error:(NSError **)err {
174
+ if ([device isKindOfClass:[SimCamFakeDevice class]]) {
175
+ if (err) *err = nil;
176
+ struct objc_super sup = { self, [NSObject class] };
177
+ id obj = ((id (*)(struct objc_super *, SEL))objc_msgSendSuper)(&sup, @selector(init));
178
+ if (obj) {
179
+ SimCamMarkFakeInput(obj, device);
180
+ SimCamSetPosition(obj, device.position);
181
+ SimCamMarkCameraInUse();
182
+ }
183
+ return obj;
184
+ }
185
+ return [self simcam_initWithDevice:device error:err];
186
+ }
187
+ - (AVCaptureDevice *)simcam_device {
188
+ AVCaptureDevice *fake = SimCamFakeInputDevice(self);
189
+ if (fake) return fake;
190
+ return [self simcam_device];
191
+ }
192
+ - (NSArray *)simcam_ports {
193
+ if (SimCamIsFakeInput(self)) return @[];
194
+ return [self simcam_ports];
195
+ }
196
+ @end
197
+
198
+ #pragma mark - AVCaptureSession swizzles
199
+
200
+ static char kSimCamSessionRunningKey;
201
+ static char kSimCamSessionInputsKey;
202
+ static char kSimCamSessionOutputsKey;
203
+ static char kSimCamOutputAttachedToFakeSessionKey;
204
+
205
+ static NSMutableArray *SimCamSessionTrackedInputs(AVCaptureSession *s) {
206
+ NSMutableArray *arr = objc_getAssociatedObject(s, &kSimCamSessionInputsKey);
207
+ if (!arr) {
208
+ arr = [NSMutableArray new];
209
+ objc_setAssociatedObject(s, &kSimCamSessionInputsKey, arr, OBJC_ASSOCIATION_RETAIN);
210
+ }
211
+ return arr;
212
+ }
213
+ static NSMutableArray *SimCamSessionTrackedOutputs(AVCaptureSession *s) {
214
+ NSMutableArray *arr = objc_getAssociatedObject(s, &kSimCamSessionOutputsKey);
215
+ if (!arr) {
216
+ arr = [NSMutableArray new];
217
+ objc_setAssociatedObject(s, &kSimCamSessionOutputsKey, arr, OBJC_ASSOCIATION_RETAIN);
218
+ }
219
+ return arr;
220
+ }
221
+
222
+ static AVCaptureInput *SimCamFirstFakeInputForSession(AVCaptureSession *s) {
223
+ for (AVCaptureInput *candidate in SimCamSessionTrackedInputs(s)) {
224
+ if (SimCamIsFakeInput(candidate)) return candidate;
225
+ }
226
+ return nil;
227
+ }
228
+
229
+ // Real AVFoundation only exposes output connections after an output has been
230
+ // attached to a session. Keep this per-output so newly created outputs still
231
+ // look disconnected during client-side session configuration checks.
232
+ static void SimCamMarkOutputAttachedToFakeSession(AVCaptureSession *s, AVCaptureOutput *output) {
233
+ if (!output) return;
234
+ objc_setAssociatedObject(output, &kSimCamOutputAttachedToFakeSessionKey, @YES, OBJC_ASSOCIATION_RETAIN);
235
+ SimCamSetOutputInput(output, SimCamFirstFakeInputForSession(s));
236
+ }
237
+ static void SimCamUnmarkOutputAttachedToFakeSession(AVCaptureOutput *output) {
238
+ if (!output) return;
239
+ objc_setAssociatedObject(output, &kSimCamOutputAttachedToFakeSessionKey, nil, OBJC_ASSOCIATION_RETAIN);
240
+ SimCamSetOutputInput(output, nil);
241
+ }
242
+ static BOOL SimCamOutputAttachedToFakeSession(AVCaptureOutput *output) {
243
+ if (!output) return NO;
244
+ return [objc_getAssociatedObject(output, &kSimCamOutputAttachedToFakeSessionKey) boolValue];
245
+ }
246
+ static void SimCamRefreshAttachedOutputInputsForSession(AVCaptureSession *s) {
247
+ AVCaptureInput *input = SimCamFirstFakeInputForSession(s);
248
+ for (AVCaptureOutput *output in SimCamSessionTrackedOutputs(s)) {
249
+ if (SimCamOutputAttachedToFakeSession(output)) {
250
+ SimCamSetOutputInput(output, input);
251
+ }
252
+ }
253
+ }
254
+
255
+ @interface AVCaptureSession (SimCam)
256
+ @end
257
+ @implementation AVCaptureSession (SimCam)
258
+ - (void)simcam_addInput:(AVCaptureInput *)input {
259
+ if (SimCamIsFakeInput(input)) {
260
+ AVCaptureDevicePosition p = SimCamPositionOf(input);
261
+ SimCamSetPosition(self, p);
262
+ SimCamMarkCameraInUse();
263
+ SimCamMarkSessionUsingFakeCamera(self, YES);
264
+ NSMutableArray *tracked = SimCamSessionTrackedInputs(self);
265
+ if (![tracked containsObject:input]) [tracked addObject:input];
266
+ SimCamRefreshAttachedOutputInputsForSession(self);
267
+ simcam_log(@"addInput: fake input (%@) — tracked (count=%lu), skipping native add",
268
+ p == AVCaptureDevicePositionBack ? @"back" : @"front",
269
+ (unsigned long)tracked.count);
270
+ return;
271
+ }
272
+ [self simcam_addInput:input];
273
+ }
274
+ - (BOOL)simcam_canAddInput:(AVCaptureInput *)input {
275
+ if (SimCamIsFakeInput(input)) return YES;
276
+ return [self simcam_canAddInput:input];
277
+ }
278
+ - (void)simcam_addInputWithNoConnections:(AVCaptureInput *)input {
279
+ if (SimCamIsFakeInput(input)) {
280
+ AVCaptureDevicePosition p = SimCamPositionOf(input);
281
+ SimCamSetPosition(self, p);
282
+ SimCamMarkCameraInUse();
283
+ SimCamMarkSessionUsingFakeCamera(self, YES);
284
+ NSMutableArray *tracked = SimCamSessionTrackedInputs(self);
285
+ if (![tracked containsObject:input]) [tracked addObject:input];
286
+ SimCamRefreshAttachedOutputInputsForSession(self);
287
+ simcam_log(@"addInputWithNoConnections: fake input (%@) — tracked (count=%lu), skipping native add",
288
+ p == AVCaptureDevicePositionBack ? @"back" : @"front",
289
+ (unsigned long)tracked.count);
290
+ return;
291
+ }
292
+ [self simcam_addInputWithNoConnections:input];
293
+ }
294
+ - (void)simcam_removeInput:(AVCaptureInput *)input {
295
+ if (SimCamIsFakeInput(input)) {
296
+ NSMutableArray *tracked = SimCamSessionTrackedInputs(self);
297
+ [tracked removeObject:input];
298
+ SimCamRefreshAttachedOutputInputsForSession(self);
299
+ if (tracked.count == 0) SimCamMarkSessionUsingFakeCamera(self, NO);
300
+ simcam_log(@"removeInput: fake input — untracked (count=%lu)",
301
+ (unsigned long)tracked.count);
302
+ return;
303
+ }
304
+ [self simcam_removeInput:input];
305
+ }
306
+ - (void)simcam_addOutput:(AVCaptureOutput *)output {
307
+ SimCamSetPosition(output, SimCamPositionOf(self));
308
+ SimCamMarkOutputAttachedToFakeSession(self, output);
309
+ SimCamMarkCameraInUse();
310
+ NSMutableArray *tracked = SimCamSessionTrackedOutputs(self);
311
+ if (![tracked containsObject:output]) [tracked addObject:output];
312
+ simcam_log(@"addOutput: %@ (intercepted, tracked count=%lu, pos=%d)",
313
+ NSStringFromClass([output class]),
314
+ (unsigned long)tracked.count,
315
+ (int)SimCamPositionOf(self));
316
+ }
317
+ - (BOOL)simcam_canAddOutput:(AVCaptureOutput *)output { return YES; }
318
+ - (void)simcam_addOutputWithNoConnections:(AVCaptureOutput *)output {
319
+ SimCamSetPosition(output, SimCamPositionOf(self));
320
+ SimCamMarkOutputAttachedToFakeSession(self, output);
321
+ SimCamMarkCameraInUse();
322
+ NSMutableArray *tracked = SimCamSessionTrackedOutputs(self);
323
+ if (![tracked containsObject:output]) [tracked addObject:output];
324
+ simcam_log(@"addOutputWithNoConnections: %@ (intercepted, tracked count=%lu, pos=%d)",
325
+ NSStringFromClass([output class]),
326
+ (unsigned long)tracked.count,
327
+ (int)SimCamPositionOf(self));
328
+ }
329
+ - (void)simcam_removeOutput:(AVCaptureOutput *)output {
330
+ SimCamUnmarkOutputAttachedToFakeSession(output);
331
+ NSMutableArray *tracked = SimCamSessionTrackedOutputs(self);
332
+ [tracked removeObject:output];
333
+ simcam_log(@"removeOutput: %@ — untracked (count=%lu)",
334
+ NSStringFromClass([output class]), (unsigned long)tracked.count);
335
+ }
336
+ - (void)simcam_beginConfiguration {
337
+ simcam_log(@"beginConfiguration intercepted (session=%p)", self);
338
+ }
339
+ - (void)simcam_commitConfiguration {
340
+ NSUInteger inCount =
341
+ ((NSArray *)objc_getAssociatedObject(self, &kSimCamSessionInputsKey)).count;
342
+ NSUInteger outCount =
343
+ ((NSArray *)objc_getAssociatedObject(self, &kSimCamSessionOutputsKey)).count;
344
+ simcam_log(@"commitConfiguration intercepted (session=%p, fakeInputs=%lu, fakeOutputs=%lu)",
345
+ self, (unsigned long)inCount, (unsigned long)outCount);
346
+ }
347
+ - (BOOL)simcam_canAddConnection:(AVCaptureConnection *)c { (void)c; return YES; }
348
+ - (void)simcam_addConnection:(AVCaptureConnection *)c {
349
+ simcam_log(@"addConnection intercepted (session=%p, conn=%p)", self, c);
350
+ }
351
+ - (NSArray<AVCaptureInput *> *)simcam_inputs {
352
+ NSMutableArray *tracked = objc_getAssociatedObject(self, &kSimCamSessionInputsKey);
353
+ NSArray *native = [self simcam_inputs];
354
+ if (tracked.count == 0) return native ?: @[];
355
+ if (native.count == 0) return [tracked copy];
356
+ NSMutableArray *merged = [tracked mutableCopy];
357
+ for (AVCaptureInput *n in native) {
358
+ if (![merged containsObject:n]) [merged addObject:n];
359
+ }
360
+ return [merged copy];
361
+ }
362
+ - (NSArray<AVCaptureOutput *> *)simcam_outputs {
363
+ NSMutableArray *tracked = objc_getAssociatedObject(self, &kSimCamSessionOutputsKey);
364
+ NSArray *native = [self simcam_outputs];
365
+ if (tracked.count == 0) return native ?: @[];
366
+ if (native.count == 0) return [tracked copy];
367
+ NSMutableArray *merged = [tracked mutableCopy];
368
+ for (AVCaptureOutput *n in native) {
369
+ if (![merged containsObject:n]) [merged addObject:n];
370
+ }
371
+ return [merged copy];
372
+ }
373
+ - (NSArray<AVCaptureConnection *> *)simcam_connections {
374
+ NSMutableArray *trackedOut = objc_getAssociatedObject(self, &kSimCamSessionOutputsKey);
375
+ NSArray *native = [self simcam_connections];
376
+ if (trackedOut.count == 0) return native ?: @[];
377
+ NSMutableArray *merged = [NSMutableArray arrayWithCapacity:trackedOut.count + native.count];
378
+ for (AVCaptureOutput *o in trackedOut) {
379
+ AVCaptureConnection *c = SimCamFakeConnectionForOutput(o);
380
+ if (c) [merged addObject:c];
381
+ }
382
+ for (AVCaptureConnection *n in native) {
383
+ if (![merged containsObject:n]) [merged addObject:n];
384
+ }
385
+ return [merged copy];
386
+ }
387
+ - (void)simcam_startRunning {
388
+ objc_setAssociatedObject(self, &kSimCamSessionRunningKey, @YES, OBJC_ASSOCIATION_RETAIN);
389
+ SimCamMarkCameraInUse();
390
+ NSUInteger inCount =
391
+ ((NSArray *)objc_getAssociatedObject(self, &kSimCamSessionInputsKey)).count;
392
+ NSUInteger outCount =
393
+ ((NSArray *)objc_getAssociatedObject(self, &kSimCamSessionOutputsKey)).count;
394
+ simcam_log(@"startRunning intercepted (fake inputs=%lu outputs=%lu)",
395
+ (unsigned long)inCount, (unsigned long)outCount);
396
+ [[SimCamRegistry shared] startPumpingIfNeeded];
397
+ [self willChangeValueForKey:@"running"];
398
+ [self didChangeValueForKey:@"running"];
399
+ AVCaptureSession *strong = self;
400
+ dispatch_async(dispatch_get_main_queue(), ^{
401
+ [[NSNotificationCenter defaultCenter]
402
+ postNotificationName:AVCaptureSessionDidStartRunningNotification
403
+ object:strong];
404
+ });
405
+ }
406
+ - (void)simcam_stopRunning {
407
+ objc_setAssociatedObject(self, &kSimCamSessionRunningKey, @NO, OBJC_ASSOCIATION_RETAIN);
408
+ simcam_log(@"stopRunning intercepted");
409
+ [self willChangeValueForKey:@"running"];
410
+ [self didChangeValueForKey:@"running"];
411
+ AVCaptureSession *strong = self;
412
+ dispatch_async(dispatch_get_main_queue(), ^{
413
+ [[NSNotificationCenter defaultCenter]
414
+ postNotificationName:AVCaptureSessionDidStopRunningNotification
415
+ object:strong];
416
+ });
417
+ }
418
+ - (BOOL)simcam_isRunning {
419
+ NSNumber *v = objc_getAssociatedObject(self, &kSimCamSessionRunningKey);
420
+ return v.boolValue;
421
+ }
422
+ @end
423
+
424
+ #pragma mark - AVCaptureVideoDataOutput swizzle
425
+
426
+ @interface AVCaptureVideoDataOutput (SimCam)
427
+ @end
428
+ @implementation AVCaptureVideoDataOutput (SimCam)
429
+ - (void)simcam_setSampleBufferDelegate:(id<AVCaptureVideoDataOutputSampleBufferDelegate>)delegate
430
+ queue:(dispatch_queue_t)queue {
431
+ [self simcam_setSampleBufferDelegate:delegate queue:queue];
432
+ SimCamMarkCameraInUse();
433
+ if (delegate) {
434
+ [[SimCamRegistry shared] addOutput:self delegate:delegate queue:queue];
435
+ } else {
436
+ [[SimCamRegistry shared] removeOutput:self];
437
+ simcam_log(@"setSampleBufferDelegate nil — removed output %p", self);
438
+ }
439
+ }
440
+ @end
441
+
442
+ #pragma mark - AVCaptureVideoPreviewLayer swizzle
443
+
444
+ @interface AVCaptureVideoPreviewLayer (SimCam)
445
+ @end
446
+ @implementation AVCaptureVideoPreviewLayer (SimCam)
447
+ - (void)simcam_setSession:(AVCaptureSession *)session {
448
+ [self simcam_setSession:session];
449
+ AVCaptureDevicePosition p = SimCamPositionOf(session);
450
+ SimCamSetPosition(self, p);
451
+ SimCamMarkCameraInUse();
452
+ [[SimCamRegistry shared] addPreviewLayer:self];
453
+ }
454
+ @end
455
+
456
+ #pragma mark - AVCaptureDeviceFormat private-accessor swizzle
457
+
458
+ @interface AVCaptureDeviceFormat (SimCamPrivate)
459
+ - (id)figCaptureSourceVideoFormat;
460
+ @end
461
+
462
+ @interface AVCaptureDeviceFormat (SimCam)
463
+ @end
464
+ @implementation AVCaptureDeviceFormat (SimCam)
465
+ - (id)simcam_figCaptureSourceVideoFormat {
466
+ if ([self isKindOfClass:[SimCamFakeFormat class]]) return nil;
467
+ return [self simcam_figCaptureSourceVideoFormat];
468
+ }
469
+ @end
470
+
471
+ #pragma mark - AVCaptureOutput connection swizzles
472
+
473
+ @interface AVCaptureOutput (SimCamConn)
474
+ @end
475
+ @implementation AVCaptureOutput (SimCamConn)
476
+ - (AVCaptureConnection *)simcam_connectionWithMediaType:(AVMediaType)mediaType {
477
+ AVCaptureConnection *real = [self simcam_connectionWithMediaType:mediaType];
478
+ if (real) return real;
479
+ if (!SimCamOutputAttachedToFakeSession(self)) return nil;
480
+ if (![mediaType isEqualToString:AVMediaTypeVideo]) return nil;
481
+ AVCaptureConnection *fake = SimCamFakeConnectionForOutput(self);
482
+ simcam_log(@"connectionWithMediaType:%@ → fake %p for %@ %p",
483
+ mediaType, fake, NSStringFromClass([self class]), self);
484
+ return fake;
485
+ }
486
+ - (NSArray<AVCaptureConnection *> *)simcam_connections {
487
+ NSArray *real = [self simcam_connections];
488
+ if (real.count > 0) return real;
489
+ if (!SimCamOutputAttachedToFakeSession(self)) return real ?: @[];
490
+ AVCaptureConnection *fake = SimCamFakeConnectionForOutput(self);
491
+ return fake ? @[fake] : @[];
492
+ }
493
+ @end
494
+
495
+ #pragma mark - AVCaptureOutput codec enumeration swizzle
496
+
497
+ @interface AVCaptureOutput (SimCamPrivate)
498
+ + (NSArray<AVVideoCodecType> *)availableVideoCodecTypesForSourceDevice:(AVCaptureDevice *)device
499
+ sourceFormat:(AVCaptureDeviceFormat *)format
500
+ outputDimensions:(CMVideoDimensions)dims
501
+ fileType:(AVFileType)fileType
502
+ videoCodecTypesAllowList:(NSArray<AVVideoCodecType> *)allow;
503
+ @end
504
+
505
+ @interface AVCaptureOutput (SimCam)
506
+ @end
507
+ @implementation AVCaptureOutput (SimCam)
508
+ + (NSArray<AVVideoCodecType> *)simcam_availableVideoCodecTypesForSourceDevice:(AVCaptureDevice *)device
509
+ sourceFormat:(AVCaptureDeviceFormat *)format
510
+ outputDimensions:(CMVideoDimensions)dims
511
+ fileType:(AVFileType)fileType
512
+ videoCodecTypesAllowList:(NSArray<AVVideoCodecType> *)allow {
513
+ BOOL fakeDevice = [device isKindOfClass:[SimCamFakeDevice class]];
514
+ BOOL fakeFormat = [format isKindOfClass:[SimCamFakeFormat class]];
515
+ BOOL nilArgs = (device == nil) && (format == nil);
516
+ if (fakeDevice || fakeFormat || nilArgs) {
517
+ simcam_log(@"availableVideoCodecTypes intercepted (device=%@ format=%@ allow=%lu)",
518
+ device ? NSStringFromClass([device class]) : @"<nil>",
519
+ format ? NSStringFromClass([format class]) : @"<nil>",
520
+ (unsigned long)allow.count);
521
+ NSArray *defaults = @[ AVVideoCodecTypeJPEG, AVVideoCodecTypeHEVC ];
522
+ if (allow.count == 0) return defaults;
523
+ NSMutableArray *filtered = [NSMutableArray new];
524
+ for (AVVideoCodecType t in defaults) if ([allow containsObject:t]) [filtered addObject:t];
525
+ return filtered.count > 0 ? [filtered copy] : defaults;
526
+ }
527
+ return [self simcam_availableVideoCodecTypesForSourceDevice:device
528
+ sourceFormat:format
529
+ outputDimensions:dims
530
+ fileType:fileType
531
+ videoCodecTypesAllowList:allow];
532
+ }
533
+ @end
534
+
535
+ #pragma mark - AVCapturePhotoOutput swizzle
536
+
537
+ @interface AVCapturePhotoOutput (SimCam)
538
+ @end
539
+ @implementation AVCapturePhotoOutput (SimCam)
540
+ - (void)simcam_capturePhotoWithSettings:(AVCapturePhotoSettings *)settings
541
+ delegate:(id<AVCapturePhotoCaptureDelegate>)delegate {
542
+ if (!delegate) return;
543
+ SimCamRegistry *reg = [SimCamRegistry shared];
544
+ CVPixelBufferRef pb = [reg currentPixelBuffer];
545
+ AVCaptureDevicePosition p = SimCamPositionOf(self);
546
+ if (p == 0) p = AVCaptureDevicePositionFront;
547
+ BOOL mirror = SimCamShouldMirror(p);
548
+ if (SimCamGetMirrorMode() == SimCamMirrorAuto) {
549
+ AVCaptureConnection *conn = [self connectionWithMediaType:AVMediaTypeVideo];
550
+ if (conn && conn.isVideoMirroringSupported && !conn.automaticallyAdjustsVideoMirroring) {
551
+ mirror = conn.isVideoMirrored;
552
+ }
553
+ }
554
+ CGImageRef cg = NULL;
555
+ if (pb) {
556
+ CIImage *ci = [CIImage imageWithCVPixelBuffer:pb];
557
+ if (mirror) ci = [ci imageByApplyingOrientation:kCGImagePropertyOrientationUpMirrored];
558
+ static CIContext *ctx = nil; static dispatch_once_t once;
559
+ dispatch_once(&once, ^{ ctx = [CIContext contextWithOptions:nil]; });
560
+ cg = [ctx createCGImage:ci fromRect:ci.extent];
561
+ CVPixelBufferRelease(pb);
562
+ }
563
+ if (!cg) {
564
+ simcam_log(@"capturePhoto: no source frame, synthesizing 1x1 black");
565
+ size_t bpr = 4;
566
+ CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
567
+ CGContextRef bmp = CGBitmapContextCreate(NULL, 1, 1, 8, bpr, cs,
568
+ kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Little);
569
+ CGContextSetFillColorWithColor(bmp, [UIColor blackColor].CGColor);
570
+ CGContextFillRect(bmp, CGRectMake(0, 0, 1, 1));
571
+ cg = CGBitmapContextCreateImage(bmp);
572
+ CGContextRelease(bmp);
573
+ CGColorSpaceRelease(cs);
574
+ }
575
+ SimCamFakePhoto *photo = [SimCamFakePhoto photoFromImage:cg
576
+ jpegQuality:0.92
577
+ mirrored:mirror];
578
+ if (cg) CGImageRelease(cg);
579
+ AVCaptureResolvedPhotoSettings *resolved = photo.resolvedSettings;
580
+ simcam_log(@"capturePhoto intercepted (pos=%d, mirror=%d, jpeg=%lu bytes, dims=%dx%d)",
581
+ (int)p, (int)mirror, (unsigned long)photo.fileDataRepresentation.length,
582
+ resolved.photoDimensions.width, resolved.photoDimensions.height);
583
+ AVCapturePhotoOutput *output = self;
584
+ SEL selWillBegin = @selector(photoOutput:willBeginCaptureForResolvedSettings:);
585
+ SEL selWillCapture = @selector(photoOutput:willCapturePhotoForResolvedSettings:);
586
+ SEL selDidProcess = @selector(photoOutput:didFinishProcessingPhoto:error:);
587
+ SEL selDidCapture = @selector(photoOutput:didCapturePhotoForResolvedSettings:);
588
+ SEL selDidFinish = @selector(photoOutput:didFinishCaptureForResolvedSettings:error:);
589
+ dispatch_async(dispatch_get_main_queue(), ^{
590
+ if ([delegate respondsToSelector:selWillBegin]) {
591
+ ((void (*)(id, SEL, AVCapturePhotoOutput *, AVCaptureResolvedPhotoSettings *))
592
+ objc_msgSend)(delegate, selWillBegin, output, resolved);
593
+ }
594
+ if ([delegate respondsToSelector:selWillCapture]) {
595
+ ((void (*)(id, SEL, AVCapturePhotoOutput *, AVCaptureResolvedPhotoSettings *))
596
+ objc_msgSend)(delegate, selWillCapture, output, resolved);
597
+ }
598
+ BOOL delivered = NO;
599
+ if ([delegate respondsToSelector:selDidProcess]) {
600
+ ((void (*)(id, SEL, AVCapturePhotoOutput *, AVCapturePhoto *, NSError *))
601
+ objc_msgSend)(delegate, selDidProcess, output, photo, (NSError *)nil);
602
+ delivered = YES;
603
+ }
604
+ if ([delegate respondsToSelector:selDidCapture]) {
605
+ ((void (*)(id, SEL, AVCapturePhotoOutput *, AVCaptureResolvedPhotoSettings *))
606
+ objc_msgSend)(delegate, selDidCapture, output, resolved);
607
+ }
608
+ if ([delegate respondsToSelector:selDidFinish]) {
609
+ ((void (*)(id, SEL, AVCapturePhotoOutput *, AVCaptureResolvedPhotoSettings *, NSError *))
610
+ objc_msgSend)(delegate, selDidFinish, output, resolved, (NSError *)nil);
611
+ }
612
+ simcam_log(@"capturePhoto lifecycle complete (delivered photo=%d)", (int)delivered);
613
+ });
614
+ }
615
+ @end
616
+
617
+ #pragma mark - NSData write redirect (expo-camera placeholder substitution)
618
+
619
+ static BOOL SimCamLooksLikeCameraDropPath(NSString *path) {
620
+ if (!path.length) return NO;
621
+ if (![path containsString:@"/Camera/"]) return NO;
622
+ NSString *lower = path.lowercaseString;
623
+ return [lower hasSuffix:@".jpg"] || [lower hasSuffix:@".jpeg"];
624
+ }
625
+
626
+ @interface NSData (SimCam)
627
+ @end
628
+ @implementation NSData (SimCam)
629
+
630
+ - (BOOL)simcam_writeToURL:(NSURL *)url
631
+ options:(NSDataWritingOptions)opts
632
+ error:(NSError **)err {
633
+ NSString *path = url.isFileURL ? url.path : nil;
634
+ if (SimCamLooksLikeCameraDropPath(path)) {
635
+ NSData *snap = [[SimCamRegistry shared] currentSnapshotJPEGAtQuality:0.92];
636
+ if (snap.length > 0) {
637
+ simcam_log(@"NSData writeToURL → substituted %lu→%lu bytes (%@)",
638
+ (unsigned long)self.length, (unsigned long)snap.length, path.lastPathComponent);
639
+ return [snap simcam_writeToURL:url options:opts error:err];
640
+ }
641
+ }
642
+ return [self simcam_writeToURL:url options:opts error:err];
643
+ }
644
+
645
+ - (BOOL)simcam_writeToFile:(NSString *)path
646
+ options:(NSDataWritingOptions)opts
647
+ error:(NSError **)err {
648
+ if (SimCamLooksLikeCameraDropPath(path)) {
649
+ NSData *snap = [[SimCamRegistry shared] currentSnapshotJPEGAtQuality:0.92];
650
+ if (snap.length > 0) {
651
+ simcam_log(@"NSData writeToFile → substituted %lu→%lu bytes (%@)",
652
+ (unsigned long)self.length, (unsigned long)snap.length, path.lastPathComponent);
653
+ return [snap simcam_writeToFile:path options:opts error:err];
654
+ }
655
+ }
656
+ return [self simcam_writeToFile:path options:opts error:err];
657
+ }
658
+
659
+ @end
660
+
661
+ #pragma mark - UIGraphicsImageRenderer redirect (camera-placeholder generators)
662
+
663
+ static BOOL SimCamCallerLooksLikeCameraPlaceholder(void) {
664
+ if (!SimCamCameraIsInUse()) return NO;
665
+
666
+ void *frames[12];
667
+ int n = backtrace(frames, 12);
668
+ if (n < 4) return NO;
669
+
670
+ static _Atomic uintptr_t cachedFrame = 0;
671
+ static _Atomic int cachedAnswer = -1;
672
+ uintptr_t topFrame = (uintptr_t)frames[3];
673
+ if (atomic_load_explicit(&cachedFrame, memory_order_relaxed) == topFrame) {
674
+ int v = atomic_load_explicit(&cachedAnswer, memory_order_relaxed);
675
+ if (v >= 0) return (BOOL)v;
676
+ }
677
+
678
+ BOOL match = NO;
679
+ int end = (n < 12) ? n : 12;
680
+ for (int i = 3; i < end; i++) {
681
+ Dl_info info;
682
+ if (dladdr(frames[i], &info) == 0 || !info.dli_sname) continue;
683
+ const char *name = info.dli_sname;
684
+ if (strstr(name, "generatePhoto") ||
685
+ strstr(name, "generatePicture") ||
686
+ strstr(name, "generateImage") ||
687
+ strstr(name, "placeholderPhoto") ||
688
+ strstr(name, "placeholderImage") ||
689
+ strstr(name, "simulatorPhoto") ||
690
+ strstr(name, "PictureForSimulator") ||
691
+ strstr(name, "PhotoForSimulator") ||
692
+ strstr(name, "ImageForSimulator") ||
693
+ strstr(name, "mockPhoto") ||
694
+ strstr(name, "fakePhoto")) { match = YES; break; }
695
+ if (strstr(name, "Camera") || strstr(name, "camera")) {
696
+ if (strstr(name, "Simulator") ||
697
+ strstr(name, "simulator") ||
698
+ strstr(name, "Placeholder") ||
699
+ strstr(name, "placeholder") ||
700
+ strstr(name, "generate")) { match = YES; break; }
701
+ }
702
+ }
703
+ atomic_store_explicit(&cachedFrame, topFrame, memory_order_relaxed);
704
+ atomic_store_explicit(&cachedAnswer, (int)match, memory_order_relaxed);
705
+ return match;
706
+ }
707
+
708
+ @interface UIGraphicsImageRenderer (SimCam)
709
+ @end
710
+ @implementation UIGraphicsImageRenderer (SimCam)
711
+ - (UIImage *)simcam_imageWithActions:(void (NS_NOESCAPE ^)(UIGraphicsImageRendererContext *))actions {
712
+ if (SimCamCallerLooksLikeCameraPlaceholder()) {
713
+ NSData *jpeg = [[SimCamRegistry shared] currentSnapshotJPEGAtQuality:0.92];
714
+ if (jpeg.length > 0) {
715
+ UIImage *snap = [UIImage imageWithData:jpeg];
716
+ if (snap) {
717
+ simcam_log(@"UIGraphicsImageRenderer image: → live frame (jpeg %lu bytes)",
718
+ (unsigned long)jpeg.length);
719
+ return snap;
720
+ }
721
+ }
722
+ }
723
+ return [self simcam_imageWithActions:actions];
724
+ }
725
+ @end
726
+
727
+ #pragma mark - CoreMotion stubs
728
+
729
+ static char kSimCamAccelTimerKey;
730
+ static char kSimCamGyroTimerKey;
731
+ static char kSimCamMagTimerKey;
732
+ static char kSimCamDeviceMotionTimerKey;
733
+
734
+ static void SimCamStartTimer(id manager, char *key, NSTimeInterval interval,
735
+ dispatch_block_t tick) {
736
+ if (interval <= 0) interval = 0.1;
737
+ dispatch_source_t existing = objc_getAssociatedObject(manager, key);
738
+ if (existing) dispatch_source_cancel(existing);
739
+ dispatch_queue_t q = dispatch_queue_create("dev.servesim.simcam.motion",
740
+ DISPATCH_QUEUE_SERIAL);
741
+ dispatch_source_t t = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q);
742
+ uint64_t ns = (uint64_t)(interval * NSEC_PER_SEC);
743
+ dispatch_source_set_timer(t, DISPATCH_TIME_NOW, ns, ns / 10);
744
+ dispatch_source_set_event_handler(t, tick);
745
+ dispatch_resume(t);
746
+ objc_setAssociatedObject(manager, key, t, OBJC_ASSOCIATION_RETAIN);
747
+ }
748
+
749
+ static void SimCamStopTimer(id manager, char *key) {
750
+ dispatch_source_t t = objc_getAssociatedObject(manager, key);
751
+ if (t) {
752
+ dispatch_source_cancel(t);
753
+ objc_setAssociatedObject(manager, key, nil, OBJC_ASSOCIATION_RETAIN);
754
+ }
755
+ }
756
+
757
+ @interface CMMotionManager (SimCam)
758
+ @end
759
+ @implementation CMMotionManager (SimCam)
760
+
761
+ - (BOOL)simcam_isAccelerometerAvailable { return YES; }
762
+ - (BOOL)simcam_isGyroAvailable { return YES; }
763
+ - (BOOL)simcam_isMagnetometerAvailable { return YES; }
764
+ - (BOOL)simcam_isDeviceMotionAvailable { return YES; }
765
+
766
+ - (BOOL)simcam_isAccelerometerActive {
767
+ return objc_getAssociatedObject(self, &kSimCamAccelTimerKey) != nil;
768
+ }
769
+ - (BOOL)simcam_isGyroActive {
770
+ return objc_getAssociatedObject(self, &kSimCamGyroTimerKey) != nil;
771
+ }
772
+ - (BOOL)simcam_isMagnetometerActive {
773
+ return objc_getAssociatedObject(self, &kSimCamMagTimerKey) != nil;
774
+ }
775
+ - (BOOL)simcam_isDeviceMotionActive {
776
+ return objc_getAssociatedObject(self, &kSimCamDeviceMotionTimerKey) != nil;
777
+ }
778
+
779
+ - (CMAccelerometerData *)simcam_accelerometerData { return SimCamSharedAccelerometerData(); }
780
+ - (CMGyroData *)simcam_gyroData { return SimCamSharedGyroData(); }
781
+ - (CMMagnetometerData *)simcam_magnetometerData { return SimCamSharedMagnetometerData(); }
782
+ - (CMDeviceMotion *)simcam_deviceMotion { return SimCamSharedDeviceMotion(); }
783
+
784
+ - (void)simcam_startAccelerometerUpdates {
785
+ if ([self simcam_isAccelerometerActive]) return;
786
+ SimCamStartTimer(self, &kSimCamAccelTimerKey,
787
+ self.accelerometerUpdateInterval, ^{});
788
+ }
789
+ - (void)simcam_startAccelerometerUpdatesToQueue:(NSOperationQueue *)queue
790
+ withHandler:(CMAccelerometerHandler)handler {
791
+ if (!handler) { [self simcam_startAccelerometerUpdates]; return; }
792
+ CMAccelerometerHandler block = [handler copy];
793
+ SimCamStartTimer(self, &kSimCamAccelTimerKey,
794
+ self.accelerometerUpdateInterval, ^{
795
+ CMAccelerometerData *data = SimCamSharedAccelerometerData();
796
+ if (queue) [queue addOperationWithBlock:^{ block(data, nil); }];
797
+ else block(data, nil);
798
+ });
799
+ }
800
+ - (void)simcam_stopAccelerometerUpdates {
801
+ SimCamStopTimer(self, &kSimCamAccelTimerKey);
802
+ }
803
+
804
+ - (void)simcam_startGyroUpdates {
805
+ if ([self simcam_isGyroActive]) return;
806
+ SimCamStartTimer(self, &kSimCamGyroTimerKey,
807
+ self.gyroUpdateInterval, ^{});
808
+ }
809
+ - (void)simcam_startGyroUpdatesToQueue:(NSOperationQueue *)queue
810
+ withHandler:(CMGyroHandler)handler {
811
+ if (!handler) { [self simcam_startGyroUpdates]; return; }
812
+ CMGyroHandler block = [handler copy];
813
+ SimCamStartTimer(self, &kSimCamGyroTimerKey, self.gyroUpdateInterval, ^{
814
+ CMGyroData *data = SimCamSharedGyroData();
815
+ if (queue) [queue addOperationWithBlock:^{ block(data, nil); }];
816
+ else block(data, nil);
817
+ });
818
+ }
819
+ - (void)simcam_stopGyroUpdates {
820
+ SimCamStopTimer(self, &kSimCamGyroTimerKey);
821
+ }
822
+
823
+ - (void)simcam_startMagnetometerUpdates {
824
+ if ([self simcam_isMagnetometerActive]) return;
825
+ SimCamStartTimer(self, &kSimCamMagTimerKey,
826
+ self.magnetometerUpdateInterval, ^{});
827
+ }
828
+ - (void)simcam_startMagnetometerUpdatesToQueue:(NSOperationQueue *)queue
829
+ withHandler:(CMMagnetometerHandler)handler {
830
+ if (!handler) { [self simcam_startMagnetometerUpdates]; return; }
831
+ CMMagnetometerHandler block = [handler copy];
832
+ SimCamStartTimer(self, &kSimCamMagTimerKey, self.magnetometerUpdateInterval, ^{
833
+ CMMagnetometerData *data = SimCamSharedMagnetometerData();
834
+ if (queue) [queue addOperationWithBlock:^{ block(data, nil); }];
835
+ else block(data, nil);
836
+ });
837
+ }
838
+ - (void)simcam_stopMagnetometerUpdates {
839
+ SimCamStopTimer(self, &kSimCamMagTimerKey);
840
+ }
841
+
842
+ - (void)simcam_startDeviceMotionUpdates {
843
+ if ([self simcam_isDeviceMotionActive]) return;
844
+ SimCamStartTimer(self, &kSimCamDeviceMotionTimerKey,
845
+ self.deviceMotionUpdateInterval, ^{});
846
+ }
847
+ - (void)simcam_startDeviceMotionUpdatesUsingReferenceFrame:(CMAttitudeReferenceFrame)frame {
848
+ (void)frame;
849
+ [self simcam_startDeviceMotionUpdates];
850
+ }
851
+ - (void)simcam_startDeviceMotionUpdatesToQueue:(NSOperationQueue *)queue
852
+ withHandler:(CMDeviceMotionHandler)handler {
853
+ if (!handler) { [self simcam_startDeviceMotionUpdates]; return; }
854
+ CMDeviceMotionHandler block = [handler copy];
855
+ SimCamStartTimer(self, &kSimCamDeviceMotionTimerKey,
856
+ self.deviceMotionUpdateInterval, ^{
857
+ CMDeviceMotion *data = SimCamSharedDeviceMotion();
858
+ if (queue) [queue addOperationWithBlock:^{ block(data, nil); }];
859
+ else block(data, nil);
860
+ });
861
+ }
862
+ - (void)simcam_startDeviceMotionUpdatesUsingReferenceFrame:(CMAttitudeReferenceFrame)frame
863
+ toQueue:(NSOperationQueue *)queue
864
+ withHandler:(CMDeviceMotionHandler)handler {
865
+ (void)frame;
866
+ [self simcam_startDeviceMotionUpdatesToQueue:queue withHandler:handler];
867
+ }
868
+ - (void)simcam_stopDeviceMotionUpdates {
869
+ SimCamStopTimer(self, &kSimCamDeviceMotionTimerKey);
870
+ }
871
+
872
+ @end
873
+
874
+ static void InstallCoreMotionSwizzles(void) {
875
+ Class mm = [CMMotionManager class];
876
+ if (!mm) return;
877
+ SwizzleInstanceMethod(mm, @selector(isAccelerometerAvailable),
878
+ @selector(simcam_isAccelerometerAvailable));
879
+ SwizzleInstanceMethod(mm, @selector(isGyroAvailable),
880
+ @selector(simcam_isGyroAvailable));
881
+ SwizzleInstanceMethod(mm, @selector(isMagnetometerAvailable),
882
+ @selector(simcam_isMagnetometerAvailable));
883
+ SwizzleInstanceMethod(mm, @selector(isDeviceMotionAvailable),
884
+ @selector(simcam_isDeviceMotionAvailable));
885
+ SwizzleInstanceMethod(mm, @selector(isAccelerometerActive),
886
+ @selector(simcam_isAccelerometerActive));
887
+ SwizzleInstanceMethod(mm, @selector(isGyroActive),
888
+ @selector(simcam_isGyroActive));
889
+ SwizzleInstanceMethod(mm, @selector(isMagnetometerActive),
890
+ @selector(simcam_isMagnetometerActive));
891
+ SwizzleInstanceMethod(mm, @selector(isDeviceMotionActive),
892
+ @selector(simcam_isDeviceMotionActive));
893
+ SwizzleInstanceMethod(mm, @selector(accelerometerData),
894
+ @selector(simcam_accelerometerData));
895
+ SwizzleInstanceMethod(mm, @selector(gyroData),
896
+ @selector(simcam_gyroData));
897
+ SwizzleInstanceMethod(mm, @selector(magnetometerData),
898
+ @selector(simcam_magnetometerData));
899
+ SwizzleInstanceMethod(mm, @selector(deviceMotion),
900
+ @selector(simcam_deviceMotion));
901
+ SwizzleInstanceMethod(mm, @selector(startAccelerometerUpdates),
902
+ @selector(simcam_startAccelerometerUpdates));
903
+ SwizzleInstanceMethod(mm, @selector(startAccelerometerUpdatesToQueue:withHandler:),
904
+ @selector(simcam_startAccelerometerUpdatesToQueue:withHandler:));
905
+ SwizzleInstanceMethod(mm, @selector(stopAccelerometerUpdates),
906
+ @selector(simcam_stopAccelerometerUpdates));
907
+ SwizzleInstanceMethod(mm, @selector(startGyroUpdates),
908
+ @selector(simcam_startGyroUpdates));
909
+ SwizzleInstanceMethod(mm, @selector(startGyroUpdatesToQueue:withHandler:),
910
+ @selector(simcam_startGyroUpdatesToQueue:withHandler:));
911
+ SwizzleInstanceMethod(mm, @selector(stopGyroUpdates),
912
+ @selector(simcam_stopGyroUpdates));
913
+ SwizzleInstanceMethod(mm, @selector(startMagnetometerUpdates),
914
+ @selector(simcam_startMagnetometerUpdates));
915
+ SwizzleInstanceMethod(mm, @selector(startMagnetometerUpdatesToQueue:withHandler:),
916
+ @selector(simcam_startMagnetometerUpdatesToQueue:withHandler:));
917
+ SwizzleInstanceMethod(mm, @selector(stopMagnetometerUpdates),
918
+ @selector(simcam_stopMagnetometerUpdates));
919
+ SwizzleInstanceMethod(mm, @selector(startDeviceMotionUpdates),
920
+ @selector(simcam_startDeviceMotionUpdates));
921
+ SwizzleInstanceMethod(mm, @selector(startDeviceMotionUpdatesUsingReferenceFrame:),
922
+ @selector(simcam_startDeviceMotionUpdatesUsingReferenceFrame:));
923
+ SwizzleInstanceMethod(mm, @selector(startDeviceMotionUpdatesToQueue:withHandler:),
924
+ @selector(simcam_startDeviceMotionUpdatesToQueue:withHandler:));
925
+ SwizzleInstanceMethod(mm,
926
+ @selector(startDeviceMotionUpdatesUsingReferenceFrame:toQueue:withHandler:),
927
+ @selector(simcam_startDeviceMotionUpdatesUsingReferenceFrame:toQueue:withHandler:));
928
+ SwizzleInstanceMethod(mm, @selector(stopDeviceMotionUpdates),
929
+ @selector(simcam_stopDeviceMotionUpdates));
930
+ simcam_log(@"CoreMotion stubs installed (portrait, face-up)");
931
+ }
932
+
933
+ #pragma mark - Install
934
+
935
+ static void SimCamInstallPickerSwizzles(void); // defined below
936
+
937
+ void SimCamInstallSwizzles(void) {
938
+ Class dev = [AVCaptureDevice class];
939
+ SwizzleClassMethod(dev,
940
+ @selector(defaultDeviceWithDeviceType:mediaType:position:),
941
+ @selector(simcam_defaultDeviceWithDeviceType:mediaType:position:));
942
+ SwizzleClassMethod(dev,
943
+ @selector(devicesWithMediaType:),
944
+ @selector(simcam_devicesWithMediaType:));
945
+ SwizzleClassMethod(dev, @selector(devices), @selector(simcam_devices));
946
+
947
+ Class disc = [AVCaptureDeviceDiscoverySession class];
948
+ SwizzleClassMethod(disc,
949
+ @selector(discoverySessionWithDeviceTypes:mediaType:position:),
950
+ @selector(simcam_discoverySessionWithDeviceTypes:mediaType:position:));
951
+
952
+ Class input = [AVCaptureDeviceInput class];
953
+ SwizzleInstanceMethod(input,
954
+ @selector(initWithDevice:error:),
955
+ @selector(simcam_initWithDevice:error:));
956
+ SwizzleInstanceMethod(input, @selector(device), @selector(simcam_device));
957
+ SwizzleInstanceMethod(input, @selector(ports), @selector(simcam_ports));
958
+
959
+ Class sess = [AVCaptureSession class];
960
+ SwizzleInstanceMethod(sess, @selector(addInput:), @selector(simcam_addInput:));
961
+ SwizzleInstanceMethod(sess, @selector(canAddInput:), @selector(simcam_canAddInput:));
962
+ SwizzleInstanceMethod(sess, @selector(addOutput:), @selector(simcam_addOutput:));
963
+ SwizzleInstanceMethod(sess, @selector(canAddOutput:), @selector(simcam_canAddOutput:));
964
+ SwizzleInstanceMethod(sess, @selector(startRunning), @selector(simcam_startRunning));
965
+ SwizzleInstanceMethod(sess, @selector(stopRunning), @selector(simcam_stopRunning));
966
+ SwizzleInstanceMethod(sess, @selector(isRunning), @selector(simcam_isRunning));
967
+ SwizzleInstanceMethod(sess, @selector(inputs), @selector(simcam_inputs));
968
+ SwizzleInstanceMethod(sess, @selector(outputs), @selector(simcam_outputs));
969
+ SwizzleInstanceMethod(sess, @selector(connections), @selector(simcam_connections));
970
+ SwizzleInstanceMethod(sess,
971
+ @selector(addInputWithNoConnections:),
972
+ @selector(simcam_addInputWithNoConnections:));
973
+ SwizzleInstanceMethod(sess,
974
+ @selector(addOutputWithNoConnections:),
975
+ @selector(simcam_addOutputWithNoConnections:));
976
+ SwizzleInstanceMethod(sess, @selector(removeInput:), @selector(simcam_removeInput:));
977
+ SwizzleInstanceMethod(sess, @selector(removeOutput:), @selector(simcam_removeOutput:));
978
+ SwizzleInstanceMethod(sess,
979
+ @selector(beginConfiguration),
980
+ @selector(simcam_beginConfiguration));
981
+ SwizzleInstanceMethod(sess,
982
+ @selector(commitConfiguration),
983
+ @selector(simcam_commitConfiguration));
984
+ SwizzleInstanceMethod(sess,
985
+ @selector(addConnection:),
986
+ @selector(simcam_addConnection:));
987
+ SwizzleInstanceMethod(sess,
988
+ @selector(canAddConnection:),
989
+ @selector(simcam_canAddConnection:));
990
+
991
+ Class nc = [NSNotificationCenter class];
992
+ SwizzleInstanceMethod(nc,
993
+ @selector(postNotificationName:object:userInfo:),
994
+ @selector(simcam_postNotificationName:object:userInfo:));
995
+ SwizzleInstanceMethod(nc,
996
+ @selector(postNotificationName:object:),
997
+ @selector(simcam_postNotificationName:object:));
998
+ SwizzleInstanceMethod(nc,
999
+ @selector(postNotification:),
1000
+ @selector(simcam_postNotification:));
1001
+ simcam_log(@"NSNotificationCenter swizzles installed (AVCaptureSessionRuntimeErrorNotification gated)");
1002
+
1003
+ [[NSNotificationCenter defaultCenter]
1004
+ addObserverForName:AVCaptureSessionRuntimeErrorNotification
1005
+ object:nil
1006
+ queue:nil
1007
+ usingBlock:^(NSNotification *note) {
1008
+ NSError *err = note.userInfo[AVCaptureSessionErrorKey];
1009
+ simcam_log(@"DIAG runtime-error delivered (post swizzle MISSED) object=%@ code=%ld desc=%@",
1010
+ NSStringFromClass([note.object class]),
1011
+ (long)err.code,
1012
+ err.localizedDescription ?: @"<nil>");
1013
+ }];
1014
+
1015
+ Class out = [AVCaptureVideoDataOutput class];
1016
+ SwizzleInstanceMethod(out,
1017
+ @selector(setSampleBufferDelegate:queue:),
1018
+ @selector(simcam_setSampleBufferDelegate:queue:));
1019
+
1020
+ Class outBase = [AVCaptureOutput class];
1021
+ SwizzleInstanceMethod(outBase,
1022
+ @selector(connectionWithMediaType:),
1023
+ @selector(simcam_connectionWithMediaType:));
1024
+ SwizzleInstanceMethod(outBase,
1025
+ @selector(connections),
1026
+ @selector(simcam_connections));
1027
+
1028
+ Class pl = [AVCaptureVideoPreviewLayer class];
1029
+ SwizzleInstanceMethod(pl, @selector(setSession:), @selector(simcam_setSession:));
1030
+
1031
+ Class fmtClass = [AVCaptureDeviceFormat class];
1032
+ SEL figFmtSel = NSSelectorFromString(@"figCaptureSourceVideoFormat");
1033
+ SEL figFmtSwizSel = @selector(simcam_figCaptureSourceVideoFormat);
1034
+ BOOL figOk = SwizzleInstanceMethod(fmtClass, figFmtSel, figFmtSwizSel);
1035
+ simcam_log(@"swizzle -[AVCaptureDeviceFormat figCaptureSourceVideoFormat] → %@",
1036
+ figOk ? @"installed" : @"FAILED");
1037
+
1038
+ Class outClass = [AVCaptureOutput class];
1039
+ SEL availCodecsSel = NSSelectorFromString(
1040
+ @"availableVideoCodecTypesForSourceDevice:sourceFormat:outputDimensions:fileType:videoCodecTypesAllowList:");
1041
+ SEL availCodecsSwizSel = @selector(simcam_availableVideoCodecTypesForSourceDevice:sourceFormat:outputDimensions:fileType:videoCodecTypesAllowList:);
1042
+ Method origAvailCodecs = class_getClassMethod(outClass, availCodecsSel);
1043
+ Method swizAvailCodecs = class_getClassMethod(outClass, availCodecsSwizSel);
1044
+ if (origAvailCodecs && swizAvailCodecs) {
1045
+ method_exchangeImplementations(origAvailCodecs, swizAvailCodecs);
1046
+ simcam_log(@"swizzle +[AVCaptureOutput availableVideoCodecTypes…] → installed");
1047
+ } else {
1048
+ simcam_log(@"swizzle +[AVCaptureOutput availableVideoCodecTypes…] FAILED (orig=%p swiz=%p)",
1049
+ origAvailCodecs, swizAvailCodecs);
1050
+ }
1051
+
1052
+ Class photoOut = [AVCapturePhotoOutput class];
1053
+ SwizzleInstanceMethod(photoOut,
1054
+ @selector(capturePhotoWithSettings:delegate:),
1055
+ @selector(simcam_capturePhotoWithSettings:delegate:));
1056
+
1057
+ Class data = [NSData class];
1058
+ SwizzleInstanceMethod(data,
1059
+ @selector(writeToURL:options:error:),
1060
+ @selector(simcam_writeToURL:options:error:));
1061
+ SwizzleInstanceMethod(data,
1062
+ @selector(writeToFile:options:error:),
1063
+ @selector(simcam_writeToFile:options:error:));
1064
+
1065
+ Class renderer = [UIGraphicsImageRenderer class];
1066
+ SwizzleInstanceMethod(renderer,
1067
+ @selector(imageWithActions:),
1068
+ @selector(simcam_imageWithActions:));
1069
+
1070
+ InstallCoreMotionSwizzles();
1071
+ SimCamInstallPickerSwizzles();
1072
+ }
1073
+
1074
+ #pragma mark - UIImagePickerController native-UI bridge
1075
+
1076
+ // UIImagePickerController with sourceType=.camera renders Apple's own UI
1077
+ // in the simulator (CAMPreviewView, CAMDynamicShutterControl, flash & switch
1078
+ // buttons, etc.) but its viewfinder shows a gray "no camera" placeholder
1079
+ // (CAMSnapshotView) and the shutter is permanently disabled because there's
1080
+ // no real camera. We make the picker work by, on viewDidAppear:
1081
+ //
1082
+ // 1. Hiding CAMSnapshotView so our frames (already being pushed into the
1083
+ // picker's AVCaptureVideoPreviewLayer via the existing setSession:
1084
+ // swizzle) show through.
1085
+ // 2. Capturing CAMPreviewView's aspect so the captured photo can be
1086
+ // center-cropped to match the live framing.
1087
+ // 3. Wrapping CAMDynamicShutterControl.delegate to catch
1088
+ // shutterControlTouchAttemptedWhileDisabled: (fired even on a disabled
1089
+ // shutter) and deliver our current frame to picker.delegate.
1090
+ //
1091
+ // All private class names are looked up by string with try/catch — if Apple
1092
+ // renames them in a future iOS, the picker degrades to "back to gray + no
1093
+ // shutter" rather than crashing.
1094
+ //
1095
+ // Credit: this is the approach pioneered by baguette's SimCamInject.
1096
+
1097
+ static NSString *const SimCamPickerUTImage = @"public.image";
1098
+
1099
+ // Set during the view-tree walk so SimCamShutterDelegateWrapper can reach
1100
+ // the host picker without changing its delegate-protocol signature.
1101
+ static __weak UIImagePickerController *gSimCamCurrentPicker = nil;
1102
+ static const void *kSimCamShutterWrappedDelegateKey = &kSimCamShutterWrappedDelegateKey;
1103
+
1104
+ static UIImage *SimCamPickerSnapshotImageMirrored(BOOL mirror) {
1105
+ CVPixelBufferRef pb = [[SimCamRegistry shared] currentPixelBuffer];
1106
+ if (!pb) return nil;
1107
+ CIImage *ci = [CIImage imageWithCVPixelBuffer:pb];
1108
+ if (mirror) ci = [ci imageByApplyingOrientation:kCGImagePropertyOrientationUpMirrored];
1109
+ static CIContext *ctx = nil; static dispatch_once_t once;
1110
+ dispatch_once(&once, ^{ ctx = [CIContext contextWithOptions:nil]; });
1111
+ CGImageRef cg = [ctx createCGImage:ci fromRect:ci.extent];
1112
+ CVPixelBufferRelease(pb);
1113
+ if (!cg) return nil;
1114
+ UIImage *img = [UIImage imageWithCGImage:cg];
1115
+ CGImageRelease(cg);
1116
+ return img;
1117
+ }
1118
+
1119
+ static void SimCamDeliverFrameToPicker(UIImagePickerController *picker) {
1120
+ if (!picker) return;
1121
+ AVCaptureDevicePosition pos =
1122
+ (picker.cameraDevice == UIImagePickerControllerCameraDeviceFront)
1123
+ ? AVCaptureDevicePositionFront
1124
+ : AVCaptureDevicePositionBack;
1125
+ BOOL mirror = SimCamShouldMirror(pos);
1126
+ UIImage *image = SimCamPickerSnapshotImageMirrored(mirror);
1127
+ if (!image) {
1128
+ simcam_log(@"picker shutter: no frame available — skipping delivery");
1129
+ return;
1130
+ }
1131
+ id<UIImagePickerControllerDelegate, UINavigationControllerDelegate> delegate =
1132
+ (id<UIImagePickerControllerDelegate, UINavigationControllerDelegate>)picker.delegate;
1133
+ if (![delegate respondsToSelector:@selector(imagePickerController:didFinishPickingMediaWithInfo:)]) {
1134
+ simcam_log(@"picker shutter: delegate %@ doesn't implement didFinishPickingMediaWithInfo:",
1135
+ NSStringFromClass([(id)delegate class]));
1136
+ return;
1137
+ }
1138
+ NSMutableDictionary *info = [NSMutableDictionary dictionaryWithDictionary:@{
1139
+ UIImagePickerControllerOriginalImage: image,
1140
+ UIImagePickerControllerMediaType: SimCamPickerUTImage,
1141
+ }];
1142
+ // With allowsEditing, Apple shows an edit screen before delivery and
1143
+ // populates editedImage + cropRect. We skip the edit UI, but pass the
1144
+ // image through both keys with a full-image crop so apps that read
1145
+ // editedImage don't get nil.
1146
+ if (picker.allowsEditing) {
1147
+ info[UIImagePickerControllerEditedImage] = image;
1148
+ info[UIImagePickerControllerCropRect] =
1149
+ [NSValue valueWithCGRect:CGRectMake(0, 0, image.size.width, image.size.height)];
1150
+ }
1151
+ simcam_log(@"picker shutter → delivering %.0fx%.0f (edit=%d) to %@",
1152
+ image.size.width, image.size.height, (int)picker.allowsEditing,
1153
+ NSStringFromClass([(id)delegate class]));
1154
+ [delegate imagePickerController:picker didFinishPickingMediaWithInfo:info];
1155
+ }
1156
+
1157
+ // Wraps CAMDynamicShutterControl's delegate. In the simulator the shutter
1158
+ // is always disabled (no real camera), so taps come through as
1159
+ // shutterControlTouchAttemptedWhileDisabled: rather than the usual
1160
+ // short-press selector. Catch both and deliver a frame; forward everything
1161
+ // else to the original delegate via message forwarding so Apple's chrome
1162
+ // keeps working.
1163
+ @interface SimCamShutterDelegateWrapper : NSObject
1164
+ @property (nonatomic, weak) id originalDelegate;
1165
+ @property (nonatomic, weak) UIImagePickerController *picker;
1166
+ @property (nonatomic, assign) BOOL hasDelivered;
1167
+ @end
1168
+
1169
+ @implementation SimCamShutterDelegateWrapper
1170
+ - (void)shutterControlTouchAttemptedWhileDisabled:(id)control {
1171
+ if (self.hasDelivered) return;
1172
+ self.hasDelivered = YES;
1173
+ simcam_log(@"intercepted shutterControlTouchAttemptedWhileDisabled");
1174
+ SimCamDeliverFrameToPicker(self.picker);
1175
+ }
1176
+ - (void)dynamicShutterControlDidShortPress:(id)control {
1177
+ if (self.hasDelivered) return;
1178
+ self.hasDelivered = YES;
1179
+ simcam_log(@"intercepted dynamicShutterControlDidShortPress");
1180
+ SimCamDeliverFrameToPicker(self.picker);
1181
+ }
1182
+ - (BOOL)respondsToSelector:(SEL)sel {
1183
+ return [super respondsToSelector:sel] || [self.originalDelegate respondsToSelector:sel];
1184
+ }
1185
+ - (id)forwardingTargetForSelector:(SEL)sel {
1186
+ if ([self.originalDelegate respondsToSelector:sel]) return self.originalDelegate;
1187
+ return nil;
1188
+ }
1189
+ - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
1190
+ NSMethodSignature *sig = [super methodSignatureForSelector:sel];
1191
+ if (sig) return sig;
1192
+ return [(NSObject *)self.originalDelegate methodSignatureForSelector:sel];
1193
+ }
1194
+ @end
1195
+
1196
+ static void SimCamWalkPickerTree(UIView *view) {
1197
+ NSString *cls = NSStringFromClass([view class]);
1198
+
1199
+ // CAMPreviewView is where Apple shows the live viewfinder. On iOS 26
1200
+ // simulator it's force-hidden (no camera), and its inner CALayers have
1201
+ // nil contents (the AVCaptureVideoPreviewLayer was created but never
1202
+ // attached because the system errored). Unhide the view, hide the
1203
+ // "Live Preview" UILabel placeholder, and register the empty content
1204
+ // CALayer with the pump so our frames stream into it.
1205
+ if ([cls isEqualToString:@"CAMPreviewView"]) {
1206
+ if (view.hidden) {
1207
+ view.hidden = NO;
1208
+ simcam_log(@"un-hid CAMPreviewView (%@)", NSStringFromCGRect(view.frame));
1209
+ }
1210
+ // Hide the "Live Preview" placeholder label that Apple ships in the
1211
+ // sim build. Real device doesn't have it; simulator does.
1212
+ for (UIView *sub in view.subviews) {
1213
+ if ([sub isKindOfClass:[UILabel class]] && !sub.hidden) {
1214
+ sub.hidden = YES;
1215
+ simcam_log(@"hid CAMPreviewView UILabel placeholder");
1216
+ }
1217
+ }
1218
+ // Register the full-size content layer with the pump so it gets
1219
+ // frames pushed into it via setContents:. Tag with the picker's
1220
+ // current cameraDevice so SimCamShouldMirror picks the right axis
1221
+ // (front → mirrored, back → not).
1222
+ for (CALayer *sub in view.layer.sublayers) {
1223
+ if (CGRectEqualToRect(sub.frame, view.bounds) ||
1224
+ (sub.frame.size.width >= view.bounds.size.width * 0.95 &&
1225
+ sub.frame.size.height >= view.bounds.size.height * 0.95)) {
1226
+ AVCaptureDevicePosition pos = AVCaptureDevicePositionBack;
1227
+ if (gSimCamCurrentPicker.cameraDevice ==
1228
+ UIImagePickerControllerCameraDeviceFront) {
1229
+ pos = AVCaptureDevicePositionFront;
1230
+ }
1231
+ SimCamSetPosition(sub, pos);
1232
+ [[SimCamRegistry shared] addPreviewLayer:(AVCaptureVideoPreviewLayer *)sub];
1233
+ simcam_log(@"registered CAMPreviewView content layer %@ pos=%d",
1234
+ NSStringFromClass([sub class]), (int)pos);
1235
+ break;
1236
+ }
1237
+ }
1238
+ }
1239
+
1240
+ // CAMSnapshotView is a full-screen sibling that covers everything with
1241
+ // a gray "viewfinder closed" image. Hide it so the preview shows.
1242
+ if ([cls isEqualToString:@"CAMSnapshotView"] && !view.hidden) {
1243
+ view.hidden = YES;
1244
+ simcam_log(@"hid CAMSnapshotView to clear gray cover");
1245
+ }
1246
+
1247
+ // CAMDynamicShutterControl — wrap its delegate so taps on the
1248
+ // (disabled) shutter still deliver a frame. Apple reuses the same
1249
+ // control instance across picker presentations, so re-seat picker and
1250
+ // reset hasDelivered on every walk; otherwise the second shot would be
1251
+ // silently dropped.
1252
+ if ([cls isEqualToString:@"CAMDynamicShutterControl"] && gSimCamCurrentPicker) {
1253
+ @try {
1254
+ SimCamShutterDelegateWrapper *existing =
1255
+ objc_getAssociatedObject(view, kSimCamShutterWrappedDelegateKey);
1256
+ id currentDelegate = [view valueForKey:@"delegate"];
1257
+ if (existing) {
1258
+ existing.picker = gSimCamCurrentPicker;
1259
+ existing.hasDelivered = NO;
1260
+ if (currentDelegate != existing) {
1261
+ existing.originalDelegate = currentDelegate;
1262
+ [view setValue:existing forKey:@"delegate"];
1263
+ simcam_log(@"re-seated shutter wrapper (orig: %@)",
1264
+ NSStringFromClass([currentDelegate class]));
1265
+ }
1266
+ } else {
1267
+ SimCamShutterDelegateWrapper *wrapper = [SimCamShutterDelegateWrapper new];
1268
+ wrapper.originalDelegate = currentDelegate;
1269
+ wrapper.picker = gSimCamCurrentPicker;
1270
+ objc_setAssociatedObject(view, kSimCamShutterWrappedDelegateKey, wrapper,
1271
+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);
1272
+ [view setValue:wrapper forKey:@"delegate"];
1273
+ simcam_log(@"hijacked %@.delegate (orig: %@)",
1274
+ cls, NSStringFromClass([currentDelegate class]));
1275
+ }
1276
+ } @catch (NSException *e) {
1277
+ simcam_log(@"failed to hijack shutter delegate: %@", e);
1278
+ }
1279
+ }
1280
+
1281
+ for (UIView *child in view.subviews) SimCamWalkPickerTree(child);
1282
+ }
1283
+
1284
+ @interface UIImagePickerController (SimCam)
1285
+ @end
1286
+ @implementation UIImagePickerController (SimCam)
1287
+
1288
+ + (BOOL)simcam_isSourceTypeAvailable:(UIImagePickerControllerSourceType)t {
1289
+ if (t == UIImagePickerControllerSourceTypeCamera) return YES;
1290
+ return [self simcam_isSourceTypeAvailable:t];
1291
+ }
1292
+ + (NSArray<NSString *> *)simcam_availableMediaTypesForSourceType:(UIImagePickerControllerSourceType)t {
1293
+ if (t == UIImagePickerControllerSourceTypeCamera) return @[SimCamPickerUTImage];
1294
+ return [self simcam_availableMediaTypesForSourceType:t];
1295
+ }
1296
+ + (NSArray<NSNumber *> *)simcam_availableCaptureModesForCameraDevice:(UIImagePickerControllerCameraDevice)d {
1297
+ (void)d; return @[ @(UIImagePickerControllerCameraCaptureModePhoto) ];
1298
+ }
1299
+ + (BOOL)simcam_isCameraDeviceAvailable:(UIImagePickerControllerCameraDevice)d { (void)d; return YES; }
1300
+ + (BOOL)simcam_isFlashAvailableForCameraDevice:(UIImagePickerControllerCameraDevice)d { (void)d; return NO; }
1301
+
1302
+ - (void)simcam_viewDidAppear:(BOOL)animated {
1303
+ [self simcam_viewDidAppear:animated];
1304
+ if (self.sourceType != UIImagePickerControllerSourceTypeCamera) return;
1305
+ gSimCamCurrentPicker = self;
1306
+ SimCamWalkPickerTree(self.view);
1307
+ gSimCamCurrentPicker = nil;
1308
+ }
1309
+
1310
+ @end
1311
+
1312
+ static void SimCamInstallPickerSwizzles(void) {
1313
+ // method_exchangeImplementations is its own inverse — a second call
1314
+ // would un-install. Guard with dispatch_once.
1315
+ static dispatch_once_t once;
1316
+ dispatch_once(&once, ^{
1317
+ Class picker = [UIImagePickerController class];
1318
+ SwizzleClassMethod(picker,
1319
+ @selector(isSourceTypeAvailable:),
1320
+ @selector(simcam_isSourceTypeAvailable:));
1321
+ SwizzleClassMethod(picker,
1322
+ @selector(availableMediaTypesForSourceType:),
1323
+ @selector(simcam_availableMediaTypesForSourceType:));
1324
+ SwizzleClassMethod(picker,
1325
+ @selector(availableCaptureModesForCameraDevice:),
1326
+ @selector(simcam_availableCaptureModesForCameraDevice:));
1327
+ SwizzleClassMethod(picker,
1328
+ @selector(isCameraDeviceAvailable:),
1329
+ @selector(simcam_isCameraDeviceAvailable:));
1330
+ SwizzleClassMethod(picker,
1331
+ @selector(isFlashAvailableForCameraDevice:),
1332
+ @selector(simcam_isFlashAvailableForCameraDevice:));
1333
+ SwizzleInstanceMethod(picker,
1334
+ @selector(viewDidAppear:),
1335
+ @selector(simcam_viewDidAppear:));
1336
+ simcam_log(@"UIImagePickerController swizzles installed");
1337
+ });
1338
+ }