@appium/coresim 1.7.0 → 1.9.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +5 -3
  3. package/binding.gyp +3 -1
  4. package/lib/src/commands/jpeg-stream.d.ts +44 -0
  5. package/lib/src/commands/jpeg-stream.d.ts.map +1 -0
  6. package/lib/src/commands/jpeg-stream.js +198 -0
  7. package/lib/src/commands/jpeg-stream.js.map +1 -0
  8. package/lib/src/commands/ui.d.ts +21 -0
  9. package/lib/src/commands/ui.d.ts.map +1 -1
  10. package/lib/src/commands/ui.js +22 -0
  11. package/lib/src/commands/ui.js.map +1 -1
  12. package/lib/src/index.d.ts +2 -1
  13. package/lib/src/index.d.ts.map +1 -1
  14. package/lib/src/index.js +2 -1
  15. package/lib/src/index.js.map +1 -1
  16. package/lib/src/native-simctl.d.ts +2 -0
  17. package/lib/src/native-simctl.d.ts.map +1 -1
  18. package/lib/src/native-simctl.js +6 -1
  19. package/lib/src/native-simctl.js.map +1 -1
  20. package/lib/src/types.d.ts +64 -2
  21. package/lib/src/types.d.ts.map +1 -1
  22. package/lib/src/types.js +8 -0
  23. package/lib/src/types.js.map +1 -1
  24. package/package.json +1 -1
  25. package/prebuilds/darwin-arm64/@appium+coresim.node +0 -0
  26. package/src/commands/jpeg-stream.ts +233 -0
  27. package/src/commands/ui.ts +27 -0
  28. package/src/coresim.mm +199 -6
  29. package/src/index.ts +4 -0
  30. package/src/native/audio_encoder.mm +1 -1
  31. package/src/native/av_recording.h +1 -1
  32. package/src/native/av_recording.mm +76 -27
  33. package/src/native/sim_audio_tap.mm +3 -3
  34. package/src/native/sim_device.h +11 -0
  35. package/src/native/sim_device.mm +53 -0
  36. package/src/native/sim_jpeg_stream.h +69 -0
  37. package/src/native/sim_jpeg_stream.mm +322 -0
  38. package/src/native/sim_orientation.h +16 -0
  39. package/src/native/sim_orientation.mm +58 -0
  40. package/src/native/sim_pasteboard.mm +3 -3
  41. package/src/native/sim_process.mm +1 -1
  42. package/src/native/sim_screenshot.h +13 -0
  43. package/src/native/sim_screenshot.mm +50 -25
  44. package/src/native/sim_video_recording.mm +2 -43
  45. package/src/native/tcc_privacy.mm +1 -1
  46. package/src/native/video_encoder.h +9 -2
  47. package/src/native/video_encoder.mm +241 -21
  48. package/src/native-simctl.ts +7 -0
  49. package/src/types.ts +69 -2
package/src/coresim.mm CHANGED
@@ -31,6 +31,8 @@
31
31
  #include "native/objc_runtime.h"
32
32
  #include "native/sim_device.h"
33
33
  #include "native/sim_device_set.h"
34
+ #include "native/sim_jpeg_stream.h"
35
+ #include "native/sim_orientation.h"
34
36
  #include "native/sim_pasteboard.h"
35
37
  #include "native/sim_process.h"
36
38
  #include "native/sim_screenshot.h"
@@ -99,7 +101,7 @@ NSError* MakeDescriptorError(NSString* which, int savedErrno) {
99
101
  }
100
102
 
101
103
  NSError* MakeSpawnPathError(NSString* message) {
102
- return [NSError errorWithDomain:@"com.appium.coresim.spawn" code:1 userInfo:@{NSLocalizedDescriptionKey : message}];
104
+ return [NSError errorWithDomain:@"io.appium.coresim.spawn" code:1 userInfo:@{NSLocalizedDescriptionKey : message}];
103
105
  }
104
106
 
105
107
  // Standard bin dirs to search, in order, when `path` is a bare command name (no `/`) — mirrors
@@ -258,9 +260,11 @@ struct AddonInstanceData {
258
260
  Napi::FunctionReference avStreamConstructor;
259
261
  Napi::FunctionReference avRecordingConstructor;
260
262
  Napi::FunctionReference privateRecordingConstructor;
263
+ Napi::FunctionReference jpegStreamConstructor;
261
264
  ActiveSessionRegistry<coresim::VideoStreamSession> activeVideoStreams;
262
265
  ActiveSessionRegistry<coresim::AVStreamSession> activeAVStreams;
263
266
  ActiveSessionRegistry<coresim::AVRecordingSession> activeAVRecordings;
267
+ ActiveSessionRegistry<coresim::JpegStreamSession> activeJpegStreams;
264
268
  };
265
269
 
266
270
  } // namespace
@@ -326,6 +330,58 @@ Napi::Object NativeVideoStream::NewInstance(Napi::Env env, std::shared_ptr<cores
326
330
  env, boxed, [](Napi::Env /*env*/, std::shared_ptr<coresim::VideoStreamSession>* data) { delete data; })});
327
331
  }
328
332
 
