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