serve-sim 0.1.25 → 0.1.27

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.
@@ -1,1160 +1,19 @@
1
- // SimCameraInjector — MVP camera frame injector for the iOS Simulator.
2
- //
3
- // Loaded via DYLD_INSERT_LIBRARIES into a simulator app process.
4
- // Reads SIMCAM_IMAGE_PATH (PNG/JPEG); feeds it as the camera feed.
5
- //
6
- // Strategy:
7
- // 1. Method-swizzle AVCaptureDevice discovery so apps see a fake device.
8
- // 2. Allow AVCaptureDeviceInput to wrap the fake device.
9
- // 3. Track AVCaptureVideoDataOutput delegates and pump CMSampleBuffers
10
- // from the loaded image at ~30fps when the session starts running.
11
- // 4. Mirror the same image as `contents` on AVCaptureVideoPreviewLayer
12
- // so the visible preview path also shows the injected frames.
13
-
14
- #import <AVFoundation/AVFoundation.h>
15
- #import <CoreImage/CoreImage.h>
16
- #import <CoreMedia/CoreMedia.h>
17
- #import <CoreVideo/CoreVideo.h>
18
1
  #import <UIKit/UIKit.h>
19
- #import <objc/runtime.h>
20
- #import <objc/message.h>
21
- #include <fcntl.h>
22
- #include <sys/mman.h>
23
- #include <sys/stat.h>
24
- #include <stdatomic.h>
25
- #include <errno.h>
26
- #include "include/SimCamShared.h"
27
-
28
- static UIImage *gSourceImage = nil;
29
- static CGImageRef gSourceCGImage = NULL;
30
- static size_t kFrameWidth = 1280;
31
- static size_t kFrameHeight = 720;
32
- static const double kFrameRate = 30.0;
33
-
34
- // Shared-memory webcam source (optional).
35
- static SimCamShmHeader *gShmHeader = NULL;
36
- static const uint8_t *gShmPixels = NULL;
37
- static size_t gShmTotalSize = 0;
38
- static uint64_t gLastSeenSeq = 0;
39
-
40
- #pragma mark - Logging
41
-
42
- static void simcam_log(NSString *fmt, ...) {
43
- va_list args; va_start(args, fmt);
44
- NSString *msg = [[NSString alloc] initWithFormat:fmt arguments:args];
45
- va_end(args);
46
- fprintf(stderr, "[SimCam] %s\n", msg.UTF8String);
47
- }
48
-
49
- #pragma mark - Fake device
50
-
51
- // Position is stored as an associated object so we can use a single
52
- // SimCamFakeDevice subclass for both front and back instances. Using
53
- // class_createInstance bypasses normal -init, so we avoid synthesized ivars.
54
- static char kFakePositionKey;
55
-
56
- // WebKit's getUserMedia path validates the requested mediaStreamConstraints
57
- // against `device.formats[*].videoSupportedFrameRateRanges` and rejects with
58
- // "InvalidConstraint" if no format matches. Frameworks that don't probe
59
- // formats (expo-camera, vision-camera on simulator) won't notice these
60
- // existing, but Safari (and any AVF code that walks formats) needs at least
61
- // one well-formed entry. AVCaptureDeviceFormat / AVFrameRateRange both have
62
- // private inits so we subclass and route via class_createInstance.
63
-
64
- @interface SimCamFakeFrameRateRange : AVFrameRateRange
65
- @end
66
- @implementation SimCamFakeFrameRateRange
67
- - (Float64)minFrameRate { return 1.0; }
68
- - (Float64)maxFrameRate { return 60.0; }
69
- - (CMTime)minFrameDuration { return CMTimeMake(1, 60); }
70
- - (CMTime)maxFrameDuration { return CMTimeMake(1, 1); }
71
- @end
72
-
73
- @interface SimCamFakeFormat : AVCaptureDeviceFormat
74
- @end
75
- @implementation SimCamFakeFormat {
76
- CMVideoFormatDescriptionRef _fd;
77
- NSArray<AVFrameRateRange *> *_ranges;
78
- }
79
- - (CMFormatDescriptionRef)formatDescription {
80
- if (!_fd) {
81
- CMVideoFormatDescriptionCreate(kCFAllocatorDefault,
82
- kCVPixelFormatType_32BGRA, 1280, 720, NULL, &_fd);
83
- }
84
- return _fd;
85
- }
86
- - (NSArray<AVFrameRateRange *> *)videoSupportedFrameRateRanges {
87
- if (!_ranges) {
88
- AVFrameRateRange *r = (AVFrameRateRange *)class_createInstance(
89
- [SimCamFakeFrameRateRange class], 0);
90
- _ranges = r ? @[r] : @[];
91
- }
92
- return _ranges;
93
- }
94
- - (NSString *)mediaType { return AVMediaTypeVideo; }
95
- - (FourCharCode)mediaSubType { return kCVPixelFormatType_32BGRA; }
96
- - (CMVideoDimensions)highResolutionStillImageDimensions {
97
- return (CMVideoDimensions){ 1280, 720 };
98
- }
99
- - (BOOL)isHighestPhotoQualitySupported { return YES; }
100
- - (BOOL)isVideoBinned { return NO; }
101
- - (BOOL)isVideoStabilizationModeSupported:(AVCaptureVideoStabilizationMode)m { return NO; }
102
- - (CGFloat)videoMaxZoomFactor { return 16.0; }
103
- - (CGFloat)videoZoomFactorUpscaleThreshold { return 1.0; }
104
- - (NSArray *)autoFocusSystem { return @[]; }
105
- - (BOOL)isMultiCamSupported { return NO; }
106
- - (NSArray *)supportedColorSpaces { return @[]; }
107
- - (NSArray *)supportedDepthDataFormats { return @[]; }
108
- - (BOOL)isPortraitEffectSupported { return NO; }
109
- - (CGFloat)minISO { return 25.0; }
110
- - (CGFloat)maxISO { return 6400.0; }
111
- - (CMTime)minExposureDuration { return CMTimeMake(1, 8000); }
112
- - (CMTime)maxExposureDuration { return CMTimeMake(1, 30); }
113
- - (void)dealloc { if (_fd) CFRelease(_fd); }
114
- @end
2
+ #include <unistd.h>
115
3
 
