@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.
- package/CHANGELOG.md +12 -0
- package/README.md +5 -3
- package/binding.gyp +3 -1
- package/lib/src/commands/jpeg-stream.d.ts +44 -0
- package/lib/src/commands/jpeg-stream.d.ts.map +1 -0
- package/lib/src/commands/jpeg-stream.js +198 -0
- package/lib/src/commands/jpeg-stream.js.map +1 -0
- package/lib/src/commands/ui.d.ts +21 -0
- package/lib/src/commands/ui.d.ts.map +1 -1
- package/lib/src/commands/ui.js +22 -0
- package/lib/src/commands/ui.js.map +1 -1
- package/lib/src/index.d.ts +2 -1
- package/lib/src/index.d.ts.map +1 -1
- package/lib/src/index.js +2 -1
- package/lib/src/index.js.map +1 -1
- package/lib/src/native-simctl.d.ts +2 -0
- package/lib/src/native-simctl.d.ts.map +1 -1
- package/lib/src/native-simctl.js +6 -1
- package/lib/src/native-simctl.js.map +1 -1
- package/lib/src/types.d.ts +64 -2
- package/lib/src/types.d.ts.map +1 -1
- package/lib/src/types.js +8 -0
- package/lib/src/types.js.map +1 -1
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/@appium+coresim.node +0 -0
- package/src/commands/jpeg-stream.ts +233 -0
- package/src/commands/ui.ts +27 -0
- package/src/coresim.mm +199 -6
- package/src/index.ts +4 -0
- package/src/native/audio_encoder.mm +1 -1
- package/src/native/av_recording.h +1 -1
- package/src/native/av_recording.mm +76 -27
- package/src/native/sim_audio_tap.mm +3 -3
- package/src/native/sim_device.h +11 -0
- package/src/native/sim_device.mm +53 -0
- package/src/native/sim_jpeg_stream.h +69 -0
- package/src/native/sim_jpeg_stream.mm +322 -0
- package/src/native/sim_orientation.h +16 -0
- package/src/native/sim_orientation.mm +58 -0
- package/src/native/sim_pasteboard.mm +3 -3
- package/src/native/sim_process.mm +1 -1
- package/src/native/sim_screenshot.h +13 -0
- package/src/native/sim_screenshot.mm +50 -25
- package/src/native/sim_video_recording.mm +2 -43
- package/src/native/tcc_privacy.mm +1 -1
- package/src/native/video_encoder.h +9 -2
- package/src/native/video_encoder.mm +241 -21
- package/src/native-simctl.ts +7 -0
- 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:@"
|
|
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("
|
|
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("
|
|
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 =
|
|
1052
|
-
int bitrate =
|
|
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("
|
|
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 = @"
|
|
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}];
|
|
@@ -13,7 +13,7 @@ namespace coresim {
|
|
|
13
13
|
|
|
14
14
|
namespace {
|
|
15
15
|
|
|
16
|
-
NSString* const kAVRecordingErrorDomain = @"
|
|
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_,
|
|
56
|
-
[
|
|
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
|
-
|
|
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(),
|
|
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::
|
|
87
|
-
device_, videoOptions_,
|
|
88
|
-
[
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
|
318
|
-
// not holding mutex_ (could deadlock against that
|
|
319
|
-
//
|
|
320
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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::
|
|
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::
|
|
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 = @"
|
|
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("
|
|
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:@"
|
|
157
|
+
[NSString stringWithFormat:@"io.appium.coresim.audio.%@.%@", udid_, uuid.UUIDString],
|
|
158
158
|
@(kAudioAggregateDeviceIsPrivateKey) : @YES,
|
|
159
159
|
@(kAudioAggregateDeviceTapAutoStartKey) : @NO,
|
|
160
160
|
@(kAudioAggregateDeviceTapListKey) : @[ @{
|
package/src/native/sim_device.h
CHANGED
|
@@ -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
|
|