@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.
- package/CHANGELOG.md +12 -0
- package/README.md +14 -0
- package/binding.gyp +14 -1
- package/lib/src/commands/video-recording.d.ts +57 -0
- package/lib/src/commands/video-recording.d.ts.map +1 -0
- package/lib/src/commands/video-recording.js +123 -0
- package/lib/src/commands/video-recording.js.map +1 -0
- package/lib/src/commands/video-stream.d.ts +92 -0
- package/lib/src/commands/video-stream.d.ts.map +1 -0
- package/lib/src/commands/video-stream.js +242 -0
- package/lib/src/commands/video-stream.js.map +1 -0
- package/lib/src/index.d.ts +2 -1
- package/lib/src/index.d.ts.map +1 -1
- package/lib/src/index.js +1 -0
- package/lib/src/index.js.map +1 -1
- package/lib/src/native-simctl.d.ts +4 -0
- package/lib/src/native-simctl.d.ts.map +1 -1
- package/lib/src/native-simctl.js +21 -1
- package/lib/src/native-simctl.js.map +1 -1
- package/lib/src/types.d.ts +134 -0
- package/lib/src/types.d.ts.map +1 -1
- package/lib/src/utils/index.d.ts +1 -1
- package/lib/src/utils/index.d.ts.map +1 -1
- package/lib/src/utils/index.js +1 -1
- package/lib/src/utils/index.js.map +1 -1
- package/lib/src/utils/run-catching.d.ts +4 -0
- package/lib/src/utils/run-catching.d.ts.map +1 -1
- package/lib/src/utils/run-catching.js +11 -0
- package/lib/src/utils/run-catching.js.map +1 -1
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/@appium+coresim.node +0 -0
- package/src/commands/video-recording.ts +158 -0
- package/src/commands/video-stream.ts +283 -0
- package/src/coresim.mm +708 -1
- package/src/index.ts +4 -0
- package/src/native/audio_encoder.h +65 -0
- package/src/native/audio_encoder.mm +270 -0
- package/src/native/av_recording.h +52 -0
- package/src/native/av_recording.mm +424 -0
- package/src/native/av_stream.h +70 -0
- package/src/native/av_stream.mm +217 -0
- package/src/native/monotonic_clock.h +20 -0
- package/src/native/sim_audio_tap.h +67 -0
- package/src/native/sim_audio_tap.mm +350 -0
- package/src/native/sim_process.h +10 -0
- package/src/native/sim_process.mm +81 -18
- package/src/native/sim_screenshot.h +10 -0
- package/src/native/sim_screenshot.mm +11 -6
- package/src/native/sim_video_recording.h +27 -0
- package/src/native/sim_video_recording.mm +87 -0
- package/src/native/sim_video_stream.h +64 -0
- package/src/native/sim_video_stream.mm +71 -0
- package/src/native/video_encoder.h +76 -0
- package/src/native/video_encoder.mm +417 -0
- package/src/native-simctl.ts +22 -1
- package/src/types.ts +149 -0
- package/src/utils/index.ts +1 -1
- 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
|
-
//
|
|
29
|
-
//
|
|
30
|
-
|
|
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
|
|
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*
|
|
44
|
-
const char*
|
|
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
|
-
//
|
|
56
|
-
|
|
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
|
|
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
|
|
110
|
+
return {};
|
|
66
111
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|