serve-sim 0.1.25 → 0.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/Sources/SimCameraHelper/build.sh +1 -1
- package/Sources/SimCameraHelper/main.m +28 -17
- package/Sources/SimCameraInjector/SimCamFakes.h +86 -0
- package/Sources/SimCameraInjector/SimCamFakes.m +635 -0
- package/Sources/SimCameraInjector/SimCamFrameSource.h +26 -0
- package/Sources/SimCameraInjector/SimCamFrameSource.m +546 -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 +1014 -0
- package/Sources/SimCameraInjector/SimCameraInjector.m +9 -1150
- package/Sources/SimCameraInjector/build.sh +6 -1
- package/bin/serve-sim-bin +0 -0
- package/dist/middleware.js +18 -18
- package/dist/serve-sim.js +32 -32
- package/dist/simcam/libSimCameraInjector.dylib +0 -0
- package/dist/simcam/serve-sim-camera-helper +0 -0
- package/package.json +3 -1
- package/src/middleware.ts +39 -23
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
#import "SimCamFrameSource.h"
|
|
2
|
+
#import "SimCamFakes.h"
|
|
3
|
+
#import "SimCamLog.h"
|
|
4
|
+
#include "include/SimCamShared.h"
|
|
5
|
+
|
|
6
|
+
#import <CoreImage/CoreImage.h>
|
|
7
|
+
#import <CoreMedia/CoreMedia.h>
|
|
8
|
+
#import <CoreVideo/CoreVideo.h>
|
|
9
|
+
#import <UIKit/UIKit.h>
|
|
10
|
+
#import <QuartzCore/QuartzCore.h>
|
|
11
|
+
#import <objc/runtime.h>
|
|
12
|
+
#import <objc/message.h>
|
|
13
|
+
#include <fcntl.h>
|
|
14
|
+
#include <sys/mman.h>
|
|
15
|
+
#include <sys/stat.h>
|
|
16
|
+
#include <stdatomic.h>
|
|
17
|
+
#include <errno.h>
|
|
18
|
+
#include <string.h>
|
|
19
|
+
|
|
20
|
+
#pragma mark - Source globals
|
|
21
|
+
|
|
22
|
+
static UIImage *gSourceImage = nil;
|
|
23
|
+
static CGImageRef gSourceCGImage = NULL;
|
|
24
|
+
static size_t kFrameWidth = 1280;
|
|
25
|
+
static size_t kFrameHeight = 720;
|
|
26
|
+
static const double kFrameRate = 30.0;
|
|
27
|
+
|
|
28
|
+
static SimCamShmHeader *gShmHeader = NULL;
|
|
29
|
+
static const uint8_t *gShmPixels = NULL;
|
|
30
|
+
static size_t gShmTotalSize = 0;
|
|
31
|
+
static uint64_t gLastSeenSeq = 0;
|
|
32
|
+
|
|
33
|
+
#pragma mark - Last-frame cache
|
|
34
|
+
|
|
35
|
+
static CVPixelBufferRef gLastFramePB = NULL;
|
|
36
|
+
static CGImageRef gLastFrameCGImage = NULL;
|
|
37
|
+
static NSLock *gFrameCacheLock = nil;
|
|
38
|
+
static dispatch_once_t gFrameCacheOnce;
|
|
39
|
+
|
|
40
|
+
static inline NSLock *SimCamFrameCacheLock(void) {
|
|
41
|
+
dispatch_once(&gFrameCacheOnce, ^{ gFrameCacheLock = [NSLock new]; });
|
|
42
|
+
return gFrameCacheLock;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static void SimCamCacheFrame(CVPixelBufferRef pb) {
|
|
46
|
+
if (!pb) return;
|
|
47
|
+
CIImage *ci = [CIImage imageWithCVPixelBuffer:pb];
|
|
48
|
+
static CIContext *ctx = nil; static dispatch_once_t ctxOnce;
|
|
49
|
+
dispatch_once(&ctxOnce, ^{ ctx = [CIContext contextWithOptions:nil]; });
|
|
50
|
+
CGImageRef cg = [ctx createCGImage:ci fromRect:ci.extent];
|
|
51
|
+
NSLock *lock = SimCamFrameCacheLock();
|
|
52
|
+
[lock lock];
|
|
53
|
+
CVPixelBufferRef oldPB = gLastFramePB;
|
|
54
|
+
CGImageRef oldCG = gLastFrameCGImage;
|
|
55
|
+
gLastFramePB = (CVPixelBufferRef)CFRetain(pb);
|
|
56
|
+
gLastFrameCGImage = cg;
|
|
57
|
+
[lock unlock];
|
|
58
|
+
if (oldPB) CVPixelBufferRelease(oldPB);
|
|
59
|
+
if (oldCG) CGImageRelease(oldCG);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static CVPixelBufferRef SimCamAcquireCachedPB(void) CF_RETURNS_RETAINED {
|
|
63
|
+
NSLock *lock = SimCamFrameCacheLock();
|
|
64
|
+
[lock lock];
|
|
65
|
+
CVPixelBufferRef pb = gLastFramePB;
|
|
66
|
+
if (pb) CFRetain(pb);
|
|
67
|
+
[lock unlock];
|
|
68
|
+
return pb;
|
|
69
|
+
}
|
|
70
|
+
static CGImageRef SimCamAcquireCachedCGImage(void) CF_RETURNS_RETAINED {
|
|
71
|
+
NSLock *lock = SimCamFrameCacheLock();
|
|
72
|
+
[lock lock];
|
|
73
|
+
CGImageRef cg = gLastFrameCGImage;
|
|
74
|
+
if (cg) CGImageRetain(cg);
|
|
75
|
+
[lock unlock];
|
|
76
|
+
return cg;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#pragma mark - Output delegate registry
|
|
80
|
+
|
|
81
|
+
@implementation SimCamRegistry {
|
|
82
|
+
NSMutableArray *_entries;
|
|
83
|
+
NSHashTable<AVCaptureVideoPreviewLayer *> *_layers;
|
|
84
|
+
dispatch_source_t _timer;
|
|
85
|
+
dispatch_queue_t _timerQueue;
|
|
86
|
+
NSLock *_lock;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
+ (instancetype)shared {
|
|
90
|
+
static SimCamRegistry *s; static dispatch_once_t o;
|
|
91
|
+
dispatch_once(&o, ^{ s = [SimCamRegistry new]; });
|
|
92
|
+
return s;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
- (instancetype)init {
|
|
96
|
+
if ((self = [super init])) {
|
|
97
|
+
_entries = [NSMutableArray new];
|
|
98
|
+
_layers = [NSHashTable weakObjectsHashTable];
|
|
99
|
+
_timerQueue = dispatch_queue_create("dev.servesim.simcam.pump", DISPATCH_QUEUE_SERIAL);
|
|
100
|
+
_lock = [NSLock new];
|
|
101
|
+
}
|
|
102
|
+
return self;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
- (void)addOutput:(AVCaptureVideoDataOutput *)out
|
|
106
|
+
delegate:(id<AVCaptureVideoDataOutputSampleBufferDelegate>)delegate
|
|
107
|
+
queue:(dispatch_queue_t)queue {
|
|
108
|
+
if (!out || !delegate) return;
|
|
109
|
+
SimCamWeakRef *ref = [SimCamWeakRef new];
|
|
110
|
+
ref.target = delegate;
|
|
111
|
+
[_lock lock];
|
|
112
|
+
NSMutableIndexSet *toRemove = [NSMutableIndexSet new];
|
|
113
|
+
[_entries enumerateObjectsUsingBlock:^(NSDictionary *e, NSUInteger i, BOOL *stop) {
|
|
114
|
+
if (e[@"out"] == out) [toRemove addIndex:i];
|
|
115
|
+
}];
|
|
116
|
+
[_entries removeObjectsAtIndexes:toRemove];
|
|
117
|
+
[_entries addObject:@{
|
|
118
|
+
@"out": out,
|
|
119
|
+
@"del": ref,
|
|
120
|
+
@"queue": queue ?: dispatch_get_main_queue(),
|
|
121
|
+
}];
|
|
122
|
+
NSUInteger entryCount = _entries.count;
|
|
123
|
+
[_lock unlock];
|
|
124
|
+
simcam_log(@"addOutput delegate=%p out=%p queue=%p pos=%d (entries=%lu, replaced=%lu)",
|
|
125
|
+
delegate, out, queue, (int)SimCamPositionOf(out),
|
|
126
|
+
(unsigned long)entryCount, (unsigned long)toRemove.count);
|
|
127
|
+
|
|
128
|
+
CVPixelBufferRef cached = SimCamAcquireCachedPB();
|
|
129
|
+
if (cached) {
|
|
130
|
+
CMVideoFormatDescriptionRef fd = NULL;
|
|
131
|
+
CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault, cached, &fd);
|
|
132
|
+
CMSampleBufferRef sb = NULL;
|
|
133
|
+
CMSampleTimingInfo timing = {
|
|
134
|
+
.duration = CMTimeMake(1, (int32_t)kFrameRate),
|
|
135
|
+
.presentationTimeStamp = CMTimeMake(0, (int32_t)kFrameRate),
|
|
136
|
+
.decodeTimeStamp = kCMTimeInvalid,
|
|
137
|
+
};
|
|
138
|
+
if (fd) {
|
|
139
|
+
CMSampleBufferCreateForImageBuffer(kCFAllocatorDefault, cached, true,
|
|
140
|
+
NULL, NULL, fd, &timing, &sb);
|
|
141
|
+
CFRelease(fd);
|
|
142
|
+
}
|
|
143
|
+
if (sb) {
|
|
144
|
+
AVCaptureVideoDataOutput *outRef = out;
|
|
145
|
+
dispatch_queue_t q = queue ?: dispatch_get_main_queue();
|
|
146
|
+
__weak SimCamWeakRef *weakRef = ref;
|
|
147
|
+
dispatch_async(q, ^{
|
|
148
|
+
id<AVCaptureVideoDataOutputSampleBufferDelegate> del = weakRef.target;
|
|
149
|
+
if (del && [del respondsToSelector:@selector(captureOutput:didOutputSampleBuffer:fromConnection:)]) {
|
|
150
|
+
AVCaptureConnection *conn = SimCamFakeConnectionForOutput(outRef);
|
|
151
|
+
[del captureOutput:outRef didOutputSampleBuffer:sb fromConnection:conn];
|
|
152
|
+
}
|
|
153
|
+
CFRelease(sb);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
CVPixelBufferRelease(cached);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
[self startPumpingIfNeeded];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
- (void)removeOutput:(AVCaptureVideoDataOutput *)out {
|
|
163
|
+
[_lock lock];
|
|
164
|
+
NSMutableIndexSet *toRemove = [NSMutableIndexSet new];
|
|
165
|
+
[_entries enumerateObjectsUsingBlock:^(NSDictionary *e, NSUInteger i, BOOL *stop) {
|
|
166
|
+
if (e[@"out"] == out) [toRemove addIndex:i];
|
|
167
|
+
}];
|
|
168
|
+
[_entries removeObjectsAtIndexes:toRemove];
|
|
169
|
+
[_lock unlock];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
- (void)addPreviewLayer:(AVCaptureVideoPreviewLayer *)layer {
|
|
173
|
+
if (!layer) return;
|
|
174
|
+
[_lock lock];
|
|
175
|
+
[_layers addObject:layer];
|
|
176
|
+
[_lock unlock];
|
|
177
|
+
BOOL mirror = SimCamShouldMirror(SimCamPositionOf(layer));
|
|
178
|
+
CGImageRef primed = SimCamAcquireCachedCGImage();
|
|
179
|
+
if (!primed && gSourceCGImage && !gShmHeader) {
|
|
180
|
+
primed = CGImageRetain(gSourceCGImage);
|
|
181
|
+
}
|
|
182
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
183
|
+
layer.contentsGravity = kCAGravityResizeAspectFill;
|
|
184
|
+
if (mirror) layer.transform = CATransform3DMakeScale(-1.f, 1.f, 1.f);
|
|
185
|
+
if (primed) {
|
|
186
|
+
layer.contents = (__bridge id)primed;
|
|
187
|
+
CGImageRelease(primed);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
simcam_log(@"addPreviewLayer %p (mirror=%d, primed=%s, shm=%s)",
|
|
191
|
+
layer, (int)mirror, primed ? "yes" : "no", gShmHeader ? "yes" : "no");
|
|
192
|
+
[self startPumpingIfNeeded];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
- (void)reapplyMirrorToLayers {
|
|
196
|
+
NSArray *layerSnapshot;
|
|
197
|
+
[_lock lock]; layerSnapshot = _layers.allObjects; [_lock unlock];
|
|
198
|
+
if (layerSnapshot.count == 0) return;
|
|
199
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
200
|
+
[CATransaction begin];
|
|
201
|
+
[CATransaction setDisableActions:YES];
|
|
202
|
+
for (AVCaptureVideoPreviewLayer *l in layerSnapshot) {
|
|
203
|
+
BOOL m = SimCamShouldMirror(SimCamPositionOf(l));
|
|
204
|
+
l.transform = m ? CATransform3DMakeScale(-1.f, 1.f, 1.f)
|
|
205
|
+
: CATransform3DIdentity;
|
|
206
|
+
}
|
|
207
|
+
[CATransaction commit];
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
- (void)pushFrameToLayers:(CVPixelBufferRef)pb {
|
|
212
|
+
if (!pb) return;
|
|
213
|
+
NSArray *layerSnapshot;
|
|
214
|
+
[_lock lock]; layerSnapshot = _layers.allObjects; [_lock unlock];
|
|
215
|
+
if (layerSnapshot.count == 0) return;
|
|
216
|
+
|
|
217
|
+
CGImageRef cg = NULL;
|
|
218
|
+
NSLock *lock = SimCamFrameCacheLock();
|
|
219
|
+
[lock lock];
|
|
220
|
+
if (pb == gLastFramePB && gLastFrameCGImage) {
|
|
221
|
+
cg = CGImageRetain(gLastFrameCGImage);
|
|
222
|
+
}
|
|
223
|
+
[lock unlock];
|
|
224
|
+
if (!cg) {
|
|
225
|
+
CIImage *ci = [CIImage imageWithCVPixelBuffer:pb];
|
|
226
|
+
static CIContext *ciCtx = nil; static dispatch_once_t once;
|
|
227
|
+
dispatch_once(&once, ^{ ciCtx = [CIContext contextWithOptions:nil]; });
|
|
228
|
+
cg = [ciCtx createCGImage:ci fromRect:ci.extent];
|
|
229
|
+
if (!cg) return;
|
|
230
|
+
}
|
|
231
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
232
|
+
for (AVCaptureVideoPreviewLayer *l in layerSnapshot) {
|
|
233
|
+
l.contents = (__bridge id)cg;
|
|
234
|
+
}
|
|
235
|
+
CGImageRelease(cg);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
- (CVPixelBufferRef)newPixelBufferFromShm CF_RETURNS_RETAINED {
|
|
240
|
+
return [self newPixelBufferFromShmForceFresh:NO];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
- (CVPixelBufferRef)newPixelBufferFromShmForceFresh:(BOOL)force CF_RETURNS_RETAINED {
|
|
244
|
+
if (!gShmHeader || !gShmPixels) return NULL;
|
|
245
|
+
if (gShmHeader->magic != SIMCAM_SHM_MAGIC) return NULL;
|
|
246
|
+
uint64_t seqA = gShmHeader->frameSeq;
|
|
247
|
+
if (seqA == 0) return NULL;
|
|
248
|
+
if (!force && seqA == gLastSeenSeq) return NULL;
|
|
249
|
+
uint32_t w = gShmHeader->width;
|
|
250
|
+
uint32_t h = gShmHeader->height;
|
|
251
|
+
uint32_t bpr = gShmHeader->bytesPerRow;
|
|
252
|
+
if (!w || !h || bpr < w * 4) return NULL;
|
|
253
|
+
if (sizeof(SimCamShmHeader) + (size_t)bpr * h > gShmTotalSize) return NULL;
|
|
254
|
+
|
|
255
|
+
CVPixelBufferRef pb = NULL;
|
|
256
|
+
NSDictionary *attrs = @{ (id)kCVPixelBufferIOSurfacePropertiesKey: @{} };
|
|
257
|
+
CVReturn r = CVPixelBufferCreate(kCFAllocatorDefault, w, h,
|
|
258
|
+
kCVPixelFormatType_32BGRA, (__bridge CFDictionaryRef)attrs, &pb);
|
|
259
|
+
if (r != kCVReturnSuccess || !pb) return NULL;
|
|
260
|
+
CVPixelBufferLockBaseAddress(pb, 0);
|
|
261
|
+
uint8_t *dst = (uint8_t *)CVPixelBufferGetBaseAddress(pb);
|
|
262
|
+
size_t dstBpr = CVPixelBufferGetBytesPerRow(pb);
|
|
263
|
+
size_t copyBpr = MIN((size_t)bpr, dstBpr);
|
|
264
|
+
for (uint32_t y = 0; y < h; y++) {
|
|
265
|
+
memcpy(dst + y * dstBpr, gShmPixels + y * bpr, copyBpr);
|
|
266
|
+
}
|
|
267
|
+
CVPixelBufferUnlockBaseAddress(pb, 0);
|
|
268
|
+
|
|
269
|
+
atomic_thread_fence(memory_order_acquire);
|
|
270
|
+
uint64_t seqB = gShmHeader->frameSeq;
|
|
271
|
+
if (!force && seqA != seqB) {
|
|
272
|
+
CVPixelBufferRelease(pb);
|
|
273
|
+
return NULL;
|
|
274
|
+
}
|
|
275
|
+
gLastSeenSeq = seqA;
|
|
276
|
+
return pb;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
- (CVPixelBufferRef)currentPixelBuffer CF_RETURNS_RETAINED {
|
|
280
|
+
CVPixelBufferRef pb = [self newPixelBufferFromShmForceFresh:YES];
|
|
281
|
+
if (!pb) pb = [self newPixelBufferFromImage];
|
|
282
|
+
return pb;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
- (NSData *)currentSnapshotJPEGAtQuality:(CGFloat)q {
|
|
286
|
+
CVPixelBufferRef pb = [self currentPixelBuffer];
|
|
287
|
+
if (!pb) return nil;
|
|
288
|
+
CIImage *ci = [CIImage imageWithCVPixelBuffer:pb];
|
|
289
|
+
if (SimCamShouldMirror(AVCaptureDevicePositionFront)) {
|
|
290
|
+
ci = [ci imageByApplyingOrientation:kCGImagePropertyOrientationUpMirrored];
|
|
291
|
+
}
|
|
292
|
+
static CIContext *ctx = nil; static dispatch_once_t once;
|
|
293
|
+
dispatch_once(&once, ^{ ctx = [CIContext contextWithOptions:nil]; });
|
|
294
|
+
CGImageRef cg = [ctx createCGImage:ci fromRect:ci.extent];
|
|
295
|
+
CVPixelBufferRelease(pb);
|
|
296
|
+
if (!cg) return nil;
|
|
297
|
+
UIImage *ui = [UIImage imageWithCGImage:cg];
|
|
298
|
+
NSData *data = UIImageJPEGRepresentation(ui, q);
|
|
299
|
+
CGImageRelease(cg);
|
|
300
|
+
return data;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
- (CVPixelBufferRef)newPixelBufferFromImage CF_RETURNS_RETAINED {
|
|
304
|
+
if (!gSourceCGImage) return NULL;
|
|
305
|
+
CVPixelBufferRef pb = NULL;
|
|
306
|
+
NSDictionary *attrs = @{ (id)kCVPixelBufferIOSurfacePropertiesKey: @{} };
|
|
307
|
+
CVReturn r = CVPixelBufferCreate(kCFAllocatorDefault, kFrameWidth, kFrameHeight,
|
|
308
|
+
kCVPixelFormatType_32BGRA, (__bridge CFDictionaryRef)attrs, &pb);
|
|
309
|
+
if (r != kCVReturnSuccess || !pb) return NULL;
|
|
310
|
+
CVPixelBufferLockBaseAddress(pb, 0);
|
|
311
|
+
void *base = CVPixelBufferGetBaseAddress(pb);
|
|
312
|
+
size_t bpr = CVPixelBufferGetBytesPerRow(pb);
|
|
313
|
+
CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
|
|
314
|
+
CGContextRef ctx = CGBitmapContextCreate(base, kFrameWidth, kFrameHeight, 8, bpr, cs,
|
|
315
|
+
kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Little);
|
|
316
|
+
CGContextSetFillColorWithColor(ctx, [UIColor blackColor].CGColor);
|
|
317
|
+
CGContextFillRect(ctx, CGRectMake(0, 0, kFrameWidth, kFrameHeight));
|
|
318
|
+
size_t iw = CGImageGetWidth(gSourceCGImage), ih = CGImageGetHeight(gSourceCGImage);
|
|
319
|
+
double sx = (double)kFrameWidth / iw, sy = (double)kFrameHeight / ih;
|
|
320
|
+
double s = MAX(sx, sy);
|
|
321
|
+
double dw = iw * s, dh = ih * s;
|
|
322
|
+
CGRect dst = CGRectMake((kFrameWidth - dw)/2.0, (kFrameHeight - dh)/2.0, dw, dh);
|
|
323
|
+
CGContextDrawImage(ctx, dst, gSourceCGImage);
|
|
324
|
+
CGContextRelease(ctx);
|
|
325
|
+
CGColorSpaceRelease(cs);
|
|
326
|
+
CVPixelBufferUnlockBaseAddress(pb, 0);
|
|
327
|
+
return pb;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
- (CVPixelBufferRef)newPixelBufferNoSignal CF_RETURNS_RETAINED {
|
|
331
|
+
CVPixelBufferRef pb = NULL;
|
|
332
|
+
NSDictionary *attrs = @{ (id)kCVPixelBufferIOSurfacePropertiesKey: @{} };
|
|
333
|
+
CVReturn r = CVPixelBufferCreate(kCFAllocatorDefault, kFrameWidth, kFrameHeight,
|
|
334
|
+
kCVPixelFormatType_32BGRA, (__bridge CFDictionaryRef)attrs, &pb);
|
|
335
|
+
if (r != kCVReturnSuccess || !pb) return NULL;
|
|
336
|
+
CVPixelBufferLockBaseAddress(pb, 0);
|
|
337
|
+
uint8_t *base = (uint8_t *)CVPixelBufferGetBaseAddress(pb);
|
|
338
|
+
size_t bpr = CVPixelBufferGetBytesPerRow(pb);
|
|
339
|
+
for (size_t y = 0; y < kFrameHeight; y++) {
|
|
340
|
+
uint8_t *row = base + y * bpr;
|
|
341
|
+
for (size_t x = 0; x < kFrameWidth; x++) {
|
|
342
|
+
row[x * 4 + 0] = 0x18;
|
|
343
|
+
row[x * 4 + 1] = 0x18;
|
|
344
|
+
row[x * 4 + 2] = 0x18;
|
|
345
|
+
row[x * 4 + 3] = 0xFF;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
CVPixelBufferUnlockBaseAddress(pb, 0);
|
|
349
|
+
return pb;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
- (CMSampleBufferRef)newSampleBufferAtTime:(CMTime)pts CF_RETURNS_RETAINED {
|
|
353
|
+
CVPixelBufferRef pb = [self newPixelBufferFromShm];
|
|
354
|
+
if (!pb) pb = [self newPixelBufferFromImage];
|
|
355
|
+
if (pb) {
|
|
356
|
+
SimCamCacheFrame(pb);
|
|
357
|
+
} else {
|
|
358
|
+
pb = SimCamAcquireCachedPB();
|
|
359
|
+
}
|
|
360
|
+
if (!pb) {
|
|
361
|
+
static dispatch_once_t logOnce;
|
|
362
|
+
dispatch_once(&logOnce, ^{
|
|
363
|
+
simcam_log(@"no-signal fallback: shm=%@ frameSeq=%llu cache=empty",
|
|
364
|
+
gShmHeader ? @"attached" : @"unattached",
|
|
365
|
+
(unsigned long long)(gShmHeader ? gShmHeader->frameSeq : 0));
|
|
366
|
+
});
|
|
367
|
+
pb = [self newPixelBufferNoSignal];
|
|
368
|
+
}
|
|
369
|
+
if (!pb) return NULL;
|
|
370
|
+
|
|
371
|
+
CMVideoFormatDescriptionRef fd = NULL;
|
|
372
|
+
CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault, pb, &fd);
|
|
373
|
+
CMSampleTimingInfo timing = {
|
|
374
|
+
.duration = CMTimeMake(1, (int32_t)kFrameRate),
|
|
375
|
+
.presentationTimeStamp = pts,
|
|
376
|
+
.decodeTimeStamp = kCMTimeInvalid,
|
|
377
|
+
};
|
|
378
|
+
CMSampleBufferRef sb = NULL;
|
|
379
|
+
CMSampleBufferCreateForImageBuffer(kCFAllocatorDefault, pb, true, NULL, NULL, fd, &timing, &sb);
|
|
380
|
+
if (fd) CFRelease(fd);
|
|
381
|
+
CVPixelBufferRelease(pb);
|
|
382
|
+
return sb;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
- (void)startPumpingIfNeeded {
|
|
386
|
+
[_lock lock];
|
|
387
|
+
if (_timer) { [_lock unlock]; return; }
|
|
388
|
+
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _timerQueue);
|
|
389
|
+
uint64_t intervalNs = (uint64_t)(NSEC_PER_SEC / kFrameRate);
|
|
390
|
+
dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, 0), intervalNs, intervalNs / 10);
|
|
391
|
+
__weak __typeof(self) weakSelf = self;
|
|
392
|
+
__block int64_t frameIdx = 0;
|
|
393
|
+
__block uint8_t lastMirrorByte = SIMCAM_MIRROR_UNSET;
|
|
394
|
+
dispatch_source_set_event_handler(_timer, ^{
|
|
395
|
+
__strong __typeof(weakSelf) self = weakSelf; if (!self) return;
|
|
396
|
+
if (gShmHeader) {
|
|
397
|
+
uint8_t m = gShmHeader->mirrorMode;
|
|
398
|
+
if (m != lastMirrorByte) {
|
|
399
|
+
lastMirrorByte = m;
|
|
400
|
+
if (m != SIMCAM_MIRROR_UNSET) {
|
|
401
|
+
SimCamMirrorMode prev = SimCamGetMirrorMode();
|
|
402
|
+
SimCamMirrorMode next = prev;
|
|
403
|
+
if (m == SIMCAM_MIRROR_ON) next = SimCamMirrorForceOn;
|
|
404
|
+
else if (m == SIMCAM_MIRROR_OFF) next = SimCamMirrorForceOff;
|
|
405
|
+
else next = SimCamMirrorAuto;
|
|
406
|
+
if (prev != next) {
|
|
407
|
+
SimCamSetMirrorMode(next);
|
|
408
|
+
simcam_log(@"mirror mode → %d (from shm)", (int)next);
|
|
409
|
+
[self reapplyMirrorToLayers];
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
CMTime pts = CMTimeMake(frameIdx++, (int32_t)kFrameRate);
|
|
415
|
+
CMSampleBufferRef sb = [self newSampleBufferAtTime:pts];
|
|
416
|
+
if (!sb) return;
|
|
417
|
+
CVImageBufferRef pb = CMSampleBufferGetImageBuffer(sb);
|
|
418
|
+
if (gShmHeader || gLastFramePB) [self pushFrameToLayers:pb];
|
|
419
|
+
NSArray *snapshot;
|
|
420
|
+
[self->_lock lock]; snapshot = [self->_entries copy]; [self->_lock unlock];
|
|
421
|
+
BOOL anyDead = NO;
|
|
422
|
+
for (NSDictionary *e in snapshot) {
|
|
423
|
+
AVCaptureVideoDataOutput *out = e[@"out"];
|
|
424
|
+
SimCamWeakRef *ref = e[@"del"];
|
|
425
|
+
id<AVCaptureVideoDataOutputSampleBufferDelegate> del = ref.target;
|
|
426
|
+
dispatch_queue_t q = e[@"queue"];
|
|
427
|
+
if (!del) { anyDead = YES; continue; }
|
|
428
|
+
if (!out) continue;
|
|
429
|
+
CFRetain(sb);
|
|
430
|
+
__weak SimCamWeakRef *weakRef = ref;
|
|
431
|
+
dispatch_async(q, ^{
|
|
432
|
+
id<AVCaptureVideoDataOutputSampleBufferDelegate> d = weakRef.target;
|
|
433
|
+
if (d && [d respondsToSelector:@selector(captureOutput:didOutputSampleBuffer:fromConnection:)]) {
|
|
434
|
+
AVCaptureConnection *conn = SimCamFakeConnectionForOutput(out);
|
|
435
|
+
[d captureOutput:out didOutputSampleBuffer:sb fromConnection:conn];
|
|
436
|
+
}
|
|
437
|
+
CFRelease(sb);
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
if (anyDead) {
|
|
441
|
+
[self->_lock lock];
|
|
442
|
+
NSMutableIndexSet *idx = [NSMutableIndexSet new];
|
|
443
|
+
[self->_entries enumerateObjectsUsingBlock:^(NSDictionary *entry, NSUInteger i, BOOL *stop) {
|
|
444
|
+
SimCamWeakRef *r = entry[@"del"];
|
|
445
|
+
if (!r.target) [idx addIndex:i];
|
|
446
|
+
}];
|
|
447
|
+
if (idx.count) {
|
|
448
|
+
NSUInteger before = self->_entries.count;
|
|
449
|
+
[self->_entries removeObjectsAtIndexes:idx];
|
|
450
|
+
simcam_log(@"pump: pruned %lu dead delegate entr%@ (%lu→%lu)",
|
|
451
|
+
(unsigned long)idx.count, idx.count == 1 ? @"y" : @"ies",
|
|
452
|
+
(unsigned long)before, (unsigned long)self->_entries.count);
|
|
453
|
+
}
|
|
454
|
+
[self->_lock unlock];
|
|
455
|
+
}
|
|
456
|
+
CFRelease(sb);
|
|
457
|
+
});
|
|
458
|
+
dispatch_resume(_timer);
|
|
459
|
+
[_lock unlock];
|
|
460
|
+
simcam_log(@"started frame pump @ %.0f fps", kFrameRate);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
- (void)stopPumping {
|
|
464
|
+
[_lock lock];
|
|
465
|
+
if (_timer) { dispatch_source_cancel(_timer); _timer = NULL; }
|
|
466
|
+
[_lock unlock];
|
|
467
|
+
}
|
|
468
|
+
@end
|
|
469
|
+
|
|
470
|
+
#pragma mark - Source loaders
|
|
471
|
+
|
|
472
|
+
BOOL SimCamFrameSourceIsShmAttached(void) {
|
|
473
|
+
return gShmHeader != NULL;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
void SimCamFrameSourceLoadImage(void) {
|
|
477
|
+
const char *envPath = getenv("SIMCAM_IMAGE_PATH");
|
|
478
|
+
NSString *path = envPath ? [NSString stringWithUTF8String:envPath] : nil;
|
|
479
|
+
if (!path.length) {
|
|
480
|
+
simcam_log(@"SIMCAM_IMAGE_PATH not set — generating gradient placeholder");
|
|
481
|
+
UIGraphicsImageRenderer *r = [[UIGraphicsImageRenderer alloc]
|
|
482
|
+
initWithSize:CGSizeMake(kFrameWidth, kFrameHeight)];
|
|
483
|
+
gSourceImage = [r imageWithActions:^(UIGraphicsImageRendererContext *ctx) {
|
|
484
|
+
CGContextRef c = ctx.CGContext;
|
|
485
|
+
CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
|
|
486
|
+
CGFloat colors[] = {0.10,0.45,0.95,1.0, 0.95,0.20,0.55,1.0};
|
|
487
|
+
CGFloat locs[] = {0.0, 1.0};
|
|
488
|
+
CGGradientRef g = CGGradientCreateWithColorComponents(cs, colors, locs, 2);
|
|
489
|
+
CGContextDrawLinearGradient(c, g, CGPointZero,
|
|
490
|
+
CGPointMake(kFrameWidth, kFrameHeight), 0);
|
|
491
|
+
CGGradientRelease(g);
|
|
492
|
+
CGColorSpaceRelease(cs);
|
|
493
|
+
NSDictionary *attrs = @{
|
|
494
|
+
NSFontAttributeName: [UIFont boldSystemFontOfSize:96],
|
|
495
|
+
NSForegroundColorAttributeName: UIColor.whiteColor,
|
|
496
|
+
};
|
|
497
|
+
[@"serve-sim camera" drawAtPoint:CGPointMake(60, 60) withAttributes:attrs];
|
|
498
|
+
}];
|
|
499
|
+
} else {
|
|
500
|
+
gSourceImage = [UIImage imageWithContentsOfFile:path];
|
|
501
|
+
if (!gSourceImage) {
|
|
502
|
+
simcam_log(@"failed to load image at %@", path);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
simcam_log(@"loaded source image %@ (%.0fx%.0f)", path,
|
|
506
|
+
gSourceImage.size.width, gSourceImage.size.height);
|
|
507
|
+
}
|
|
508
|
+
if (gSourceImage.CGImage) {
|
|
509
|
+
gSourceCGImage = CGImageRetain(gSourceImage.CGImage);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
void SimCamFrameSourceOpenShmIfRequested(void) {
|
|
514
|
+
const char *shmName = getenv("SIMCAM_SHM_NAME");
|
|
515
|
+
if (!shmName || !*shmName) return;
|
|
516
|
+
int fd = shm_open(shmName, O_RDONLY, 0);
|
|
517
|
+
if (fd < 0) {
|
|
518
|
+
simcam_log(@"shm_open(%s) failed: %s", shmName, strerror(errno));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
struct stat st;
|
|
522
|
+
if (fstat(fd, &st) < 0 || st.st_size < (off_t)sizeof(SimCamShmHeader)) {
|
|
523
|
+
simcam_log(@"shm fstat failed or too small");
|
|
524
|
+
close(fd);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
void *map = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_SHARED, fd, 0);
|
|
528
|
+
close(fd);
|
|
529
|
+
if (map == MAP_FAILED) {
|
|
530
|
+
simcam_log(@"shm mmap failed: %s", strerror(errno));
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
SimCamShmHeader *hdr = (SimCamShmHeader *)map;
|
|
534
|
+
if (hdr->magic != SIMCAM_SHM_MAGIC) {
|
|
535
|
+
simcam_log(@"shm magic mismatch: 0x%x", hdr->magic);
|
|
536
|
+
munmap(map, (size_t)st.st_size);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
gShmHeader = hdr;
|
|
540
|
+
gShmPixels = (const uint8_t *)map + sizeof(SimCamShmHeader);
|
|
541
|
+
gShmTotalSize = (size_t)st.st_size;
|
|
542
|
+
kFrameWidth = hdr->width;
|
|
543
|
+
kFrameHeight = hdr->height;
|
|
544
|
+
simcam_log(@"shm \"%s\" attached (%ux%u, %llu bytes)",
|
|
545
|
+
shmName, hdr->width, hdr->height, (unsigned long long)st.st_size);
|
|
546
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#import "SimCamLog.h"
|
|
2
|
+
|
|
3
|
+
void simcam_log(NSString *fmt, ...) {
|
|
4
|
+
va_list args; va_start(args, fmt);
|
|
5
|
+
NSString *msg = [[NSString alloc] initWithFormat:fmt arguments:args];
|
|
6
|
+
va_end(args);
|
|
7
|
+
fprintf(stderr, "[SimCam] %s\n", msg.UTF8String);
|
|
8
|
+
NSLog(@"[SimCam] %@", msg);
|
|
9
|
+
}
|