@appium/coresim 1.6.0 → 1.8.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 (42) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +5 -3
  3. package/binding.gyp +2 -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/video-recording.d.ts +12 -3
  9. package/lib/src/commands/video-recording.d.ts.map +1 -1
  10. package/lib/src/commands/video-recording.js +19 -5
  11. package/lib/src/commands/video-recording.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 +1 -0
  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 +3 -0
  19. package/lib/src/native-simctl.js.map +1 -1
  20. package/lib/src/types.d.ts +65 -2
  21. package/lib/src/types.d.ts.map +1 -1
  22. package/package.json +1 -1
  23. package/prebuilds/darwin-arm64/@appium+coresim.node +0 -0
  24. package/src/commands/jpeg-stream.ts +233 -0
  25. package/src/commands/video-recording.ts +27 -7
  26. package/src/coresim.mm +170 -6
  27. package/src/index.ts +4 -0
  28. package/src/native/audio_encoder.mm +1 -1
  29. package/src/native/av_recording.mm +1 -1
  30. package/src/native/sim_audio_tap.mm +3 -3
  31. package/src/native/sim_jpeg_stream.h +69 -0
  32. package/src/native/sim_jpeg_stream.mm +322 -0
  33. package/src/native/sim_pasteboard.mm +3 -3
  34. package/src/native/sim_process.mm +1 -1
  35. package/src/native/sim_screenshot.h +8 -0
  36. package/src/native/sim_screenshot.mm +29 -25
  37. package/src/native/sim_video_recording.mm +1 -1
  38. package/src/native/tcc_privacy.mm +1 -1
  39. package/src/native/video_encoder.h +2 -2
  40. package/src/native/video_encoder.mm +2 -2
  41. package/src/native-simctl.ts +3 -0
  42. package/src/types.ts +70 -2
