@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
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
#include "av_recording.h"
|
|
2
|
+
|
|
3
|
+
#include <atomic>
|
|
4
|
+
#include <mutex>
|
|
5
|
+
#include <vector>
|
|
6
|
+
|
|
7
|
+
#include "audio_encoder.h"
|
|
8
|
+
#include "monotonic_clock.h"
|
|
9
|
+
#include "nserror_bridge.h"
|
|
10
|
+
#include "sim_audio_tap.h"
|
|
11
|
+
|
|
12
|
+
namespace coresim {
|
|
13
|
+
|
|
14
|
+
namespace {
|
|
15
|
+
|
|
16
|
+
NSString* const kAVRecordingErrorDomain = @"com.appium.coresim.AVRecording";
|
|
17
|
+
|
|
18
|
+
NSError* MakeError(NSInteger code, NSString* message) {
|
|
19
|
+
return [NSError errorWithDomain:kAVRecordingErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : message}];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
constexpr size_t kMaxPendingAudioSamples = 500; // a few seconds of AAC packets — see HandleAudioSample
|
|
23
|
+
|
|
24
|
+
} // namespace
|
|
25
|
+
|
|
26
|
+
class AVRecordingSession::Impl {
|
|
27
|
+
public:
|
|
28
|
+
Impl(id device, NSString* udid, VideoEncoderOptions videoOptions, NSString* outputFile, bool captureAudio)
|
|
29
|
+
: device_(device),
|
|
30
|
+
udid_(udid),
|
|
31
|
+
videoOptions_(videoOptions),
|
|
32
|
+
outputFile_(outputFile),
|
|
33
|
+
captureAudio_(captureAudio) {}
|
|
34
|
+
|
|
35
|
+
~Impl() { TearDownIfNeeded(); }
|
|
36
|
+
|
|
37
|
+
void Start(std::function<void()> onFirstSample, std::function<void(NSError*)> onError, std::function<void()> onEnd) {
|
|
38
|
+
onFirstSample_ = std::move(onFirstSample);
|
|
39
|
+
onError_ = std::move(onError);
|
|
40
|
+
onEnd_ = std::move(onEnd);
|
|
41
|
+
clockOrigin_ = MonotonicSeconds();
|
|
42
|
+
|
|
43
|
+
[[NSFileManager defaultManager] removeItemAtPath:outputFile_ error:nil];
|
|
44
|
+
NSError* writerError = nil;
|
|
45
|
+
writer_ = [AVAssetWriter assetWriterWithURL:[NSURL fileURLWithPath:outputFile_]
|
|
46
|
+
fileType:AVFileTypeMPEG4
|
|
47
|
+
error:&writerError];
|
|
48
|
+
if (writer_ == nil) {
|
|
49
|
+
throw NSErrorException(writerError ?: MakeError(1, @"Failed to create an AVAssetWriter"));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
if (captureAudio_) {
|
|
54
|
+
audioTap_ = std::make_unique<AudioTapSession>(
|
|
55
|
+
udid_, [this](const AudioBufferList* data, const AudioTimeStamp* time) { HandleAudioPCM(data, time); },
|
|
56
|
+
[this](NSError* error) {
|
|
57
|
+
// A failed process-list refresh (kAudioTapNonFatalProcessListRefreshErrorCode) isn't
|
|
58
|
+
// fatal — the tap keeps running. Everything else is.
|
|
59
|
+
if ([error.domain isEqualToString:kAudioTapErrorDomain] &&
|
|
60
|
+
error.code == kAudioTapNonFatalProcessListRefreshErrorCode) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
Fail(error, /*fromVideo=*/false);
|
|
64
|
+
},
|
|
65
|
+
[] {});
|
|
66
|
+
audioTap_->Start();
|
|
67
|
+
|
|
68
|
+
// Start() can invoke HandleAudioPCM before returning, so audioEncoder_ is written under
|
|
69
|
+
// mutex_ (same lock HandleAudioPCM reads it under) to avoid a torn/racy pointer read.
|
|
70
|
+
auto encoder = std::make_unique<AudioEncoder>(
|
|
71
|
+
audioTap_->Format(), [this](CMSampleBufferRef sampleBuffer) { HandleAudioSample(sampleBuffer); },
|
|
72
|
+
&clockOrigin_);
|
|
73
|
+
{
|
|
74
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
75
|
+
audioEncoder_ = std::move(encoder);
|
|
76
|
+
}
|
|
77
|
+
// Audio's format is known immediately (unlike video's, learned from its first sample), so
|
|
78
|
+
// its input can be added to the writer right away — well before startWriting is called.
|
|
79
|
+
audioInput_ = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio
|
|
80
|
+
outputSettings:nil
|
|
81
|
+
sourceFormatHint:audioEncoder_->OutputFormatDescription()];
|
|
82
|
+
audioInput_.expectsMediaDataInRealTime = YES;
|
|
83
|
+
[writer_ addInput:audioInput_];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
videoEncoder_ = std::make_unique<VideoFrameEncoder>(
|
|
87
|
+
device_, videoOptions_, [this](CMSampleBufferRef sampleBuffer) { HandleVideoSample(sampleBuffer); },
|
|
88
|
+
[this](NSError* error) { Fail(error, /*fromVideo=*/true); }, [] {}, &clockOrigin_);
|
|
89
|
+
videoEncoder_->Start();
|
|
90
|
+
} catch (...) {
|
|
91
|
+
TearDownIfNeeded();
|
|
92
|
+
throw;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Must be fully idempotent, including concurrently: the env cleanup hook (coresim.mm) also
|
|
97
|
+
// unconditionally calls Stop() on every still-registered session, including one already stopped
|
|
98
|
+
// moments earlier whose JS wrapper hasn't GC'd yet. Finalizing an already-`.completed`
|
|
99
|
+
// AVAssetWriter throws an uncaught NSException and aborts the process — so only the FIRST call
|
|
100
|
+
// ever touches the writer/encoders; every later (or concurrent) call just replays that outcome.
|
|
101
|
+
void Stop(std::function<void(NSError*)> onFinished) {
|
|
102
|
+
bool isFirstCall;
|
|
103
|
+
bool resultReady;
|
|
104
|
+
NSError* resultError = nil;
|
|
105
|
+
{
|
|
106
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
107
|
+
isFirstCall = !stopped_;
|
|
108
|
+
stopped_ = true;
|
|
109
|
+
resultReady = stopResultReady_;
|
|
110
|
+
resultError = stopResultError_;
|
|
111
|
+
if (!isFirstCall && !resultReady) {
|
|
112
|
+
pendingStopCallbacks_.push_back(std::move(onFinished));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!isFirstCall) {
|
|
117
|
+
if (onFinished) {
|
|
118
|
+
onFinished(resultError);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Blocks until each has fully torn down — safe to finalize the writer afterward without racing
|
|
124
|
+
// a further sample callback, and also the point past which no new Fail() can start (both
|
|
125
|
+
// encoders' onError channels are now silenced) — which is what makes the stopResultReady_
|
|
126
|
+
// check below race-free against a concurrent Fail() (e.g. the audio tap's own poll timer).
|
|
127
|
+
if (videoEncoder_) {
|
|
128
|
+
videoEncoder_->Stop();
|
|
129
|
+
}
|
|
130
|
+
if (audioTap_) {
|
|
131
|
+
audioTap_->Stop();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
bool started;
|
|
135
|
+
bool alreadyFailed;
|
|
136
|
+
NSError* failedError = nil;
|
|
137
|
+
{
|
|
138
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
139
|
+
started = writerStarted_;
|
|
140
|
+
alreadyFailed = stopResultReady_;
|
|
141
|
+
failedError = stopResultError_;
|
|
142
|
+
ClearPendingAudioLocked();
|
|
143
|
+
}
|
|
144
|
+
if (alreadyFailed) {
|
|
145
|
+
// A concurrent Fail() already recorded the outcome and (if started) cancelled the writer —
|
|
146
|
+
// replay that outcome instead of touching it again; finishWriting on a cancelled writer
|
|
147
|
+
// throws an uncaught NSException.
|
|
148
|
+
if (onFinished) {
|
|
149
|
+
onFinished(failedError);
|
|
150
|
+
}
|
|
151
|
+
FireEndOnce();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (!started) {
|
|
155
|
+
FinishStop(MakeError(2, @"No audio or video was captured before the recording was stopped"),
|
|
156
|
+
std::move(onFinished));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
[videoInput_ markAsFinished];
|
|
160
|
+
[audioInput_ markAsFinished]; // no-op if `captureAudio_` was never set (audioInput_ stays nil)
|
|
161
|
+
AVAssetWriter* writer = writer_;
|
|
162
|
+
[writer finishWritingWithCompletionHandler:^{
|
|
163
|
+
NSError* finishError = (writer.status == AVAssetWriterStatusCompleted) ? nil : writer.error;
|
|
164
|
+
FinishStop(finishError, std::move(onFinished));
|
|
165
|
+
}];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private:
|
|
169
|
+
void HandleVideoSample(CMSampleBufferRef sampleBuffer) {
|
|
170
|
+
NSError* writingError = nil;
|
|
171
|
+
bool justStarted = false;
|
|
172
|
+
{
|
|
173
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
174
|
+
if (stopped_) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (!writerStarted_) {
|
|
178
|
+
CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
|
|
179
|
+
videoInput_ = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo
|
|
180
|
+
outputSettings:nil
|
|
181
|
+
sourceFormatHint:format];
|
|
182
|
+
videoInput_.expectsMediaDataInRealTime = YES;
|
|
183
|
+
[writer_ addInput:videoInput_];
|
|
184
|
+
if ([writer_ startWriting]) {
|
|
185
|
+
[writer_ startSessionAtSourceTime:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)];
|
|
186
|
+
writerStarted_ = true;
|
|
187
|
+
justStarted = true;
|
|
188
|
+
AppendVideoLocked(sampleBuffer);
|
|
189
|
+
for (CMSampleBufferRef pending : pendingAudio_) {
|
|
190
|
+
AppendAudioLocked(pending);
|
|
191
|
+
CFRelease(pending);
|
|
192
|
+
}
|
|
193
|
+
pendingAudio_.clear();
|
|
194
|
+
} else {
|
|
195
|
+
writingError = writer_.error ?: MakeError(3, @"AVAssetWriter startWriting failed");
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
AppendVideoLocked(sampleBuffer);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (writingError != nil) {
|
|
202
|
+
// Detected while handling a video callback — see Fail()'s doc comment for why this can only
|
|
203
|
+
// safely stop the audio side directly from here, not video's own encoder.
|
|
204
|
+
Fail(writingError, /*fromVideo=*/true);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (justStarted) {
|
|
208
|
+
FireFirstSampleOnce();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
void HandleAudioSample(CMSampleBufferRef sampleBuffer) {
|
|
213
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
214
|
+
if (stopped_) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (!writerStarted_) {
|
|
218
|
+
// Buffered until video's first sample establishes the writer's format/session (see
|
|
219
|
+
// HandleVideoSample) — capped so a display that never resolves can't grow this unboundedly.
|
|
220
|
+
CFRetain(sampleBuffer);
|
|
221
|
+
pendingAudio_.push_back(sampleBuffer);
|
|
222
|
+
if (pendingAudio_.size() > kMaxPendingAudioSamples) {
|
|
223
|
+
CFRelease(pendingAudio_.front());
|
|
224
|
+
pendingAudio_.erase(pendingAudio_.begin());
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
AppendAudioLocked(sampleBuffer);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Runs on AudioTapSession's own queue; audioEncoder_ is guarded by mutex_ (see Start()). A throw
|
|
232
|
+
// from EncodePCM is left to propagate — AudioTapSession::HandleBuffer catches it and self-stops
|
|
233
|
+
// (see sim_audio_tap.h's onBuffer doc) — this method must never call audioTap_->Stop() itself.
|
|
234
|
+
void HandleAudioPCM(const AudioBufferList* data, const AudioTimeStamp* time) {
|
|
235
|
+
AudioEncoder* encoder;
|
|
236
|
+
{
|
|
237
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
238
|
+
if (stopped_) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
encoder = audioEncoder_.get();
|
|
242
|
+
}
|
|
243
|
+
if (encoder != nullptr) {
|
|
244
|
+
encoder->EncodePCM(data, time);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Caller holds mutex_.
|
|
249
|
+
void AppendVideoLocked(CMSampleBufferRef sampleBuffer) {
|
|
250
|
+
if (videoInput_.isReadyForMoreMediaData) {
|
|
251
|
+
[videoInput_ appendSampleBuffer:sampleBuffer];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Caller holds mutex_.
|
|
256
|
+
void AppendAudioLocked(CMSampleBufferRef sampleBuffer) {
|
|
257
|
+
if (audioInput_.isReadyForMoreMediaData) {
|
|
258
|
+
[audioInput_ appendSampleBuffer:sampleBuffer];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Caller holds mutex_.
|
|
263
|
+
void ClearPendingAudioLocked() {
|
|
264
|
+
for (CMSampleBufferRef pending : pendingAudio_) {
|
|
265
|
+
CFRelease(pending);
|
|
266
|
+
}
|
|
267
|
+
pendingAudio_.clear();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
void FireFirstSampleOnce() {
|
|
271
|
+
if (!firstSampleFired_.exchange(true)) {
|
|
272
|
+
if (onFirstSample_) {
|
|
273
|
+
onFirstSample_();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
void FireEndOnce() {
|
|
279
|
+
if (!endFired_.exchange(true)) {
|
|
280
|
+
if (onEnd_) {
|
|
281
|
+
onEnd_();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Caller holds mutex_. Records the session's single, final outcome — a no-op if already
|
|
287
|
+
// recorded. Returns any Stop() calls queued while the outcome was unknown; caller must invoke
|
|
288
|
+
// each with `error`, unlocked.
|
|
289
|
+
std::vector<std::function<void(NSError*)>> RecordResultLocked(NSError* error) {
|
|
290
|
+
if (stopResultReady_) {
|
|
291
|
+
return {};
|
|
292
|
+
}
|
|
293
|
+
stopResultReady_ = true;
|
|
294
|
+
stopResultError_ = error;
|
|
295
|
+
std::vector<std::function<void(NSError*)>> pending;
|
|
296
|
+
pending.swap(pendingStopCallbacks_);
|
|
297
|
+
return pending;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Caller holds mutex_. Returns any queued Stop() calls to flush (see RecordResultLocked).
|
|
301
|
+
std::vector<std::function<void(NSError*)>> FailLocked(NSError* error) {
|
|
302
|
+
if (stopResultReady_) {
|
|
303
|
+
return {}; // already resolved (a prior Fail() or Stop() completion)
|
|
304
|
+
}
|
|
305
|
+
stopped_ = true;
|
|
306
|
+
if (writerStarted_) {
|
|
307
|
+
[writer_ cancelWriting];
|
|
308
|
+
}
|
|
309
|
+
ClearPendingAudioLocked();
|
|
310
|
+
auto pending = RecordResultLocked(error);
|
|
311
|
+
if (onError_) {
|
|
312
|
+
onError_(error);
|
|
313
|
+
}
|
|
314
|
+
return pending;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Called from either encoder's onError, or from HandleVideoSample on a writer-level failure —
|
|
318
|
+
// not holding mutex_ (could deadlock against that same encoder's own queue). Stops only the
|
|
319
|
+
// OTHER (still-running) encoder — the one whose callback we're inside is already tearing itself
|
|
320
|
+
// down on its own error path, and calling its own blocking Stop() here would deadlock.
|
|
321
|
+
void Fail(NSError* error, bool fromVideo) {
|
|
322
|
+
if (fromVideo) {
|
|
323
|
+
if (audioTap_) {
|
|
324
|
+
audioTap_->Stop();
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
if (videoEncoder_) {
|
|
328
|
+
videoEncoder_->Stop();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
std::vector<std::function<void(NSError*)>> pending;
|
|
332
|
+
{
|
|
333
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
334
|
+
pending = FailLocked(error);
|
|
335
|
+
}
|
|
336
|
+
for (auto& callback : pending) {
|
|
337
|
+
if (callback) {
|
|
338
|
+
callback(error);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
FireEndOnce();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// The single point where a real (first-ever) Stop() attempt's outcome becomes known — records
|
|
345
|
+
// it, replies to `onFinished` and any callers that arrived while it was still in flight, then
|
|
346
|
+
// fires onEnd_. Never called a second time for the same session (see Stop()'s own guard).
|
|
347
|
+
void FinishStop(NSError* error, std::function<void(NSError*)> onFinished) {
|
|
348
|
+
std::vector<std::function<void(NSError*)>> pending;
|
|
349
|
+
{
|
|
350
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
351
|
+
pending = RecordResultLocked(error);
|
|
352
|
+
}
|
|
353
|
+
if (onFinished) {
|
|
354
|
+
onFinished(error);
|
|
355
|
+
}
|
|
356
|
+
for (auto& callback : pending) {
|
|
357
|
+
if (callback) {
|
|
358
|
+
callback(error);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
FireEndOnce();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Best-effort, synchronous cleanup for the destructor path (Start() throwing partway through,
|
|
365
|
+
// or the session being destroyed without an explicit Stop()) — no completion callback, unlike
|
|
366
|
+
// the public Stop().
|
|
367
|
+
void TearDownIfNeeded() {
|
|
368
|
+
if (videoEncoder_) {
|
|
369
|
+
videoEncoder_->Stop();
|
|
370
|
+
}
|
|
371
|
+
if (audioTap_) {
|
|
372
|
+
audioTap_->Stop();
|
|
373
|
+
}
|
|
374
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
375
|
+
if (writerStarted_ && writer_ != nil) {
|
|
376
|
+
[writer_ cancelWriting];
|
|
377
|
+
}
|
|
378
|
+
ClearPendingAudioLocked();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
id device_;
|
|
382
|
+
NSString* udid_;
|
|
383
|
+
VideoEncoderOptions videoOptions_;
|
|
384
|
+
NSString* outputFile_;
|
|
385
|
+
bool captureAudio_;
|
|
386
|
+
|
|
387
|
+
std::function<void()> onFirstSample_;
|
|
388
|
+
std::function<void(NSError*)> onError_;
|
|
389
|
+
std::function<void()> onEnd_;
|
|
390
|
+
|
|
391
|
+
double clockOrigin_ = 0;
|
|
392
|
+
std::unique_ptr<VideoFrameEncoder> videoEncoder_;
|
|
393
|
+
std::unique_ptr<AudioTapSession> audioTap_;
|
|
394
|
+
std::unique_ptr<AudioEncoder> audioEncoder_;
|
|
395
|
+
|
|
396
|
+
AVAssetWriter* writer_ = nil;
|
|
397
|
+
AVAssetWriterInput* videoInput_ = nil;
|
|
398
|
+
AVAssetWriterInput* audioInput_ = nil;
|
|
399
|
+
|
|
400
|
+
std::mutex mutex_;
|
|
401
|
+
bool writerStarted_ = false;
|
|
402
|
+
bool stopped_ = false; // true from the first Stop() (or Fail()) call onward
|
|
403
|
+
bool stopResultReady_ = false;
|
|
404
|
+
NSError* stopResultError_ = nil;
|
|
405
|
+
std::vector<std::function<void(NSError*)>> pendingStopCallbacks_; // Stop() calls awaiting stopResultReady_
|
|
406
|
+
std::vector<CMSampleBufferRef> pendingAudio_;
|
|
407
|
+
std::atomic<bool> firstSampleFired_{false};
|
|
408
|
+
std::atomic<bool> endFired_{false};
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
AVRecordingSession::AVRecordingSession(id device, NSString* udid, VideoEncoderOptions videoOptions,
|
|
412
|
+
NSString* outputFile, bool captureAudio)
|
|
413
|
+
: impl_(std::make_unique<Impl>(device, udid, videoOptions, outputFile, captureAudio)) {}
|
|
414
|
+
|
|
415
|
+
AVRecordingSession::~AVRecordingSession() = default;
|
|
416
|
+
|
|
417
|
+
void AVRecordingSession::Start(std::function<void()> onFirstSample, std::function<void(NSError*)> onError,
|
|
418
|
+
std::function<void()> onEnd) {
|
|
419
|
+
impl_->Start(std::move(onFirstSample), std::move(onError), std::move(onEnd));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
void AVRecordingSession::Stop(std::function<void(NSError*)> onFinished) { impl_->Stop(std::move(onFinished)); }
|
|
423
|
+
|
|
424
|
+
} // namespace coresim
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
enum class AVTrack { kVideo, kAudio };
|
|
15
|
+
|
|
16
|
+
// One encoded unit from AVStreamSession::onAccessUnit — either an Annex-B video access unit
|
|
17
|
+
// (same wire format as VideoAccessUnit) or a raw AAC-LC packet, discriminated by `track`.
|
|
18
|
+
// `isKeyFrame` is video-only (always true for audio — every AAC-LC packet is independently
|
|
19
|
+
// decodable); `sequence` counts per track, not globally, since interleaving order between the two
|
|
20
|
+
// tracks carries no meaning of its own — only `timestampMicros`, on the shared session clock
|
|
21
|
+
// (monotonic_clock.h), does.
|
|
22
|
+
struct AVAccessUnit {
|
|
23
|
+
AVTrack track = AVTrack::kVideo;
|
|
24
|
+
std::vector<uint8_t> data;
|
|
25
|
+
bool isKeyFrame = false;
|
|
26
|
+
uint64_t sequence = 0;
|
|
27
|
+
int64_t timestampMicros = 0;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Combined audio+video real-time streaming — the streaming counterpart to AVRecordingSession
|
|
31
|
+
// (av_recording.h). Drives the same VideoFrameEncoder/AudioTapSession/AudioEncoder trio on one
|
|
32
|
+
// shared PTS clock, but delivers each encoded unit live via `onAccessUnit` (interleaved, in
|
|
33
|
+
// production order) instead of muxing into a file.
|
|
34
|
+
//
|
|
35
|
+
// IMPORTANT: needs the host's "System Audio Recording Only" TCC permission — see sim_audio_tap.h.
|
|
36
|
+
class AVStreamSession {
|
|
37
|
+
public:
|
|
38
|
+
// `onAbortDelivery`, if set, is invoked by AbortDelivery() below — not called by this class on
|
|
39
|
+
// its own. See VideoStreamSession's identical parameter for why it exists.
|
|
40
|
+
AVStreamSession(id device, NSString* udid, VideoEncoderOptions videoOptions,
|
|
41
|
+
std::function<void(AVAccessUnit)> onAccessUnit, std::function<void(NSError*)> onError,
|
|
42
|
+
std::function<void()> onEnd, std::function<void()> onAbortDelivery = nullptr);
|
|
43
|
+
~AVStreamSession();
|
|
44
|
+
|
|
45
|
+
AVStreamSession(const AVStreamSession&) = delete;
|
|
46
|
+
AVStreamSession& operator=(const AVStreamSession&) = delete;
|
|
47
|
+
|
|
48
|
+
// Resolves the display and starts the audio tap + both encoders; throws synchronously on setup
|
|
49
|
+
// failure (`onAccessUnit`/`onError`/`onEnd` never called then). Later failures from either
|
|
50
|
+
// encoder go to `onError`, then `onEnd` — mirrors VideoStreamSession's contract.
|
|
51
|
+
void Start();
|
|
52
|
+
|
|
53
|
+
// Idempotent; blocks until both encoders have fully stopped. Never call from inside
|
|
54
|
+
// onAccessUnit/onError/onEnd — same queues this blocks on, so it would deadlock.
|
|
55
|
+
void Stop();
|
|
56
|
+
|
|
57
|
+
// See VideoStreamSession::AbortDelivery — same contract, call before Stop() only when nothing
|
|
58
|
+
// else will drain the delivery queue concurrently.
|
|
59
|
+
void AbortDelivery();
|
|
60
|
+
|
|
61
|
+
// Forces the next encoded video frame to be a keyframe — see VideoFrameEncoder::RequestKeyFrame.
|
|
62
|
+
// No audio equivalent (every AAC-LC packet already self-decodable).
|
|
63
|
+
void RequestKeyFrame();
|
|
64
|
+
|
|
65
|
+
private:
|
|
66
|
+
class Impl;
|
|
67
|
+
std::unique_ptr<Impl> impl_;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
} // namespace coresim
|