333
+ // Wraps a live coresim::JpegStreamSession — same shape as NativeVideoStream minus
334
+ // `requestKeyFrame()` (meaningless here: every JPEG frame is already independently decodable, see
335
+ // sim_jpeg_stream.h). Frames/errors are delivered live via the callbacks passed directly to
336
+ // startJpegStream, not through this object — it only exposes `stop()`.
337
+ class NativeJpegStream : public Napi::ObjectWrap<NativeJpegStream> {
338
+ public:
339
+ static void Init(Napi::Env env);
340
+ static Napi::Object NewInstance(Napi::Env env, std::shared_ptr<coresim::JpegStreamSession> session);
341
+ explicit NativeJpegStream(const Napi::CallbackInfo& info);
342
+
343
+ private:
344
+ std::shared_ptr<coresim::JpegStreamSession> session_;
345
+
346
+ Napi::Value Stop(const Napi::CallbackInfo& info) {
347
+ Napi::Env env = info.Env();
348
+ auto session = session_;
349
+ return RunAsyncVoid(env, [session]() { session->Stop(); });
350
+ }
351
+
352
+ // See NativeVideoStream::Finalize for why this hands off to a background queue.
353
+ void Finalize(Napi::Env env) override {
354
+ auto session = std::move(session_);
355
+ if (session) {
356
+ env.GetInstanceData<AddonInstanceData>()->activeJpegStreams.Deregister(session);
357
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
358
+ session->Stop();
359
+ });
360
+ }
361
+ }
362
+ };
363
+
364
+ NativeJpegStream::NativeJpegStream(const Napi::CallbackInfo& info) : Napi::ObjectWrap<NativeJpegStream>(info) {
365
+ auto* boxed = info[0].As<Napi::External<std::shared_ptr<coresim::JpegStreamSession>>>().Data();
366
+ session_ = *boxed;
367
+ info.Env().GetInstanceData<AddonInstanceData>()->activeJpegStreams.Register(session_);
368
+ }
369
+
370
+ void NativeJpegStream::Init(Napi::Env env) {
371
+ Napi::Function ctor = DefineClass(env, "NativeJpegStream",
372
+ {
373
+ InstanceMethod<&NativeJpegStream::Stop>("stop"),
374
+ });
375
+ env.GetInstanceData<AddonInstanceData>()->jpegStreamConstructor = Napi::Persistent(ctor);
376
+ }
377
+
378
+ Napi::Object NativeJpegStream::NewInstance(Napi::Env env, std::shared_ptr<coresim::JpegStreamSession> session) {
379
+ auto* boxed = new std::shared_ptr<coresim::JpegStreamSession>(std::move(session));
380
+ Napi::Function ctor = env.GetInstanceData<AddonInstanceData>()->jpegStreamConstructor.Value();
381
+ return ctor.New({Napi::External<std::shared_ptr<coresim::JpegStreamSession>>::New(
382
+ env, boxed, [](Napi::Env /*env*/, std::shared_ptr<coresim::JpegStreamSession>* data) { delete data; })});
383
+ }
384
+
329
385
  // Wraps a live coresim::AVStreamSession — the combined-AV counterpart to NativeVideoStream above,
330
386
  // same shape (only `stop()`/`requestKeyFrame()`; access units/errors are delivered live via the
331
387
  // callbacks passed directly to startAVStream).
@@ -473,7 +529,7 @@ class NativePrivateRecordingHandle : public Napi::ObjectWrap<NativePrivateRecord
473
529
  id device = device_;