@@ -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) : @[ @{
@@ -0,0 +1,69 @@
1
+ #pragma once
2
+
3
+ #import <Foundation/Foundation.h>
4
+
5
+ #include <cstdint>
6
+ #include <functional>
7
+ #include <memory>
8
+ #include <vector>
9
+
10
+ namespace coresim {
11
+
12
+ struct JpegStreamOptions {
13
+ NSString* displayId = nil;
14
+ double fps = 60.0;
15
+ // 0-100 percent; same semantics as CaptureScreenshot's jpegQualityPercent (sim_screenshot.h),
16
+ // nil-able for ImageIO's own default (near-lossless, so a much larger frame) — but defaulted to
17
+ // 80 by coresim.mm rather than left nil, since a continuous live stream should default to
18
+ // noticeably smaller frames, unlike a one-off CaptureScreenshot call.
19
+ NSNumber* jpegQualityPercent = @80;
20
+ // 0.0-1.0 fraction of the original frame's width/height; 1.0 (default) performs no scaling.
21
+ // Already normalized from the JS-facing 1-100 percent option by coresim.mm.
22
+ double scale = 1.0;
23
+ };
24
+
25
+ // One JPEG-encoded frame. Unlike VideoAccessUnit, every frame is independently decodable — there's
26
+ // no keyframe/interframe distinction, so no resync semantics are needed on the consuming side.
27
+ struct JpegFrame {
28
+ std::vector<uint8_t> data;
29
+ uint64_t sequence = 0;
30
+ // Microseconds since the stream started.
31
+ int64_t timestampMicros = 0;
32
+ };
33
+
34
+ // Polls the live display IOSurface (sim_screenshot.h) on a serial queue and JPEG-encodes changed
35
+ // frames via ImageIO, delivering each live — see CLAUDE.md for how this relates to
36
+ // VideoStreamSession and CaptureScreenshot.
37
+ class JpegStreamSession {
38
+ public:
39
+ // `onAbortDelivery`, if set, is invoked by AbortDelivery() below — not called by this class on
40
+ // its own.
41
+ JpegStreamSession(id device, JpegStreamOptions options, std::function<void(JpegFrame)> onFrame,
42
+ std::function<void(NSError*)> onError, std::function<void()> onEnd,
43
+ std::function<void()> onAbortDelivery = nullptr);
44
+ ~JpegStreamSession();
45
+
46
+ JpegStreamSession(const JpegStreamSession&) = delete;
47
+ JpegStreamSession& operator=(const JpegStreamSession&) = delete;
48
+
49
+ // Resolves the display and starts the polling loop; throws synchronously on resolution/setup
50
+ // failure (`onEnd` never called then). Later failures go to `onError`, then `onEnd`.
51
+ void Start();
52
+
53
+ // Idempotent; blocks until the loop has fully stopped. Never call from inside onFrame/onError/
54
+ // onEnd — same queue this blocks on, so it would deadlock.
55
+ void Stop();
56
+
57
+ // Runs the constructor's `onAbortDelivery` (if any) — e.g. aborting a bounded delivery queue so
58
+ // a producer thread blocked pushing into it unblocks. Call before Stop() when the caller can't
59
+ // rely on anything else draining that queue concurrently (e.g. process-exit cleanup running
60
+ // synchronously on the same thread `Stop()` would otherwise wait on) — a normal Stop() doesn't
61
+ // need this. Safe from any thread; a no-op if unset.
62
+ void AbortDelivery();
63
+
64
+ private:
65
+ class Impl;
66
+ std::unique_ptr<Impl> impl_;
67
+ };
68
+
69
+ } // namespace coresim
@@ -0,0 +1,322 @@
1
+ #include "sim_jpeg_stream.h"
2
+
3
+ #import <CoreImage/CoreImage.h>
4
+ #import <IOSurface/IOSurface.h>
5
+
6
+ #include <algorithm>
7
+ #include <atomic>
8
+ #include <cmath>
9
+
10
+ #include "monotonic_clock.h"
11
+ #include "nserror_bridge.h"
12
+ #include "sim_screenshot.h"
13
+
14
+ namespace coresim {
15
+
16
+ namespace {
17
+
18
+ NSString* const kJpegStreamErrorDomain = @"io.appium.coresim.JpegStream";
19
+
20
+ // A scale this close to 1.0 (from a 1-100 percent option divided by 100.0 — never exactly 1.0
21
+ // except at 100%) is treated as "no scaling" — comparing doubles for exact equality is unreliable.
22
+ constexpr double kScaleEpsilon = 1e-9;
23
+
24
+ // If no frame can be produced for this long — the display surface staying unavailable, or the
25
+ // CIImage/CGImage render failing — for that whole stretch, something is genuinely wrong (a
26
+ // permanently dropped connection, say) rather than a one-off transient hiccup; escalate to a real
27
+ // error instead of polling forever with nothing to show for it and no error ever reported.
28
+ constexpr double kMaxStallSeconds = 10.0;
29
+
30
+ NSError* MakeError(NSInteger code, NSString* message) {
31
+ return [NSError errorWithDomain:kJpegStreamErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : message}];
32
+ }
33
+
34
+ } // namespace
35
+
36
+ class JpegStreamSession::Impl {
37
+ public:
38
+ Impl(id device, JpegStreamOptions options, std::function<void(JpegFrame)> onFrame,
39
+ std::function<void(NSError*)> onError, std::function<void()> onEnd, std::function<void()> onAbortDelivery)
40
+ : device_(device),
41
+ options_(options),
42
+ onFrame_(std::move(onFrame)),
43
+ onError_(std::move(onError)),
44
+ onEnd_(std::move(onEnd)),
45
+ onAbortDelivery_(std::move(onAbortDelivery)) {
46
+ queue_ = dispatch_queue_create("io.appium.coresim.jpegStream", DISPATCH_QUEUE_SERIAL);
47
+ // Persistent, unlike CaptureScreenshot's own one-shot CIContext (see sim_screenshot.mm) — that
48
+ // file's comment calls this out as exactly the tradeoff a continuous streaming path should
49
+ // make instead.
50
+ context_ = [CIContext contextWithOptions:nil];
51
+ }
52
+
53
+ ~Impl() { Stop(); }
54
+
55
+ void Start() {
56
+ NSError* error = nil;
57
+ id descriptor = ResolveCaptureDisplay(device_, options_.displayId, &error);
58
+ if (descriptor == nil) {
59
+ throw NSErrorException(error);
60
+ }
61
+ id surfaceObj = CurrentDisplaySurface(descriptor);
62
+ if (surfaceObj == nil) {
63
+ throw NSErrorException(MakeError(1, @"The device's display surface is not available yet"));
64
+ }
65
+ IOSurfaceRef surface = (__bridge IOSurfaceRef)surfaceObj;
66
+
67
+ startTime_ = MonotonicSeconds();
68
+ // Set before the initial encode below — EmitFrame measures elapsed time from it, and running_
69
+ // gates whether a frame is delivered at all (see Tick()'s own check).
70
+ running_ = true;
71
+ // Encode immediately rather than waiting for a *changed* seed on the first tick, or the stream
72
+ // would stay silent until the display changes again — unlike Tick(), this doesn't check
73
+ // lastSeed_ first (it's still its default 0), so EncodeCurrentSeedAndEmit always runs once here.
74
+ NSError* encodeError = nil;
75
+ EncodeCurrentSeedAndEmit(surface, &encodeError);
76
+ if (encodeError != nil) {
77
+ running_ = false;
78
+ throw NSErrorException(encodeError);
79
+ }
80
+
81
+ double interval = 1.0 / std::max(options_.fps, 1.0);
82
+ dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue_);
83
+ dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, 0),
84
+ static_cast<uint64_t>(interval * NSEC_PER_SEC),
85
+ static_cast<uint64_t>(interval * NSEC_PER_SEC / 10));
86
+ // `this` outlives the timer: Stop()/StopFromQueue() always cancel or outrun it before `this`
87
+ // can be destroyed (see their comments below).
88
+ dispatch_source_set_event_handler(timer, ^{
89
+ Tick();
90
+ });
91
+ timer_ = timer;
92
+ dispatch_resume(timer_);
93
+ }
94
+
95
+ // Callable from any thread except `queue_` itself (would deadlock on the dispatch_sync below).
96
+ void Stop() {
97
+ if (!running_.exchange(false)) {
98
+ return; // idempotent
99
+ }
100
+ if (timer_ != nullptr) {
101
+ dispatch_source_cancel(timer_);
102
+ // dispatch_source_cancel doesn't preempt a currently-executing handler — an already-running
103
+ // Tick() (past its own `running_` check) can still run to completion and emit one last frame
104
+ // via onFrame_ before this returns. This blocks until that happens, so onEnd_ below (which
105
+ // the caller uses to release resources onFrame_ needs, e.g. a ThreadSafeFunction) never races
106
+ // a still-in-flight emit — not, as such, a guarantee that no further frame is ever emitted.
107
+ dispatch_sync(queue_, ^{
108
+ });
109
+ timer_ = nullptr;
110
+ }
111
+ if (onEnd_) {
112
+ onEnd_();
113
+ }
114
+ }
115
+
116
+ void AbortDelivery() {
117
+ if (onAbortDelivery_) {
118
+ onAbortDelivery_();
119
+ }
120
+ }
121
+
122
+ private:
123
+ // Same as Stop() minus the dispatch_sync barrier — only safe from within Tick() itself, already
124
+ // serialized on `queue_`; would race a concurrent Tick() from any other thread.
125
+ void StopFromQueue() {
126
+ if (!running_.exchange(false)) {
127
+ return; // idempotent — e.g. an external Stop() already won this race
128
+ }
129
+ if (timer_ != nullptr) {
130
+ dispatch_source_cancel(timer_);
131
+ timer_ = nullptr;
132
+ }
133
+ if (onEnd_) {
134
+ onEnd_();
135
+ }
136
+ }
137
+
138
+ void Tick() {
139
+ if (!running_) {
140
+ return;
141
+ }
142
+ @autoreleasepool {
143
+ try {
144
+ // Re-resolved every tick (like CaptureScreenshot does), not cached once in Start(), so a
145
+ // deleted device or disconnected display surfaces a real error instead of Tick() quietly
146
+ // doing nothing forever.
147
+ NSError* resolveError = nil;
148
+ id descriptor = ResolveCaptureDisplay(device_, options_.displayId, &resolveError);
149
+ if (descriptor == nil) {
150
+ if (onError_) {
151
+ onError_(resolveError);
152
+ }
153
+ StopFromQueue();
154
+ return;
155
+ }
156
+ id surfaceObj = CurrentDisplaySurface(descriptor);
157
+ if (surfaceObj == nil) {
158
+ ReportIfStalledTooLong(); // transient — the connection may not have a frame ready yet
159
+ return;
160
+ }
161
+ IOSurfaceRef surface = (__bridge IOSurfaceRef)surfaceObj;
162
+ uint32_t seed = IOSurfaceGetSeed(surface);
163
+ if (seed == lastSeed_) {
164
+ // Unchanged since the last tick (mirrors VideoFrameEncoder's own seed check) — the
165
+ // surface itself is fine, just nothing new to encode, so this resolves any stall.
166
+ stalledSince_ = 0;
167
+ return;
168
+ }
169
+ NSError* encodeError = nil;
170
+ bool encoded = EncodeCurrentSeedAndEmit(surface, &encodeError);
171
+ if (encodeError != nil) {
172
+ if (onError_) {
173
+ onError_(encodeError);
174
+ }
175
+ StopFromQueue();
176
+ return;
177
+ }
178
+ if (encoded) {
179
+ stalledSince_ = 0;
180
+ } else {
181
+ ReportIfStalledTooLong(); // transient — e.g. a momentary CIImage/CGImage render failure
182
+ }
183
+ } catch (const std::exception& e) {
184
+ // Without this, an exception here (e.g. a dropped display-proxy connection, which surfaces
185
+ // as NativeSimUnavailableError/ObjCException — both std::exception subtypes — via
186
+ // ResolveCaptureDisplay's own dynamic dispatch) would escape this bare GCD timer handler
187
+ // uncaught and crash the whole process (see CLAUDE.md; mirrors VideoFrameEncoder::Tick()).
188
+ if (onError_) {
189
+ onError_(MakeError(4, [NSString stringWithFormat:@"JPEG streaming failed: %s", e.what()]));
190
+ }
191
+ StopFromQueue();
192
+ }
193
+ }
194
+ }
195
+
196
+ // Called from Tick() on a tick that produced nothing but also wasn't a genuine (reported) error —
197
+ // starts a stall timer on the first such tick, and escalates to a real onError_/StopFromQueue()
198
+ // once it's run past kMaxStallSeconds without a single successful tick (a frame, or an unchanged-
199
+ // seed check) in between. Without this, a display surface that never comes back (or a
200
+ // CIImage/CGImage render that never succeeds again) would poll forever with nothing to show for
201
+ // it and no error ever reported (see CLAUDE.md).
202
+ void ReportIfStalledTooLong() {
203
+ double now = MonotonicSeconds();
204
+ if (stalledSince_ == 0) {
205
+ stalledSince_ = now;
206
+ return;
207
+ }
208
+ if (now - stalledSince_ > kMaxStallSeconds) {
209
+ if (onError_) {
210
+ onError_(MakeError(5, @"No JPEG frame could be produced for too long"));
211
+ }
212
+ StopFromQueue();
213
+ }
214
+ }
215
+
216
+ // Reads `surface`'s current seed, attempts one encode, and — only on success — commits exactly
217
+ // that pre-encode seed to lastSeed_ and emits the frame. Reading the seed before encoding (never
218
+ // a value re-read afterward) matters: if the display changes again while EncodeSurface() is still
219
+ // running, the pre-encode seed still correctly identifies which content this frame captures, so
220
+ // the next caller sees the surface's now-newer seed as "changed" and retries — reading it after
221
+ // encoding instead would wrongly commit the *newer* seed against the *older* frame just emitted,
222
+ // permanently losing that update (the following unchanged-seed check would then treat it as
223
+ // already delivered, silently, since it looks identical to a genuinely static display). Shared by
224
+ // Start() (always called once, regardless of lastSeed_) and Tick() (only once a changed seed was
225
+ // already observed) so this ordering can't independently drift between the two again.
226
+ //
227
+ // Returns whether a frame was produced. Sets *error only for a genuine encode failure that
228
+ // should end the whole session; a false return with *error left nil means "transient, retry
229
+ // next tick" (mirrors VideoFrameEncoder::EncodeSurface's identical CVPixelBufferCreateWithIOSurface
230
+ // transient-failure contract in video_encoder.mm) — lastSeed_ is deliberately left stale then, so
231
+ // the next attempt retries this same content instead of silently skipping it forever.
232
+ bool EncodeCurrentSeedAndEmit(IOSurfaceRef surface, NSError** error) {
233
+ uint32_t seed = IOSurfaceGetSeed(surface);
234
+ NSData* data = nil;
235
+ bool encoded = EncodeSurface(surface, &data, error);
236
+ if (encoded) {
237
+ lastSeed_ = seed;
238
+ EmitFrame(data);
239
+ }
240
+ return encoded;
241
+ }
242
+
243
+ // Returns whether a frame was produced. Sets *error only for a genuine encode failure that
244
+ // should end the whole session; a false return with *error left nil means "transient, retry
245
+ // next tick" (mirrors VideoFrameEncoder::EncodeSurface's identical CVPixelBufferCreateWithIOSurface
246
+ // transient-failure contract in video_encoder.mm).
247
+ bool EncodeSurface(IOSurfaceRef surface, NSData** outData, NSError** error) {
248
+ CIImage* ciImage = [CIImage imageWithIOSurface:surface];
249
+ if (ciImage == nil) {
250
+ return false;
251
+ }
252
+ // Scaling the CIImage before rendering (rather than resizing an already-encoded JPEG
253
+ // afterward) means the CGImage/JPEG below is produced at the target resolution directly — no
254
+ // extra decode/resize/re-encode round trip.
255
+ if (std::fabs(options_.scale - 1.0) > kScaleEpsilon) {
256
+ ciImage = [ciImage imageByApplyingTransform:CGAffineTransformMakeScale(options_.scale, options_.scale)];
257
+ }
258
+ CGImageRef cgImage = [context_ createCGImage:ciImage fromRect:ciImage.extent];
259
+ if (cgImage == nil) {
260
+ return false;
261
+ }
262
+ // Same CGImageDestination-based encode CaptureScreenshot's kJPEG format uses (sim_screenshot.mm).
263
+ NSData* imageData = EncodeImage(cgImage, ScreenshotFormat::kJPEG, options_.jpegQualityPercent, error);
264
+ CGImageRelease(cgImage);
265
+ if (imageData == nil) {
266
+ return false;
267
+ }
268
+ *outData = imageData;
269
+ return true;
270
+ }
271
+
272
+ // Called either from Start() (on whichever thread calls it, before the timer/queue_ even starts
273
+ // running) or from Tick() (already serialized on queue_, once the timer is live) — never both at
274
+ // once, since Start() always finishes (and only then resumes the timer) before Tick() can fire.
275
+ // So sequence_ needs no synchronization, unlike VideoStreamSession's (whose encoder callback can
276
+ // run concurrently with the poll loop on a different thread).
277
+ void EmitFrame(NSData* data) {
278
+ JpegFrame frame;
279
+ frame.sequence = sequence_++;
280
+ frame.timestampMicros = static_cast<int64_t>((MonotonicSeconds() - startTime_) * 1000000);
281
+ const uint8_t* bytes = static_cast<const uint8_t*>(data.bytes);
282
+ frame.data.assign(bytes, bytes + data.length);
283
+ if (onFrame_) {
284
+ onFrame_(std::move(frame));
285
+ }
286
+ }
287
+
288
+ id device_;
289
+ JpegStreamOptions options_;
290
+ std::function<void(JpegFrame)> onFrame_;
291
+ std::function<void(NSError*)> onError_;
292
+ std::function<void()> onEnd_;
293
+ std::function<void()> onAbortDelivery_;
294
+
295
+ dispatch_queue_t queue_ = nullptr;
296
+ dispatch_source_t timer_ = nullptr;
297
+ CIContext* context_ = nil;
298
+ uint32_t lastSeed_ = 0;
299
+ double startTime_ = 0;
300
+ uint64_t sequence_ = 0;
301
+ // 0 means "no stall in progress" — set to MonotonicSeconds() by ReportIfStalledTooLong() on the
302
+ // first unproductive tick of a run, cleared back to 0 by any tick that makes real progress
303
+ // (a produced frame, or an unchanged-seed check confirming the surface itself is still fine).
304
+ double stalledSince_ = 0;
305
+ std::atomic<bool> running_{false};
306
+ };
307
+
308
+ JpegStreamSession::JpegStreamSession(id device, JpegStreamOptions options, std::function<void(JpegFrame)> onFrame,
309
+ std::function<void(NSError*)> onError, std::function<void()> onEnd,
310
+ std::function<void()> onAbortDelivery)
311
+ : impl_(std::make_unique<Impl>(device, options, std::move(onFrame), std::move(onError), std::move(onEnd),
312
+ std::move(onAbortDelivery))) {}
313
+
314
+ JpegStreamSession::~JpegStreamSession() = default;
315
+
316
+ void JpegStreamSession::Start() { impl_->Start(); }
317
+
318
+ void JpegStreamSession::Stop() { impl_->Stop(); }
319
+
320
+ void JpegStreamSession::AbortDelivery() { impl_->AbortDelivery(); }
321
+
322
+ } // namespace coresim
@@ -69,7 +69,7 @@ namespace coresim {
69
69
 
70
70
  namespace {
71
71
 
72
- NSString* const kPasteboardErrorDomain = @"com.appium.coresim.Pasteboard";
72
+ NSString* const kPasteboardErrorDomain = @"io.appium.coresim.Pasteboard";
73
73
 
74
74
  // The UTI both pasteboard mechanisms below exchange plain text under.
75
75
  NSString* const kPlainTextUTI = @"public.utf8-plain-text";
@@ -166,7 +166,7 @@ NSString* PullPasteboardStringModern(id device, NSError** error) {
166
166
  CoresimPasteboardDelegate* delegate = [CoresimPasteboardDelegate new];
167
167
  delegate.activeSema = dispatch_semaphore_create(0);
168
168
  delegate.appliedSema = dispatch_semaphore_create(0);
169
- dispatch_queue_t queue = dispatch_queue_create("com.appium.coresim.pasteboard.pull", DISPATCH_QUEUE_SERIAL);
169
+ dispatch_queue_t queue = dispatch_queue_create("io.appium.coresim.pasteboard.pull", DISPATCH_QUEUE_SERIAL);
170
170
 
171
171
  id interface = ConnectPasteboardInterface(device, pasteboard, delegate, queue);
172
172
  SafeInvoke([&] {
@@ -200,7 +200,7 @@ BOOL PushPasteboardStringModern(id device, NSString* content, NSError** error) {
200
200
  CoresimPasteboardDelegate* delegate = [CoresimPasteboardDelegate new];
201
201
  delegate.activeSema = dispatch_semaphore_create(0);
202
202
  delegate.appliedSema = dispatch_semaphore_create(0);
203
- dispatch_queue_t queue = dispatch_queue_create("com.appium.coresim.pasteboard.push", DISPATCH_QUEUE_SERIAL);
203
+ dispatch_queue_t queue = dispatch_queue_create("io.appium.coresim.pasteboard.push", DISPATCH_QUEUE_SERIAL);
204
204
 
205
205
  id interface = ConnectPasteboardInterface(device, pasteboard, delegate, queue);
206
206
  SafeInvoke([&] {
@@ -13,7 +13,7 @@ namespace coresim {
13
13
 
14
14
  namespace {
15
15
 
16
- NSString* const kProcessErrorDomain = @"com.appium.coresim.Process";
16
+ NSString* const kProcessErrorDomain = @"io.appium.coresim.Process";
17
17
  NSString* const kWebInspectorSocketSuffix = @"com.apple.webinspectord_sim.socket";
18
18
 
19
19
  NSError* MakeError(NSInteger code, NSString* message) {
@@ -1,5 +1,6 @@
1
1
  #pragma once
2
2
 
3
+ #import <CoreGraphics/CoreGraphics.h>
3
4
  #import <Foundation/Foundation.h>
4
5
 
5
6
  namespace coresim {
@@ -24,6 +25,13 @@ id CurrentDisplaySurface(id descriptor);
24
25
 
25
26
  enum class ScreenshotFormat { kPNG, kJPEG };
26
27
 
28
+ // Encodes `cgImage` via ImageIO into `format` — the same CGImageDestination-based encode
29
+ // CaptureScreenshot uses for its own final step, exposed for a caller that already has a
30
+ // CGImageRef from its own CIContext (e.g. JpegStreamSession's persistent one — see
31
+ // sim_jpeg_stream.mm). `jpegQualityPercent` semantics match CaptureScreenshot's own parameter of
32
+ // the same name. Returns nil (and sets *error) on encode failure. Does not release `cgImage`.
33
+ NSData* EncodeImage(CGImageRef cgImage, ScreenshotFormat format, NSNumber* jpegQualityPercent, NSError** error);
34
+
27
35
  // Captures a display as an image, reading the same in-process framebuffer surface `simctl io
28
36
  // <udid> screenshot` itself reads (see sim_screenshot.mm) — no temp file, no subprocess.
29
37
  // `displayId`, if non-nil, selects a specific port by its UUID (see ListDisplays); nil selects the
@@ -12,7 +12,7 @@ namespace coresim {
12
12
 
13
13
  namespace {
14
14
 
15
- NSString* const kScreenshotErrorDomain = @"com.appium.coresim.Screenshot";
15
+ NSString* const kScreenshotErrorDomain = @"io.appium.coresim.Screenshot";
16
16
 
17
17
  NSError* MakeError(NSInteger code, NSString* message) {
18
18
  return [NSError errorWithDomain:kScreenshotErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : message}];
@@ -171,6 +171,32 @@ NSArray<NSDictionary*>* ListDisplays(id device, NSError** error) {
171
171
  return result;
172
172
  }
173
173
 
174
+ NSData* EncodeImage(CGImageRef cgImage, ScreenshotFormat format, NSNumber* jpegQualityPercent, NSError** error) {
175
+ NSString* uti = format == ScreenshotFormat::kJPEG ? kJPEGUTI : kPNGUTI;
176
+ NSMutableData* imageData = [NSMutableData data];
177
+ CGImageDestinationRef destination =
178
+ CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, (__bridge CFStringRef)uti, 1, NULL);
179
+ if (destination == nullptr) {
180
+ *error = MakeError(7, @"Failed to create an image encoder");
181
+ return nil;
182
+ }
183
+ // kCGImageDestinationLossyCompressionQuality is meaningless for PNG (always lossless) — ImageIO
184
+ // silently ignores properties a format doesn't use, so this is only gated on jpegQualityPercent
185
+ // being present, not on `format` too.
186
+ NSDictionary* properties =
187
+ jpegQualityPercent != nil
188
+ ? @{(NSString*)kCGImageDestinationLossyCompressionQuality : @(jpegQualityPercent.doubleValue / 100.0)}
189
+ : nil;
190
+ CGImageDestinationAddImage(destination, cgImage, (__bridge CFDictionaryRef)properties);
191
+ BOOL ok = CGImageDestinationFinalize(destination);
192
+ CFRelease(destination);
193
+ if (!ok) {
194
+ *error = MakeError(8, @"Failed to encode the image");
195
+ return nil;
196
+ }
197
+ return imageData;
198
+ }
199
+
174
200
  NSData* CaptureScreenshot(id device, NSString* displayId, ScreenshotFormat format, NSNumber* jpegQualityPercent,
175
201
  NSError** error) {
176
202
  id descriptor = ResolveCaptureDisplay(device, displayId, error);
@@ -193,7 +219,7 @@ NSData* CaptureScreenshot(id device, NSString* displayId, ScreenshotFormat forma
193
219
 
194
220
  // A fresh CIContext per call, matching this operation's one-shot semantics (mirrors
195
221
  // simctl's own screenshot command) rather than the persistent, reused context a
196
- // continuous video/streaming path would want.
222
+ // continuous video/streaming path would want (JpegStreamSession — see sim_jpeg_stream.mm).
197
223
  CIContext* context = [CIContext contextWithOptions:nil];
198
224
  CGImageRef cgImage = [context createCGImage:ciImage fromRect:ciImage.extent];
199
225
  if (cgImage == nil) {
@@ -201,30 +227,8 @@ NSData* CaptureScreenshot(id device, NSString* displayId, ScreenshotFormat forma
201
227
  return nil;
202
228
  }
203
229
 
204
- NSString* uti = format == ScreenshotFormat::kJPEG ? kJPEGUTI : kPNGUTI;
205
- NSMutableData* imageData = [NSMutableData data];
206
- CGImageDestinationRef destination =
207
- CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, (__bridge CFStringRef)uti, 1, NULL);
208
- if (destination == nullptr) {
209
- CGImageRelease(cgImage);
210
- *error = MakeError(7, @"Failed to create an image encoder for the captured screenshot");
211
- return nil;
212
- }
213
- // kCGImageDestinationLossyCompressionQuality is meaningless for PNG (always lossless) — ImageIO
214
- // silently ignores properties a format doesn't use, so this is only gated on jpegQualityPercent
215
- // being present, not on `format` too.
216
- NSDictionary* properties =
217
- jpegQualityPercent != nil
218
- ? @{(NSString*)kCGImageDestinationLossyCompressionQuality : @(jpegQualityPercent.doubleValue / 100.0)}
219
- : nil;
220
- CGImageDestinationAddImage(destination, cgImage, (__bridge CFDictionaryRef)properties);
221
- BOOL ok = CGImageDestinationFinalize(destination);
222
- CFRelease(destination);
230
+ NSData* imageData = EncodeImage(cgImage, format, jpegQualityPercent, error);
223
231
  CGImageRelease(cgImage);
224
- if (!ok) {
225
- *error = MakeError(8, @"Failed to encode the captured screenshot");
226
- return nil;
227
- }
228
232
  return imageData;
229
233
  }
230
234
 
@@ -10,7 +10,7 @@ namespace coresim {
10
10
 
11
11
  namespace {
12
12
 
13
- NSString* const kVideoRecordingErrorDomain = @"com.appium.coresim.VideoRecording";
13
+ NSString* const kVideoRecordingErrorDomain = @"io.appium.coresim.VideoRecording";
14
14
 
15
15
  NSError* MakeError(NSInteger code, NSString* message) {
16
16
  return [NSError errorWithDomain:kVideoRecordingErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : message}];
@@ -9,7 +9,7 @@ namespace coresim {
9
9
 
10
10
  namespace {
11
11
 
12
- NSString* const kTCCErrorDomain = @"com.appium.coresim.TCCPrivacy";
12
+ NSString* const kTCCErrorDomain = @"io.appium.coresim.TCCPrivacy";
13
13
 
14
14
  NSError* MakeError(int code, NSString* message) {
15
15
  return [NSError errorWithDomain:kTCCErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : message}];
@@ -15,8 +15,8 @@ enum class VideoStreamCodec { kH264, kHEVC };
15
15
  struct VideoEncoderOptions {
16
16
  VideoStreamCodec codec = VideoStreamCodec::kH264;
17
17
  NSString* displayId = nil;
18
- double fps = 15.0;
19
- int bitrate = 2000000;
18
+ double fps = 60.0;
19
+ int bitrate = 4000000;
20
20
  };
21
21
 
22
22
  // Polls the live display IOSurface (sim_screenshot.h) on a serial queue and encodes changed
@@ -16,7 +16,7 @@ namespace coresim {
16
16
 
17
17
  namespace {
18
18
 
19
- NSString* const kVideoEncoderErrorDomain = @"com.appium.coresim.VideoEncoder";
19
+ NSString* const kVideoEncoderErrorDomain = @"io.appium.coresim.VideoEncoder";
20
20
 
21
21
  NSError* MakeError(NSInteger code, NSString* message) {
22
22
  return [NSError errorWithDomain:kVideoEncoderErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : message}];
@@ -117,7 +117,7 @@ class VideoFrameEncoder::Impl {
117
117
  onError_(std::move(onError)),
118
118
  onEnd_(std::move(onEnd)),
119
119
  sharedClockOrigin_(sharedClockOrigin) {
120
- queue_ = dispatch_queue_create("com.appium.coresim.videoEncoder", DISPATCH_QUEUE_SERIAL);
120
+ queue_ = dispatch_queue_create("io.appium.coresim.videoEncoder", DISPATCH_QUEUE_SERIAL);
121
121
  }
122
122
 
123
123
  ~Impl() { Stop(); }
@@ -20,6 +20,7 @@ import {
20
20
  setDarwinNotificationState,
21
21
  } from './commands/darwin-notification.js';
22
22
  import {clearLocation, getEnv, openUrl, pushNotification, setLocation} from './commands/interaction.js';
23
+ import {startJpegStream} from './commands/jpeg-stream.js';
23
24
  import {addCertificate, addRootCertificate, resetKeychain} from './commands/keychain.js';
24
25
  import {
25
26
  bootDevice,
@@ -60,6 +61,7 @@ import './commands/app.js';
60
61
  import './commands/biometric.js';
61
62
  import './commands/darwin-notification.js';
62
63
  import './commands/interaction.js';
64
+ import './commands/jpeg-stream.js';
63
65
  import './commands/keychain.js';
64
66
  import './commands/lifecycle.js';
65
67
  import './commands/media.js';
@@ -256,6 +258,7 @@ Object.assign(NativeSimctl.prototype, {
256
258
  stopVideoRecording,
257
259
  isVideoRecording,
258
260
  startVideoStream,
261
+ startJpegStream,
259
262
 
260
263
  // webinspector
261
264
  getWebInspectorSocket,