@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.
@@ -1,15 +1,19 @@
1
1
  #include "video_encoder.h"
2
2
 
3
+ #import <CoreImage/CoreImage.h>
3
4
  #import <CoreVideo/CoreVideo.h>
4
5
  #import <IOSurface/IOSurface.h>
5
6
  #import <VideoToolbox/VideoToolbox.h>
6
7
 
7
8
  #include <algorithm>
8
9
  #include <atomic>
10
+ #include <cmath>
11
+ #include <memory>
9
12
 
10
13
  #include "monotonic_clock.h"
11
14
  #include "nserror_bridge.h"
12
15
  #include "safe_dispatch.h"
16
+ #include "sim_orientation.h"
13
17
  #include "sim_screenshot.h"
14
18
 
15
19
  namespace coresim {
@@ -81,6 +85,79 @@ void AppendParameterSets(std::vector<uint8_t>& out, CMFormatDescriptionRef forma
81
85
  }
82
86
  }
83
87
 
88
+ // Clockwise degrees to visually rotate a captured frame to correct for `orientation` — verified
89
+ // empirically against a live device, not assumed from the enum names (see CLAUDE.md).
90
+ int RotationDegreesForOrientation(int32_t orientation) {
91
+ switch (orientation) {
92
+ case 2:
93
+ return 180;
94
+ case 3:
95
+ return 270;
96
+ case 4:
97
+ return 90;
98
+ default:
99
+ return 0;
100
+ }
101
+ }
102
+
103
+ // A rotated copy of `surface`'s frame, via CoreImage — the raw surface itself never reflects a live
104
+ // rotation (see CLAUDE.md). Caller owns the result (CVPixelBufferRelease). Returns nullptr on any
105
+ // (transient) failure.
106
+ //
107
+ // `canvasWidth`/`canvasHeight` (both 0, or both positive): 0 sizes the output to the rotated
108
+ // content itself (swapped at 90/270 — the default, used for streaming). A positive size instead
109
+ // freezes the output at that size regardless of rotation, letterboxing the (possibly
110
+ // differently-shaped) rotated content into it — required for AVRecordingSession, whose
111
+ // AVAssetWriter track's dimensions are fixed for the file's life once the first frame is appended;
112
+ // resizing later frames instead of letterboxing them corrupts playback (squashed video — see
113
+ // CLAUDE.md).
114
+ CVPixelBufferRef RotatedPixelBuffer(IOSurfaceRef surface, int degrees, CIContext* context, int32_t canvasWidth,
115
+ int32_t canvasHeight) {
116
+ CIImage* image = [CIImage imageWithIOSurface:surface];
117
+ if (image == nil) {
118
+ return nullptr;
119
+ }
120
+ // CoreImage is Y-up, unlike a raster's Y-down row order, so a visual CW rotation needs a
121
+ // negative angle here (verified against ground truth, not just derived — see CLAUDE.md). The
122
+ // translation afterward re-zeroes the extent's origin so CIContext renders it at (0, 0).
123
+ CGAffineTransform rotate = CGAffineTransformMakeRotation(-degrees * M_PI / 180.0);
124
+ CIImage* rotated = [image imageByApplyingTransform:rotate];
125
+ CIImage* normalized = [rotated
126
+ imageByApplyingTransform:CGAffineTransformMakeTranslation(-rotated.extent.origin.x, -rotated.extent.origin.y)];
127
+
128
+ BOOL letterbox = canvasWidth > 0 && canvasHeight > 0;
129
+ size_t width =
130
+ letterbox ? static_cast<size_t>(canvasWidth) : static_cast<size_t>(std::lround(normalized.extent.size.width));
131
+ size_t height =
132
+ letterbox ? static_cast<size_t>(canvasHeight) : static_cast<size_t>(std::lround(normalized.extent.size.height));
133
+ if (width == 0 || height == 0) {
134
+ return nullptr;
135
+ }
136
+
137
+ CIImage* fitted = normalized;
138
+ CGRect bounds = normalized.extent;
139
+ if (letterbox) {
140
+ double scale = std::min(width / normalized.extent.size.width, height / normalized.extent.size.height);
141
+ CIImage* scaled = [normalized imageByApplyingTransform:CGAffineTransformMakeScale(scale, scale)];
142
+ double dx = (width - scaled.extent.size.width) / 2.0 - scaled.extent.origin.x;
143
+ double dy = (height - scaled.extent.size.height) / 2.0 - scaled.extent.origin.y;
144
+ CIImage* centered = [scaled imageByApplyingTransform:CGAffineTransformMakeTranslation(dx, dy)];
145
+ bounds = CGRectMake(0, 0, width, height);
146
+ // Composited over black so the letterboxed margins render as black bars, not undefined content
147
+ // (a fresh CVPixelBuffer's contents aren't otherwise guaranteed zeroed).
148
+ fitted = [centered imageByCompositingOverImage:[CIImage imageWithColor:[CIColor colorWithRed:0 green:0 blue:0]]];
149
+ }
150
+
151
+ CVPixelBufferRef pixelBuffer = nullptr;
152
+ CVReturn status =
153
+ CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_32BGRA, nullptr, &pixelBuffer);
154
+ if (status != kCVReturnSuccess || pixelBuffer == nullptr) {
155
+ return nullptr;
156
+ }
157
+ [context render:fitted toCVPixelBuffer:pixelBuffer bounds:bounds colorSpace:nil];
158
+ return pixelBuffer;
159
+ }
160
+
84
161
  } // namespace