116
- static AVCaptureDeviceFormat *SimCamSharedFakeFormat(void) {
117
- static AVCaptureDeviceFormat *f = nil;
118
- static dispatch_once_t once;
119
- dispatch_once(&once, ^{
120
- f = (AVCaptureDeviceFormat *)class_createInstance([SimCamFakeFormat class], 0);
121
- });
122
- return f;
123
- }
124
-
125
- @interface SimCamFakeDevice : AVCaptureDevice
126
- @end
127
-
128
- @implementation SimCamFakeDevice
129
- - (AVCaptureDevicePosition)position {
130
- NSNumber *n = objc_getAssociatedObject(self, &kFakePositionKey);
131
- return n ? (AVCaptureDevicePosition)n.intValue : AVCaptureDevicePositionFront;
132
- }
133
- - (NSString *)uniqueID {
134
- return self.position == AVCaptureDevicePositionBack
135
- ? @"sim-cam-fake-back-0" : @"sim-cam-fake-front-0";
136
- }
137
- - (NSString *)modelID { return @"SimCamFakeCamera"; }
138
- - (NSString *)localizedName {
139
- return self.position == AVCaptureDevicePositionBack
140
- ? @"Simulated Camera Back (serve-sim)"
141
- : @"Simulated Camera Front (serve-sim)";
142
- }
143
- - (NSString *)manufacturer { return @"serve-sim"; }
144
- - (BOOL)hasMediaType:(AVMediaType)mediaType { return [mediaType isEqualToString:AVMediaTypeVideo]; }
145
- - (BOOL)supportsAVCaptureSessionPreset:(AVCaptureSessionPreset)preset { return YES; }
146
- - (AVCaptureDeviceType)deviceType { return AVCaptureDeviceTypeBuiltInWideAngleCamera; }
147
- - (NSArray<AVCaptureDeviceFormat *> *)formats {
148
- AVCaptureDeviceFormat *f = SimCamSharedFakeFormat();
149
- return f ? @[f] : @[];
150
- }
151
- - (BOOL)isConnected { return YES; }
152
- - (BOOL)isSuspended { return NO; }
153
- - (BOOL)lockForConfiguration:(NSError **)e { return YES; }
154
- - (void)unlockForConfiguration { }
155
- // Properties read by camera frameworks (RN-Vision-Camera, expo-camera).
156
- // Override every accessor that Apple's implementation would otherwise reach
157
- // into private ivars for — they're zero on our class_createInstance object.
158
- - (AVCaptureDeviceFormat *)activeFormat { return SimCamSharedFakeFormat(); }
159
- - (CMTime)activeVideoMinFrameDuration { return CMTimeMake(1, 30); }
160
- - (CMTime)activeVideoMaxFrameDuration { return CMTimeMake(1, 30); }
161
- - (CGFloat)videoZoomFactor { return 1.0; }
162
- - (void)setVideoZoomFactor:(CGFloat)v { (void)v; }
163
- - (void)rampToVideoZoomFactor:(CGFloat)f withRate:(float)r { (void)f; (void)r; }
164
- - (void)cancelVideoZoomRamp { }
165
- - (BOOL)isRampingVideoZoom { return NO; }
166
- - (CGFloat)minAvailableVideoZoomFactor { return 1.0; }
167
- - (CGFloat)maxAvailableVideoZoomFactor { return 16.0; }
168
- - (CGFloat)dualCameraSwitchOverVideoZoomFactor { return 2.0; }
169
- - (NSArray<NSNumber *> *)virtualDeviceSwitchOverVideoZoomFactors { return @[]; }
170
- - (NSArray *)constituentDevices { return @[]; }
171
- - (BOOL)isVirtualDevice { return NO; }
172
- - (BOOL)hasTorch { return NO; }
173
- - (BOOL)hasFlash { return NO; }
174
- - (BOOL)isTorchAvailable { return NO; }
175
- - (BOOL)isTorchActive { return NO; }
176
- - (AVCaptureTorchMode)torchMode { return AVCaptureTorchModeOff; }
177
- - (void)setTorchMode:(AVCaptureTorchMode)m { (void)m; }
178
- - (BOOL)isTorchModeSupported:(AVCaptureTorchMode)m { (void)m; return NO; }
179
- - (BOOL)setTorchModeOnWithLevel:(float)l error:(NSError **)e { (void)l; if (e) *e = nil; return YES; }
180
- - (AVCaptureFocusMode)focusMode { return AVCaptureFocusModeContinuousAutoFocus; }
181
- - (void)setFocusMode:(AVCaptureFocusMode)m { (void)m; }
182
- - (BOOL)isFocusModeSupported:(AVCaptureFocusMode)m { (void)m; return YES; }
183
- - (CGPoint)focusPointOfInterest { return CGPointMake(0.5, 0.5); }
184
- - (void)setFocusPointOfInterest:(CGPoint)p { (void)p; }
185
- - (BOOL)isFocusPointOfInterestSupported { return YES; }
186
- - (BOOL)isAdjustingFocus { return NO; }
187
- - (BOOL)isSmoothAutoFocusEnabled { return NO; }
188
- - (void)setSmoothAutoFocusEnabled:(BOOL)b { (void)b; }
189
- - (BOOL)isSmoothAutoFocusSupported { return NO; }
190
- - (AVCaptureAutoFocusRangeRestriction)autoFocusRangeRestriction { return AVCaptureAutoFocusRangeRestrictionNone; }
191
- - (void)setAutoFocusRangeRestriction:(AVCaptureAutoFocusRangeRestriction)r { (void)r; }
192
- - (BOOL)isAutoFocusRangeRestrictionSupported { return NO; }
193
- - (AVCaptureExposureMode)exposureMode { return AVCaptureExposureModeContinuousAutoExposure; }
194
- - (void)setExposureMode:(AVCaptureExposureMode)m { (void)m; }
195
- - (BOOL)isExposureModeSupported:(AVCaptureExposureMode)m { (void)m; return YES; }
196
- - (CGPoint)exposurePointOfInterest { return CGPointMake(0.5, 0.5); }
197
- - (void)setExposurePointOfInterest:(CGPoint)p { (void)p; }
198
- - (BOOL)isExposurePointOfInterestSupported { return YES; }
199
- - (BOOL)isAdjustingExposure { return NO; }
200
- - (float)exposureTargetBias { return 0.0f; }
201
- - (float)minExposureTargetBias { return -8.0f; }
202
- - (float)maxExposureTargetBias { return 8.0f; }
203
- - (CMTime)exposureDuration { return CMTimeMake(1, 30); }
204
- - (float)ISO { return 100.0f; }
205
- - (float)minISO { return 25.0f; }
206
- - (float)maxISO { return 6400.0f; }
207
- - (CMTime)activeMinExposureDuration { return CMTimeMake(1, 8000); }
208
- - (CMTime)activeMaxExposureDuration { return CMTimeMake(1, 30); }
209
- - (AVCaptureWhiteBalanceMode)whiteBalanceMode { return AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance; }
210
- - (void)setWhiteBalanceMode:(AVCaptureWhiteBalanceMode)m { (void)m; }
211
- - (BOOL)isWhiteBalanceModeSupported:(AVCaptureWhiteBalanceMode)m { (void)m; return YES; }
212
- - (BOOL)isAdjustingWhiteBalance { return NO; }
213
- - (BOOL)isFlashAvailable { return NO; }
214
- - (BOOL)videoHDREnabled { return NO; }
215
- - (void)setVideoHDREnabled:(BOOL)b { (void)b; }
216
- - (BOOL)automaticallyAdjustsVideoHDREnabled { return NO; }
217
- - (void)setAutomaticallyAdjustsVideoHDREnabled:(BOOL)b { (void)b; }
218
- - (BOOL)isLowLightBoostSupported { return NO; }
219
- - (BOOL)isLowLightBoostEnabled { return NO; }
220
- - (BOOL)automaticallyEnablesLowLightBoostWhenAvailable { return NO; }
221
- - (void)setAutomaticallyEnablesLowLightBoostWhenAvailable:(BOOL)b { (void)b; }
222
- - (NSArray *)linkedDevices { return @[]; }
223
- @end
224
-
225
- static AVCaptureDevice *SimCamFakeDeviceForPosition(AVCaptureDevicePosition p) {
226
- static AVCaptureDevice *front = nil;
227
- static AVCaptureDevice *back = nil;
228
- static dispatch_once_t once;
229
- dispatch_once(&once, ^{
230
- front = (AVCaptureDevice *)class_createInstance([SimCamFakeDevice class], 0);
231
- objc_setAssociatedObject(front, &kFakePositionKey,
232
- @(AVCaptureDevicePositionFront), OBJC_ASSOCIATION_RETAIN);
233
- back = (AVCaptureDevice *)class_createInstance([SimCamFakeDevice class], 0);
234
- objc_setAssociatedObject(back, &kFakePositionKey,
235
- @(AVCaptureDevicePositionBack), OBJC_ASSOCIATION_RETAIN);
236
- });
237
- return p == AVCaptureDevicePositionBack ? back : front;
238
- }
239
-
240
- // Look up the position the app picked for any object we tagged in the chain.
241
- static char kSimCamPositionKey;
242
-
243
- static AVCaptureDevicePosition SimCamPositionOf(id obj) {
244
- if (!obj) return AVCaptureDevicePositionFront;
245
- NSNumber *n = objc_getAssociatedObject(obj, &kSimCamPositionKey);
246
- return n ? (AVCaptureDevicePosition)n.intValue : AVCaptureDevicePositionFront;
247
- }
248
- static void SimCamSetPosition(id obj, AVCaptureDevicePosition p) {
249
- objc_setAssociatedObject(obj, &kSimCamPositionKey, @(p), OBJC_ASSOCIATION_RETAIN);
250
- }
251
- // SIMCAM_MIRROR_MODE = "auto" (default), "on", "off". Override applies to
252
- // preview layer transform; data-output buffers are never auto-mirrored
253
- // because AVCaptureConnection.isVideoMirroring defaults to NO on real HW.
254
- typedef NS_ENUM(NSInteger, SimCamMirrorMode) {
255
- SimCamMirrorAuto = 0,
256
- SimCamMirrorForceOn,
257
- SimCamMirrorForceOff,
258
- };
259
- static SimCamMirrorMode gMirrorMode = SimCamMirrorAuto;
260
-
261
- static BOOL SimCamShouldMirror(AVCaptureDevicePosition p) {
262
- if (gMirrorMode == SimCamMirrorForceOn) return YES;
263
- if (gMirrorMode == SimCamMirrorForceOff) return NO;
264
- // Auto: front camera mirrors (matches AVCaptureVideoPreviewLayer's default
265
- // automaticallyAdjustsVideoMirroring=YES on real hardware), back doesn't.
266
- return p == AVCaptureDevicePositionFront;
267
- }
268
-
269
- #pragma mark - Output delegate registry
270
-
271
- @interface SimCamRegistry : NSObject
272
- + (instancetype)shared;
273
- - (void)addOutput:(AVCaptureVideoDataOutput *)out
274
- delegate:(id<AVCaptureVideoDataOutputSampleBufferDelegate>)delegate
275
- queue:(dispatch_queue_t)queue;
276
- - (void)removeOutput:(AVCaptureVideoDataOutput *)out;
277
- - (void)addPreviewLayer:(AVCaptureVideoPreviewLayer *)layer;
278
- - (void)reapplyMirrorToLayers; // re-evaluate every known layer's transform
279
- - (NSData *)currentSnapshotJPEGAtQuality:(CGFloat)q;
280
- - (void)startPumpingIfNeeded;
281
- - (void)stopPumping;
282
- @end
283
-
284
- @implementation SimCamRegistry {
285
- NSMutableArray *_entries; // each: @{ @"out": out, @"del": del, @"queue": q }
286
- NSHashTable<AVCaptureVideoPreviewLayer *> *_layers;
287
- dispatch_source_t _timer;
288
- dispatch_queue_t _timerQueue;
289
- NSLock *_lock;
290
- }
291
-
292
- + (instancetype)shared {
293
- static SimCamRegistry *s; static dispatch_once_t o;
294
- dispatch_once(&o, ^{ s = [SimCamRegistry new]; });
295
- return s;
296
- }
297
-
298
- - (instancetype)init {
299
- if ((self = [super init])) {
300
- _entries = [NSMutableArray new];
301
- _layers = [NSHashTable weakObjectsHashTable];
302
- _timerQueue = dispatch_queue_create("dev.servesim.simcam.pump", DISPATCH_QUEUE_SERIAL);
303
- _lock = [NSLock new];
304
- }
305
- return self;
306
- }
307
-
308
- - (void)addOutput:(AVCaptureVideoDataOutput *)out
309
- delegate:(id<AVCaptureVideoDataOutputSampleBufferDelegate>)delegate
310
- queue:(dispatch_queue_t)queue {
311
- if (!out || !delegate) return;
312
- [_lock lock];
313
- // Strong-retain the output (we never let the native session retain it).
314
- // The delegate is held weakly via NSValue; AVFoundation contract is that
315
- // setSampleBufferDelegate: does not retain its delegate either.
316
- [_entries addObject:@{
317
- @"out": out,
318
- @"del": [NSValue valueWithNonretainedObject:delegate],
319
- @"queue": queue ?: dispatch_get_main_queue(),
320
- }];
321
- [_lock unlock];
322
- simcam_log(@"registered video data output delegate %@ (pos=%d)",
323
- delegate, (int)SimCamPositionOf(out));
324
- // Auto-kick the pump: some frameworks (notably expo-camera) gate
325
- // `session.startRunning()` behind `#if !targetEnvironment(simulator)`,
326
- // so we'd never see startRunning. Start as soon as there's a consumer.
327
- [self startPumpingIfNeeded];
328
- }
329
-
330
- - (void)removeOutput:(AVCaptureVideoDataOutput *)out {
331
- [_lock lock];
332
- NSMutableIndexSet *toRemove = [NSMutableIndexSet new];
333
- [_entries enumerateObjectsUsingBlock:^(NSDictionary *e, NSUInteger i, BOOL *stop) {
334
- if (e[@"out"] == out) [toRemove addIndex:i];
335
- }];
336
- [_entries removeObjectsAtIndexes:toRemove];
337
- [_lock unlock];
338
- }
339
-
340
- - (void)addPreviewLayer:(AVCaptureVideoPreviewLayer *)layer {
341
- if (!layer) return;
342
- [_lock lock];
343
- [_layers addObject:layer];
344
- [_lock unlock];
345
- BOOL mirror = SimCamShouldMirror(SimCamPositionOf(layer));
346
- dispatch_async(dispatch_get_main_queue(), ^{
347
- layer.contentsGravity = kCAGravityResizeAspectFill;
348
- // Match AVCaptureVideoPreviewLayer's default front-camera mirroring
349
- // by negating X scale on the layer transform. Cheaper than flipping
350
- // pixels every frame and keeps geometry consistent for both static
351
- // and live sources.
352
- if (mirror) layer.transform = CATransform3DMakeScale(-1.f, 1.f, 1.f);
353
- if (gSourceCGImage && !gShmHeader) {
354
- layer.contents = (__bridge id)gSourceCGImage;
355
- }
356
- });
357
- simcam_log(@"hooked preview layer %p (mirror=%d)", layer, (int)mirror);
358
- [self startPumpingIfNeeded];
359
- }
360
-
361
- - (void)reapplyMirrorToLayers {
362
- NSArray *layerSnapshot;
363
- [_lock lock]; layerSnapshot = _layers.allObjects; [_lock unlock];
364
- if (layerSnapshot.count == 0) return;
365
- dispatch_async(dispatch_get_main_queue(), ^{
366
- // Disable implicit animations so the flip is instantaneous.
367
- [CATransaction begin];
368
- [CATransaction setDisableActions:YES];
369
- for (AVCaptureVideoPreviewLayer *l in layerSnapshot) {
370
- BOOL m = SimCamShouldMirror(SimCamPositionOf(l));
371
- l.transform = m ? CATransform3DMakeScale(-1.f, 1.f, 1.f)
372
- : CATransform3DIdentity;
373
- }
374
- [CATransaction commit];
375
- });
376
- }
377
-
378
- - (void)pushFrameToLayers:(CVPixelBufferRef)pb {
379
- if (!pb) return;
380
- NSArray *layerSnapshot;
381
- [_lock lock]; layerSnapshot = _layers.allObjects; [_lock unlock];
382
- if (layerSnapshot.count == 0) return;
383
-
384
- CIImage *ci = [CIImage imageWithCVPixelBuffer:pb];
385
- static CIContext *ciCtx = nil; static dispatch_once_t once;
386
- dispatch_once(&once, ^{ ciCtx = [CIContext contextWithOptions:nil]; });
387
- CGImageRef cg = [ciCtx createCGImage:ci fromRect:ci.extent];
388
- if (!cg) return;
389
- dispatch_async(dispatch_get_main_queue(), ^{
390
- for (AVCaptureVideoPreviewLayer *l in layerSnapshot) {
391
- l.contents = (__bridge id)cg;
392
- }
393
- CGImageRelease(cg);
394
- });
395
- }
396
-
397
- - (CVPixelBufferRef)newPixelBufferFromShm CF_RETURNS_RETAINED {
398
- return [self newPixelBufferFromShmForceFresh:NO];
399
- }
400
-
401
- - (CVPixelBufferRef)newPixelBufferFromShmForceFresh:(BOOL)force CF_RETURNS_RETAINED {
402
- if (!gShmHeader || !gShmPixels) return NULL;
403
- if (gShmHeader->magic != SIMCAM_SHM_MAGIC) return NULL;
404
- uint64_t seqA = gShmHeader->frameSeq;
405
- if (seqA == 0) return NULL;
406
- if (!force && seqA == gLastSeenSeq) return NULL; // no new frame
407
- uint32_t w = gShmHeader->width;
408
- uint32_t h = gShmHeader->height;
409
- uint32_t bpr = gShmHeader->bytesPerRow;
410
- if (!w || !h || bpr < w * 4) return NULL;
411
- if (sizeof(SimCamShmHeader) + (size_t)bpr * h > gShmTotalSize) return NULL;
412
-
413
- CVPixelBufferRef pb = NULL;
414
- NSDictionary *attrs = @{ (id)kCVPixelBufferIOSurfacePropertiesKey: @{} };
415
- CVReturn r = CVPixelBufferCreate(kCFAllocatorDefault, w, h,
416
- kCVPixelFormatType_32BGRA, (__bridge CFDictionaryRef)attrs, &pb);
417
- if (r != kCVReturnSuccess || !pb) return NULL;
418
- CVPixelBufferLockBaseAddress(pb, 0);
419
- uint8_t *dst = (uint8_t *)CVPixelBufferGetBaseAddress(pb);
420
- size_t dstBpr = CVPixelBufferGetBytesPerRow(pb);
421
- size_t copyBpr = MIN((size_t)bpr, dstBpr);
422
- for (uint32_t y = 0; y < h; y++) {
423
- memcpy(dst + y * dstBpr, gShmPixels + y * bpr, copyBpr);
424
- }
425
- CVPixelBufferUnlockBaseAddress(pb, 0);
426
-
427
- atomic_thread_fence(memory_order_acquire);
428
- uint64_t seqB = gShmHeader->frameSeq;
429
- if (!force && seqA != seqB) {
430
- // Tear: writer updated mid-copy. Drop this frame; we'll catch the next one.
431
- CVPixelBufferRelease(pb);
432
- return NULL;
433
- }
434
- gLastSeenSeq = seqA;
435
- return pb;
436
- }
437
-
438
- // Latest frame from whichever source is active, ignoring per-frame dedup.
439
- // Used for one-shot consumers like AVCapturePhotoOutput.capturePhoto.
440
- - (CVPixelBufferRef)currentPixelBuffer CF_RETURNS_RETAINED {
441
- CVPixelBufferRef pb = [self newPixelBufferFromShmForceFresh:YES];
442
- if (!pb) pb = [self newPixelBufferFromImage];
443
- return pb;
444
- }
445
-
446
- - (NSData *)currentSnapshotJPEGAtQuality:(CGFloat)q {
447
- CVPixelBufferRef pb = [self currentPixelBuffer];
448
- if (!pb) return nil;
449
- CIImage *ci = [CIImage imageWithCVPixelBuffer:pb];
450
- // Placeholder-substitution callers (expo-camera, etc.) have no
451
- // AVCaptureConnection to consult; assume the front camera, which is
452
- // what these libraries default to in the simulator and what the
453
- // preview is mirroring.
454
- if (SimCamShouldMirror(AVCaptureDevicePositionFront)) {
455
- ci = [ci imageByApplyingOrientation:kCGImagePropertyOrientationUpMirrored];
456
- }
457
- static CIContext *ctx = nil; static dispatch_once_t once;
458
- dispatch_once(&once, ^{ ctx = [CIContext contextWithOptions:nil]; });
459
- CGImageRef cg = [ctx createCGImage:ci fromRect:ci.extent];
460
- CVPixelBufferRelease(pb);
461
- if (!cg) return nil;
462
- UIImage *ui = [UIImage imageWithCGImage:cg];
463
- NSData *data = UIImageJPEGRepresentation(ui, q);
464
- CGImageRelease(cg);
465
- return data;
466
- }
467
-
468
- - (CVPixelBufferRef)newPixelBufferFromImage CF_RETURNS_RETAINED {
469
- if (!gSourceCGImage) return NULL;
470
- CVPixelBufferRef pb = NULL;
471
- NSDictionary *attrs = @{ (id)kCVPixelBufferIOSurfacePropertiesKey: @{} };
472
- CVReturn r = CVPixelBufferCreate(kCFAllocatorDefault, kFrameWidth, kFrameHeight,
473
- kCVPixelFormatType_32BGRA, (__bridge CFDictionaryRef)attrs, &pb);
474
- if (r != kCVReturnSuccess || !pb) return NULL;
475
- CVPixelBufferLockBaseAddress(pb, 0);
476
- void *base = CVPixelBufferGetBaseAddress(pb);
477
- size_t bpr = CVPixelBufferGetBytesPerRow(pb);
478
- CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
479
- CGContextRef ctx = CGBitmapContextCreate(base, kFrameWidth, kFrameHeight, 8, bpr, cs,
480
- kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Little);
481
- CGContextSetFillColorWithColor(ctx, [UIColor blackColor].CGColor);
482
- CGContextFillRect(ctx, CGRectMake(0, 0, kFrameWidth, kFrameHeight));
483
- size_t iw = CGImageGetWidth(gSourceCGImage), ih = CGImageGetHeight(gSourceCGImage);
484
- double sx = (double)kFrameWidth / iw, sy = (double)kFrameHeight / ih;
485
- double s = MAX(sx, sy);
486
- double dw = iw * s, dh = ih * s;
487
- CGRect dst = CGRectMake((kFrameWidth - dw)/2.0, (kFrameHeight - dh)/2.0, dw, dh);
488
- CGContextDrawImage(ctx, dst, gSourceCGImage);
489
- CGContextRelease(ctx);
490
- CGColorSpaceRelease(cs);
491
- CVPixelBufferUnlockBaseAddress(pb, 0);
492
- return pb;
493
- }
494
-
495
- - (CMSampleBufferRef)newSampleBufferAtTime:(CMTime)pts CF_RETURNS_RETAINED {
496
- CVPixelBufferRef pb = [self newPixelBufferFromShm];
497
- if (!pb) pb = [self newPixelBufferFromImage];
498
- if (!pb) return NULL;
499
-
500
- CMVideoFormatDescriptionRef fd = NULL;
501
- CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault, pb, &fd);
502
- CMSampleTimingInfo timing = {
503
- .duration = CMTimeMake(1, (int32_t)kFrameRate),
504
- .presentationTimeStamp = pts,
505
- .decodeTimeStamp = kCMTimeInvalid,
506
- };
507
- CMSampleBufferRef sb = NULL;
508
- CMSampleBufferCreateForImageBuffer(kCFAllocatorDefault, pb, true, NULL, NULL, fd, &timing, &sb);
509
- if (fd) CFRelease(fd);
510
- CVPixelBufferRelease(pb);
511
- return sb;
512
- }
513
-
514
- - (void)startPumpingIfNeeded {
515
- [_lock lock];
516
- if (_timer) { [_lock unlock]; return; }
517
- _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _timerQueue);
518
- uint64_t intervalNs = (uint64_t)(NSEC_PER_SEC / kFrameRate);
519
- dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, 0), intervalNs, intervalNs / 10);
520
- __weak __typeof(self) weakSelf = self;
521
- __block int64_t frameIdx = 0;
522
- __block uint8_t lastMirrorByte = SIMCAM_MIRROR_UNSET;
523
- dispatch_source_set_event_handler(_timer, ^{
524
- __strong __typeof(weakSelf) self = weakSelf; if (!self) return;
525
- // Watch the mirror byte the helper writes to the shm header. When
526
- // it changes (and isn't the UNSET sentinel), update gMirrorMode
527
- // and re-evaluate every known preview layer's transform.
528
- if (gShmHeader) {
529
- uint8_t m = gShmHeader->mirrorMode;
530
- if (m != lastMirrorByte) {
531
- lastMirrorByte = m;
532
- if (m != SIMCAM_MIRROR_UNSET) {
533
- SimCamMirrorMode prev = gMirrorMode;
534
- if (m == SIMCAM_MIRROR_ON) gMirrorMode = SimCamMirrorForceOn;
535
- else if (m == SIMCAM_MIRROR_OFF) gMirrorMode = SimCamMirrorForceOff;
536
- else gMirrorMode = SimCamMirrorAuto;
537
- if (prev != gMirrorMode) {
538
- simcam_log(@"mirror mode → %d (from shm)", (int)gMirrorMode);
539
- [self reapplyMirrorToLayers];
540
- }
541
- }
542
- }
543
- }
544
- CMTime pts = CMTimeMake(frameIdx++, (int32_t)kFrameRate);
545
- CMSampleBufferRef sb = [self newSampleBufferAtTime:pts];
546
- if (!sb) return;
547
- CVImageBufferRef pb = CMSampleBufferGetImageBuffer(sb);
548
- if (gShmHeader) [self pushFrameToLayers:pb];
549
- NSArray *snapshot;
550
- [self->_lock lock]; snapshot = [self->_entries copy]; [self->_lock unlock];
551
- for (NSDictionary *e in snapshot) {
552
- AVCaptureVideoDataOutput *out = e[@"out"];
553
- id<AVCaptureVideoDataOutputSampleBufferDelegate> del =
554
- ((NSValue *)e[@"del"]).nonretainedObjectValue;
555
- dispatch_queue_t q = e[@"queue"];
556
- if (!out || !del) continue;
557
- CFRetain(sb);
558
- dispatch_async(q, ^{
559
- if ([del respondsToSelector:@selector(captureOutput:didOutputSampleBuffer:fromConnection:)]) {
560
- AVCaptureConnection *connArg = (AVCaptureConnection *)(id)nil;
561
- [del captureOutput:out didOutputSampleBuffer:sb fromConnection:connArg];
562
- }
563
- CFRelease(sb);
564
- });
565
- }
566
- CFRelease(sb);
567
- });
568
- dispatch_resume(_timer);
569
- [_lock unlock];
570
- simcam_log(@"started frame pump @ %.0f fps", kFrameRate);
571
- }
572
-
573
- - (void)stopPumping {
574
- [_lock lock];
575
- if (_timer) { dispatch_source_cancel(_timer); _timer = NULL; }
576
- [_lock unlock];
577
- }
578
- @end
579
-
580
- #pragma mark - Swizzling helpers
581
-
582
- static void SwizzleClassMethod(Class cls, SEL orig, SEL swiz) {
583
- Method o = class_getClassMethod(cls, orig);
584
- Method s = class_getClassMethod(cls, swiz);
585
- if (o && s) method_exchangeImplementations(o, s);
586
- }
587
- static void SwizzleInstanceMethod(Class cls, SEL orig, SEL swiz) {
588
- Method o = class_getInstanceMethod(cls, orig);
589
- Method s = class_getInstanceMethod(cls, swiz);
590
- if (o && s) method_exchangeImplementations(o, s);
591
- }
592
-
593
- #pragma mark - AVCaptureDevice swizzles
594
-
595
- @interface AVCaptureDevice (SimCam)
596
- @end
597
- @implementation AVCaptureDevice (SimCam)
598
- + (AVCaptureDevice *)simcam_defaultDeviceWithDeviceType:(AVCaptureDeviceType)t
599
- mediaType:(AVMediaType)m
600
- position:(AVCaptureDevicePosition)p {
601
- if ([m isEqualToString:AVMediaTypeVideo] || m == nil) {
602
- AVCaptureDevicePosition resolved =
603
- (p == AVCaptureDevicePositionBack) ? AVCaptureDevicePositionBack
604
- : AVCaptureDevicePositionFront;
605
- simcam_log(@"defaultDeviceWithDeviceType: %@ position: %d → fake",
606
- t, (int)resolved);
607
- return SimCamFakeDeviceForPosition(resolved);
608
- }
609
- return [self simcam_defaultDeviceWithDeviceType:t mediaType:m position:p];
610
- }
611
- + (NSArray<AVCaptureDevice *> *)simcam_devicesWithMediaType:(AVMediaType)m {
612
- if ([m isEqualToString:AVMediaTypeVideo]) {
613
- return @[
614
- SimCamFakeDeviceForPosition(AVCaptureDevicePositionFront),
615
- SimCamFakeDeviceForPosition(AVCaptureDevicePositionBack),
616
- ];
617
- }
618
- return [self simcam_devicesWithMediaType:m];
619
- }
620
- + (NSArray<AVCaptureDevice *> *)simcam_devices {
621
- NSArray *real = [self simcam_devices];
622
- NSArray *fakes = @[
623
- SimCamFakeDeviceForPosition(AVCaptureDevicePositionFront),
624
- SimCamFakeDeviceForPosition(AVCaptureDevicePositionBack),
625
- ];
626
- return [fakes arrayByAddingObjectsFromArray:real ?: @[]];
627
- }
628
- @end
629
-
630
- #pragma mark - AVCaptureDeviceDiscoverySession swizzles
631
-
632
- @interface SimCamFakeDiscoverySession : NSObject
633
- @property (nonatomic, strong) NSArray<AVCaptureDevice *> *devices;
634
- @end
635
- @implementation SimCamFakeDiscoverySession
636
- @end
637
-
638
- @interface AVCaptureDeviceDiscoverySession (SimCam)
639
- @end
640
- @implementation AVCaptureDeviceDiscoverySession (SimCam)
641
- + (AVCaptureDeviceDiscoverySession *)simcam_discoverySessionWithDeviceTypes:(NSArray<AVCaptureDeviceType> *)types
642
- mediaType:(AVMediaType)m
643
- position:(AVCaptureDevicePosition)p {
644
- AVCaptureDeviceDiscoverySession *real =
645
- [self simcam_discoverySessionWithDeviceTypes:types mediaType:m position:p];
646
- if ([m isEqualToString:AVMediaTypeVideo] || m == nil) {
647
- NSMutableArray *list = [NSMutableArray new];
648
- if (p == AVCaptureDevicePositionUnspecified || p == AVCaptureDevicePositionFront)
649
- [list addObject:SimCamFakeDeviceForPosition(AVCaptureDevicePositionFront)];
650
- if (p == AVCaptureDevicePositionUnspecified || p == AVCaptureDevicePositionBack)
651
- [list addObject:SimCamFakeDeviceForPosition(AVCaptureDevicePositionBack)];
652
- @try {
653
- [real setValue:list forKey:@"devices"];
654
- } @catch (__unused id e) {
655
- simcam_log(@"could not override discovery session devices");
656
- }
657
- }
658
- return real;
659
- }
660
- @end
661
-
662
- #pragma mark - AVCaptureDeviceInput swizzle
663
-
664
- static char kSimCamFakeInputKey;
665
- static char kSimCamFakeInputDeviceKey;
666
-
667
- @interface AVCaptureDeviceInput (SimCam)
668
- @end
669
- @implementation AVCaptureDeviceInput (SimCam)
670
- - (instancetype)simcam_initWithDevice:(AVCaptureDevice *)device error:(NSError **)err {
671
- if ([device isKindOfClass:[SimCamFakeDevice class]]) {
672
- if (err) *err = nil;
673
- // Bypass AVCaptureDeviceInput's hardware init via NSObject's init —
674
- // skips the hardware probe but leaves AVF's private ivars at zero.
675
- // Anything that later reads those ivars (e.g. the original -device
676
- // accessor) crashes, so we swizzle the relevant accessors below.
677
- struct objc_super sup = { self, [NSObject class] };
678
- id obj = ((id (*)(struct objc_super *, SEL))objc_msgSendSuper)(&sup, @selector(init));
679
- if (obj) {
680
- objc_setAssociatedObject(obj, &kSimCamFakeInputKey, @YES, OBJC_ASSOCIATION_RETAIN);
681
- objc_setAssociatedObject(obj, &kSimCamFakeInputDeviceKey, device, OBJC_ASSOCIATION_RETAIN);
682
- SimCamSetPosition(obj, device.position);
683
- }
684
- return obj;
685
- }
686
- return [self simcam_initWithDevice:device error:err];
687
- }
688
- - (AVCaptureDevice *)simcam_device {
689
- AVCaptureDevice *fake = objc_getAssociatedObject(self, &kSimCamFakeInputDeviceKey);
690
- if (fake) return fake;
691
- return [self simcam_device];
692
- }
693
- - (NSArray *)simcam_ports {
694
- if (objc_getAssociatedObject(self, &kSimCamFakeInputKey)) return @[];
695
- return [self simcam_ports];
696
- }
697
- @end
698
-
699
- static BOOL SimCamIsFakeInput(id input) {
700
- if (!input) return NO;
701
- return [objc_getAssociatedObject(input, &kSimCamFakeInputKey) boolValue];
702
- }
703
-
704
- #pragma mark - AVCaptureSession swizzles
705
-
706
- static char kSimCamSessionRunningKey;
707
-
708
- @interface AVCaptureSession (SimCam)
709
- @end
710
- @implementation AVCaptureSession (SimCam)
711
- - (void)simcam_addInput:(AVCaptureInput *)input {
712
- if (SimCamIsFakeInput(input)) {
713
- AVCaptureDevicePosition p = SimCamPositionOf(input);
714
- SimCamSetPosition(self, p);
715
- simcam_log(@"addInput: fake input (%@) — skipping native add",
716
- p == AVCaptureDevicePositionBack ? @"back" : @"front");
717
- return;
718
- }
719
- [self simcam_addInput:input];
720
- }
721
- - (BOOL)simcam_canAddInput:(AVCaptureInput *)input {
722
- if (SimCamIsFakeInput(input)) return YES;
723
- return [self simcam_canAddInput:input];
724
- }
725
- - (void)simcam_addOutput:(AVCaptureOutput *)output {
726
- // Always skip native add — without a real input the native session would
727
- // refuse outputs anyway; we drive frames from our pump.
728
- SimCamSetPosition(output, SimCamPositionOf(self));
729
- simcam_log(@"addOutput: %@ (intercepted, pos=%d)",
730
- NSStringFromClass([output class]), (int)SimCamPositionOf(self));
731
- }
732
- - (BOOL)simcam_canAddOutput:(AVCaptureOutput *)output { return YES; }
733
- - (void)simcam_startRunning {
734
- objc_setAssociatedObject(self, &kSimCamSessionRunningKey, @YES, OBJC_ASSOCIATION_RETAIN);
735
- simcam_log(@"startRunning intercepted");
736
- [[SimCamRegistry shared] startPumpingIfNeeded];
737
- // Notify observers that session is running.
738
- [self willChangeValueForKey:@"running"];
739
- [self didChangeValueForKey:@"running"];
740
- }
741
- - (void)simcam_stopRunning {
742
- objc_setAssociatedObject(self, &kSimCamSessionRunningKey, @NO, OBJC_ASSOCIATION_RETAIN);
743
- simcam_log(@"stopRunning intercepted");
744
- // Don't stop the global pump — other sessions may be running.
745
- }
746
- - (BOOL)simcam_isRunning {
747
- NSNumber *v = objc_getAssociatedObject(self, &kSimCamSessionRunningKey);
748
- return v.boolValue;
749
- }
750
- @end
751
-
752
- #pragma mark - AVCaptureVideoDataOutput swizzle
753
-
754
- @interface AVCaptureVideoDataOutput (SimCam)
755
- @end
756
- @implementation AVCaptureVideoDataOutput (SimCam)
757
- - (void)simcam_setSampleBufferDelegate:(id<AVCaptureVideoDataOutputSampleBufferDelegate>)delegate
758
- queue:(dispatch_queue_t)queue {
759
- [self simcam_setSampleBufferDelegate:delegate queue:queue];
760
- [[SimCamRegistry shared] addOutput:self delegate:delegate queue:queue];
761
- }
762
- @end
763
-
764
- #pragma mark - AVCaptureVideoPreviewLayer swizzle
765
-
766
- @interface AVCaptureVideoPreviewLayer (SimCam)
767
- @end
768
- @implementation AVCaptureVideoPreviewLayer (SimCam)
769
- - (void)simcam_setSession:(AVCaptureSession *)session {
770
- [self simcam_setSession:session];
771
- AVCaptureDevicePosition p = SimCamPositionOf(session);
772
- SimCamSetPosition(self, p);
773
- [[SimCamRegistry shared] addPreviewLayer:self];
774
- }
775
- @end
776
-
777
- #pragma mark - AVCapturePhotoOutput swizzle
778
-
779
- // AVCapturePhoto subclass that returns app-injected pixel data when asked
780
- // for its file/CGImage representations. We bypass AVCapturePhoto's private
781
- // init (similar to our fake AVCaptureDeviceInput) and override only the
782
- // accessors that real consumers actually call.
783
- @interface SimCamFakePhoto : AVCapturePhoto
784
- @end
785
- @implementation SimCamFakePhoto {
786
- NSData *_jpegData;
787
- CGImageRef _cgImage; // owned
788
- NSDictionary *_metadata;
789
- }
790
- // Bypass AVCapturePhoto's class-cluster placeholder. Default +allocWithZone:
791
- // hands back a private subclass instance that requires AVCapturePhoto's real
792
- // init to populate ivars; we only need the obj-c isa to dispatch to our
793
- // overrides, so allocate raw memory typed to our class instead.
794
- + (instancetype)allocWithZone:(NSZone *)zone {
795
- return class_createInstance([SimCamFakePhoto class], 0);
796
- }
797
- + (instancetype)photoFromImage:(CGImageRef)cgImage jpegQuality:(CGFloat)q {
798
- if (!cgImage) return nil;
799
- // AVCapturePhoto declares -init as NS_UNAVAILABLE so we can't call it,
800
- // but +alloc -> class_createInstance gives us a fully-formed instance
801
- // with our isa already set; no init call is needed.
802
- SimCamFakePhoto *p = [SimCamFakePhoto alloc];
803
- if (p) {
804
- p->_cgImage = CGImageRetain(cgImage);
805
- UIImage *ui = [UIImage imageWithCGImage:cgImage];
806
- p->_jpegData = UIImageJPEGRepresentation(ui, q);
807
- p->_metadata = @{};
808
- }
809
- return p;
810
- }
811
- - (NSData *)fileDataRepresentation { return _jpegData; }
812
- - (NSData *)fileDataRepresentationWithCustomizer:(id)c { return _jpegData; }
813
- - (NSData *)fileDataRepresentationWithReplacementMetadata:(NSDictionary *)m
814
- replacementEmbeddedThumbnailPhotoFormat:(NSDictionary *)t
815
- replacementEmbeddedThumbnailPixelBuffer:(CVPixelBufferRef)pb
816
- replacementDepthData:(id)d { return _jpegData; }
817
- - (CGImageRef)CGImageRepresentation { return _cgImage; }
818
- - (CGImageRef)previewCGImageRepresentation { return _cgImage; }
819
- - (NSDictionary *)metadata { return _metadata; }
820
- - (CVPixelBufferRef)pixelBuffer { return NULL; }
821
- - (NSInteger)photoCount { return 1; }
822
- - (NSInteger)sequenceCount { return 1; }
823
- - (CMTime)timestamp { return CMTimeMake(0, 30); }
824
- - (BOOL)isRawPhoto { return NO; }
825
- - (void)dealloc { if (_cgImage) CGImageRelease(_cgImage); }
826
- @end
827
-
828
- @interface AVCapturePhotoOutput (SimCam)
829
- @end
830
- @implementation AVCapturePhotoOutput (SimCam)
831
- - (void)simcam_capturePhotoWithSettings:(AVCapturePhotoSettings *)settings
832
- delegate:(id<AVCapturePhotoCaptureDelegate>)delegate {
833
- if (!delegate) return;
834
- SimCamRegistry *reg = [SimCamRegistry shared];
835
- CVPixelBufferRef pb = [reg currentPixelBuffer];
836
- AVCaptureDevicePosition p = SimCamPositionOf(self);
837
- if (p == 0) p = AVCaptureDevicePositionFront;
838
- // Mirror the captured image to match the preview the user sees. The
839
- // preview layer is flipped via CATransform3DMakeScale(-1,1,1) when
840
- // SimCamShouldMirror is YES, so the photo must apply the same flip
841
- // to its pixels — otherwise users see a mirrored preview but a
842
- // non-mirrored photo. Honor the photo connection's explicit
843
- // isVideoMirrored if the app set one, otherwise fall back to the
844
- // position-based default.
845
- BOOL mirror = SimCamShouldMirror(p);
846
- AVCaptureConnection *conn = [self connectionWithMediaType:AVMediaTypeVideo];
847
- if (conn && conn.isVideoMirroringSupported) mirror = conn.isVideoMirrored;
848
- CGImageRef cg = NULL;
849
- if (pb) {
850
- CIImage *ci = [CIImage imageWithCVPixelBuffer:pb];
851
- if (mirror) ci = [ci imageByApplyingOrientation:kCGImagePropertyOrientationUpMirrored];
852
- static CIContext *ctx = nil; static dispatch_once_t once;
853
- dispatch_once(&once, ^{ ctx = [CIContext contextWithOptions:nil]; });
854
- cg = [ctx createCGImage:ci fromRect:ci.extent];
855
- CVPixelBufferRelease(pb);
856
- }
857
- SimCamFakePhoto *photo = [SimCamFakePhoto photoFromImage:cg jpegQuality:0.92];
858
- if (cg) CGImageRelease(cg);
859
- simcam_log(@"capturePhoto intercepted (pos=%d, mirror=%d, jpeg=%lu bytes)",
860
- (int)p, (int)mirror, (unsigned long)photo.fileDataRepresentation.length);
861
- AVCapturePhotoOutput *output = self;
862
- dispatch_async(dispatch_get_main_queue(), ^{
863
- if ([delegate respondsToSelector:@selector(captureOutput:didFinishProcessingPhoto:error:)]) {
864
- [delegate captureOutput:output didFinishProcessingPhoto:photo error:nil];
865
- }
866
- });
867
- }
868
- @end
869
-
870
- #pragma mark - NSData write redirect (expo-camera placeholder substitution)
871
-
872
- // expo-camera SDK 54 explicitly bypasses AVCapturePhotoOutput on simulator
873
- // (CameraViewModule.swift:`#if targetEnvironment(simulator)`) and instead
874
- // generates a 200×200 black-square placeholder, JPEG-encodes it, and writes
875
- // to <Caches>/Camera/<uuid>.jpg via Swift's `Data.write(to:options:)`. The
876
- // AsyncFunction returns that file URL to JS, so users see the placeholder
877
- // instead of the live feed even when our injection is active.
878
- //
879
- // We can't reliably reach into Swift static-method dispatch from Obj-C, but
880
- // `Data.write(to:options:)` bridges down to NSData's writeToURL/writeToFile
881
- // methods — those ARE in the Obj-C runtime. Swizzle them and substitute
882
- // the bytes when the destination URL looks like an expo-camera placeholder
883
- // drop site (path contains /Camera/, suffix .jpg or .jpeg).
884
-
885
- static BOOL SimCamLooksLikeCameraDropPath(NSString *path) {
886
- if (!path.length) return NO;
887
- if (![path containsString:@"/Camera/"]) return NO;
888
- NSString *lower = path.lowercaseString;
889
- return [lower hasSuffix:@".jpg"] || [lower hasSuffix:@".jpeg"];
890
- }
891
-
892
- @interface NSData (SimCam)
893
- @end
894
- @implementation NSData (SimCam)
895
-
896
- - (BOOL)simcam_writeToURL:(NSURL *)url
897
- options:(NSDataWritingOptions)opts
898
- error:(NSError **)err {
899
- NSString *path = url.isFileURL ? url.path : nil;
900
- if (SimCamLooksLikeCameraDropPath(path)) {
901
- NSData *snap = [[SimCamRegistry shared] currentSnapshotJPEGAtQuality:0.92];
902
- if (snap.length > 0) {
903
- simcam_log(@"NSData writeToURL → substituted %lu→%lu bytes (%@)",
904
- (unsigned long)self.length, (unsigned long)snap.length, path.lastPathComponent);
905
- return [snap simcam_writeToURL:url options:opts error:err];
906
- }
907
- }
908
- return [self simcam_writeToURL:url options:opts error:err];
909
- }
910
-
911
- - (BOOL)simcam_writeToFile:(NSString *)path
912
- options:(NSDataWritingOptions)opts
913
- error:(NSError **)err {
914
- if (SimCamLooksLikeCameraDropPath(path)) {
915
- NSData *snap = [[SimCamRegistry shared] currentSnapshotJPEGAtQuality:0.92];
916
- if (snap.length > 0) {
917
- simcam_log(@"NSData writeToFile → substituted %lu→%lu bytes (%@)",
918
- (unsigned long)self.length, (unsigned long)snap.length, path.lastPathComponent);
919
- return [snap simcam_writeToFile:path options:opts error:err];
920
- }
921
- }
922
- return [self simcam_writeToFile:path options:opts error:err];
923
- }
924
-
925
- @end
926
-
927
- #pragma mark - UIGraphicsImageRenderer redirect (camera-placeholder generators)
928
-
929
- // Swift's `Data.write(to:options:)` reaches Foundation through CFData
930
- // internals that bypass the NSData Obj-C swizzles above, so frameworks
931
- // that JPEG-encode a fake photo and write it via Swift `Data.write` slip
932
- // past those hooks. Hook one level higher: most simulator camera
933
- // placeholder generators allocate a UIGraphicsImageRenderer and return its
934
- // `image(actions:)` result. That dispatches through the Obj-C runtime.
935
- //
936
- // Rather than name a specific framework, we match the *behavior* via call
937
- // stack symbol substrings. The pattern is "this frame is some camera lib
938
- // generating a placeholder / simulator / fake photo" — the union of common
939
- // naming conventions. Frameworks whose authors name their generator with
940
- // any of these tokens get redirected automatically.
941
- //
942
- // If you find a camera framework that slips past this, the next-most-
943
- // agnostic option is to interpose `UIImageJPEGRepresentation` via fishhook
944
- // (cross-image C symbol rebind) and filter the same way. Not done here to
945
- // keep the dylib free of vendored deps.
946
-
947
- static BOOL SimCamCallerLooksLikeCameraPlaceholder(void) {
948
- NSArray<NSString *> *stack = [NSThread callStackSymbols];
949
- // Skip the top two frames (this fn + the swizzle thunk). Camera-side
950
- // generators sit close to the top; bound the walk to keep this cheap
951
- // for the common (no-match) case.
952
- NSUInteger limit = MIN((NSUInteger)16, stack.count);
953
- for (NSUInteger i = 2; i < limit; i++) {
954
- NSString *frame = stack[i];
955
- // Generator-naming tokens, by themselves enough to identify a fake
956
- // photo path regardless of which framework owns it.
957
- if ([frame containsString:@"generatePhoto"] ||
958
- [frame containsString:@"generatePicture"] ||
959
- [frame containsString:@"generateImage"] ||
960
- [frame containsString:@"placeholderPhoto"] ||
961
- [frame containsString:@"placeholderImage"] ||
962
- [frame containsString:@"simulatorPhoto"] ||
963
- [frame containsString:@"PictureForSimulator"] ||
964
- [frame containsString:@"PhotoForSimulator"] ||
965
- [frame containsString:@"ImageForSimulator"] ||
966
- [frame containsString:@"mockPhoto"] ||
967
- [frame containsString:@"fakePhoto"]) return YES;
968
- // Camera-namespaced frames combined with a simulator / placeholder
969
- // / generator hint catch the rest (e.g. ExpoCamera, RNCamera,
970
- // VisionCamera, AnyCamera with a "Simulator" or "Placeholder"
971
- // helper).
972
- if ([frame containsString:@"Camera"] || [frame containsString:@"camera"]) {
973
- if ([frame containsString:@"Simulator"] ||
974
- [frame containsString:@"simulator"] ||
975
- [frame containsString:@"Placeholder"] ||
976
- [frame containsString:@"placeholder"] ||
977
- [frame containsString:@"generate"]) return YES;
978
- }
979
- }
980
- return NO;
981
- }
982
-
983
- @interface UIGraphicsImageRenderer (SimCam)
984
- @end
985
- @implementation UIGraphicsImageRenderer (SimCam)
986
- - (UIImage *)simcam_imageWithActions:(void (NS_NOESCAPE ^)(UIGraphicsImageRendererContext *))actions {
987
- if (SimCamCallerLooksLikeCameraPlaceholder()) {
988
- NSData *jpeg = [[SimCamRegistry shared] currentSnapshotJPEGAtQuality:0.92];
989
- if (jpeg.length > 0) {
990
- UIImage *snap = [UIImage imageWithData:jpeg];
991
- if (snap) {
992
- simcam_log(@"UIGraphicsImageRenderer image: → live frame (jpeg %lu bytes)",
993
- (unsigned long)jpeg.length);
994
- return snap;
995
- }
996
- }
997
- }
998
- return [self simcam_imageWithActions:actions];
999
- }
1000
- @end
1001
-
1002
- #pragma mark - Image loading
1003
-
1004
- static void LoadSourceImage(void) {
1005
- const char *envPath = getenv("SIMCAM_IMAGE_PATH");
1006
- NSString *path = envPath ? [NSString stringWithUTF8String:envPath] : nil;
1007
- if (!path.length) {
1008
- simcam_log(@"SIMCAM_IMAGE_PATH not set — generating gradient placeholder");
1009
- UIGraphicsImageRenderer *r = [[UIGraphicsImageRenderer alloc]
1010
- initWithSize:CGSizeMake(kFrameWidth, kFrameHeight)];
1011
- gSourceImage = [r imageWithActions:^(UIGraphicsImageRendererContext *ctx) {
1012
- CGContextRef c = ctx.CGContext;
1013
- CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
1014
- CGFloat colors[] = {0.10,0.45,0.95,1.0, 0.95,0.20,0.55,1.0};
1015
- CGFloat locs[] = {0.0, 1.0};
1016
- CGGradientRef g = CGGradientCreateWithColorComponents(cs, colors, locs, 2);
1017
- CGContextDrawLinearGradient(c, g, CGPointZero,
1018
- CGPointMake(kFrameWidth, kFrameHeight), 0);
1019
- CGGradientRelease(g);
1020
- CGColorSpaceRelease(cs);
1021
- NSDictionary *attrs = @{
1022
- NSFontAttributeName: [UIFont boldSystemFontOfSize:96],
1023
- NSForegroundColorAttributeName: UIColor.whiteColor,
1024
- };
1025
- [@"serve-sim camera" drawAtPoint:CGPointMake(60, 60) withAttributes:attrs];
1026
- }];
1027
- } else {
1028
- gSourceImage = [UIImage imageWithContentsOfFile:path];
1029
- if (!gSourceImage) {
1030
- simcam_log(@"failed to load image at %@", path);
1031
- return;
1032
- }
1033
- simcam_log(@"loaded source image %@ (%.0fx%.0f)", path,
1034
- gSourceImage.size.width, gSourceImage.size.height);
1035
- }
1036
- if (gSourceImage.CGImage) {
1037
- gSourceCGImage = CGImageRetain(gSourceImage.CGImage);
1038
- }
1039
- }
1040
-
1041
- #pragma mark - Install
1042
-
1043
- static void InstallSwizzles(void) {
1044
- Class dev = [AVCaptureDevice class];
1045
- SwizzleClassMethod(dev,
1046
- @selector(defaultDeviceWithDeviceType:mediaType:position:),
1047
- @selector(simcam_defaultDeviceWithDeviceType:mediaType:position:));
1048
- SwizzleClassMethod(dev,
1049
- @selector(devicesWithMediaType:),
1050
- @selector(simcam_devicesWithMediaType:));
1051
- SwizzleClassMethod(dev, @selector(devices), @selector(simcam_devices));
1052
-
1053
- Class disc = [AVCaptureDeviceDiscoverySession class];
1054
- SwizzleClassMethod(disc,
1055
- @selector(discoverySessionWithDeviceTypes:mediaType:position:),
1056
- @selector(simcam_discoverySessionWithDeviceTypes:mediaType:position:));
1057
-
1058
- Class input = [AVCaptureDeviceInput class];
1059
- SwizzleInstanceMethod(input,
1060
- @selector(initWithDevice:error:),
1061
- @selector(simcam_initWithDevice:error:));
1062
- SwizzleInstanceMethod(input, @selector(device), @selector(simcam_device));
1063
- SwizzleInstanceMethod(input, @selector(ports), @selector(simcam_ports));
1064
-
1065
- Class sess = [AVCaptureSession class];
1066
- SwizzleInstanceMethod(sess, @selector(addInput:), @selector(simcam_addInput:));
1067
- SwizzleInstanceMethod(sess, @selector(canAddInput:), @selector(simcam_canAddInput:));
1068
- SwizzleInstanceMethod(sess, @selector(addOutput:), @selector(simcam_addOutput:));
1069
- SwizzleInstanceMethod(sess, @selector(canAddOutput:), @selector(simcam_canAddOutput:));
1070
- SwizzleInstanceMethod(sess, @selector(startRunning), @selector(simcam_startRunning));
1071
- SwizzleInstanceMethod(sess, @selector(stopRunning), @selector(simcam_stopRunning));
1072
- SwizzleInstanceMethod(sess, @selector(isRunning), @selector(simcam_isRunning));
1073
-
1074
- Class out = [AVCaptureVideoDataOutput class];
1075
- SwizzleInstanceMethod(out,
1076
- @selector(setSampleBufferDelegate:queue:),
1077
- @selector(simcam_setSampleBufferDelegate:queue:));
1078
-
1079
- Class pl = [AVCaptureVideoPreviewLayer class];
1080
- SwizzleInstanceMethod(pl, @selector(setSession:), @selector(simcam_setSession:));
1081
-
1082
- Class photoOut = [AVCapturePhotoOutput class];
1083
- SwizzleInstanceMethod(photoOut,
1084
- @selector(capturePhotoWithSettings:delegate:),
1085
- @selector(simcam_capturePhotoWithSettings:delegate:));
1086
-
1087
- Class data = [NSData class];
1088
- SwizzleInstanceMethod(data,
1089
- @selector(writeToURL:options:error:),
1090
- @selector(simcam_writeToURL:options:error:));
1091
- SwizzleInstanceMethod(data,
1092
- @selector(writeToFile:options:error:),
1093
- @selector(simcam_writeToFile:options:error:));
1094
-
1095
- Class renderer = [UIGraphicsImageRenderer class];
1096
- SwizzleInstanceMethod(renderer,
1097
- @selector(imageWithActions:),
1098
- @selector(simcam_imageWithActions:));
1099
- }
1100
-
1101
- static void OpenShmIfRequested(void) {
1102
- const char *shmName = getenv("SIMCAM_SHM_NAME");
1103
- if (!shmName || !*shmName) return;
1104
- int fd = shm_open(shmName, O_RDONLY, 0);
1105
- if (fd < 0) {
1106
- simcam_log(@"shm_open(%s) failed: %s", shmName, strerror(errno));
1107
- return;
1108
- }
1109
- struct stat st;
1110
- if (fstat(fd, &st) < 0 || st.st_size < (off_t)sizeof(SimCamShmHeader)) {
1111
- simcam_log(@"shm fstat failed or too small");
1112
- close(fd);
1113
- return;
1114
- }
1115
- void *map = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_SHARED, fd, 0);
1116
- close(fd);
1117
- if (map == MAP_FAILED) {
1118
- simcam_log(@"shm mmap failed: %s", strerror(errno));
1119
- return;
1120
- }
1121
- SimCamShmHeader *hdr = (SimCamShmHeader *)map;
1122
- if (hdr->magic != SIMCAM_SHM_MAGIC) {
1123
- simcam_log(@"shm magic mismatch: 0x%x", hdr->magic);
1124
- munmap(map, (size_t)st.st_size);
1125
- return;
1126
- }
1127
- gShmHeader = hdr;
1128
- gShmPixels = (const uint8_t *)map + sizeof(SimCamShmHeader);
1129
- gShmTotalSize = (size_t)st.st_size;
1130
- kFrameWidth = hdr->width;
1131
- kFrameHeight = hdr->height;
1132
- simcam_log(@"shm \"%s\" attached (%ux%u, %llu bytes)",
1133
- shmName, hdr->width, hdr->height, (unsigned long long)st.st_size);
1134
- }
1135
-
1136
- static void ReadMirrorMode(void) {
1137
- const char *m = getenv("SIMCAM_MIRROR_MODE");
1138
- if (!m) return;
1139
- if (!strcasecmp(m, "on") || !strcmp(m, "1") || !strcasecmp(m, "true")) {
1140
- gMirrorMode = SimCamMirrorForceOn;
1141
- simcam_log(@"mirror mode forced ON");
1142
- } else if (!strcasecmp(m, "off") || !strcmp(m, "0") || !strcasecmp(m, "false")) {
1143
- gMirrorMode = SimCamMirrorForceOff;
1144
- simcam_log(@"mirror mode forced OFF");
1145
- } else if (!strcasecmp(m, "auto")) {
1146
- gMirrorMode = SimCamMirrorAuto;
1147
- }
1148
- }
4
+ #import "SimCamFakes.h"
5
+ #import "SimCamFrameSource.h"
6
+ #import "SimCamLog.h"
7
+ #import "SimCamSwizzles.h"
1149
8
 
1150
9
  __attribute__((constructor))
1151
10
  static void SimCamInit(void) {
1152
11
  @autoreleasepool {
1153
12
  simcam_log(@"loaded into pid %d", getpid());
1154
- ReadMirrorMode();
1155
- OpenShmIfRequested();
1156
- if (!gShmHeader) LoadSourceImage();
1157
- InstallSwizzles();
13
+ SimCamReadMirrorModeFromEnv();
14
+ SimCamFrameSourceOpenShmIfRequested();
15
+ if (!SimCamFrameSourceIsShmAttached()) SimCamFrameSourceLoadImage();
16
+ SimCamInstallSwizzles();
1158
17
  simcam_log(@"swizzles installed");
1159
18
  }
1160
19
  }