serve-sim 0.1.49-beta.141.1 → 0.1.49

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.
@@ -2,6 +2,7 @@
2
2
  #import "SimCamFakes.h"
3
3
  #import "SimCamFrameSource.h"
4
4
  #import "SimCamLog.h"
5
+ #import "SimCamMovieOutput.h"
5
6
 
6
7
  #import <AVFoundation/AVFoundation.h>
7
8
  #import <CoreImage/CoreImage.h>
@@ -14,6 +15,7 @@
14
15
  #include <stdatomic.h>
15
16
  #include <execinfo.h>
16
17
  #include <dlfcn.h>
18
+ #include <mach-o/getsect.h>
17
19
  #include <string.h>
18
20
 
19
21
  #pragma mark - Swizzling helpers
@@ -35,7 +37,7 @@ static BOOL SwizzleClassMethod(Class cls, SEL orig, SEL swiz) {
35
37
  method_exchangeImplementations(o, s);
36
38
  return YES;
37
39
  }
38
- static BOOL SwizzleInstanceMethod(Class cls, SEL orig, SEL swiz) {
40
+ BOOL SwizzleInstanceMethod(Class cls, SEL orig, SEL swiz) {
39
41
  Method o = class_getInstanceMethod(cls, orig);
40
42
  Method s = class_getInstanceMethod(cls, swiz);
41
43
  if (!o) {
@@ -120,6 +122,13 @@ static BOOL SwizzleInstanceMethod(Class cls, SEL orig, SEL swiz) {
120
122
  }
121
123
  return [self simcam_defaultDeviceWithDeviceType:t mediaType:m position:p];
122
124
  }
125
+ + (AVCaptureDevice *)simcam_defaultDeviceWithMediaType:(AVMediaType)m {
126
+ if ([m isEqualToString:AVMediaTypeVideo]) {
127
+ simcam_log(@"defaultDeviceWithMediaType: video → fake back camera");
128
+ return SimCamFakeDeviceForPosition(AVCaptureDevicePositionBack);
129
+ }
130
+ return [self simcam_defaultDeviceWithMediaType:m];
131
+ }
123
132
  + (NSArray<AVCaptureDevice *> *)simcam_devicesWithMediaType:(AVMediaType)m {
124
133
  if ([m isEqualToString:AVMediaTypeVideo]) {
125
134
  return @[
@@ -197,12 +206,53 @@ static BOOL SwizzleInstanceMethod(Class cls, SEL orig, SEL swiz) {
197
206
 
198
207
  #pragma mark - AVCaptureSession swizzles
199
208
 
209
+ // AVFoundation reads a session's and an output's own lists when it rebuilds
210
+ // the native graph (a preview layer attached or detached, a configuration
211
+ // committed), and the fake objects have none of the private state it reads
212
+ // there. So a getter whose caller is AVFoundation returns the native list
213
+ // only, and the app sees the tracked objects too.
214
+ //
215
+ // The caller is told apart by its return address: AVFoundation's call when
216
+ // it falls in the __TEXT of the image that defines AVCaptureSession
217
+ // (AVFCapture on current iOS), resolved once at install. This assumes every
218
+ // AVFoundation-internal reader of these lists lives in that image and calls
219
+ // the getter directly. A reader in another image (AVFCore, CMCapture, a Swift
220
+ // overlay), or one that reaches the getter through a tail call, a trampoline
221
+ // or key-value coding, gets the fakes and crashes in their missing private
222
+ // state as before. The install log names the image and its range, and a
223
+ // getter logs each time it answers AVFoundation, so such a crash shows which
224
+ // reads were told apart before it.
225
+ static uintptr_t gSimCamCaptureTextStart;
226
+ static uintptr_t gSimCamCaptureTextSize;
227
+
228
+ static void SimCamRecordCaptureImage(void) {
229
+ Dl_info info;
230
+ if (!dladdr((__bridge const void *)[AVCaptureSession class], &info)) {
231
+ simcam_log(@"caller check: no image for AVCaptureSession; AVFoundation will read the fakes");
232
+ return;
233
+ }
234
+ unsigned long size = 0;
235
+ uint8_t *text = getsegmentdata((const struct mach_header_64 *)info.dli_fbase, "__TEXT", &size);
236
+ gSimCamCaptureTextStart = (uintptr_t)text;
237
+ gSimCamCaptureTextSize = size;
238
+ simcam_log(@"caller check: AVFoundation's reads come from %s __TEXT [%p, +0x%lx)",
239
+ info.dli_fname, text, size);
240
+ }
241
+ static BOOL SimCamCalledFromCaptureImage(const void *returnAddress, id receiver, SEL selector) {
242
+ if ((uintptr_t)returnAddress - gSimCamCaptureTextStart >= gSimCamCaptureTextSize) return NO;
243
+ simcam_log(@"caller check: -[%@ %@] answered AVFoundation with the native list only",
244
+ NSStringFromClass([receiver class]), NSStringFromSelector(selector));
245
+ return YES;
246
+ }
247
+
200
248
  static char kSimCamSessionRunningKey;
201
249
  static char kSimCamSessionInputsKey;
202
250
  static char kSimCamSessionOutputsKey;
203
251
  static char kSimCamSessionConnectionsKey;
252
+ static char kSimCamSessionSentinelKey;
204
253
  static char kSimCamOutputAttachedToFakeSessionKey;
205
254
  static char kSimCamOutputSessionKey;
255
+ static char kSimCamOutputOwnerKey;
206
256
  static char kSimCamMaxPhotoDimensionsKey;
207
257
 
208
258
  static NSMutableArray *SimCamSessionTrackedInputs(AVCaptureSession *s) {
@@ -213,11 +263,62 @@ static NSMutableArray *SimCamSessionTrackedInputs(AVCaptureSession *s) {
213
263
  }
214
264
  return arr;
215
265
  }
266
+ // AVFoundation keeps an output in one session at a time: canAddOutput: says
267
+ // NO while another session holds it, and adding it throws. So one session's
268
+ // teardown, stop or release never reaches another's recording. Sessions may
269
+ // be configured on different queues, so a change of owner holds the output's
270
+ // lock.
271
+ static AVCaptureSession *SimCamOutputOwner(AVCaptureOutput *output) {
272
+ return ((SimCamWeakRef *)objc_getAssociatedObject(output, &kSimCamOutputOwnerKey)).target;
273
+ }
274
+ static void SimCamTakeOutput(AVCaptureSession *s, AVCaptureOutput *output, SEL adding) {
275
+ @synchronized (output) {
276
+ AVCaptureSession *owner = SimCamOutputOwner(output);
277
+ if (owner == s) return;
278
+ if (owner) {
279
+ [NSException raise:NSInvalidArgumentException
280
+ format:@"*** -[AVCaptureSession %@] An AVCaptureOutput instance may not be added to more than one session",
281
+ NSStringFromSelector(adding)];
282
+ }
283
+ SimCamWeakRef *ref = [SimCamWeakRef new];
284
+ ref.target = s;
285
+ objc_setAssociatedObject(output, &kSimCamOutputOwnerKey, ref, OBJC_ASSOCIATION_RETAIN);
286
+ }
287
+ }
288
+ // For a session that stopped or went away: the fake recordings of the outputs
289
+ // it still owns end as a stop. A deallocating session reads as nil through an
290
+ // output's weak owner, so the sentinel passes nil, and an output another
291
+ // session took meanwhile keeps its recording.
292
+ static void SimCamStopOutputRecordings(NSArray<AVCaptureOutput *> *outputs, AVCaptureSession *session) {
293
+ for (AVCaptureOutput *output in [outputs copy]) {
294
+ @synchronized (output) {
295
+ if (SimCamOutputOwner(output) == session) SimCamFakeMovieOutputSessionStopped(output);
296
+ }
297
+ }
298
+ }
299
+
300
+ // Associated with a session beside its tracked outputs, so it goes when the
301
+ // session does: a session the app lets go without stopping it ends its fake
302
+ // recordings, as AVFoundation finishes the file when the session goes away.
303
+ // It holds the outputs itself, since the session's other associated objects
304
+ // may be released first, and never messages the session.
305
+ @interface SimCamSessionSentinel : NSObject
306
+ @property (nonatomic, strong) NSMutableArray<AVCaptureOutput *> *outputs;
307
+ @end
308
+ @implementation SimCamSessionSentinel
309
+ - (void)dealloc {
310
+ SimCamStopOutputRecordings(_outputs, nil);
311
+ }
312
+ @end
313
+
216
314
  static NSMutableArray *SimCamSessionTrackedOutputs(AVCaptureSession *s) {
217
315
  NSMutableArray *arr = objc_getAssociatedObject(s, &kSimCamSessionOutputsKey);
218
316
  if (!arr) {
219
317
  arr = [NSMutableArray new];
220
318
  objc_setAssociatedObject(s, &kSimCamSessionOutputsKey, arr, OBJC_ASSOCIATION_RETAIN);
319
+ SimCamSessionSentinel *sentinel = [SimCamSessionSentinel new];
320
+ sentinel.outputs = arr;
321
+ objc_setAssociatedObject(s, &kSimCamSessionSentinelKey, sentinel, OBJC_ASSOCIATION_RETAIN);
221
322
  }
222
323
  return arr;
223
324
  }
@@ -242,6 +343,7 @@ static AVCaptureInput *SimCamFirstFakeInputForSession(AVCaptureSession *s) {
242
343
  // look disconnected during client-side session configuration checks.
243
344
  static void SimCamUnmarkOutputAttachedToFakeSession(AVCaptureOutput *output) {
244
345
  if (!output) return;
346
+ SimCamFakeMovieOutputLeftSession(output);
245
347
  objc_setAssociatedObject(output, &kSimCamOutputAttachedToFakeSessionKey, nil, OBJC_ASSOCIATION_RETAIN);
246
348
  objc_setAssociatedObject(output, &kSimCamOutputSessionKey, nil, OBJC_ASSOCIATION_RETAIN);
247
349
  objc_setAssociatedObject(output, &kSimCamMaxPhotoDimensionsKey, nil, OBJC_ASSOCIATION_RETAIN);
@@ -260,7 +362,7 @@ static void SimCamMarkOutputAttachedToFakeSession(AVCaptureSession *s, AVCapture
260
362
  objc_setAssociatedObject(output, &kSimCamOutputSessionKey, sessionRef, OBJC_ASSOCIATION_RETAIN);
261
363
  SimCamSetOutputInput(output, input);
262
364
  }
263
- static BOOL SimCamOutputAttachedToFakeSession(AVCaptureOutput *output) {
365
+ BOOL SimCamOutputAttachedToFakeSession(AVCaptureOutput *output) {
264
366
  if (!output) return NO;
265
367
  return [objc_getAssociatedObject(output, &kSimCamOutputAttachedToFakeSessionKey) boolValue];
266
368
  }
@@ -269,7 +371,7 @@ static void SimCamRefreshAttachedOutputInputsForSession(AVCaptureSession *s) {
269
371
  SimCamMarkOutputAttachedToFakeSession(s, output);
270
372
  }
271
373
  }
272
- static AVCaptureSession *SimCamFakeSessionForOutput(AVCaptureOutput *output) {
374
+ AVCaptureSession *SimCamFakeSessionForOutput(AVCaptureOutput *output) {
273
375
  SimCamWeakRef *ref = objc_getAssociatedObject(output, &kSimCamOutputSessionKey);
274
376
  return ref.target;
275
377
  }
@@ -279,9 +381,22 @@ static BOOL SimCamConnectionUsesInput(AVCaptureConnection *connection, AVCapture
279
381
  }
280
382
  return NO;
281
383
  }
282
- static BOOL SimCamConnectionUsesFakeInput(AVCaptureConnection *connection) {
384
+ // A connection the session keeps itself: one from a fake input, or from an
385
+ // input the session tracks instead of adding natively (the microphone beside
386
+ // a fake camera).
387
+ static BOOL SimCamConnectionUsesTrackedInput(
388
+ AVCaptureConnection *connection,
389
+ AVCaptureSession *session
390
+ ) {
391
+ NSArray<AVCaptureInput *> *inputs = SimCamSessionTrackedInputs(session);
392
+ for (AVCaptureInputPort *port in connection.inputPorts) {
393
+ if (SimCamIsFakeInput(port.input) || [inputs containsObject:port.input]) return YES;
394
+ }
395
+ return NO;
396
+ }
397
+ BOOL SimCamConnectionCarries(AVCaptureConnection *connection, AVMediaType mediaType) {
283
398
  for (AVCaptureInputPort *port in connection.inputPorts) {
284
- if (SimCamIsFakeInput(port.input)) return YES;
399
+ if ([port.mediaType isEqualToString:mediaType]) return YES;
285
400
  }
286
401
  return NO;
287
402
  }
@@ -298,7 +413,7 @@ static BOOL SimCamConnectionBelongsToSession(
298
413
  AVCaptureOutput *output = connection.output;
299
414
  return !output || [SimCamSessionTrackedOutputs(session) containsObject:output];
300
415
  }
301
- static NSArray<AVCaptureConnection *> *SimCamConnectionsForOutput(AVCaptureOutput *output) {
416
+ NSArray<AVCaptureConnection *> *SimCamConnectionsForOutput(AVCaptureOutput *output) {
302
417
  AVCaptureSession *session = SimCamFakeSessionForOutput(output);
303
418
  if (!session) return @[];
304
419
  NSMutableArray *result = [NSMutableArray new];
@@ -332,60 +447,59 @@ static void SimCamAddAutomaticConnections(AVCaptureSession *session) {
332
447
  }
333
448
  }
334
449
 
450
+ // startRunning never reaches AVFoundation, so no session in the injected
451
+ // process runs a native graph, and every input is tracked rather than added
452
+ // natively. A native input, such as expo-camera's microphone, would make the
453
+ // session build its graph, which reads the fake connections' private state
454
+ // and crashes.
455
+ static void SimCamTrackInput(AVCaptureSession *s, AVCaptureInput *input,
456
+ BOOL automaticConnections, NSString *via) {
457
+ NSMutableArray *tracked = SimCamSessionTrackedInputs(s);
458
+ if (![tracked containsObject:input]) [tracked addObject:input];
459
+ if (!SimCamIsFakeInput(input)) {
460
+ simcam_log(@"%@: %@ — tracked (count=%lu), skipping native add",
461
+ via, NSStringFromClass([input class]), (unsigned long)tracked.count);
462
+ return;
463
+ }
464
+ AVCaptureDevicePosition p = SimCamPositionOf(input);
465
+ SimCamSetPosition(s, p);
466
+ SimCamMarkCameraInUse();
467
+ SimCamMarkSessionUsingFakeCamera(s, YES);
468
+ SimCamRefreshAttachedOutputInputsForSession(s);
469
+ if (automaticConnections) SimCamAddAutomaticConnections(s);
470
+ simcam_log(@"%@: fake input (%@) — tracked (count=%lu), skipping native add",
471
+ via, p == AVCaptureDevicePositionBack ? @"back" : @"front",
472
+ (unsigned long)tracked.count);
473
+ }
474
+
335
475
  @interface AVCaptureSession (SimCam)
336
476
  @end
337
477
  @implementation AVCaptureSession (SimCam)
338
478
  - (void)simcam_addInput:(AVCaptureInput *)input {
339
- if (SimCamIsFakeInput(input)) {
340
- AVCaptureDevicePosition p = SimCamPositionOf(input);
341
- SimCamSetPosition(self, p);
342
- SimCamMarkCameraInUse();
343
- SimCamMarkSessionUsingFakeCamera(self, YES);
344
- NSMutableArray *tracked = SimCamSessionTrackedInputs(self);
345
- if (![tracked containsObject:input]) [tracked addObject:input];
346
- SimCamRefreshAttachedOutputInputsForSession(self);
347
- SimCamAddAutomaticConnections(self);
348
- simcam_log(@"addInput: fake input (%@) — tracked (count=%lu), skipping native add",
349
- p == AVCaptureDevicePositionBack ? @"back" : @"front",
350
- (unsigned long)tracked.count);
351
- return;
352
- }
353
- [self simcam_addInput:input];
479
+ SimCamTrackInput(self, input, YES, @"addInput");
354
480
  }
355
481
  - (BOOL)simcam_canAddInput:(AVCaptureInput *)input {
356
482
  if (SimCamIsFakeInput(input)) return YES;
357
483
  return [self simcam_canAddInput:input];
358
484
  }
359
485
  - (void)simcam_addInputWithNoConnections:(AVCaptureInput *)input {
360
- if (SimCamIsFakeInput(input)) {
361
- AVCaptureDevicePosition p = SimCamPositionOf(input);
362
- SimCamSetPosition(self, p);
363
- SimCamMarkCameraInUse();
364
- SimCamMarkSessionUsingFakeCamera(self, YES);
365
- NSMutableArray *tracked = SimCamSessionTrackedInputs(self);
366
- if (![tracked containsObject:input]) [tracked addObject:input];
367
- SimCamRefreshAttachedOutputInputsForSession(self);
368
- simcam_log(@"addInputWithNoConnections: fake input (%@) — tracked (count=%lu), skipping native add",
369
- p == AVCaptureDevicePositionBack ? @"back" : @"front",
370
- (unsigned long)tracked.count);
371
- return;
372
- }
373
- [self simcam_addInputWithNoConnections:input];
486
+ SimCamTrackInput(self, input, NO, @"addInputWithNoConnections");
374
487
  }
375
488
  - (void)simcam_removeInput:(AVCaptureInput *)input {
376
- if (SimCamIsFakeInput(input)) {
377
- NSMutableArray *tracked = SimCamSessionTrackedInputs(self);
378
- SimCamRemoveConnectionsForInput(self, input);
379
- [tracked removeObject:input];
380
- SimCamRefreshAttachedOutputInputsForSession(self);
381
- if (tracked.count == 0) SimCamMarkSessionUsingFakeCamera(self, NO);
382
- simcam_log(@"removeInput: fake input — untracked (count=%lu)",
383
- (unsigned long)tracked.count);
489
+ NSMutableArray *tracked = SimCamSessionTrackedInputs(self);
490
+ if (![tracked containsObject:input]) {
491
+ [self simcam_removeInput:input];
384
492
  return;
385
493
  }
386
- [self simcam_removeInput:input];
494
+ SimCamRemoveConnectionsForInput(self, input);
495
+ [tracked removeObject:input];
496
+ SimCamRefreshAttachedOutputInputsForSession(self);
497
+ if (!SimCamFirstFakeInputForSession(self)) SimCamMarkSessionUsingFakeCamera(self, NO);
498
+ simcam_log(@"removeInput: %@ — untracked (count=%lu)",
499
+ NSStringFromClass([input class]), (unsigned long)tracked.count);
387
500
  }
388
501
  - (void)simcam_addOutput:(AVCaptureOutput *)output {
502
+ SimCamTakeOutput(self, output, _cmd);
389
503
  SimCamSetPosition(output, SimCamPositionOf(self));
390
504
  SimCamMarkCameraInUse();
391
505
  NSMutableArray *tracked = SimCamSessionTrackedOutputs(self);
@@ -399,8 +513,12 @@ static void SimCamAddAutomaticConnections(AVCaptureSession *session) {
399
513
  (unsigned long)tracked.count,
400
514
  (int)SimCamPositionOf(self));
401
515
  }
402
- - (BOOL)simcam_canAddOutput:(AVCaptureOutput *)output { return YES; }
516
+ - (BOOL)simcam_canAddOutput:(AVCaptureOutput *)output {
517
+ AVCaptureSession *owner = SimCamOutputOwner(output);
518
+ return !owner || owner == self;
519
+ }
403
520
  - (void)simcam_addOutputWithNoConnections:(AVCaptureOutput *)output {
521
+ SimCamTakeOutput(self, output, _cmd);
404
522
  SimCamSetPosition(output, SimCamPositionOf(self));
405
523
  SimCamMarkCameraInUse();
406
524
  NSMutableArray *tracked = SimCamSessionTrackedOutputs(self);
@@ -412,8 +530,14 @@ static void SimCamAddAutomaticConnections(AVCaptureSession *session) {
412
530
  (int)SimCamPositionOf(self));
413
531
  }
414
532
  - (void)simcam_removeOutput:(AVCaptureOutput *)output {
415
- SimCamRemoveConnectionsForOutput(self, output);
416
- SimCamUnmarkOutputAttachedToFakeSession(output);
533
+ // The owner goes last, so no other session takes the output until it is
534
+ // unmarked here.
535
+ @synchronized (output) {
536
+ if (SimCamOutputOwner(output) != self) return;
537
+ SimCamRemoveConnectionsForOutput(self, output);
538
+ SimCamUnmarkOutputAttachedToFakeSession(output);
539
+ objc_setAssociatedObject(output, &kSimCamOutputOwnerKey, nil, OBJC_ASSOCIATION_RETAIN);
540
+ }
417
541
  NSMutableArray *tracked = SimCamSessionTrackedOutputs(self);
418
542
  [tracked removeObject:output];
419
543
  simcam_log(@"removeOutput: %@ — untracked (count=%lu)",
@@ -427,18 +551,18 @@ static void SimCamAddAutomaticConnections(AVCaptureSession *session) {
427
551
  ((NSArray *)objc_getAssociatedObject(self, &kSimCamSessionInputsKey)).count;
428
552
  NSUInteger outCount =
429
553
  ((NSArray *)objc_getAssociatedObject(self, &kSimCamSessionOutputsKey)).count;
430
- simcam_log(@"commitConfiguration intercepted (session=%p, fakeInputs=%lu, fakeOutputs=%lu)",
554
+ simcam_log(@"commitConfiguration intercepted (session=%p, trackedInputs=%lu, trackedOutputs=%lu)",
431
555
  self, (unsigned long)inCount, (unsigned long)outCount);
432
556
  }
433
557
  - (BOOL)simcam_canAddConnection:(AVCaptureConnection *)connection {
434
- if (SimCamConnectionUsesFakeInput(connection)) {
558
+ if (SimCamConnectionUsesTrackedInput(connection, self)) {
435
559
  return SimCamConnectionBelongsToSession(connection, self) &&
436
560
  ![SimCamSessionTrackedConnections(self) containsObject:connection];
437
561
  }
438
562
  return [self simcam_canAddConnection:connection];
439
563
  }
440
564
  - (void)simcam_addConnection:(AVCaptureConnection *)c {
441
- if (!SimCamConnectionUsesFakeInput(c)) {
565
+ if (!SimCamConnectionUsesTrackedInput(c, self)) {
442
566
  [self simcam_addConnection:c];
443
567
  return;
444
568
  }
@@ -448,19 +572,20 @@ static void SimCamAddAutomaticConnections(AVCaptureSession *session) {
448
572
  }
449
573
  AVCaptureOutput *output = c.output;
450
574
  SimCamRecordConnection(self, c);
451
- simcam_log(@"addConnection: fake input (session=%p, conn=%p, output=%p)", self, c, output);
575
+ simcam_log(@"addConnection: tracked input (session=%p, conn=%p, output=%p)", self, c, output);
452
576
  }
453
577
  - (void)simcam_removeConnection:(AVCaptureConnection *)c {
454
- if (!SimCamConnectionUsesFakeInput(c)) {
578
+ if (!SimCamConnectionUsesTrackedInput(c, self)) {
455
579
  [self simcam_removeConnection:c];
456
580
  return;
457
581
  }
458
582
  [SimCamSessionTrackedConnections(self) removeObject:c];
459
- simcam_log(@"removeConnection: fake input (session=%p, conn=%p, output=%p)", self, c, c.output);
583
+ simcam_log(@"removeConnection: tracked input (session=%p, conn=%p, output=%p)", self, c, c.output);
460
584
  }
461
585
  - (NSArray<AVCaptureInput *> *)simcam_inputs {
462
- NSMutableArray *tracked = objc_getAssociatedObject(self, &kSimCamSessionInputsKey);
463
586
  NSArray *native = [self simcam_inputs];
587
+ if (SimCamCalledFromCaptureImage(__builtin_return_address(0), self, _cmd)) return native;
588
+ NSMutableArray *tracked = objc_getAssociatedObject(self, &kSimCamSessionInputsKey);
464
589
  if (tracked.count == 0) return native ?: @[];
465
590
  if (native.count == 0) return [tracked copy];
466
591
  NSMutableArray *merged = [tracked mutableCopy];
@@ -470,8 +595,9 @@ static void SimCamAddAutomaticConnections(AVCaptureSession *session) {
470
595
  return [merged copy];
471
596
  }
472
597
  - (NSArray<AVCaptureOutput *> *)simcam_outputs {
473
- NSMutableArray *tracked = objc_getAssociatedObject(self, &kSimCamSessionOutputsKey);
474
598
  NSArray *native = [self simcam_outputs];
599
+ if (SimCamCalledFromCaptureImage(__builtin_return_address(0), self, _cmd)) return native;
600
+ NSMutableArray *tracked = objc_getAssociatedObject(self, &kSimCamSessionOutputsKey);
475
601
  if (tracked.count == 0) return native ?: @[];
476
602
  if (native.count == 0) return [tracked copy];
477
603
  NSMutableArray *merged = [tracked mutableCopy];
@@ -481,8 +607,9 @@ static void SimCamAddAutomaticConnections(AVCaptureSession *session) {
481
607
  return [merged copy];
482
608
  }
483
609
  - (NSArray<AVCaptureConnection *> *)simcam_connections {
484
- NSMutableArray *tracked = objc_getAssociatedObject(self, &kSimCamSessionConnectionsKey);
485
610
  NSArray *native = [self simcam_connections];
611
+ if (SimCamCalledFromCaptureImage(__builtin_return_address(0), self, _cmd)) return native;
612
+ NSMutableArray *tracked = objc_getAssociatedObject(self, &kSimCamSessionConnectionsKey);
486
613
  if (tracked.count == 0) return native ?: @[];
487
614
  NSMutableArray *merged = [tracked mutableCopy];
488
615
  for (AVCaptureConnection *n in native) {
@@ -497,7 +624,7 @@ static void SimCamAddAutomaticConnections(AVCaptureSession *session) {
497
624
  ((NSArray *)objc_getAssociatedObject(self, &kSimCamSessionInputsKey)).count;
498
625
  NSUInteger outCount =
499
626
  ((NSArray *)objc_getAssociatedObject(self, &kSimCamSessionOutputsKey)).count;
500
- simcam_log(@"startRunning intercepted (fake inputs=%lu outputs=%lu)",
627
+ simcam_log(@"startRunning intercepted (tracked inputs=%lu outputs=%lu)",
501
628
  (unsigned long)inCount, (unsigned long)outCount);
502
629
  [[SimCamRegistry shared] startPumpingIfNeeded];
503
630
  [self willChangeValueForKey:@"running"];
@@ -511,6 +638,7 @@ static void SimCamAddAutomaticConnections(AVCaptureSession *session) {
511
638
  }
512
639
  - (void)simcam_stopRunning {
513
640
  objc_setAssociatedObject(self, &kSimCamSessionRunningKey, @NO, OBJC_ASSOCIATION_RETAIN);
641
+ SimCamStopOutputRecordings(objc_getAssociatedObject(self, &kSimCamSessionOutputsKey), self);
514
642
  simcam_log(@"stopRunning intercepted");
515
643
  [self willChangeValueForKey:@"running"];
516
644
  [self didChangeValueForKey:@"running"];
@@ -547,15 +675,22 @@ static void SimCamAddAutomaticConnections(AVCaptureSession *session) {
547
675
 
548
676
  #pragma mark - AVCaptureVideoPreviewLayer swizzle
549
677
 
678
+ static void SimCamRegisterPreviewLayer(AVCaptureVideoPreviewLayer *layer, AVCaptureSession *session) {
679
+ SimCamSetPosition(layer, SimCamPositionOf(session));
680
+ SimCamMarkCameraInUse();
681
+ [[SimCamRegistry shared] addPreviewLayer:layer];
682
+ }
683
+
550
684
  @interface AVCaptureVideoPreviewLayer (SimCam)
551
685
  @end
552
686
  @implementation AVCaptureVideoPreviewLayer (SimCam)
553
687
  - (void)simcam_setSession:(AVCaptureSession *)session {
554
688
  [self simcam_setSession:session];
555
- AVCaptureDevicePosition p = SimCamPositionOf(session);
556
- SimCamSetPosition(self, p);
557
- SimCamMarkCameraInUse();
558
- [[SimCamRegistry shared] addPreviewLayer:self];
689
+ SimCamRegisterPreviewLayer(self, session);
690
+ }
691
+ - (void)simcam_setSessionWithNoConnection:(AVCaptureSession *)session {
692
+ [self simcam_setSessionWithNoConnection:session];
693
+ SimCamRegisterPreviewLayer(self, session);
559
694
  }
560
695
  @end
561
696
 
@@ -581,16 +716,19 @@ static void SimCamAddAutomaticConnections(AVCaptureSession *session) {
581
716
  @implementation AVCaptureOutput (SimCamConn)
582
717
  - (AVCaptureConnection *)simcam_connectionWithMediaType:(AVMediaType)mediaType {
583
718
  AVCaptureConnection *real = [self simcam_connectionWithMediaType:mediaType];
584
- if (real) return real;
719
+ if (real || SimCamCalledFromCaptureImage(__builtin_return_address(0), self, _cmd)) return real;
585
720
  if (!SimCamOutputAttachedToFakeSession(self)) return nil;
586
- if (![mediaType isEqualToString:AVMediaTypeVideo]) return nil;
587
- AVCaptureConnection *connection = SimCamConnectionsForOutput(self).firstObject;
721
+ AVCaptureConnection *connection = nil;
722
+ for (AVCaptureConnection *candidate in SimCamConnectionsForOutput(self)) {
723
+ if (SimCamConnectionCarries(candidate, mediaType)) { connection = candidate; break; }
724
+ }
588
725
  simcam_log(@"connectionWithMediaType:%@ → tracked %p for %@ %p",
589
726
  mediaType, connection, NSStringFromClass([self class]), self);
590
727
  return connection;
591
728
  }
592
729
  - (NSArray<AVCaptureConnection *> *)simcam_connections {
593
730
  NSArray *real = [self simcam_connections];
731
+ if (SimCamCalledFromCaptureImage(__builtin_return_address(0), self, _cmd)) return real;
594
732
  if (!SimCamOutputAttachedToFakeSession(self)) return real ?: @[];
595
733
  NSArray *tracked = SimCamConnectionsForOutput(self);
596
734
  if (tracked.count == 0) return real ?: @[];
@@ -1071,10 +1209,14 @@ static void InstallCoreMotionSwizzles(void) {
1071
1209
  static void SimCamInstallPickerSwizzles(void); // defined below
1072
1210
 
1073
1211
  void SimCamInstallSwizzles(void) {
1212
+ SimCamRecordCaptureImage();
1074
1213
  Class dev = [AVCaptureDevice class];
1075
1214
  SwizzleClassMethod(dev,
1076
1215
  @selector(defaultDeviceWithDeviceType:mediaType:position:),
1077
1216
  @selector(simcam_defaultDeviceWithDeviceType:mediaType:position:));
1217
+ SwizzleClassMethod(dev,
1218
+ @selector(defaultDeviceWithMediaType:),
1219
+ @selector(simcam_defaultDeviceWithMediaType:));
1078
1220
  SwizzleClassMethod(dev,
1079
1221
  @selector(devicesWithMediaType:),
1080
1222
  @selector(simcam_devicesWithMediaType:));
@@ -1166,6 +1308,9 @@ void SimCamInstallSwizzles(void) {
1166
1308
 
1167
1309
  Class pl = [AVCaptureVideoPreviewLayer class];
1168
1310
  SwizzleInstanceMethod(pl, @selector(setSession:), @selector(simcam_setSession:));
1311
+ SwizzleInstanceMethod(pl,
1312
+ @selector(setSessionWithNoConnection:),
1313
+ @selector(simcam_setSessionWithNoConnection:));
1169
1314
 
1170
1315
  Class fmtClass = [AVCaptureDeviceFormat class];
1171
1316
  SEL figFmtSel = NSSelectorFromString(@"figCaptureSourceVideoFormat");
@@ -1201,6 +1346,8 @@ void SimCamInstallSwizzles(void) {
1201
1346
  @selector(capturePhotoWithSettings:delegate:),
1202
1347
  @selector(simcam_capturePhotoWithSettings:delegate:));
1203
1348
 
1349
+ SimCamInstallMovieOutputSwizzles();
1350
+
1204
1351
  Class data = [NSData class];
1205
1352
  SwizzleInstanceMethod(data,
1206
1353
  @selector(writeToURL:options:error:),
@@ -37,6 +37,8 @@ xcrun --sdk iphonesimulator clang \
37
37
  "$HERE/SimCamLog.m" \
38
38
  "$HERE/SimCamFakes.m" \
39
39
  "$HERE/SimCamFrameSource.m" \
40
+ "$HERE/SimCamMovieOutput.m" \
41
+ "$HERE/SimCamMovieRecorder.m" \
40
42
  "$HERE/SimCamSwizzles.m"
41
43
 
42
44
  echo "Built: $DYLIB"
@@ -1,5 +1,6 @@
1
1
  import Foundation
2
2
  import Darwin
3
+ import SimNativeSupport
3
4
 
4
5
  /// A lazily started guest service. Calls are serialized by HIDInjector's actor;
5
6
  /// replies acknowledge dispatch, so a failed command never switches capture.
@@ -34,7 +35,7 @@ final class DuoHIDBridge {
34
35
  let child = Process()
35
36
  let stdin = Pipe(), stdout = Pipe()
36
37
  child.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun")
37
- child.arguments = ["simctl", "spawn", udid, executable]
38
+ child.arguments = DeviceSet.simctlArguments(["spawn", udid, executable])
38
39
  child.standardInput = stdin
39
40
  child.standardOutput = stdout
40
41
  child.standardError = FileHandle.standardError
@@ -592,9 +592,17 @@ actor FrameCapture {
592
592
  let sharedSel = NSSelectorFromString("sharedServiceContextForDeveloperDir:error:")
593
593
  guard let context = contextClass.perform(sharedSel, with: developerDir, with: nil)?
594
594
  .takeUnretainedValue() as? NSObject else { return nil }
595
- let deviceSetSel = NSSelectorFromString("defaultDeviceSetWithError:")
596
- guard let deviceSet = context.perform(deviceSetSel, with: nil)?
597
- .takeUnretainedValue() as? NSObject else { return nil }
595
+ guard let deviceSet = DeviceSet.lookUp({ selectorName, path -> NSObject? in
596
+ let deviceSetSel = NSSelectorFromString(selectorName)
597
+ guard context.responds(to: deviceSetSel) else { return nil }
598
+ let deviceSetResult: Unmanaged<AnyObject>?
599
+ if let path {
600
+ deviceSetResult = context.perform(deviceSetSel, with: path, with: nil)
601
+ } else {
602
+ deviceSetResult = context.perform(deviceSetSel, with: nil)
603
+ }
604
+ return deviceSetResult?.takeUnretainedValue() as? NSObject
605
+ }) else { return nil }
598
606
  guard let devices = deviceSet.value(forKey: "devices") as? [NSObject] else { return nil }
599
607
  return devices.first(where: {
600
608
  ($0.value(forKey: "UDID") as? NSUUID)?.uuidString == udid
@@ -621,7 +621,7 @@ actor HIDInjector {
621
621
  private func launchSpringBoard(deviceUDID: String) {
622
622
  let process = Process()
623
623
  process.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun")
624
- process.arguments = ["simctl", "launch", deviceUDID, "com.apple.springboard"]
624
+ process.arguments = DeviceSet.simctlArguments(["launch", deviceUDID, "com.apple.springboard"])
625
625
  try? process.run()
626
626
  }
627
627
 
@@ -1,5 +1,6 @@
1
1
  import Foundation
2
2
  import NodeAPI
3
+ import SimNativeSupport
3
4
 
4
5
  /// Safe Int→UInt32 for HID codes coming from JS. Plain `UInt32(x)` traps on
5
6
  /// negative or too-large values, which would crash the in-process server on
@@ -278,6 +279,10 @@ private func axQuery(
278
279
  }
279
280
 
280
281
  #NodeModule(exports: [
282
+ // setDeviceSet(path | null): the `--device-set` lookups and simctl calls use.
283
+ "setDeviceSet": try NodeFunction { (path: String?) in
284
+ DeviceSet.configure(path: path)
285
+ },
281
286
  "SimHID": SimHID.deferredConstructor,
282
287
  "SimCapture": SimCapture.deferredConstructor,
283
288
  // axDescribe(udid): Promise<string> — axe-shaped accessibility JSON.
@@ -0,0 +1,34 @@
1
+ import Foundation
2
+
3
+ /// The CoreSimulator device set serve-sim targets: Xcode's default set, or
4
+ /// the one `serve-sim --device-set` names (`simctl --set`). The JS side
5
+ /// resolves it once and hands it to the addon with `setDeviceSet` as it
6
+ /// loads the addon, before any capture, input or accessibility call.
7
+ public enum DeviceSet {
8
+ private static let lock = NSLock()
9
+ nonisolated(unsafe) private static var configuredPath: String?
10
+
11
+ public static func configure(path: String?) {
12
+ lock.withLock { configuredPath = path }
13
+ }
14
+
15
+ /// The configured set's path, or nil for Xcode's default set.
16
+ public static func path() -> String? {
17
+ lock.withLock { configuredPath }
18
+ }
19
+
20
+ /// The device set serve-sim targets, found by `lookup`, which sends
21
+ /// CoreSimulator's `selector` with `path`: `deviceSetWithPath:error:` and
22
+ /// the path for a configured set, `defaultDeviceSetWithError:` and nil for
23
+ /// Xcode's default. Nil when `lookup` finds no set.
24
+ public static func lookUp<Found>(_ lookup: (_ selector: String, _ path: String?) -> Found?) -> Found? {
25
+ let path = path()
26
+ return lookup(path == nil ? "defaultDeviceSetWithError:" : "deviceSetWithPath:error:", path)
27
+ }
28
+
29
+ /// `xcrun` arguments that run `simctl <arguments>` against the configured set.
30
+ public static func simctlArguments(_ arguments: [String]) -> [String] {
31
+ guard let path = path() else { return ["simctl"] + arguments }
32
+ return ["simctl", "--set", path] + arguments
33
+ }
34
+ }