85
162
 
86
163
  bool IsKeyFrame(CMSampleBufferRef sampleBuffer) {
@@ -118,6 +195,9 @@ class VideoFrameEncoder::Impl {
118
195
  onEnd_(std::move(onEnd)),
119
196
  sharedClockOrigin_(sharedClockOrigin) {
120
197
  queue_ = dispatch_queue_create("io.appium.coresim.videoEncoder", DISPATCH_QUEUE_SERIAL);
198
+ // Persistent, like JpegStreamSession's own (see sim_jpeg_stream.mm) — a continuous stream, not
199
+ // CaptureScreenshot's deliberately one-shot context.
200
+ ciContext_ = [CIContext contextWithOptions:nil];
121
201
  }
122
202
 
123
203
  ~Impl() { Stop(); }
@@ -133,10 +213,14 @@ class VideoFrameEncoder::Impl {
133
213
  throw NSErrorException(MakeError(4, @"The device's display surface is not available yet"));
134
214
  }
135
215
  IOSurfaceRef surface = (__bridge IOSurfaceRef)surfaceObj;
216
+ // One-time blocking read so the first frame is already correctly oriented, before the
217
+ // periodic poll below even starts.
218
+ int32_t orientation = BlockingReadOrientation();
219
+ polledOrientation_->store(orientation, std::memory_order_relaxed);
136
220
  // Set up synchronously (not lazily on the first Tick()) so a setup failure rejects Start()
137
221
  // directly rather than only reaching onError, which the caller may not be listening for yet.
138
222
  NSError* setupError = nil;
139
- if (!SetUpSession(surface, &setupError)) {
223
+ if (!SetUpSession(surface, orientation, &setupError)) {
140
224
  throw NSErrorException(setupError);
141
225
  }
142
226
  // Must be set before EncodeSurface below — both it and HandleEncodedSample measure elapsed
@@ -156,7 +240,7 @@ class VideoFrameEncoder::Impl {
156
240
  running_ = true;
157
241
  bool encoded = false;
158
242
  try {
159
- encoded = EncodeSurface(surface);
243
+ encoded = EncodeSurface(surface, orientation);
160
244
  } catch (...) {
161
245
  running_ = false;
162
246
  VTCompressionSessionInvalidate(session_);
@@ -165,11 +249,13 @@ class VideoFrameEncoder::Impl {
165
249
  throw;
166
250
  }
167
251
  // Only commit the seed once a frame was actually submitted — a transient pixel-buffer
168
- // creation failure (EncodeSurface returning false) otherwise leaves lastSeed_ at its default
169
- // 0, so the first Tick() sees the real seed as "changed" and retries automatically instead of
170
- // the stream going silent forever on a display that never changes again.
252
+ // creation failure (EncodeSurface returning false) otherwise leaves lastSeed_/
253
+ // hasEncodedSinceSetup_ at their defaults, so the first Tick() sees this as still needing a
254
+ // frame and retries automatically instead of the stream going silent forever on a display
255
+ // that never changes again.
171
256
  if (encoded) {
172
257
  lastSeed_ = IOSurfaceGetSeed(surface);
258
+ hasEncodedSinceSetup_ = true;
173
259
  }
174
260
 
175
261
  double interval = 1.0 / std::max(options_.fps, 1.0);
@@ -184,6 +270,7 @@ class VideoFrameEncoder::Impl {
184
270
  });
185
271
  timer_ = timer;
186
272
  dispatch_resume(timer_);
273
+ StartOrientationPoll();
187
274
  }
188
275
 
189
276
  // Callable from any thread except `queue_` itself (would deadlock on the dispatch_sync below).
@@ -191,6 +278,7 @@ class VideoFrameEncoder::Impl {
191
278
  if (!running_.exchange(false)) {
192
279
  return; // idempotent
193
280
  }
281
+ StopOrientationPoll();
194
282
  if (timer_ != nullptr) {
195
283
  dispatch_source_cancel(timer_);
196
284
  // Blocks until any in-flight Tick() finishes — by then running_ is already false, so it
@@ -205,12 +293,53 @@ class VideoFrameEncoder::Impl {
205
293
  void RequestKeyFrame() { forceKeyFrame_ = true; }
206
294
 
207
295
  private:
296
+ int32_t BlockingReadOrientation() {
297
+ dispatch_semaphore_t sema = dispatch_semaphore_create(0);
298
+ __block int32_t result = 1;
299
+ ReadGuestOrientation(device_, ^(int32_t orientation) {
300
+ result = orientation;
301
+ dispatch_semaphore_signal(sema);
302
+ });
303
+ dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
304
+ return result;
305
+ }
306
+
307
+ // Keeps polledOrientation_ fresh against a rotation from any source (see CLAUDE.md). Off queue_ by
308
+ // design, not because it's slow — Tick() must stay cheap regardless.
309
+ void StartOrientationPoll() {
310
+ std::shared_ptr<std::atomic<int32_t>> cell = polledOrientation_;
311
+ id device = device_;
312
+ dispatch_queue_t pollQueue =
313
+ dispatch_queue_create("io.appium.coresim.videoEncoder.orientationPoll", DISPATCH_QUEUE_SERIAL);
314
+ dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, pollQueue);
315
+ constexpr int64_t kPollIntervalSeconds = 3;
316
+ // Starts one interval out — Start() already seeded polledOrientation_ synchronously.
317
+ dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, kPollIntervalSeconds * NSEC_PER_SEC),
318
+ kPollIntervalSeconds * NSEC_PER_SEC, NSEC_PER_SEC);
319
+ // `cell`, not `this` — see polledOrientation_'s own comment.
320
+ dispatch_source_set_event_handler(timer, ^{
321
+ ReadGuestOrientation(device, ^(int32_t orientation) {
322
+ cell->store(orientation, std::memory_order_relaxed);
323
+ });
324
+ });
325
+ orientationPollTimer_ = timer;
326
+ dispatch_resume(orientationPollTimer_);
327
+ }
328
+
329
+ void StopOrientationPoll() {
330
+ if (orientationPollTimer_ != nullptr) {
331
+ dispatch_source_cancel(orientationPollTimer_);
332
+ orientationPollTimer_ = nullptr;
333
+ }
334
+ }
335
+
208
336
  // Same as Stop() minus the dispatch_sync barrier — only safe from within Tick() itself, already
209
337
  // serialized on `queue_`; would race a concurrent Tick() from any other thread.
210
338
  void StopFromQueue() {
211
339
  if (!running_.exchange(false)) {
212
340
  return; // idempotent — e.g. an external Stop() already won this race
213
341
  }
342
+ StopOrientationPoll();
214
343
  if (timer_ != nullptr) {
215
344
  dispatch_source_cancel(timer_);
216
345
  timer_ = nullptr;
@@ -266,16 +395,46 @@ class VideoFrameEncoder::Impl {
266
395
  return; // transient — the connection may not have a frame ready yet, try again next tick
267
396
  }
268
397
  IOSurfaceRef surface = (__bridge IOSurfaceRef)surfaceObj;
398
+ int32_t orientation = polledOrientation_->load(std::memory_order_relaxed);
399
+ int32_t rawWidth = static_cast<int32_t>(IOSurfaceGetWidth(surface));
400
+ int32_t rawHeight = static_cast<int32_t>(IOSurfaceGetHeight(surface));
401
+ if (options_.fixedFrameSize) {
402
+ // The session's encoded size is frozen at whatever it was on the first frame (see
403
+ // VideoEncoderOptions) — EncodeSurface letterboxes into it below, so a raw resize or
404
+ // rotation just needs this bookkeeping updated, never a session rebuild. But the raw
405
+ // surface's seed alone doesn't change on a rotation of an otherwise-static screen (see
406
+ // CLAUDE.md) — force one frame through below despite the seed-equality skip, or a
407
+ // rotation could go uncorrected until the screen's content happens to change on its own.
408
+ if (rawWidth != rawSurfaceWidth_ || rawHeight != rawSurfaceHeight_ || orientation != sessionOrientation_) {
409
+ hasEncodedSinceSetup_ = false;
410
+ }
411
+ rawSurfaceWidth_ = rawWidth;
412
+ rawSurfaceHeight_ = rawHeight;
413
+ sessionOrientation_ = orientation;
414
+ } else if (rawWidth != rawSurfaceWidth_ || rawHeight != rawSurfaceHeight_ ||
415
+ orientation != sessionOrientation_) {
416
+ // Raw resize (some CoreSimulator versions) or an orientation change — either needs the
417
+ // session rebuilt for the new effective dimensions.
418
+ NSError* resizeError = nil;
419
+ if (!RecreateSessionForResize(surface, orientation, &resizeError)) {
420
+ if (onError_) {
421
+ onError_(resizeError);
422
+ }
423
+ StopFromQueue();
424
+ return;
425
+ }
426
+ }
269
427
  uint32_t seed = IOSurfaceGetSeed(surface);
270
- if (seed == lastSeed_) {
271
- return; // unchanged since the last tick — mirrors CoreSimulator's own recorder, which
272
- // only encodes a frame when the display actually changes (see CLAUDE.md)
428
+ // A differently-shaped replacement surface can collide with the old seed value (confirmed
429
+ // empirically), so a just-rebuilt session must submit one frame before this skip applies.
430
+ if (seed == lastSeed_ && hasEncodedSinceSetup_) {
431
+ return; // unchanged since the last tick (see CLAUDE.md)
273
432
  }
274
- // Only commit the new seed once EncodeSurface actually submits it — a transient failure
275
- // (pixel-buffer creation) must leave lastSeed_ stale so the next tick retries this same
276
- // frame instead of silently going quiet until the display changes again.
277
- if (EncodeSurface(surface)) {
433
+ // Leave lastSeed_/hasEncodedSinceSetup_ stale on a transient EncodeSurface failure so the
434
+ // next tick retries.
435
+ if (EncodeSurface(surface, orientation)) {
278
436
  lastSeed_ = seed;
437
+ hasEncodedSinceSetup_ = true;
279
438
  }
280
439
  } catch (const std::exception& e) {
281
440
  // Without this, an exception here (e.g. a dropped display-proxy connection) would escape
@@ -288,9 +447,34 @@ class VideoFrameEncoder::Impl {
288
447
  }
289
448
  }
290
449
 
291
- bool SetUpSession(IOSurfaceRef surface, NSError** error) {
292
- int32_t width = static_cast<int32_t>(IOSurfaceGetWidth(surface));
293
- int32_t height = static_cast<int32_t>(IOSurfaceGetHeight(surface));
450
+ // Tears down `session_` (if any) and recreates it for `surface`'s current dimensions, rotated
451
+ // for `orientation`. No explicit RequestKeyFrame() needed — a fresh session's first frame is a
452
+ // keyframe regardless.
453
+ bool RecreateSessionForResize(IOSurfaceRef surface, int32_t orientation, NSError** error) {
454
+ if (session_ != nullptr) {
455
+ VTCompressionSessionCompleteFrames(session_, kCMTimeInvalid);
456
+ VTCompressionSessionInvalidate(session_);
457
+ CFRelease(session_);
458
+ session_ = nullptr;
459
+ }
460
+ return SetUpSession(surface, orientation, error);
461
+ }
462
+
463
+ bool SetUpSession(IOSurfaceRef surface, int32_t orientation, NSError** error) {
464
+ int32_t rawWidth = static_cast<int32_t>(IOSurfaceGetWidth(surface));
465
+ int32_t rawHeight = static_cast<int32_t>(IOSurfaceGetHeight(surface));
466
+ rawSurfaceWidth_ = rawWidth;
467
+ rawSurfaceHeight_ = rawHeight;
468
+ sessionOrientation_ = orientation;
469
+ // A fresh/rebuilt session hasn't sent VideoToolbox a frame yet — Tick()'s own seed-equality
470
+ // skip must not apply until it has (see Tick()'s own comment on why).
471
+ hasEncodedSinceSetup_ = false;
472
+ int degrees = RotationDegreesForOrientation(orientation);
473
+ bool swapped = degrees == 90 || degrees == 270;
474
+ int32_t width = swapped ? rawHeight : rawWidth;
475
+ int32_t height = swapped ? rawWidth : rawHeight;
476
+ sessionWidth_ = width;
477
+ sessionHeight_ = height;
294
478
  CMVideoCodecType codecType =
295
479
  options_.codec == VideoStreamCodec::kHEVC ? kCMVideoCodecType_HEVC : kCMVideoCodecType_H264;
296
480
  OSStatus status = VTCompressionSessionCreate(kCFAllocatorDefault, width, height, codecType, nullptr, nullptr,
@@ -332,11 +516,28 @@ class VideoFrameEncoder::Impl {
332
516
  // Returns whether a frame was actually submitted to the encoder — false for a transient
333
517
  // pixel-buffer creation failure the caller should retry, as opposed to a real encode failure
334
518
  // (thrown, not returned, since that tears down the whole session).
335
- bool EncodeSurface(IOSurfaceRef surface) {
519
+ bool EncodeSurface(IOSurfaceRef surface, int32_t orientation) {
520
+ int degrees = RotationDegreesForOrientation(orientation);
521
+ int32_t rawWidth = static_cast<int32_t>(IOSurfaceGetWidth(surface));
522
+ int32_t rawHeight = static_cast<int32_t>(IOSurfaceGetHeight(surface));
523
+ // Zero-copy only applies when the raw surface already matches the session as-is — not just
524
+ // degrees == 0, since fixedFrameSize mode can also need letterboxing at zero rotation (a raw
525
+ // resize with no orientation change).
526
+ bool matchesSessionAsIs = degrees == 0 && rawWidth == sessionWidth_ && rawHeight == sessionHeight_;
336
527
  CVPixelBufferRef pixelBuffer = nullptr;
337
- CVReturn cvStatus = CVPixelBufferCreateWithIOSurface(kCFAllocatorDefault, surface, nullptr, &pixelBuffer);
338
- if (cvStatus != kCVReturnSuccess || pixelBuffer == nullptr) {
339
- return false; // transient — try again next tick rather than tearing down the whole session
528
+ if (matchesSessionAsIs) {
529
+ // The common case: zero-copy, exactly as before orientation correction existed.
530
+ CVReturn cvStatus = CVPixelBufferCreateWithIOSurface(kCFAllocatorDefault, surface, nullptr, &pixelBuffer);
531
+ if (cvStatus != kCVReturnSuccess || pixelBuffer == nullptr) {
532
+ return false; // transient — try again next tick rather than tearing down the whole session
533
+ }
534
+ } else {
535
+ int32_t canvasWidth = options_.fixedFrameSize ? sessionWidth_ : 0;
536
+ int32_t canvasHeight = options_.fixedFrameSize ? sessionHeight_ : 0;
537
+ pixelBuffer = RotatedPixelBuffer(surface, degrees, ciContext_, canvasWidth, canvasHeight);
538
+ if (pixelBuffer == nullptr) {
539
+ return false; // transient, same contract as the zero-copy path above
540
+ }
340
541
  }
341
542
  CMTime pts = CMTimeMake(static_cast<int64_t>((MonotonicSeconds() - startTime_) * 1000000), 1000000);
342
543
  NSDictionary* frameProperties = nil;
@@ -390,13 +591,32 @@ class VideoFrameEncoder::Impl {
390
591
  dispatch_queue_t queue_ = nullptr;
391
592
  dispatch_source_t timer_ = nullptr;
392
593
  VTCompressionSessionRef session_ = nullptr;
594
+ CIContext* ciContext_ = nil;
595
+ // The session's own post-rotation encode dimensions (what VTCompressionSessionCreate got).
596
+ int32_t sessionWidth_ = 0;
597
+ int32_t sessionHeight_ = 0;
598
+ // The raw IOSurface's dimensions as of the last SetUpSession — kept separate from
599
+ // sessionWidth_/sessionHeight_ so a rotation intentionally making them differ isn't mistaken for
600
+ // a raw resize.
601
+ int32_t rawSurfaceWidth_ = 0;
602
+ int32_t rawSurfaceHeight_ = 0;
603
+ // Orientation last baked into session_ — Tick() rebuilds when polledOrientation_ moves past this.
604
+ int32_t sessionOrientation_ = 1;
393
605
  uint32_t lastSeed_ = 0;
606
+ // Whether session_ has submitted a frame yet — see Tick()'s seed-equality skip.
607
+ bool hasEncodedSinceSetup_ = false;
394
608
  double startTime_ = 0;
395
609
  std::atomic<bool> running_{false};
396
610
  // Set by HandleEncodedSample (possibly off queue_) on an encoder failure, consumed by the next
397
611
  // Tick() (on queue_) — see both for why teardown can't just happen inline there.
398
612
  std::atomic<bool> pendingErrorTeardown_{false};
399
613
  std::atomic<bool> forceKeyFrame_{false};
614
+
615
+ // Last orientation StartOrientationPoll read; Tick() only ever reads this cheaply. A shared_ptr
616
+ // (not a plain member) since a poll's guest spawn can still be in flight when Stop()/~Impl runs
617
+ // — its completion must never touch a possibly-dead `this` (see CLAUDE.md).
618
+ std::shared_ptr<std::atomic<int32_t>> polledOrientation_ = std::make_shared<std::atomic<int32_t>>(1);
619
+ dispatch_source_t orientationPollTimer_ = nullptr;
400
620
  };
401
621
 
402
622
  VideoFrameEncoder::VideoFrameEncoder(id device, VideoEncoderOptions options,
@@ -46,9 +46,11 @@ import {
46
46
  getAppearance,
47
47
  getContentSize,
48
48
  getIncreaseContrast,
49
+ getOrientation,
49
50
  setAppearance,
50
51
  setContentSize,
51
52
  setIncreaseContrast,
53
+ setOrientation,
52
54
  } from './commands/ui.js';
53
55
  import {isVideoRecording, startVideoRecording, stopVideoRecording} from './commands/video-recording.js';
54
56
  import {startVideoStream} from './commands/video-stream.js';
@@ -229,6 +231,8 @@ Object.assign(NativeSimctl.prototype, {
229
231
  setIncreaseContrast,
230
232
  getContentSize,
231
233
  setContentSize,
234
+ setOrientation,
235
+ getOrientation,
232
236
 
233
237
  // permissions
234
238
  grantPermission,
package/src/types.ts CHANGED
@@ -7,6 +7,14 @@ export enum SimDeviceState {
7
7
  ShuttingDown = 4,
8
8
  }
9
9
 
10
+ /** Device orientations `setOrientation` accepts — the raw GSEvent wire values (see CLAUDE.md). */
11
+ export enum DeviceOrientation {
12
+ Portrait = 1,
13
+ PortraitUpsideDown = 2,
14
+ LandscapeRight = 3,
15
+ LandscapeLeft = 4,
16
+ }
17
+
10
18
  /** A device in the default device set, as returned by `NativeSimctl.getDevices()`/`createDevice()`. */
11
19
  export interface SimDeviceInfo {
12
20
  /** e.g. `"D768CB90-CBB4-4557-82F6-B89E1CD0E80B"`. */
@@ -436,6 +444,8 @@ export interface NativeDeviceHandle {
436
444
  openUrl(url: string): Promise<void>;
437
445
  setLocation(latitude: number, longitude: number): Promise<void>;
438
446
  clearLocation(): Promise<void>;
447
+ setOrientation(orientation: number): Promise<void>;
448
+ getOrientation(): Promise<number>;
439
449
  sendPushNotification(bundleId: string, payload: PushNotificationPayload): Promise<void>;
440
450
  addCertificate(path: string, trustAsRoot: boolean): Promise<void>;
441
451
  resetKeychain(): Promise<void>;