@appium/coresim 1.4.0 → 1.6.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 (58) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +14 -0
  3. package/binding.gyp +14 -1
  4. package/lib/src/commands/video-recording.d.ts +57 -0
  5. package/lib/src/commands/video-recording.d.ts.map +1 -0
  6. package/lib/src/commands/video-recording.js +123 -0
  7. package/lib/src/commands/video-recording.js.map +1 -0
  8. package/lib/src/commands/video-stream.d.ts +92 -0
  9. package/lib/src/commands/video-stream.d.ts.map +1 -0
  10. package/lib/src/commands/video-stream.js +242 -0
  11. package/lib/src/commands/video-stream.js.map +1 -0
  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 +4 -0
  17. package/lib/src/native-simctl.d.ts.map +1 -1
  18. package/lib/src/native-simctl.js +21 -1
  19. package/lib/src/native-simctl.js.map +1 -1
  20. package/lib/src/types.d.ts +134 -0
  21. package/lib/src/types.d.ts.map +1 -1
  22. package/lib/src/utils/index.d.ts +1 -1
  23. package/lib/src/utils/index.d.ts.map +1 -1
  24. package/lib/src/utils/index.js +1 -1
  25. package/lib/src/utils/index.js.map +1 -1
  26. package/lib/src/utils/run-catching.d.ts +4 -0
  27. package/lib/src/utils/run-catching.d.ts.map +1 -1
  28. package/lib/src/utils/run-catching.js +11 -0
  29. package/lib/src/utils/run-catching.js.map +1 -1
  30. package/package.json +1 -1
  31. package/prebuilds/darwin-arm64/@appium+coresim.node +0 -0
  32. package/src/commands/video-recording.ts +158 -0
  33. package/src/commands/video-stream.ts +283 -0
  34. package/src/coresim.mm +708 -1
  35. package/src/index.ts +4 -0
  36. package/src/native/audio_encoder.h +65 -0
  37. package/src/native/audio_encoder.mm +270 -0
  38. package/src/native/av_recording.h +52 -0
  39. package/src/native/av_recording.mm +424 -0
  40. package/src/native/av_stream.h +70 -0
  41. package/src/native/av_stream.mm +217 -0
  42. package/src/native/monotonic_clock.h +20 -0
  43. package/src/native/sim_audio_tap.h +67 -0
  44. package/src/native/sim_audio_tap.mm +350 -0
  45. package/src/native/sim_process.h +10 -0
  46. package/src/native/sim_process.mm +81 -18
  47. package/src/native/sim_screenshot.h +10 -0
  48. package/src/native/sim_screenshot.mm +11 -6
  49. package/src/native/sim_video_recording.h +27 -0
  50. package/src/native/sim_video_recording.mm +87 -0
  51. package/src/native/sim_video_stream.h +64 -0
  52. package/src/native/sim_video_stream.mm +71 -0
  53. package/src/native/video_encoder.h +76 -0
  54. package/src/native/video_encoder.mm +417 -0
  55. package/src/native-simctl.ts +22 -1
  56. package/src/types.ts +149 -0
  57. package/src/utils/index.ts +1 -1
  58. package/src/utils/run-catching.ts +11 -0
@@ -6,6 +6,7 @@
6
6
  #include <sys/un.h>
7
7
 
8
8
  #include <cstring>
9
+ #include <string>
9
10
  #include <vector>
10
11
 
