serve-sim 0.1.23 → 0.1.24
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.
- package/README.md +54 -2
- package/Sources/SimCameraHelper/build.sh +32 -0
- package/Sources/SimCameraHelper/main.m +859 -0
- package/Sources/SimCameraInjector/SimCameraInjector.m +1160 -0
- package/Sources/SimCameraInjector/build.sh +33 -0
- package/Sources/SimCameraInjector/include/SimCamShared.h +55 -0
- package/bin/serve-sim-bin +0 -0
- package/dist/middleware.js +15 -15
- package/dist/serve-sim.js +74 -30
- package/dist/simcam/libSimCameraInjector.dylib +0 -0
- package/dist/simcam/serve-sim-camera-helper +0 -0
- package/package.json +9 -2
- package/src/middleware.ts +17 -0
|
@@ -0,0 +1,1160 @@
|
|
|
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
|
+
#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
|
|
115
|
+
|
|
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
|
+
}
|
|
1149
|
+
|
|
1150
|
+
__attribute__((constructor))
|
|
1151
|
+
static void SimCamInit(void) {
|
|
1152
|
+
@autoreleasepool {
|
|
1153
|
+
simcam_log(@"loaded into pid %d", getpid());
|
|
1154
|
+
ReadMirrorMode();
|
|
1155
|
+
OpenShmIfRequested();
|
|
1156
|
+
if (!gShmHeader) LoadSourceImage();
|
|
1157
|
+
InstallSwizzles();
|
|
1158
|
+
simcam_log(@"swizzles installed");
|
|
1159
|
+
}
|
|
1160
|
+
}
|