474
530
  return RunAsyncVoid(env, [device]() {
475
531
  dispatch_semaphore_t sema = dispatch_semaphore_create(0);
476
- dispatch_queue_t queue = dispatch_queue_create("com.appium.coresim.stopRecordVideo", DISPATCH_QUEUE_SERIAL);
532
+ dispatch_queue_t queue = dispatch_queue_create("io.appium.coresim.stopRecordVideo", DISPATCH_QUEUE_SERIAL);
477
533
  __block NSError* capturedError = nil;
478
534
  NSError* resolveError = nil;
479
535
  BOOL ok = coresim::StopVideoRecording(
@@ -573,7 +629,7 @@ class NativeDevice : public Napi::ObjectWrap<NativeDevice> {
573
629
  NSDictionary* options = OptionsArg(info, 0);
574
630
  return RunAsyncVoid(env, [device, options]() {
575
631
  dispatch_semaphore_t sema = dispatch_semaphore_create(0);
576
- dispatch_queue_t queue = dispatch_queue_create("com.appium.coresim.boot", DISPATCH_QUEUE_SERIAL);
632
+ dispatch_queue_t queue = dispatch_queue_create("io.appium.coresim.boot", DISPATCH_QUEUE_SERIAL);
577
633
  __block NSError* capturedError = nil;
578
634
  BootAsync(device, options, queue, ^(NSError* error) {
579
635
  capturedError = error;
@@ -743,6 +799,15 @@ class NativeDevice : public Napi::ObjectWrap<NativeDevice> {
743
799
  });
744
800
  }
745
801
 
802
+ Napi::Value SetOrientation(const Napi::CallbackInfo& info) {
803
+ id device = device_;
804
+ int32_t orientation = info[0].As<Napi::Number>().Int32Value();
805
+ return RunAsyncVoid(info.Env(), [device, orientation]() {
806
+ NSError* error = nil;
807
+ ThrowIfFailed(coresim::SetDeviceOrientation(device, orientation, &error), error);
808
+ });
809
+ }
810
+
746
811
  Napi::Value SendPushNotification(const Napi::CallbackInfo& info) {
747
812
  id device = device_;
748
813
  NSString* bundleId = @(info[0].As<Napi::String>().Utf8Value().c_str());
@@ -1043,13 +1108,30 @@ class NativeDevice : public Napi::ObjectWrap<NativeDevice> {
1043
1108
  [](Napi::Env env, NSArray* result) -> Napi::Value { return NSObjectToJsValue(env, result); });
1044
1109
  }
1045
1110
 
1111
+ Napi::Value GetOrientation(const Napi::CallbackInfo& info) {
1112
+ id device = device_;
1113
+ return RunAsync<int32_t>(
1114
+ info.Env(),
1115
+ [device]() -> int32_t {
1116
+ dispatch_semaphore_t sema = dispatch_semaphore_create(0);
1117
+ __block int32_t result = 1;
1118
+ coresim::ReadGuestOrientation(device, ^(int32_t orientation) {
1119
+ result = orientation;
1120
+ dispatch_semaphore_signal(sema);
1121
+ });
1122
+ dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
1123
+ return result;
1124
+ },
1125
+ [](Napi::Env env, int32_t result) -> Napi::Value { return Napi::Number::New(env, result); });
1126
+ }
1127
+
1046
1128
  // Shared by StartVideoRecording (audio path)/StartVideoStream — `argIndex` is where the options
1047
1129
  // object (if any) sits in `info`.
1048
1130
  static coresim::VideoEncoderOptions ParseVideoEncoderOptions(const Napi::CallbackInfo& info, size_t argIndex) {
1049
1131
  NSString* displayId = nil;
1050
1132
  coresim::VideoStreamCodec codec = coresim::VideoStreamCodec::kH264;
1051
- double fps = 15.0;
1052
- int bitrate = 2000000;
1133
+ double fps = 60.0;
1134
+ int bitrate = 4000000;
1053
1135
  if (info.Length() > argIndex && info[argIndex].IsObject()) {
1054
1136
  Napi::Object options = info[argIndex].As<Napi::Object>();
1055
1137
  if (options.Has("displayId") && options.Get("displayId").IsString()) {
@@ -1069,6 +1151,37 @@ class NativeDevice : public Napi::ObjectWrap<NativeDevice> {
1069
1151
  return coresim::VideoEncoderOptions{codec, displayId, fps, bitrate};
1070
1152
  }
1071
1153
 
1154
+ // `quality`/`displayId`/`fps` are this addon's own options, not a CoreSimulator options
1155
+ // dictionary — the TS layer (commands/jpeg-stream.ts) already constrains them, so anything else
1156
+ // (including absent) is just defaulted here rather than validated again (mirrors Screenshot's
1157
+ // identical comment above).
1158
+ static coresim::JpegStreamOptions ParseJpegStreamOptions(const Napi::CallbackInfo& info, size_t argIndex) {
1159
+ NSString* displayId = nil;
1160
+ double fps = 60.0;
1161
+ // Defaults to 80, not ImageIO's own (near-lossless, much larger) default — see
1162
+ // JpegStreamOptions's own comment (sim_jpeg_stream.h) for why.
1163
+ NSNumber* jpegQualityPercent = @80;
1164
+ double scale = 1.0;
1165
+ if (info.Length() > argIndex && info[argIndex].IsObject()) {
1166
+ Napi::Object options = info[argIndex].As<Napi::Object>();
1167
+ if (options.Has("displayId") && options.Get("displayId").IsString()) {
1168
+ displayId = @(options.Get("displayId").As<Napi::String>().Utf8Value().c_str());
1169
+ }
1170
+ if (options.Has("fps") && options.Get("fps").IsNumber()) {
1171
+ fps = options.Get("fps").As<Napi::Number>().DoubleValue();
1172
+ }
1173
+ if (options.Has("quality") && options.Get("quality").IsNumber()) {
1174
+ jpegQualityPercent = @(options.Get("quality").As<Napi::Number>().DoubleValue());
1175
+ }
1176
+ // JS-facing `scale` is a 1-100 percent (commands/jpeg-stream.ts already validates the
1177
+ // range), converted here to the 0.0-1.0 fraction JpegStreamSession actually applies.
1178
+ if (options.Has("scale") && options.Get("scale").IsNumber()) {
1179
+ scale = options.Get("scale").As<Napi::Number>().DoubleValue() / 100.0;
1180
+ }
1181
+ }
1182
+ return coresim::JpegStreamOptions{displayId, fps, jpegQualityPercent, scale};
1183
+ }
1184
+
1072
1185
  static bool OptionsWantAudio(const Napi::CallbackInfo& info, size_t argIndex) {
1073
1186
  return info.Length() > argIndex && info[argIndex].IsObject() && info[argIndex].As<Napi::Object>().Has("audio") &&
1074
1187
  info[argIndex].As<Napi::Object>().Get("audio").IsBoolean() &&
@@ -1132,7 +1245,7 @@ class NativeDevice : public Napi::ObjectWrap<NativeDevice> {
1132
1245
  env,
1133
1246
  [device, displayId, mask, assetWriterOutputSettings, outputFile]() -> id {
1134
1247
  dispatch_semaphore_t sema = dispatch_semaphore_create(0);
1135
- dispatch_queue_t queue = dispatch_queue_create("com.appium.coresim.recordVideo", DISPATCH_QUEUE_SERIAL);
1248
+ dispatch_queue_t queue = dispatch_queue_create("io.appium.coresim.recordVideo", DISPATCH_QUEUE_SERIAL);
1136
1249
  __block NSError* capturedError = nil;
1137
1250
  NSError* resolveError = nil;
1138
1251
  BOOL ok = coresim::StartVideoRecording(
@@ -1380,6 +1493,81 @@ class NativeDevice : public Napi::ObjectWrap<NativeDevice> {
1380
1493
  });
1381
1494
  }
1382
1495
 
1496
+ // Real-time JPEG frame stream via ImageIO (see sim_jpeg_stream.mm) — a client-side MJPEG stream
1497
+ // is just this frame sequence multipart-boundary-framed over HTTP, which coresim itself has no
1498
+ // opinion about. Same TSFN/registry/teardown shape as StartVideoStream's audio-less branch,
1499
+ // minus keyframes/resync (every JPEG frame is independently decodable).
1500
+ Napi::Value StartJpegStream(const Napi::CallbackInfo& info) {
1501
+ Napi::Env env = info.Env();
1502
+ id device = device_;
1503
+ coresim::JpegStreamOptions options = ParseJpegStreamOptions(info, 0);
1504
+ Napi::Function onFrame = info[1].As<Napi::Function>();
1505
+ Napi::Function onError = info[2].As<Napi::Function>();
1506
+
1507
+ // See StartVideoStream's identical comment on accessUnitTsfn — bounded so a slow-draining
1508
+ // consumer throttles the encoder via BlockingCall instead of letting queued frames grow
1509
+ // unbounded.
1510
+ static constexpr size_t kFrameQueueSize = 60;
1511
+ Napi::ThreadSafeFunction frameTsfn =
1512
+ Napi::ThreadSafeFunction::New(env, onFrame, "coresim jpeg stream frame", kFrameQueueSize, 1);
1513
+ Napi::ThreadSafeFunction errorTsfn = Napi::ThreadSafeFunction::New(env, onError, "coresim jpeg stream error", 0, 1);
1514
+ auto tsfnGuard = std::make_shared<TsfnReleaseGuard>();
1515
+
1516
+ return RunAsync<std::shared_ptr<coresim::JpegStreamSession>>(
1517
+ env,
1518
+ [device, options, frameTsfn, errorTsfn, tsfnGuard]() mutable -> std::shared_ptr<coresim::JpegStreamSession> {
1519
+ auto session = std::make_shared<coresim::JpegStreamSession>(
1520
+ device, options,
1521
+ [frameTsfn](coresim::JpegFrame frame) mutable {
1522
+ frameTsfn.BlockingCall([frame = std::move(frame)](Napi::Env env, Napi::Function jsCallback) mutable {
1523
+ // See StartVideoStream's identical comment on why this is wrapped in try/catch.
1524
+ try {
1525
+ Napi::Object obj = Napi::Object::New(env);
1526
+ obj.Set("data", Napi::Buffer<uint8_t>::Copy(env, frame.data.data(), frame.data.size()));
1527
+ obj.Set("sequence", Napi::Number::New(env, static_cast<double>(frame.sequence)));
1528
+ obj.Set("timestampMicros", Napi::Number::New(env, static_cast<double>(frame.timestampMicros)));
1529
+ jsCallback.Call({obj});
1530
+ } catch (...) {
1531
+ }
1532
+ });
1533
+ },
1534
+ [errorTsfn](NSError* error) mutable {
1535
+ NSErrorException exception(error);
1536
+ errorTsfn.BlockingCall([exception](Napi::Env env, Napi::Function jsCallback) {
1537
+ try {
1538
+ jsCallback.Call({NSErrorExceptionToJsError(env, exception).Value()});
1539
+ } catch (...) {
1540
+ }
1541
+ });
1542
+ },
1543
+ [frameTsfn, errorTsfn, tsfnGuard]() mutable {
1544
+ ReleaseTsfnOnce(tsfnGuard, [&]() mutable {
1545
+ frameTsfn.Release();
1546
+ errorTsfn.Release();
1547
+ });
1548
+ },
1549
+ [frameTsfn, errorTsfn, tsfnGuard]() mutable {
1550
+ ReleaseTsfnOnce(tsfnGuard, [&]() mutable {
1551
+ frameTsfn.Abort();
1552
+ errorTsfn.Abort();
1553
+ });
1554
+ });
1555
+ try {
1556
+ session->Start();
1557
+ } catch (...) {
1558
+ ReleaseTsfnOnce(tsfnGuard, [&]() mutable {
1559
+ frameTsfn.Release();
1560
+ errorTsfn.Release();
1561
+ });
1562
+ throw;
1563
+ }
1564
+ return session;
1565
+ },
1566
+ [](Napi::Env env, std::shared_ptr<coresim::JpegStreamSession> session) -> Napi::Value {
1567
+ return NativeJpegStream::NewInstance(env, session);
1568
+ });
1569
+ }
1570
+
1383
1571
  // Option dictionary keys for `spawnWithPath:options:...` aren't part of the ObjC runtime
1384
1572
  // metadata this addon resolves selectors from (they're string literals inside CoreSimulator's
1385
1573
  // own implementation) — confirmed by resolving each `SimDeviceSpawnKey*` symbol at runtime via
@@ -1541,6 +1729,7 @@ void NativeDevice::Init(Napi::Env env) {
1541
1729
  InstanceMethod<&NativeDevice::OpenUrl>("openUrl"),
1542
1730
  InstanceMethod<&NativeDevice::SetLocation>("setLocation"),
1543
1731
  InstanceMethod<&NativeDevice::ClearLocation>("clearLocation"),
1732
+ InstanceMethod<&NativeDevice::SetOrientation>("setOrientation"),
1544
1733
  InstanceMethod<&NativeDevice::SendPushNotification>("sendPushNotification"),
1545
1734
  InstanceMethod<&NativeDevice::AddCertificate>("addCertificate"),
1546
1735
  InstanceMethod<&NativeDevice::ResetKeychain>("resetKeychain"),
@@ -1565,8 +1754,10 @@ void NativeDevice::Init(Napi::Env env) {
1565
1754
  InstanceMethod<&NativeDevice::GetWebInspectorSocket>("getWebInspectorSocket"),
1566
1755
  InstanceMethod<&NativeDevice::Screenshot>("screenshot"),
1567
1756
  InstanceMethod<&NativeDevice::GetDisplays>("getDisplays"),
1757
+ InstanceMethod<&NativeDevice::GetOrientation>("getOrientation"),
1568
1758
  InstanceMethod<&NativeDevice::StartVideoRecording>("startVideoRecording"),
1569
1759
  InstanceMethod<&NativeDevice::StartVideoStream>("startVideoStream"),
1760
+ InstanceMethod<&NativeDevice::StartJpegStream>("startJpegStream"),
1570
1761
  InstanceMethod<&NativeDevice::Spawn>("spawn"),
1571
1762
  });
1572
1763
  env.GetInstanceData<AddonInstanceData>()->deviceConstructor = Napi::Persistent(ctor);
@@ -1818,6 +2009,7 @@ void CleanupActiveSessions(AddonInstanceData* instanceData) {
1818
2009
  };
1819
2010
  instanceData->activeVideoStreams.StopAll(stopStream);
1820
2011
  instanceData->activeAVStreams.StopAll(stopStream);
2012
+ instanceData->activeJpegStreams.StopAll(stopStream);
1821
2013
  instanceData->activeAVRecordings.StopAll([](auto& session) {
1822
2014
  dispatch_semaphore_t sema = dispatch_semaphore_create(0);
1823
2015
  session->Stop([sema](NSError*) { dispatch_semaphore_signal(sema); });
@@ -1850,6 +2042,7 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
1850
2042
  NativeAVStream::Init(env);
1851
2043
  NativeAVRecording::Init(env);
1852
2044
  NativePrivateRecordingHandle::Init(env);
2045
+ NativeJpegStream::Init(env);
1853
2046
  exports.Set("sharedServiceContext", Napi::Function::New(env, SharedServiceContextBinding));
1854
2047
  exports.Set("frameworkVersion", Napi::Function::New(env, FrameworkVersionBinding));
1855
2048
  exports.Set("flushActiveSessions", Napi::Function::New(env, FlushActiveSessionsBinding));
package/src/index.ts CHANGED
@@ -2,14 +2,18 @@ export {NativeSimError, NativeSimUnavailableError, NativeSimDispatchError, Nativ
2
2
  export {NativeSimctl} from './native-simctl.js';
3
3
  export {SpawnedProcess} from './commands/spawn.js';
4
4
  export {VideoStream} from './commands/video-stream.js';
5
+ export {JpegStream} from './commands/jpeg-stream.js';
5
6
  export type {AppContainerType} from './commands/app.js';
6
7
  export type {BiometricName} from './commands/biometric.js';
7
8
  export {
9
+ DeviceOrientation,
8
10
  SimBootStatus,
9
11
  SimDeviceState,
10
12
  type ApnsAlert,
11
13
  type ApnsPayload,
12
14
  type ApnsSound,
15
+ type JpegFrame,
16
+ type JpegStreamOptions,
13
17
  type PushNotificationPayload,
14
18
  type ScreenshotOptions,
15
19
  type SimBootInfo,
@@ -12,7 +12,7 @@ namespace coresim {
12
12
 
13
13
  namespace {
14
14
 
15
- NSString* const kAudioEncoderErrorDomain = @"com.appium.coresim.AudioEncoder";
15
+ NSString* const kAudioEncoderErrorDomain = @"io.appium.coresim.AudioEncoder";
16
16
 
17
17
  NSError* MakeError(NSInteger code, NSString* message) {
18
18
  return [NSError errorWithDomain:kAudioEncoderErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : message}];
@@ -46,7 +46,7 @@ class AVRecordingSession {
46
46
 
47
47
  private:
48
48
  class Impl;
49
- std::unique_ptr<Impl> impl_;
49
+ std::shared_ptr<Impl> impl_;
50
50
  };
51
51
 
52
52
  } // namespace coresim
@@ -13,7 +13,7 @@ namespace coresim {
13
13
 
14
14
  namespace {
15
15
 
16
- NSString* const kAVRecordingErrorDomain = @"com.appium.coresim.AVRecording";
16
+ NSString* const kAVRecordingErrorDomain = @"io.appium.coresim.AVRecording";
17
17
 
18
18
  NSError* MakeError(NSInteger code, NSString* message) {
19
19
  return [NSError errorWithDomain:kAVRecordingErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : message}];
@@ -23,14 +23,17 @@ constexpr size_t kMaxPendingAudioSamples = 500; // a few seconds of AAC packets
23
23
 
24
24
  } // namespace
25
25
 
26
- class AVRecordingSession::Impl {
26
+ class AVRecordingSession::Impl : public std::enable_shared_from_this<Impl> {
27
27
  public:
28
28
  Impl(id device, NSString* udid, VideoEncoderOptions videoOptions, NSString* outputFile, bool captureAudio)
29
29
  : device_(device),
30
30
  udid_(udid),
31
31
  videoOptions_(videoOptions),
32
32
  outputFile_(outputFile),
33
- captureAudio_(captureAudio) {}
33
+ captureAudio_(captureAudio) {
34
+ // Forced on regardless of what the caller passed — see VideoEncoderOptions::fixedFrameSize.
35
+ videoOptions_.fixedFrameSize = true;
36
+ }
34
37
 
35
38
  ~Impl() { TearDownIfNeeded(); }
36
39
 
@@ -49,18 +52,31 @@ class AVRecordingSession::Impl {
49
52
  throw NSErrorException(writerError ?: MakeError(1, @"Failed to create an AVAssetWriter"));
50
53
  }
51
54
 
55
+ // A weak_ptr (not `this`, and not a strong shared_ptr) captured by every callback handed to a
56
+ // collaborator below: `this` would dangle if a callback fires mid-teardown (see Fail()'s own
57
+ // comment), but a strong self-capture stored in a member `this` itself owns (videoEncoder_,
58
+ // audioTap_) would leak instead, keeping this Impl alive forever via a reference cycle. Locking
59
+ // it at call time gets safety without either problem.
60
+ std::weak_ptr<Impl> weakSelf = weak_from_this();
52
61
  try {
53
62
  if (captureAudio_) {
54
63
  audioTap_ = std::make_unique<AudioTapSession>(
55
- udid_, [this](const AudioBufferList* data, const AudioTimeStamp* time) { HandleAudioPCM(data, time); },
56
- [this](NSError* error) {
64
+ udid_,
65
+ [weakSelf](const AudioBufferList* data, const AudioTimeStamp* time) {
66
+ if (auto self = weakSelf.lock()) {
67
+ self->HandleAudioPCM(data, time);
68
+ }
69
+ },
70
+ [weakSelf](NSError* error) {
57
71
  // A failed process-list refresh (kAudioTapNonFatalProcessListRefreshErrorCode) isn't
58
72
  // fatal — the tap keeps running. Everything else is.
59
73
  if ([error.domain isEqualToString:kAudioTapErrorDomain] &&
60
74
  error.code == kAudioTapNonFatalProcessListRefreshErrorCode) {
61
75
  return;
62
76
  }
63
- Fail(error, /*fromVideo=*/false);
77
+ if (auto self = weakSelf.lock()) {
78
+ self->Fail(error, /*fromVideo=*/false);
79
+ }
64
80
  },
65
81
  [] {});
66
82
  audioTap_->Start();
@@ -68,7 +84,12 @@ class AVRecordingSession::Impl {
68
84
  // Start() can invoke HandleAudioPCM before returning, so audioEncoder_ is written under
69
85
  // mutex_ (same lock HandleAudioPCM reads it under) to avoid a torn/racy pointer read.
70
86
  auto encoder = std::make_unique<AudioEncoder>(
71
- audioTap_->Format(), [this](CMSampleBufferRef sampleBuffer) { HandleAudioSample(sampleBuffer); },
87
+ audioTap_->Format(),
88
+ [weakSelf](CMSampleBufferRef sampleBuffer) {
89
+ if (auto self = weakSelf.lock()) {
90
+ self->HandleAudioSample(sampleBuffer);
91
+ }
92
+ },
72
93
  &clockOrigin_);
73
94
  {
74
95
  std::lock_guard<std::mutex> lock(mutex_);
@@ -83,9 +104,19 @@ class AVRecordingSession::Impl {
83
104
  [writer_ addInput:audioInput_];
84
105
  }
85
106
 
86
- videoEncoder_ = std::make_unique<VideoFrameEncoder>(
87
- device_, videoOptions_, [this](CMSampleBufferRef sampleBuffer) { HandleVideoSample(sampleBuffer); },
88
- [this](NSError* error) { Fail(error, /*fromVideo=*/true); }, [] {}, &clockOrigin_);
107
+ videoEncoder_ = std::make_shared<VideoFrameEncoder>(
108
+ device_, videoOptions_,
109
+ [weakSelf](CMSampleBufferRef sampleBuffer) {
110
+ if (auto self = weakSelf.lock()) {
111
+ self->HandleVideoSample(sampleBuffer);
112
+ }
113
+ },
114
+ [weakSelf](NSError* error) {
115
+ if (auto self = weakSelf.lock()) {
116
+ self->Fail(error, /*fromVideo=*/true);
117
+ }
118
+ },
119
+ [] {}, &clockOrigin_);
89
120
  videoEncoder_->Start();
90
121
  } catch (...) {
91
122
  TearDownIfNeeded();
@@ -185,7 +216,7 @@ class AVRecordingSession::Impl {
185
216
  [writer_ startSessionAtSourceTime:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)];
186
217
  writerStarted_ = true;
187
218
  justStarted = true;
188
- AppendVideoLocked(sampleBuffer);
219
+ writingError = AppendVideoLocked(sampleBuffer);
189
220
  for (CMSampleBufferRef pending : pendingAudio_) {
190
221
  AppendAudioLocked(pending);
191
222
  CFRelease(pending);
@@ -195,7 +226,7 @@ class AVRecordingSession::Impl {
195
226
  writingError = writer_.error ?: MakeError(3, @"AVAssetWriter startWriting failed");
196
227
  }
197
228
  } else {
198
- AppendVideoLocked(sampleBuffer);
229
+ writingError = AppendVideoLocked(sampleBuffer);
199
230
  }
200
231
  }
201
232
  if (writingError != nil) {
@@ -245,11 +276,16 @@ class AVRecordingSession::Impl {
245
276
  }
246
277
  }
247
278
 
248
- // Caller holds mutex_.
249
- void AppendVideoLocked(CMSampleBufferRef sampleBuffer) {
250
- if (videoInput_.isReadyForMoreMediaData) {
251
- [videoInput_ appendSampleBuffer:sampleBuffer];
279
+ // Caller holds mutex_. Returns a descriptive error if the append failed (e.g. a mid-recording
280
+ // rotation resizes the encoder but a track's dimensions are fixed for the writer's life), nil otherwise.
281
+ NSError* AppendVideoLocked(CMSampleBufferRef sampleBuffer) {
282
+ if (!videoInput_.isReadyForMoreMediaData) {
283
+ return nil; // transient backpressure, not a failure
284
+ }
285
+ if ([videoInput_ appendSampleBuffer:sampleBuffer]) {
286
+ return nil;
252
287
  }
288
+ return writer_.error ?: MakeError(4, @"Failed to append a video sample to the recording");
253
289
  }
254
290
 
255
291
  // Caller holds mutex_.
@@ -314,19 +350,23 @@ class AVRecordingSession::Impl {
314
350
  return pending;
315
351
  }
316
352
 
317
- // Called from either encoder's onError, or from HandleVideoSample on a writer-level failure —
318
- // not holding mutex_ (could deadlock against that same encoder's own queue). Stops only the
319
- // OTHER (still-running) encoder — the one whose callback we're inside is already tearing itself
320
- // down on its own error path, and calling its own blocking Stop() here would deadlock.
353
+ // Called from either encoder's onError, or from HandleVideoSample on a writer-level append
354
+ // failure — not holding mutex_ (could deadlock against that encoder's own queue).
355
+ //
356
+ // fromVideo=true also needs videoEncoder_ itself stopped: a writer append failure leaves
357
+ // VideoToolbox otherwise healthy and encoding forever. But videoEncoder_->Stop() dispatch_syncs
358
+ // onto its own queue, and HandleVideoSample (VideoToolbox's own output callback) isn't
359
+ // guaranteed to be off that queue — so it's dispatched async instead, via a shared_ptr `encoder`
360
+ // copy that can outlive this Impl. Stop() can still flush a pending frame into onSample_/onError_
361
+ // synchronously; those hold only a weak_ptr (see Start()), but `self` below keeps it resolvable
362
+ // for the duration of this block. onEnd_ waits until that's done, not fired eagerly here.
321
363
  void Fail(NSError* error, bool fromVideo) {
322
364
  if (fromVideo) {
323
365
  if (audioTap_) {
324
366
  audioTap_->Stop();
325
367
  }
326
- } else {
327
- if (videoEncoder_) {
328
- videoEncoder_->Stop();
329
- }
368
+ } else if (videoEncoder_) {
369
+ videoEncoder_->Stop();
330
370
  }
331
371
  std::vector<std::function<void(NSError*)>> pending;
332
372
  {
@@ -338,7 +378,16 @@ class AVRecordingSession::Impl {
338
378
  callback(error);
339
379
  }
340
380
  }
341
- FireEndOnce();
381
+ if (fromVideo && videoEncoder_) {
382
+ std::shared_ptr<VideoFrameEncoder> encoder = videoEncoder_;
383
+ std::shared_ptr<Impl> self = shared_from_this();
384
+ dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
385
+ encoder->Stop();
386
+ self->FireEndOnce();
387
+ });
388
+ } else {
389
+ FireEndOnce();
390
+ }
342
391
  }
343
392
 
344
393
  // The single point where a real (first-ever) Stop() attempt's outcome becomes known — records
@@ -389,7 +438,7 @@ class AVRecordingSession::Impl {
389
438
  std::function<void()> onEnd_;
390
439
 
391
440
  double clockOrigin_ = 0;
392
- std::unique_ptr<VideoFrameEncoder> videoEncoder_;
441
+ std::shared_ptr<VideoFrameEncoder> videoEncoder_;
393
442
  std::unique_ptr<AudioTapSession> audioTap_;
394
443
  std::unique_ptr<AudioEncoder> audioEncoder_;
395
444
 
@@ -410,7 +459,7 @@ class AVRecordingSession::Impl {
410
459
 
411
460
  AVRecordingSession::AVRecordingSession(id device, NSString* udid, VideoEncoderOptions videoOptions,
412
461
  NSString* outputFile, bool captureAudio)
413
- : impl_(std::make_unique<Impl>(device, udid, videoOptions, outputFile, captureAudio)) {}
462
+ : impl_(std::make_shared<Impl>(device, udid, videoOptions, outputFile, captureAudio)) {}
414
463
 
415
464
  AVRecordingSession::~AVRecordingSession() = default;
416
465
 
@@ -11,7 +11,7 @@
11
11
 
12
12
  namespace coresim {
13
13
 
14
- NSString* const kAudioTapErrorDomain = @"com.appium.coresim.AudioTap";
14
+ NSString* const kAudioTapErrorDomain = @"io.appium.coresim.AudioTap";
15
15
 
16
16
  namespace {
17
17
 
@@ -66,7 +66,7 @@ class AudioTapSession::Impl {
66
66
  Impl(NSString* udid, std::function<void(const AudioBufferList*, const AudioTimeStamp*)> onBuffer,
67
67
  std::function<void(NSError*)> onError, std::function<void()> onEnd)
68
68
  : udid_(udid), onBuffer_(std::move(onBuffer)), onError_(std::move(onError)), onEnd_(std::move(onEnd)) {
69
- queue_ = dispatch_queue_create("com.appium.coresim.audioTap", DISPATCH_QUEUE_SERIAL);
69
+ queue_ = dispatch_queue_create("io.appium.coresim.audioTap", DISPATCH_QUEUE_SERIAL);
70
70
  }
71
71
 
72
72
  ~Impl() { Stop(); }
@@ -154,7 +154,7 @@ class AudioTapSession::Impl {
154
154
  NSDictionary* aggregateDescription = @{
155
155
  @(kAudioAggregateDeviceNameKey) : [NSString stringWithFormat:@"coresim-audio-%@", udid_],
156
156
  @(kAudioAggregateDeviceUIDKey) :
157
- [NSString stringWithFormat:@"com.appium.coresim.audio.%@.%@", udid_, uuid.UUIDString],
157
+ [NSString stringWithFormat:@"io.appium.coresim.audio.%@.%@", udid_, uuid.UUIDString],
158
158
  @(kAudioAggregateDeviceIsPrivateKey) : @YES,
159
159
  @(kAudioAggregateDeviceTapAutoStartKey) : @NO,
160
160
  @(kAudioAggregateDeviceTapListKey) : @[ @{
@@ -2,6 +2,8 @@
2
2
 
3
3
  #import <Foundation/Foundation.h>
4
4
 
5
+ #include <cstdint>
6
+
5
7
  namespace coresim {
6
8
 
7
9
  // -[SimDevice UDID] / -[SimDevice name] / -[SimDevice state] / -[SimDevice deviceType] /
@@ -19,6 +21,11 @@ id DeviceRuntime(id device);
19
21
  // privacy-permission access (see tcc_privacy.h).
20
22
  NSString* DeviceDataPath(id device);
21
23
 
24
+ // -[SimDevice lastBootedAt] -> the current boot session's start time (nil if never booted). Used to
25
+ // tell a guest preference file that predates this boot from one already rewritten this boot (see
26
+ // sim_orientation.mm).
27
+ NSDate* DeviceLastBootedAt(id device);
28
+
22
29
  // -[SimDevice bootWithOptions:error:]
23
30
  BOOL Boot(id device, NSDictionary* options, NSError** error);
24
31
 
@@ -64,6 +71,10 @@ NSString* Getenv(id device, NSString* name, NSError** error);
64
71
  // the same way Apple's own Simulator.app does.
65
72
  unsigned int LookupMachPort(id device, NSString* serviceName, NSError** error);
66
73
 
74
+ // Rotates the device via a raw GSEvent mach message to SpringBoard's "PurpleWorkspacePort" (see
75
+ // CLAUDE.md). `orientation`: 1=portrait, 2=portraitUpsideDown, 3=landscapeRight, 4=landscapeLeft.
76
+ BOOL SetDeviceOrientation(id device, int32_t orientation, NSError** error);
77
+
67
78
  // -[SimDevice installApplication:withOptions:error:]
68
79
  BOOL InstallApp(id device, NSURL* appURL, NSDictionary* options, NSError** error);
69
80