11
12
  namespace coresim {
@@ -25,48 +26,96 @@ NSString* ExecutablePath(pid_t pid) {
25
26
  return size > 0 ? [NSString stringWithUTF8String:pathBuf] : nil;
26
27
  }
27
28
 
28
- // argv[1] via the unprivileged KERN_PROCARGS2 sysctl (same as `ps`/`lsof`) — for `launchd_sim`
29
- // that's its bootstrap plist path, which embeds the device's UDID.
30
- NSString* FirstArgument(pid_t pid) {
29
+ // Layout: argc, exec path, NUL padding, then argv[0..argc-1], then envp[...], each NUL-terminated
30
+ // (envp has no guaranteed empty-string sentinel within the buffer — just runs to `size`).
31
+ std::vector<char> ProcArgs2(pid_t pid) {
31
32
  int mib[3] = {CTL_KERN, KERN_PROCARGS2, pid};
32
33
  size_t size = 0;
33
34
  if (sysctl(mib, 3, nullptr, &size, nullptr, 0) != 0 || size < sizeof(int)) {
34
- return nil;
35
+ return {};
35
36
  }
36
37
  std::vector<char> buffer(size);
37
38
  if (sysctl(mib, 3, buffer.data(), &size, nullptr, 0) != 0) {
39
+ return {};
40
+ }
41
+ return buffer;
42
+ }
43
+
44
+ const char* SkipExecPathAndPadding(const char* cursor, const char* end) {
45
+ cursor += strnlen(cursor, static_cast<size_t>(end - cursor)); // skip the exec path
46
+ while (cursor < end && *cursor == '\0') cursor++; // skip the NUL padding
47
+ return cursor;
48
+ }
49
+
50
+ // Advances past `count` further NUL-terminated entries (e.g. argv), landing just after the last
51
+ // one's terminator — or at `end` if there weren't that many.
52
+ const char* SkipEntries(const char* cursor, const char* end, int count) {
53
+ for (int i = 0; i < count && cursor < end; i++) {
54
+ cursor += strnlen(cursor, static_cast<size_t>(end - cursor));
55
+ if (cursor < end) cursor++;
56
+ }
57
+ return cursor;
58
+ }
59
+
60
+ // argv[1] via the unprivileged KERN_PROCARGS2 sysctl (same as `ps`/`lsof`) — for `launchd_sim`
61
+ // that's its bootstrap plist path, which embeds the device's UDID.
62
+ NSString* FirstArgument(pid_t pid) {
63
+ std::vector<char> buffer = ProcArgs2(pid);
64
+ if (buffer.empty()) {
38
65
  return nil;
39
66
  }
40
- // Layout: argc, exec path, NUL padding, then argv[0], argv[1], ..., each NUL-terminated.
41
67
  int argc = 0;
42
68
  std::memcpy(&argc, buffer.data(), sizeof(argc));
43
- const char* cursor = buffer.data() + sizeof(argc);
44
- const char* end = buffer.data() + size;
45
- cursor += strnlen(cursor, static_cast<size_t>(end - cursor)); // skip the exec path
46
- while (cursor < end && *cursor == '\0') cursor++; // skip the NUL padding
47
- cursor += strnlen(cursor, static_cast<size_t>(end - cursor)); // skip argv[0]
48
- if (cursor < end) cursor++; // skip its NUL terminator
69
+ const char* end = buffer.data() + buffer.size();
70
+ const char* cursor = SkipEntries(SkipExecPathAndPadding(buffer.data() + sizeof(argc), end), end, 1);
49
71
  if (argc < 2 || cursor >= end) {
50
72
  return nil;
51
73
  }
52
74
  return [NSString stringWithUTF8String:cursor];
53
75
  }
54
76
 
55
- // Each booted simulator has its own `launchd_sim`; finds the one owning `udid`.
56
- pid_t FindLaunchdSimPid(NSString* udid) {
77
+ // The value of `key=...` in `pid`'s environment, past the end of its argv — same sysctl as
78
+ // FirstArgument, walked further.
79
+ NSString* FindEnvValue(pid_t pid, NSString* key) {
80
+ std::vector<char> buffer = ProcArgs2(pid);
81
+ if (buffer.empty()) {
82
+ return nil;
83
+ }
84
+ int argc = 0;
85
+ std::memcpy(&argc, buffer.data(), sizeof(argc));
86
+ const char* end = buffer.data() + buffer.size();
87
+ const char* cursor = SkipEntries(SkipExecPathAndPadding(buffer.data() + sizeof(argc), end), end, argc);
88
+ std::string prefix = std::string(key.UTF8String) + "=";
89
+ while (cursor < end && *cursor != '\0') {
90
+ size_t entryLen = strnlen(cursor, static_cast<size_t>(end - cursor));
91
+ if (entryLen > prefix.size() && std::memcmp(cursor, prefix.data(), prefix.size()) == 0) {
92
+ return [NSString stringWithUTF8String:cursor + prefix.size()];
93
+ }
94
+ cursor += entryLen;
95
+ if (cursor < end) cursor++;
96
+ }
97
+ return nil;
98
+ }
99
+
100
+ // Every live pid on the system, via the unprivileged proc_listpids sysctl.
101
+ std::vector<pid_t> AllPids() {
57
102
  int neededBytes = proc_listpids(PROC_ALL_PIDS, 0, nullptr, 0);
58
103
  if (neededBytes <= 0) {
59
- return -1;
104
+ return {};
60
105
  }
61
106
  // Headroom for processes started between the sizing call above and the listing call below.
62
107
  std::vector<pid_t> pids(neededBytes / sizeof(pid_t) + 64);
63
108
  int bytes = proc_listpids(PROC_ALL_PIDS, 0, pids.data(), static_cast<int>(pids.size() * sizeof(pid_t)));
64
109
  if (bytes <= 0) {
65
- return -1;
110
+ return {};
66
111
  }
67
- int count = bytes / static_cast<int>(sizeof(pid_t));
68
- for (int i = 0; i < count; i++) {
69
- pid_t pid = pids[i];
112
+ pids.resize(static_cast<size_t>(bytes) / sizeof(pid_t));
113
+ return pids;
114
+ }
115
+
116
+ // Each booted simulator has its own `launchd_sim`; finds the one owning `udid`.
117
+ pid_t FindLaunchdSimPid(NSString* udid) {
118
+ for (pid_t pid : AllPids()) {
70
119
  if (pid <= 0) {
71
120
  continue;
72
121
  }
@@ -136,4 +185,18 @@ NSString* FindWebInspectorSocket(NSString* udid, NSError** error) {
136
185
  return socketPath;
137
186
  }
138
187
 
188
+ std::vector<pid_t> FindGuestProcessPids(NSString* udid) {
189
+ std::vector<pid_t> result;
190
+ for (pid_t pid : AllPids()) {
191
+ if (pid <= 0) {
192
+ continue;
193
+ }
194
+ NSString* value = FindEnvValue(pid, @"SIMULATOR_UDID");
195
+ if (value != nil && [value caseInsensitiveCompare:udid] == NSOrderedSame) {
196
+ result.push_back(pid);
197
+ }
198
+ }
199
+ return result;
200
+ }
201
+
139
202
  } // namespace coresim
@@ -12,6 +12,16 @@ namespace coresim {
12
12
  // array means the device has an IO client but no renderable display.
13
13
  NSArray<NSDictionary*>* ListDisplays(id device, NSError** error);
14
14
 
15
+ // Resolves the display descriptor CaptureScreenshot itself reads from, without capturing a
16
+ // screenshot — same displayId/fallback semantics, exposed for callers that need the descriptor
17
+ // object itself (e.g. StartVideoRecording's `screen` argument).
18
+ id ResolveCaptureDisplay(id device, NSString* displayId, NSError** error);
19
+
20
+ // The descriptor's current framebuffer as an `IOSurfaceRef` (bridge-cast the returned `id`).
21
+ // Returns nil if not available yet (e.g. connection just dropped) — not an error, since that can
22
+ // legitimately happen from one call to the next on a live device.
23
+ id CurrentDisplaySurface(id descriptor);
24
+
15
25
  enum class ScreenshotFormat { kPNG, kJPEG };
16
26
 
17
27
  // Captures a display as an image, reading the same in-process framebuffer surface `simctl io
@@ -144,6 +144,16 @@ NSString* const kJPEGUTI = @"public.jpeg";
144
144
 
145
145
  } // namespace
146
146
 
147
+ id ResolveCaptureDisplay(id device, NSString* displayId, NSError** error) {
148
+ NSArray<NSDictionary*>* candidates = RenderableDisplayCandidates(device, error);
149
+ if (candidates == nil) {
150
+ return nil;
151
+ }
152
+ return ResolveDisplayDescriptor(candidates, displayId, error);
153
+ }
154
+
155
+ id CurrentDisplaySurface(id descriptor) { return RenderableSurface(descriptor); }
156
+
147
157
  NSArray<NSDictionary*>* ListDisplays(id device, NSError** error) {
148
158
  NSArray<NSDictionary*>* candidates = RenderableDisplayCandidates(device, error);
149
159
  if (candidates == nil) {
@@ -163,12 +173,7 @@ NSArray<NSDictionary*>* ListDisplays(id device, NSError** error) {
163
173
 
164
174
  NSData* CaptureScreenshot(id device, NSString* displayId, ScreenshotFormat format, NSNumber* jpegQualityPercent,
165
175
  NSError** error) {
166
- NSArray<NSDictionary*>* candidates = RenderableDisplayCandidates(device, error);
167
- if (candidates == nil) {
168
- return nil;
169
- }
170
-
171
- id descriptor = ResolveDisplayDescriptor(candidates, displayId, error);
176
+ id descriptor = ResolveCaptureDisplay(device, displayId, error);
172
177
  if (descriptor == nil) {
173
178
  return nil;
174
179
  }
@@ -0,0 +1,27 @@
1
+ #pragma once
2
+
3
+ #import <Foundation/Foundation.h>
4
+ #import <dispatch/dispatch.h>
5
+
6
+ namespace coresim {
7
+
8
+ // maskPolicy for a non-rectangular display. 0/1/2 = ignored/alpha/black, confirmed empirically —
9
+ // alpha renders identically to black (see CLAUDE.md).
10
+ enum class VideoMaskPolicy : long long {
11
+ kIgnored = 0,
12
+ kAlpha = 1,
13
+ kBlack = 2,
14
+ };
15
+
16
+ // Starts recording `displayId` (nil = primary) to `outputFile`, an absolute path, not a URL (see
17
+ // CLAUDE.md). Returns NO+*error on synchronous resolution failure (`handler` never called then);
18
+ // otherwise `handler` fires once the first frame is recorded, non-nil NSError on failure. Throws
19
+ // NativeSimUnavailableError instead if this CoreSimulator has no video capture service at all.
20
+ BOOL StartVideoRecording(id device, NSString* displayId, VideoMaskPolicy mask, NSDictionary* assetWriterOutputSettings,
21
+ NSString* outputFile, dispatch_queue_t queue, void (^handler)(NSError*), NSError** error);
22
+
23
+ // Stops the recording started by StartVideoRecording. Must not be called before its `handler` has
24
+ // already fired — see CLAUDE.md for the race that otherwise causes a silent empty-file failure.
25
+ BOOL StopVideoRecording(id device, dispatch_queue_t queue, void (^handler)(NSError*), NSError** error);
26
+
27
+ } // namespace coresim
@@ -0,0 +1,87 @@
1
+ #include "sim_video_recording.h"
2
+
3
+ #import <objc/message.h>
4
+
5
+ #include "objc_runtime.h"
6
+ #include "safe_dispatch.h"
7
+ #include "sim_screenshot.h"
8
+
9
+ namespace coresim {
10
+
11
+ namespace {
12
+
13
+ NSString* const kVideoRecordingErrorDomain = @"com.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
+ BOOL StartVideoRecording(id device, NSString* displayId, VideoMaskPolicy mask, NSDictionary* assetWriterOutputSettings,
53
+ NSString* outputFile, dispatch_queue_t queue, void (^handler)(NSError*), NSError** error) {
54
+ id captureService = ResolveVideoCaptureService(device, error);
55
+ if (captureService == nil) {
56
+ return NO;
57
+ }
58
+ id screen = ResolveCaptureDisplay(device, displayId, error);
59
+ if (screen == nil) {
60
+ return NO;
61
+ }
62
+ static const std::string kSelectorName =
63
+ "startRecordingFromScreen:maskPolicy:assetWriterOutputSettings:outputFile:completionQueue:completionHandler:";
64
+ SEL selector = NSSelectorFromString(@(kSelectorName.c_str()));
65
+ return SafeInvoke([&] {
66
+ using Fn = void (*)(id, SEL, id, long long, NSDictionary*, NSString*, dispatch_queue_t, void (^)(NSError*));
67
+ ((Fn)objc_msgSend)(captureService, selector, screen, static_cast<long long>(mask), assetWriterOutputSettings,
68
+ outputFile, queue, handler);
69
+ return YES;
70
+ });
71
+ }
72
+
73
+ BOOL StopVideoRecording(id device, dispatch_queue_t queue, void (^handler)(NSError*), NSError** error) {
74
+ id captureService = ResolveVideoCaptureService(device, error);
75
+ if (captureService == nil) {
76
+ return NO;
77
+ }
78
+ static const std::string kSelectorName = "stopRecordingWithCompletionQueue:completionHandler:";
79
+ SEL selector = NSSelectorFromString(@(kSelectorName.c_str()));
80
+ return SafeInvoke([&] {
81
+ using Fn = void (*)(id, SEL, dispatch_queue_t, void (^)(NSError*));
82
+ ((Fn)objc_msgSend)(captureService, selector, queue, handler);
83
+ return YES;
84
+ });
85
+ }
86
+
87
+ } // namespace coresim
@@ -0,0 +1,64 @@
1
+ #pragma once
2
+
3
+ #import <Foundation/Foundation.h>
4
+
5
+ #include <cstdint>
6
+ #include <functional>
7
+ #include <memory>
8
+ #include <vector>
9
+
10
+ #include "video_encoder.h"
11
+
12
+ namespace coresim {
13
+
14
+ // One encoded frame — Annex-B NAL units, concatenated. A keyframe's `data` has parameter sets
15
+ // (SPS/PPS, or VPS/SPS/PPS for HEVC) prepended, so every keyframe is self-decodable alone.
16
+ struct VideoAccessUnit {
17
+ std::vector<uint8_t> data;
18
+ bool isKeyFrame = false;
19
+ uint64_t sequence = 0;
20
+ // Microseconds since the stream started.
21
+ int64_t timestampMicros = 0;
22
+ };
23
+
24
+ // Polls the live display IOSurface (sim_screenshot.h) on a serial queue and encodes changed
25
+ // frames via the public VideoToolbox API — unlike StartVideoRecording's private, file-only
26
+ // recorder, this delivers access units live. Full thread-safety contract: see CLAUDE.md.
27
+ class VideoStreamSession {
28
+ public:
29
+ // `onAbortDelivery`, if set, is invoked by AbortDelivery() below — not called by this class on
30
+ // its own.
31
+ VideoStreamSession(id device, VideoEncoderOptions options, std::function<void(VideoAccessUnit)> onAccessUnit,
32
+ std::function<void(NSError*)> onError, std::function<void()> onEnd,
33
+ std::function<void()> onAbortDelivery = nullptr);
34
+ ~VideoStreamSession();
35
+
36
+ VideoStreamSession(const VideoStreamSession&) = delete;
37
+ VideoStreamSession& operator=(const VideoStreamSession&) = delete;
38
+
39
+ // Resolves the display and starts the polling loop; throws synchronously on resolution/setup
40
+ // failure (`onEnd` never called then). Later failures go to `onError`, then `onEnd`.
41
+ void Start();
42
+
43
+ // Idempotent; blocks until the loop has fully stopped. Never call from inside onAccessUnit/
44
+ // onError/onEnd — same queue this blocks on, so it would deadlock.
45
+ void Stop();
46
+
47
+ // Runs the constructor's `onAbortDelivery` (if any) — e.g. aborting a bounded delivery queue so
48
+ // a producer thread blocked pushing into it unblocks. Call before Stop() when the caller can't
49
+ // rely on anything else draining that queue concurrently (e.g. process-exit cleanup running
50
+ // synchronously on the same thread `Stop()` would otherwise wait on) — a normal Stop() doesn't
51
+ // need this. Safe from any thread; a no-op if unset.
52
+ void AbortDelivery();
53
+
54
+ // Forces the next encoded frame to be a keyframe (self-decodable, parameter sets included) —
55
+ // e.g. so a consumer that just resynced after dropping frames can resume cleanly instead of
56
+ // waiting for the next periodic one. Safe from any thread; just sets a flag.
57
+ void RequestKeyFrame();
58
+
59
+ private:
60
+ class Impl;
61
+ std::unique_ptr<Impl> impl_;
62
+ };
63
+
64
+ } // namespace coresim
@@ -0,0 +1,71 @@
1
+ #include "sim_video_stream.h"
2
+
3
+ #include <atomic>
4
+
5
+ #include "video_encoder.h"
6
+
7
+ namespace coresim {
8
+
9
+ class VideoStreamSession::Impl {
10
+ public:
11
+ Impl(id device, VideoEncoderOptions options, std::function<void(VideoAccessUnit)> onAccessUnit,
12
+ std::function<void(NSError*)> onError, std::function<void()> onEnd, std::function<void()> onAbortDelivery)
13
+ : options_(options), onAccessUnit_(std::move(onAccessUnit)), onAbortDelivery_(std::move(onAbortDelivery)) {
14
+ encoder_ = std::make_unique<VideoFrameEncoder>(
15
+ device, options, [this](CMSampleBufferRef sampleBuffer) { HandleEncodedSample(sampleBuffer); },
16
+ std::move(onError), std::move(onEnd));
17
+ }
18
+
19
+ void Start() { encoder_->Start(); }
20
+ void Stop() { encoder_->Stop(); }
21
+ void AbortDelivery() {
22
+ if (onAbortDelivery_) {
23
+ onAbortDelivery_();
24
+ }
25
+ }
26
+ void RequestKeyFrame() { encoder_->RequestKeyFrame(); }
27
+
28
+ private:
29
+ void HandleEncodedSample(CMSampleBufferRef sampleBuffer) {
30
+ bool isKeyFrame = IsKeyFrame(sampleBuffer);
31
+ // From the sample's own presentation timestamp (as submitted to the encoder), not a fresh
32
+ // wall-clock read here — this callback can fire well after encoding actually happened, and
33
+ // resampling "now" would report a later, jittery timestamp than when the frame was captured.
34
+ CMTime pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
35
+ VideoAccessUnit unit;
36
+ unit.isKeyFrame = isKeyFrame;
37
+ unit.sequence = sequence_++;
38
+ unit.timestampMicros = pts.timescale != 0 ? (pts.value * 1000000 / pts.timescale) : 0;
39
+ RepackAsAnnexB(unit.data, sampleBuffer, isKeyFrame, options_.codec);
40
+ if (onAccessUnit_) {
41
+ onAccessUnit_(std::move(unit));
42
+ }
43
+ }
44
+
45
+ VideoEncoderOptions options_;
46
+ std::function<void(VideoAccessUnit)> onAccessUnit_;
47
+ std::function<void()> onAbortDelivery_;
48
+ std::unique_ptr<VideoFrameEncoder> encoder_;
49
+ // VideoToolbox's output callback isn't documented as single-threaded, so this is read-modify-
50
+ // written atomically rather than assuming HandleEncodedSample never runs concurrently.
51
+ std::atomic<uint64_t> sequence_{0};
52
+ };
53
+
54
+ VideoStreamSession::VideoStreamSession(id device, VideoEncoderOptions options,
55
+ std::function<void(VideoAccessUnit)> onAccessUnit,
56
+ std::function<void(NSError*)> onError, std::function<void()> onEnd,
57
+ std::function<void()> onAbortDelivery)
58
+ : impl_(std::make_unique<Impl>(device, options, std::move(onAccessUnit), std::move(onError), std::move(onEnd),
59
+ std::move(onAbortDelivery))) {}
60
+
61
+ VideoStreamSession::~VideoStreamSession() = default;
62
+
63
+ void VideoStreamSession::Start() { impl_->Start(); }
64
+
65
+ void VideoStreamSession::Stop() { impl_->Stop(); }
66
+
67
+ void VideoStreamSession::AbortDelivery() { impl_->AbortDelivery(); }
68
+
69
+ void VideoStreamSession::RequestKeyFrame() { impl_->RequestKeyFrame(); }
70
+
71
+ } // namespace coresim
@@ -0,0 +1,76 @@
1
+ #pragma once
2
+
3
+ #import <CoreMedia/CoreMedia.h>
4
+ #import <Foundation/Foundation.h>
5
+
6
+ #include <cstdint>
7
+ #include <functional>
8
+ #include <memory>
9
+ #include <vector>
10
+
11
+ namespace coresim {
12
+
13
+ enum class VideoStreamCodec { kH264, kHEVC };
14
+
15
+ struct VideoEncoderOptions {
16
+ VideoStreamCodec codec = VideoStreamCodec::kH264;
17
+ NSString* displayId = nil;
18
+ double fps = 15.0;
19
+ int bitrate = 2000000;
20
+ };
21
+
22
+ // Polls the live display IOSurface (sim_screenshot.h) on a serial queue and encodes changed
23
+ // frames via the public VideoToolbox API, delivering each encoded `CMSampleBufferRef` live.
24
+ // Extracted from VideoStreamSession (video_stream.h) so both it and the combined AV
25
+ // recording/streaming sessions (av_recording.h/av_stream.h) share one VTCompressionSession driver
26
+ // instead of each re-deriving IOSurface-poll + compression-session setup from scratch. Callers
27
+ // that need Annex-B-framed access units (VideoAccessUnit's wire format) repack the delivered
28
+ // sample buffers themselves (see RepackAsAnnexB below) — this class only drives the encoder.
29
+ class VideoFrameEncoder {
30
+ public:
31
+ // `onSample`'s CMSampleBufferRef is only valid for the duration of the call (VideoToolbox owns
32
+ // it) — a caller that needs the data beyond that must copy it out (or CFRetain it) before
33
+ // returning, never stash the raw pointer.
34
+ //
35
+ // `sharedClockOrigin`, if non-null, is used as this encoder's PTS-zero instant (from
36
+ // monotonic_clock.h) instead of capturing its own at Start() — pass the same pointer to an
37
+ // AudioEncoder started around the same time so both tracks' presentation timestamps measure
38
+ // elapsed time from one common reference (av_recording.h/av_stream.h do this; the standalone
39
+ // VideoStreamSession, video-only, has no such peer and leaves this null).
40
+ VideoFrameEncoder(id device, VideoEncoderOptions options, std::function<void(CMSampleBufferRef)> onSample,
41
+ std::function<void(NSError*)> onError, std::function<void()> onEnd,
42
+ const double* sharedClockOrigin = nullptr);
43
+ ~VideoFrameEncoder();
44
+
45
+ VideoFrameEncoder(const VideoFrameEncoder&) = delete;
46
+ VideoFrameEncoder& operator=(const VideoFrameEncoder&) = delete;
47
+
48
+ // Resolves the display and starts the polling loop; throws synchronously on resolution/setup
49
+ // failure (`onEnd` never called then). Later failures go to `onError`, then `onEnd`.
50
+ void Start();
51
+
52
+ // Idempotent; blocks until the loop has fully stopped. Never call from inside onSample/onError/
53
+ // onEnd — same queue this blocks on, so it would deadlock.
54
+ void Stop();
55
+
56
+ // Forces the next encoded frame to be a keyframe (self-decodable, parameter sets included) —
57
+ // e.g. so a consumer that just resynced after dropping frames can resume cleanly instead of
58
+ // waiting for the next periodic one. Safe from any thread; just sets a flag.
59
+ void RequestKeyFrame();
60
+
61
+ private:
62
+ class Impl;
63
+ std::unique_ptr<Impl> impl_;
64
+ };
65
+
66
+ // Whether `sampleBuffer` is a sync (key) frame, from its sample attachments.
67
+ bool IsKeyFrame(CMSampleBufferRef sampleBuffer);
68
+
69
+ // VideoToolbox's compressed output is AVCC-framed (a 4-byte big-endian length prefix per NAL, no
70
+ // start codes) — rewrites `sampleBuffer` into Annex-B, appending to `out`. A keyframe's format
71
+ // description (SPS/PPS, or VPS/SPS/PPS for HEVC) is prepended first when `isKeyFrame`, so every
72
+ // keyframe is self-decodable alone. Shared by VideoStreamSession and the AV streaming path so both
73
+ // produce byte-identical framing for the same encoder output.
74
+ void RepackAsAnnexB(std::vector<uint8_t>& out, CMSampleBufferRef sampleBuffer, bool isKeyFrame, VideoStreamCodec codec);
75
+
76
+ } // namespace coresim