@appium/coresim 1.8.0 → 1.10.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 +18 -0
- package/binding.gyp +1 -3
- 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 +1 -1
- package/lib/src/index.d.ts.map +1 -1
- package/lib/src/index.js +1 -1
- package/lib/src/index.js.map +1 -1
- package/lib/src/native-simctl.d.ts.map +1 -1
- package/lib/src/native-simctl.js +3 -1
- package/lib/src/native-simctl.js.map +1 -1
- package/lib/src/types.d.ts +9 -0
- 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 +7 -14
- package/prebuilds/darwin-arm64/@appium+coresim.node +0 -0
- package/src/commands/ui.ts +27 -0
- package/src/coresim.mm +29 -0
- package/src/index.ts +1 -0
- package/src/native/av_recording.h +1 -1
- package/src/native/av_recording.mm +75 -26
- package/src/native/sim_device.h +11 -0
- package/src/native/sim_device.mm +53 -0
- package/src/native/sim_orientation.h +16 -0
- package/src/native/sim_orientation.mm +58 -0
- package/src/native/sim_screenshot.h +5 -0
- package/src/native/sim_screenshot.mm +21 -0
- package/src/native/sim_video_recording.mm +2 -43
- package/src/native/video_encoder.h +7 -0
- package/src/native/video_encoder.mm +239 -19
- package/src/native-simctl.ts +4 -0
- package/src/types.ts +10 -0
|
@@ -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
|
|
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
|
|
package/src/native/sim_device.mm
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#include "sim_device.h"
|
|
2
2
|
|
|
3
|
+
#import <mach/mach.h>
|
|
3
4
|
#import <objc/message.h>
|
|
4
5
|
|
|
6
|
+
#include <cstring>
|
|
7
|
+
|
|
5
8
|
#include "objc_runtime.h"
|
|
6
9
|
#include "safe_dispatch.h"
|
|
7
10
|
|
|
@@ -9,6 +12,12 @@ namespace coresim {
|
|
|
9
12
|
|
|
10
13
|
namespace {
|
|
11
14
|
|
|
15
|
+
NSString* const kOrientationErrorDomain = @"io.appium.coresim.Orientation";
|
|
16
|
+
|
|
17
|
+
NSError* MakeOrientationError(NSInteger code, NSString* message) {
|
|
18
|
+
return [NSError errorWithDomain:kOrientationErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : message}];
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
// Shared shapes reused across the many identical-signature getters/setters below.
|
|
13
22
|
id IdGetter(id target, const std::string& selectorName) {
|
|
14
23
|
RequireSelector(target, selectorName);
|
|
@@ -53,6 +62,7 @@ NSString* DeviceName(id device) { return IdGetter(device, "name"); }
|
|
|
53
62
|
id DeviceDeviceType(id device) { return IdGetter(device, "deviceType"); }
|
|
54
63
|
id DeviceRuntime(id device) { return IdGetter(device, "runtime"); }
|
|
55
64
|
NSString* DeviceDataPath(id device) { return IdGetter(device, "dataPath"); }
|
|
65
|
+
NSDate* DeviceLastBootedAt(id device) { return IdGetter(device, "lastBootedAt"); }
|
|
56
66
|
|
|
57
67
|
unsigned long long DeviceState(id device) {
|
|
58
68
|
static const std::string kSelectorName = "state";
|
|
@@ -115,6 +125,49 @@ unsigned int LookupMachPort(id device, NSString* serviceName, NSError** error) {
|
|
|
115
125
|
});
|
|
116
126
|
}
|
|
117
127
|
|
|
128
|
+
BOOL SetDeviceOrientation(id device, int32_t orientation, NSError** error) {
|
|
129
|
+
NSError* lookupError = nil;
|
|
130
|
+
unsigned int purplePort = LookupMachPort(device, @"PurpleWorkspacePort", &lookupError);
|
|
131
|
+
// The port is the authoritative success signal, not the error (see LookupMachPort's own callers,
|
|
132
|
+
// e.g. sim_pasteboard.mm).
|
|
133
|
+
if (purplePort == 0) {
|
|
134
|
+
*error = lookupError ?: MakeOrientationError(1, @"PurpleWorkspacePort is not available — is the device booted?");
|
|
135
|
+
return NO;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// GSEvent wire format, undocumented (see CLAUDE.md): mach_msg_header_t + a fixed-layout event
|
|
139
|
+
// record, in a 112-byte buffer (8-byte aligned, >= the 108-byte message mach_msg sends).
|
|
140
|
+
constexpr uint32_t kGSEventTypeDeviceOrientationChanged = 50;
|
|
141
|
+
constexpr uint32_t kGSEventHostFlag = 0x20000;
|
|
142
|
+
constexpr mach_msg_id_t kGSEventMachMessageID = 0x7B;
|
|
143
|
+
constexpr mach_msg_timeout_t kSendTimeoutMs = 2000;
|
|
144
|
+
|
|
145
|
+
uint8_t buffer[112] = {0};
|
|
146
|
+
auto* header = reinterpret_cast<mach_msg_header_t*>(buffer);
|
|
147
|
+
header->msgh_bits = 0x13; // MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0)
|
|
148
|
+
header->msgh_size = 108; // align4(4 + 0x6B) — matches Simulator.app's own sendPurpleEvent:
|
|
149
|
+
header->msgh_remote_port = purplePort;
|
|
150
|
+
header->msgh_local_port = MACH_PORT_NULL;
|
|
151
|
+
header->msgh_id = kGSEventMachMessageID;
|
|
152
|
+
|
|
153
|
+
uint32_t type = kGSEventTypeDeviceOrientationChanged | kGSEventHostFlag;
|
|
154
|
+
std::memcpy(buffer + 0x18, &type, sizeof(type));
|
|
155
|
+
uint32_t recordInfoSize = 4;
|
|
156
|
+
std::memcpy(buffer + 0x48, &recordInfoSize, sizeof(recordInfoSize));
|
|
157
|
+
uint32_t orientationValue = static_cast<uint32_t>(orientation);
|
|
158
|
+
std::memcpy(buffer + 0x4C, &orientationValue, sizeof(orientationValue));
|
|
159
|
+
|
|
160
|
+
kern_return_t kr = mach_msg(header, MACH_SEND_MSG | MACH_SEND_TIMEOUT, header->msgh_size, 0, MACH_PORT_NULL,
|
|
161
|
+
kSendTimeoutMs, MACH_PORT_NULL);
|
|
162
|
+
if (kr != KERN_SUCCESS) {
|
|
163
|
+
*error = MakeOrientationError(
|
|
164
|
+
2, [NSString stringWithFormat:@"Failed to send the orientation change (kern_return_t %d): %s", kr,
|
|
165
|
+
mach_error_string(kr)]);
|
|
166
|
+
return NO;
|
|
167
|
+
}
|
|
168
|
+
return YES;
|
|
169
|
+
}
|
|
170
|
+
|
|
118
171
|
BOOL InstallApp(id device, NSURL* appURL, NSDictionary* options, NSError** error) {
|
|
119
172
|
static const std::string kSelectorName = "installApplication:withOptions:error:";
|
|
120
173
|
RequireSelector(device, kSelectorName);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#import <Foundation/Foundation.h>
|
|
4
|
+
|
|
5
|
+
#include <cstdint>
|
|
6
|
+
|
|
7
|
+
namespace coresim {
|
|
8
|
+
|
|
9
|
+
// A live orientation read straight off backboardd's own persisted preference file on the host side
|
|
10
|
+
// (see CLAUDE.md) — reflects a rotation from any source, not just this process's own
|
|
11
|
+
// SetDeviceOrientation calls. Calls `handler` once, async, with a DeviceOrientation wire value
|
|
12
|
+
// (types.ts) — always 1 (portrait) if the device hasn't rotated yet this boot, or on any read
|
|
13
|
+
// failure.
|
|
14
|
+
void ReadGuestOrientation(id device, void (^handler)(int32_t orientation));
|
|
15
|
+
|
|
16
|
+
} // namespace coresim
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#include "sim_orientation.h"
|
|
2
|
+
|
|
3
|
+
#include "sim_device.h"
|
|
4
|
+
|
|
5
|
+
namespace coresim {
|
|
6
|
+
|
|
7
|
+
namespace {
|
|
8
|
+
|
|
9
|
+
// backboardd's persisted GraphicsOrientation swaps landscape left/right vs. our own
|
|
10
|
+
// DeviceOrientation wire values — confirmed empirically (see CLAUDE.md).
|
|
11
|
+
int32_t TranslateGraphicsOrientation(NSInteger graphicsOrientation) {
|
|
12
|
+
switch (graphicsOrientation) {
|
|
13
|
+
case 3:
|
|
14
|
+
return 4;
|
|
15
|
+
case 4:
|
|
16
|
+
return 3;
|
|
17
|
+
default:
|
|
18
|
+
return static_cast<int32_t>(graphicsOrientation);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// The live digitizer entry for the current boot, or nil if it hasn't rotated yet this boot (or
|
|
23
|
+
// `plist` is nil/malformed). Reading straight from the file (a real binary plist) gives properly
|
|
24
|
+
// typed values — no ASCII-vs-binary parsing ambiguity to handle.
|
|
25
|
+
NSNumber* CurrentGraphicsOrientation(NSDictionary* plist) {
|
|
26
|
+
NSArray* entries = [plist[@"BKDigitizerPersistentServiceProperties"] isKindOfClass:[NSArray class]]
|
|
27
|
+
? plist[@"BKDigitizerPersistentServiceProperties"]
|
|
28
|
+
: nil;
|
|
29
|
+
NSDictionary* last = [entries.lastObject isKindOfClass:[NSDictionary class]] ? entries.lastObject : nil;
|
|
30
|
+
NSDictionary* props = [last[@"props"] isKindOfClass:[NSDictionary class]] ? last[@"props"] : nil;
|
|
31
|
+
return [props[@"GraphicsOrientation"] isKindOfClass:[NSNumber class]] ? props[@"GraphicsOrientation"] : nil;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
} // namespace
|
|
35
|
+
|
|
36
|
+
void ReadGuestOrientation(id device, void (^handler)(int32_t orientation)) {
|
|
37
|
+
NSString* plistPath =
|
|
38
|
+
[DeviceDataPath(device) stringByAppendingPathComponent:@"Library/Preferences/com.apple.backboardd.plist"];
|
|
39
|
+
NSDate* lastBootedAt = DeviceLastBootedAt(device);
|
|
40
|
+
|
|
41
|
+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
|
42
|
+
NSDate* modified = [[NSFileManager defaultManager] attributesOfItemAtPath:plistPath
|
|
43
|
+
error:nil][NSFileModificationDate];
|
|
44
|
+
// The whole file predates this boot, so none of its entries — including the last one — can
|
|
45
|
+
// belong to it (confirmed empirically: a reboot leaves the previous boot's entry in place until
|
|
46
|
+
// backboardd rewrites the file, sometimes several seconds after boot completes — see CLAUDE.md).
|
|
47
|
+
// Treat exactly like "never rotated this boot" rather than trusting stale content.
|
|
48
|
+
if (lastBootedAt != nil && modified != nil && [modified compare:lastBootedAt] == NSOrderedAscending) {
|
|
49
|
+
handler(1);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
NSDictionary* plist = [NSDictionary dictionaryWithContentsOfFile:plistPath];
|
|
53
|
+
NSNumber* graphicsOrientation = CurrentGraphicsOrientation(plist);
|
|
54
|
+
handler(graphicsOrientation != nil ? TranslateGraphicsOrientation(graphicsOrientation.integerValue) : 1);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
} // namespace coresim
|
|
@@ -18,6 +18,11 @@ NSArray<NSDictionary*>* ListDisplays(id device, NSError** error);
|
|
|
18
18
|
// object itself (e.g. StartVideoRecording's `screen` argument).
|
|
19
19
|
id ResolveCaptureDisplay(id device, NSString* displayId, NSError** error);
|
|
20
20
|
|
|
21
|
+
// The device-wide "capture service" port (real protocol: SimScreenCaptureService — see CLAUDE.md),
|
|
22
|
+
// distinct from a display descriptor (see ResolveCaptureDisplay above). Found by scanning ioPorts
|
|
23
|
+
// since no header exists. Throws NativeSimUnavailableError (not NSError**) if absent.
|
|
24
|
+
id ResolveScreenCaptureService(id device, NSError** error);
|
|
25
|
+
|
|
21
26
|
// The descriptor's current framebuffer as an `IOSurfaceRef` (bridge-cast the returned `id`).
|
|
22
27
|
// Returns nil if not available yet (e.g. connection just dropped) — not an error, since that can
|
|
23
28
|
// legitimately happen from one call to the next on a live device.
|
|
@@ -152,6 +152,27 @@ id ResolveCaptureDisplay(id device, NSString* displayId, NSError** error) {
|
|
|
152
152
|
return ResolveDisplayDescriptor(candidates, displayId, error);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
// The real protocol is SimScreenCaptureService (see CLAUDE.md) — found by scanning ioPorts for
|
|
156
|
+
// whichever descriptor responds to startRecordingFromScreen:..., since no header exists for it.
|
|
157
|
+
id ResolveScreenCaptureService(id device, NSError** error) {
|
|
158
|
+
static const std::string kStartRecordingSelector =
|
|
159
|
+
"startRecordingFromScreen:maskPolicy:assetWriterOutputSettings:outputFile:completionQueue:completionHandler:";
|
|
160
|
+
id ioClient = IdGetter(device, "io");
|
|
161
|
+
if (ioClient == nil) {
|
|
162
|
+
*error = MakeError(11, @"Device has no IO client available — is it booted?");
|
|
163
|
+
return nil;
|
|
164
|
+
}
|
|
165
|
+
NSArray* ports = IdGetter(ioClient, "ioPorts");
|
|
166
|
+
SEL selector = NSSelectorFromString(@(kStartRecordingSelector.c_str()));
|
|
167
|
+
for (id port in ports) {
|
|
168
|
+
id descriptor = IdGetter(port, "descriptor");
|
|
169
|
+
if (descriptor != nil && [descriptor respondsToSelector:selector]) {
|
|
170
|
+
return descriptor;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
throw NativeSimUnavailableError("selector", kStartRecordingSelector, CoreSimulatorFrameworkVersion());
|
|
174
|
+
}
|
|
175
|
+
|
|
155
176
|
id CurrentDisplaySurface(id descriptor) { return RenderableSurface(descriptor); }
|
|
156
177
|
|
|
157
178
|
NSArray<NSDictionary*>* ListDisplays(id device, NSError** error) {
|
|
@@ -8,50 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
namespace coresim {
|
|
10
10
|
|
|
11
|
-
namespace {
|
|
12
|
-
|
|
13
|
-
NSString* const kVideoRecordingErrorDomain = @"io.appium.coresim.VideoRecording";
|
|
14
|
-
|
|
15
|
-
NSError* MakeError(NSInteger code, NSString* message) {
|
|
16
|
-
return [NSError errorWithDomain:kVideoRecordingErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : message}];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
id IdGetter(id target, const std::string& selectorName) {
|
|
20
|
-
RequireSelector(target, selectorName);
|
|
21
|
-
SEL selector = SelectorNamed(selectorName);
|
|
22
|
-
return SafeInvoke([&] {
|
|
23
|
-
using Fn = id (*)(id, SEL);
|
|
24
|
-
return ((Fn)objc_msgSend)(target, selector);
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// The device-wide "capture service" port (real protocol: SimScreenCaptureService — see CLAUDE.md),
|
|
29
|
-
// distinct from the display descriptor passed as `screen` below. Found by scanning ioPorts since
|
|
30
|
-
// no header exists. Throws NativeSimUnavailableError (not NSError**) if absent.
|
|
31
|
-
id ResolveVideoCaptureService(id device, NSError** error) {
|
|
32
|
-
static const std::string kStartRecordingSelector =
|
|
33
|
-
"startRecordingFromScreen:maskPolicy:assetWriterOutputSettings:outputFile:completionQueue:completionHandler:";
|
|
34
|
-
id ioClient = IdGetter(device, "io");
|
|
35
|
-
if (ioClient == nil) {
|
|
36
|
-
*error = MakeError(1, @"Device has no IO client available — is it booted?");
|
|
37
|
-
return nil;
|
|
38
|
-
}
|
|
39
|
-
NSArray* ports = IdGetter(ioClient, "ioPorts");
|
|
40
|
-
SEL selector = NSSelectorFromString(@(kStartRecordingSelector.c_str()));
|
|
41
|
-
for (id port in ports) {
|
|
42
|
-
id descriptor = IdGetter(port, "descriptor");
|
|
43
|
-
if (descriptor != nil && [descriptor respondsToSelector:selector]) {
|
|
44
|
-
return descriptor;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
throw NativeSimUnavailableError("selector", kStartRecordingSelector, CoreSimulatorFrameworkVersion());
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
} // namespace
|
|
51
|
-
|
|
52
11
|
BOOL StartVideoRecording(id device, NSString* displayId, VideoMaskPolicy mask, NSDictionary* assetWriterOutputSettings,
|
|
53
12
|
NSString* outputFile, dispatch_queue_t queue, void (^handler)(NSError*), NSError** error) {
|
|
54
|
-
id captureService =
|
|
13
|
+
id captureService = ResolveScreenCaptureService(device, error);
|
|
55
14
|
if (captureService == nil) {
|
|
56
15
|
return NO;
|
|
57
16
|
}
|
|
@@ -71,7 +30,7 @@ BOOL StartVideoRecording(id device, NSString* displayId, VideoMaskPolicy mask, N
|
|
|
71
30
|
}
|
|
72
31
|
|
|
73
32
|
BOOL StopVideoRecording(id device, dispatch_queue_t queue, void (^handler)(NSError*), NSError** error) {
|
|
74
|
-
id captureService =
|
|
33
|
+
id captureService = ResolveScreenCaptureService(device, error);
|
|
75
34
|
if (captureService == nil) {
|
|
76
35
|
return NO;
|
|
77
36
|
}
|
|
@@ -17,6 +17,13 @@ struct VideoEncoderOptions {
|
|
|
17
17
|
NSString* displayId = nil;
|
|
18
18
|
double fps = 60.0;
|
|
19
19
|
int bitrate = 4000000;
|
|
20
|
+
|
|
21
|
+
// Freezes the encoded frame size at whatever it is on the first frame, forever — a rotation
|
|
22
|
+
// afterward letterboxes into that size instead of resizing the session to match. Internal-only
|
|
23
|
+
// (not JS-configurable): AVRecordingSession forces this on, since its AVAssetWriter track's
|
|
24
|
+
// dimensions are fixed for the file's life (see CLAUDE.md); a live stream has no such constraint
|
|
25
|
+
// and keeps the default (false), matching its own decoded dimensions to the actual rotation.
|
|
26
|
+
bool fixedFrameSize = false;
|
|
20
27
|
};
|
|
21
28
|
|
|
22
29
|
// Polls the live display IOSurface (sim_screenshot.h) on a serial queue and encodes changed
|