@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
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export {NativeSimError, NativeSimUnavailableError, NativeSimDispatchError, NativeSimOperationError} from './errors.js';
2
2
  export {NativeSimctl} from './native-simctl.js';
3
3
  export {SpawnedProcess} from './commands/spawn.js';
4
+ export {VideoStream} from './commands/video-stream.js';
4
5
  export type {AppContainerType} from './commands/app.js';
5
6
  export type {BiometricName} from './commands/biometric.js';
6
7
  export {
@@ -20,4 +21,7 @@ export {
20
21
  type SimProcessInfo,
21
22
  type SimRuntimeInfo,
22
23
  type SpawnOptions,
24
+ type VideoAccessUnit,
25
+ type VideoRecordingOptions,
26
+ type VideoStreamOptions,
23
27
  } from './types.js';
@@ -0,0 +1,65 @@
1
+ #pragma once
2
+
3
+ #import <AudioToolbox/AudioToolbox.h>
4
+ #import <CoreMedia/CoreMedia.h>
5
+ #import <Foundation/Foundation.h>
6
+
7
+ #include <cstddef>
8
+ #include <functional>
9
+ #include <memory>
10
+ #include <vector>
11
+
12
+ namespace coresim {
13
+
14
+ // Encodes interleaved PCM (as delivered by AudioTapSession, sim_audio_tap.h) to AAC-LC via
15
+ // AudioToolbox's AudioConverterRef — the same role VideoFrameEncoder/VTCompressionSession plays
16
+ // for video. Shared by both the AV recording (av_recording.h, muxed into a file via AVAssetWriter
17
+ // passthrough) and AV streaming (av_stream.h, raw access units) paths, mirroring how a single
18
+ // VideoFrameEncoder backs both video paths.
19
+ //
20
+ // AAC packets straddle multiple PCM buffers (1024 samples/packet vs. one IO cycle's ~512 or so),
21
+ // so this buffers incoming PCM internally and emits a `CMSampleBufferRef` — via `onSample` —
22
+ // exactly when a full packet becomes available, not once per EncodePCM call.
23
+ class AudioEncoder {
24
+ public:
25
+ // `onSample`'s CMSampleBufferRef is only valid for the duration of the call, same contract as
26
+ // VideoFrameEncoder's onSample (video_encoder.h) — copy out or CFRetain before returning if
27
+ // needed beyond it.
28
+ //
29
+ // `sharedClockOrigin`, if non-null, anchors this encoder's PTS-zero to the same instant given to
30
+ // a peer VideoFrameEncoder — see that class's constructor for the full rationale. Captured once,
31
+ // at construction (when this encoder's audio effectively "starts"), not re-read afterward.
32
+ AudioEncoder(const AudioStreamBasicDescription& inputFormat, std::function<void(CMSampleBufferRef)> onSample,
33
+ const double* sharedClockOrigin = nullptr);
34
+ ~AudioEncoder();
35
+
36
+ AudioEncoder(const AudioEncoder&) = delete;
37
+ AudioEncoder& operator=(const AudioEncoder&) = delete;
38
+
39
+ // Feeds one IO cycle's interleaved PCM (as delivered directly by AudioTapSession's onBuffer —
40
+ // same AudioBufferList/AudioStreamBasicDescription shape) into the encoder. Must be called from
41
+ // a single thread/queue only — not internally synchronized, same as VideoFrameEncoder's
42
+ // single-queue contract (the caller already serializes tap delivery onto one queue). Throws
43
+ // NSErrorException on an unexpected converter failure — callers should treat it the same as
44
+ // VideoFrameEncoder's EncodeSurface throwing (a fatal error for the whole session), not retry it
45
+ // inline.
46
+ void EncodePCM(const AudioBufferList* data, const AudioTimeStamp* time);
47
+
48
+ // The output AAC-LC format — channels/sample rate mirror the input; usable as
49
+ // AVAssetWriterInput's `sourceFormatHint` for passthrough (av_recording.h).
50
+ CMFormatDescriptionRef OutputFormatDescription() const;
51
+
52
+ private:
53
+ class Impl;
54
+ std::unique_ptr<Impl> impl_;
55
+ };
56
+
57
+ // Prepends a 7-byte ADTS header (no CRC) describing `aacFrameLength` bytes of raw AAC-LC payload
58
+ // at `format`'s sample rate/channel count, appending both to `out`. Makes a streamed packet
59
+ // self-describing (decodable without an out-of-band config exchange) — the audio counterpart to
60
+ // RepackAsAnnexB's per-keyframe SPS/PPS (video_encoder.h). Not used for the muxed-file recording
61
+ // path — AVAssetWriter passthrough wants bare AAC plus AudioEncoder::OutputFormatDescription, not
62
+ // ADTS framing.
63
+ void PrependADTSHeader(std::vector<uint8_t>& out, size_t aacFrameLength, const AudioStreamBasicDescription& format);
64
+
65
+ } // namespace coresim
@@ -0,0 +1,270 @@
1
+ #include "audio_encoder.h"
2
+
3
+ #include <algorithm>
4
+ #include <cmath>
5
+ #include <utility>
6
+ #include <vector>
7
+
8
+ #include "monotonic_clock.h"
9
+ #include "nserror_bridge.h"
10
+
11
+ namespace coresim {
12
+
13
+ namespace {
14
+
15
+ NSString* const kAudioEncoderErrorDomain = @"com.appium.coresim.AudioEncoder";
16
+
17
+ NSError* MakeError(NSInteger code, NSString* message) {
18
+ return [NSError errorWithDomain:kAudioEncoderErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : message}];
19
+ }
20
+
21
+ NSError* MakeStatusError(NSInteger code, NSString* what, OSStatus status) {
22
+ return MakeError(code, [NSString stringWithFormat:@"%@ (OSStatus %d)", what, static_cast<int>(status)]);
23
+ }
24
+
25
+ // AAC-LC is spec-mandated at 1024 PCM samples/packet — unlike sample rate/channel count, this
26
+ // isn't something the converter needs to be asked for.
27
+ constexpr UInt32 kFramesPerAacPacket = 1024;
28
+
29
+ struct InputProcContext {
30
+ const uint8_t* data;
31
+ UInt32 framesAvailable;
32
+ UInt32 bytesPerFrame;
33
+ UInt32 channelsPerFrame;
34
+ };
35
+
36
+ // AudioConverterFillComplexBuffer's pull callback: hands over whatever of `framesAvailable` is
37
+ // left, or signals "nothing more right now" (0 packets, noErr — not an error/EOF condition) once
38
+ // exhausted, matching Apple's documented streaming-converter pattern.
39
+ OSStatus InputDataProc(AudioConverterRef /*inConverter*/, UInt32* ioNumberDataPackets, AudioBufferList* ioData,
40
+ AudioStreamPacketDescription** /*outPacketDescription*/, void* inUserData) {
41
+ auto* ctx = static_cast<InputProcContext*>(inUserData);
42
+ UInt32 framesToProvide = std::min(*ioNumberDataPackets, ctx->framesAvailable);
43
+ ioData->mNumberBuffers = 1;
44
+ ioData->mBuffers[0].mNumberChannels = ctx->channelsPerFrame;
45
+ if (framesToProvide == 0) {
46
+ ioData->mBuffers[0].mData = nullptr;
47
+ ioData->mBuffers[0].mDataByteSize = 0;
48
+ *ioNumberDataPackets = 0;
49
+ return noErr;
50
+ }
51
+ ioData->mBuffers[0].mData = const_cast<uint8_t*>(ctx->data);
52
+ ioData->mBuffers[0].mDataByteSize = framesToProvide * ctx->bytesPerFrame;
53
+ ctx->data += framesToProvide * ctx->bytesPerFrame;
54
+ ctx->framesAvailable -= framesToProvide;
55
+ *ioNumberDataPackets = framesToProvide;
56
+ return noErr;
57
+ }
58
+
59
+ } // namespace
60
+
61
+ class AudioEncoder::Impl {
62
+ public:
63
+ Impl(const AudioStreamBasicDescription& inputFormat, std::function<void(CMSampleBufferRef)> onSample,
64
+ const double* sharedClockOrigin)
65
+ : inputFormat_(inputFormat), onSample_(std::move(onSample)) {
66
+ outputFormat_ = {};
67
+ outputFormat_.mFormatID = kAudioFormatMPEG4AAC;
68
+ outputFormat_.mFormatFlags = kMPEG4Object_AAC_LC;
69
+ outputFormat_.mSampleRate = inputFormat_.mSampleRate;
70
+ outputFormat_.mChannelsPerFrame = inputFormat_.mChannelsPerFrame;
71
+ outputFormat_.mFramesPerPacket = kFramesPerAacPacket;
72
+
73
+ OSStatus status = AudioConverterNew(&inputFormat_, &outputFormat_, &converter_);
74
+ if (status != noErr) {
75
+ throw NSErrorException(MakeStatusError(1, @"AudioConverterNew failed", status));
76
+ }
77
+
78
+ UInt32 bitRate = 64000 * std::max<UInt32>(outputFormat_.mChannelsPerFrame, 1);
79
+ // Non-fatal if rejected — the converter's own default bitrate is still a valid AAC-LC config.
80
+ AudioConverterSetProperty(converter_, kAudioConverterEncodeBitRate, sizeof(bitRate), &bitRate);
81
+
82
+ UInt32 maxPacketSize = 0;
83
+ UInt32 maxPacketSizeFieldSize = sizeof(maxPacketSize);
84
+ status = AudioConverterGetProperty(converter_, kAudioConverterPropertyMaximumOutputPacketSize,
85
+ &maxPacketSizeFieldSize, &maxPacketSize);
86
+ maxOutputPacketSize_ = (status == noErr && maxPacketSize > 0) ? maxPacketSize : 4096;
87
+ outputBuffer_.resize(maxOutputPacketSize_);
88
+
89
+ AudioConverterPrimeInfo primeInfo = {};
90
+ UInt32 primeInfoSize = sizeof(primeInfo);
91
+ status = AudioConverterGetProperty(converter_, kAudioConverterPrimeInfo, &primeInfoSize, &primeInfo);
92
+ primingFrames_ = (status == noErr) ? primeInfo.leadingFrames : 0;
93
+
94
+ status = CMAudioFormatDescriptionCreate(kCFAllocatorDefault, &outputFormat_, 0, nullptr, 0, nullptr, nullptr,
95
+ &formatDescription_);
96
+ if (status != noErr) {
97
+ AudioConverterDispose(converter_);
98
+ converter_ = nullptr;
99
+ throw NSErrorException(MakeStatusError(2, @"CMAudioFormatDescriptionCreate failed", status));
100
+ }
101
+
102
+ // How many output frames "ahead" this encoder's PTS zero should start at, so its first
103
+ // packet's timestamp lands at the same point on the shared timeline a peer VideoFrameEncoder
104
+ // (given the same origin) would compute for something starting at this same instant — see the
105
+ // constructor doc comment.
106
+ if (sharedClockOrigin != nullptr) {
107
+ double elapsedSeconds = MonotonicSeconds() - *sharedClockOrigin;
108
+ zeroOffsetFrames_ = static_cast<int64_t>(std::llround(elapsedSeconds * outputFormat_.mSampleRate));
109
+ }
110
+ }
111
+
112
+ ~Impl() {
113
+ if (formatDescription_ != nullptr) {
114
+ CFRelease(formatDescription_);
115
+ }
116
+ if (converter_ != nullptr) {
117
+ AudioConverterDispose(converter_);
118
+ }
119
+ }
120
+
121
+ void EncodePCM(const AudioBufferList* data, const AudioTimeStamp* /*time*/) {
122
+ if (data->mNumberBuffers == 0) {
123
+ return;
124
+ }
125
+ const AudioBuffer& buf = data->mBuffers[0];
126
+ const uint8_t* bytes = static_cast<const uint8_t*>(buf.mData);
127
+ pcmBuffer_.insert(pcmBuffer_.end(), bytes, bytes + buf.mDataByteSize);
128
+
129
+ UInt32 bytesPerFrame = inputFormat_.mBytesPerFrame;
130
+ if (bytesPerFrame == 0) {
131
+ return; // malformed input format — nothing sane to do
132
+ }
133
+ while (pcmBuffer_.size() / bytesPerFrame >= kFramesPerAacPacket) {
134
+ InputProcContext ctx{pcmBuffer_.data(), static_cast<UInt32>(pcmBuffer_.size() / bytesPerFrame), bytesPerFrame,
135
+ inputFormat_.mChannelsPerFrame};
136
+
137
+ AudioBufferList outputBufferList;
138
+ outputBufferList.mNumberBuffers = 1;
139
+ outputBufferList.mBuffers[0].mNumberChannels = outputFormat_.mChannelsPerFrame;
140
+ outputBufferList.mBuffers[0].mDataByteSize = static_cast<UInt32>(outputBuffer_.size());
141
+ outputBufferList.mBuffers[0].mData = outputBuffer_.data();
142
+
143
+ AudioStreamPacketDescription packetDescription = {};
144
+ UInt32 outputPacketCount = 1;
145
+ OSStatus status = AudioConverterFillComplexBuffer(converter_, InputDataProc, &ctx, &outputPacketCount,
146
+ &outputBufferList, &packetDescription);
147
+ UInt32 framesConsumed = static_cast<UInt32>(pcmBuffer_.size() / bytesPerFrame) - ctx.framesAvailable;
148
+ pcmBuffer_.erase(pcmBuffer_.begin(), pcmBuffer_.begin() + framesConsumed * bytesPerFrame);
149
+
150
+ if (status != noErr) {
151
+ throw NSErrorException(MakeStatusError(3, @"AudioConverterFillComplexBuffer failed", status));
152
+ }
153
+ if (outputPacketCount == 0) {
154
+ break; // not enough input actually consumed to complete a packet this round
155
+ }
156
+ EmitSample(packetDescription.mDataByteSize);
157
+ }
158
+ }
159
+
160
+ CMFormatDescriptionRef OutputFormatDescription() const { return formatDescription_; }
161
+
162
+ private:
163
+ void EmitSample(UInt32 packetByteSize) {
164
+ CMBlockBufferRef blockBuffer = nullptr;
165
+ OSStatus status =
166
+ CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, nullptr, packetByteSize, kCFAllocatorDefault, nullptr,
167
+ 0, packetByteSize, kCMBlockBufferAssureMemoryNowFlag, &blockBuffer);
168
+ if (status != noErr || blockBuffer == nullptr) {
169
+ return;
170
+ }
171
+ CMBlockBufferReplaceDataBytes(outputBuffer_.data(), blockBuffer, 0, packetByteSize);
172
+
173
+ CMTime pts = CMTimeMake(zeroOffsetFrames_ + framesEncoded_, static_cast<int32_t>(outputFormat_.mSampleRate));
174
+ CMSampleTimingInfo timing = {CMTimeMake(kFramesPerAacPacket, static_cast<int32_t>(outputFormat_.mSampleRate)), pts,
175
+ kCMTimeInvalid};
176
+ size_t sampleSize = packetByteSize;
177
+ CMSampleBufferRef sampleBuffer = nullptr;
178
+ status = CMSampleBufferCreate(kCFAllocatorDefault, blockBuffer, true, nullptr, nullptr, formatDescription_, 1, 1,
179
+ &timing, 1, &sampleSize, &sampleBuffer);
180
+ CFRelease(blockBuffer);
181
+ if (status != noErr || sampleBuffer == nullptr) {
182
+ return;
183
+ }
184
+ framesEncoded_ += kFramesPerAacPacket;
185
+
186
+ if (!primingTrimApplied_) {
187
+ primingTrimApplied_ = true;
188
+ if (primingFrames_ > 0) {
189
+ CMTime trimDuration =
190
+ CMTimeMake(static_cast<int64_t>(primingFrames_), static_cast<int32_t>(outputFormat_.mSampleRate));
191
+ CFDictionaryRef trimDict = CMTimeCopyAsDictionary(trimDuration, kCFAllocatorDefault);
192
+ if (trimDict != nullptr) {
193
+ CMSetAttachment(sampleBuffer, kCMSampleBufferAttachmentKey_TrimDurationAtStart, trimDict,
194
+ kCMAttachmentMode_ShouldPropagate);
195
+ CFRelease(trimDict);
196
+ }
197
+ }
198
+ }
199
+
200
+ if (onSample_) {
201
+ onSample_(sampleBuffer);
202
+ }
203
+ CFRelease(sampleBuffer);
204
+ }
205
+
206
+ AudioStreamBasicDescription inputFormat_;
207
+ AudioStreamBasicDescription outputFormat_;
208
+ std::function<void(CMSampleBufferRef)> onSample_;
209
+
210
+ AudioConverterRef converter_ = nullptr;
211
+ CMAudioFormatDescriptionRef formatDescription_ = nullptr;
212
+ std::vector<uint8_t> pcmBuffer_;
213
+ std::vector<uint8_t> outputBuffer_;
214
+ UInt32 maxOutputPacketSize_ = 0;
215
+ UInt32 primingFrames_ = 0;
216
+ bool primingTrimApplied_ = false;
217
+ int64_t framesEncoded_ = 0;
218
+ int64_t zeroOffsetFrames_ = 0;
219
+ };
220
+
221
+ AudioEncoder::AudioEncoder(const AudioStreamBasicDescription& inputFormat,
222
+ std::function<void(CMSampleBufferRef)> onSample, const double* sharedClockOrigin)
223
+ : impl_(std::make_unique<Impl>(inputFormat, std::move(onSample), sharedClockOrigin)) {}
224
+
225
+ AudioEncoder::~AudioEncoder() = default;
226
+
227
+ void AudioEncoder::EncodePCM(const AudioBufferList* data, const AudioTimeStamp* time) { impl_->EncodePCM(data, time); }
228
+
229
+ CMFormatDescriptionRef AudioEncoder::OutputFormatDescription() const { return impl_->OutputFormatDescription(); }
230
+
231
+ namespace {
232
+
233
+ // ISO/IEC 13818-7 Table 35 — ADTS's 4-bit samplingFrequencyIndex. Falls back to 44.1kHz (index 4)
234
+ // for a rate outside this fixed table, which a real Core Audio device format should never hit.
235
+ int ADTSSamplingFrequencyIndex(double sampleRate) {
236
+ static constexpr std::pair<int, int> kIndexByRate[] = {{96000, 0}, {88200, 1}, {64000, 2}, {48000, 3}, {44100, 4},
237
+ {32000, 5}, {24000, 6}, {22050, 7}, {16000, 8}, {12000, 9},
238
+ {11025, 10}, {8000, 11}, {7350, 12}};
239
+ int rounded = static_cast<int>(std::lround(sampleRate));
240
+ for (const auto& [rate, index] : kIndexByRate) {
241
+ if (rate == rounded) {
242
+ return index;
243
+ }
244
+ }
245
+ return 4;
246
+ }
247
+
248
+ } // namespace
249
+
250
+ void PrependADTSHeader(std::vector<uint8_t>& out, size_t aacFrameLength, const AudioStreamBasicDescription& format) {
251
+ // Field layout/values match FFmpeg's own ADTS writer (libavformat/adtsenc.c): MPEG-4 ID, AAC-LC
252
+ // profile, VBR buffer fullness (all 1s), one raw data block per ADTS frame.
253
+ size_t adtsFrameLength = aacFrameLength + 7;
254
+ int samplingFrequencyIndex = ADTSSamplingFrequencyIndex(format.mSampleRate);
255
+ int channelConfig = std::max<int>(1, static_cast<int>(format.mChannelsPerFrame));
256
+ constexpr int kAacLcProfile = 1; // ADTS profile field = MPEG-4 Audio Object Type (2 for AAC-LC) minus 1
257
+
258
+ uint8_t header[7];
259
+ header[0] = 0xFF;
260
+ header[1] = 0xF1;
261
+ header[2] = static_cast<uint8_t>((kAacLcProfile << 6) | (samplingFrequencyIndex << 2) | ((channelConfig >> 2) & 0x1));
262
+ header[3] = static_cast<uint8_t>(((channelConfig & 0x3) << 6) | ((adtsFrameLength >> 11) & 0x3));
263
+ header[4] = static_cast<uint8_t>((adtsFrameLength >> 3) & 0xFF);
264
+ header[5] = static_cast<uint8_t>(((adtsFrameLength & 0x7) << 5) | 0x1F);
265
+ header[6] = 0xFC;
266
+
267
+ out.insert(out.end(), header, header + 7);
268
+ }
269
+
270
+ } // namespace coresim
@@ -0,0 +1,52 @@
1
+ #pragma once
2
+
3
+ #import <AVFoundation/AVFoundation.h>
4
+ #import <Foundation/Foundation.h>
5
+
6
+ #include <functional>
7
+ #include <memory>
8
+
9
+ #include "video_encoder.h"
10
+
11
+ namespace coresim {
12
+
13
+ // File recording via this addon's own encoders — independent of StartVideoRecording's private
14
+ // CoreSimulator recorder (sim_video_recording.h), which has no per-frame hook to mux audio into
15
+ // and no `fps` knob. Drives a VideoFrameEncoder and, when `captureAudio` is set, an
16
+ // AudioTapSession+AudioEncoder too, on one shared PTS clock (monotonic_clock.h), muxing the
17
+ // already-encoded output into one file via AVAssetWriter passthrough. Without `captureAudio`,
18
+ // reached when `fps` alone is requested — see coresim.mm's StartVideoRecording.
19
+ //
20
+ // IMPORTANT: with `captureAudio`, needs the host's "System Audio Recording Only" TCC permission —
21
+ // see sim_audio_tap.h.
22
+ class AVRecordingSession {
23
+ public:
24
+ AVRecordingSession(id device, NSString* udid, VideoEncoderOptions videoOptions, NSString* outputFile,
25
+ bool captureAudio);
26
+ ~AVRecordingSession();
27
+
28
+ AVRecordingSession(const AVRecordingSession&) = delete;
29
+ AVRecordingSession& operator=(const AVRecordingSession&) = delete;
30
+
31
+ // Resolves the display and starts the video encoder (plus the audio tap + encoder, if
32
+ // `captureAudio`); throws synchronously on setup failure (no callback fires then). `onFirstSample`
33
+ // fires once the AVAssetWriter session has actually started — mirrors StartVideoRecording's
34
+ // "resolves once the first frame is recorded" contract. `onError` covers a later encoder/writer
35
+ // failure, firing at most once; the session tears itself down before calling it, so a following
36
+ // Stop() is always safe (and required, to reclaim the writer/file). `onEnd` fires exactly once,
37
+ // always — after `onError` if it fired, or once Stop() finishes otherwise — the one safe point
38
+ // to release resources tied to this session's lifetime, mirroring VideoStreamSession.
39
+ void Start(std::function<void()> onFirstSample, std::function<void(NSError*)> onError, std::function<void()> onEnd);
40
+
41
+ // Finalizes the output file. `onFinished` fires once (non-nil NSError* on failure, including
42
+ // when `onError` already fired earlier — in that case this just reports the same failure rather
43
+ // than attempting to finalize an already-cancelled writer). Safe to call even if no frame was
44
+ // ever captured (reports an error instead of producing an empty file).
45
+ void Stop(std::function<void(NSError*)> onFinished);
46
+
47
+ private:
48
+ class Impl;
49
+ std::unique_ptr<Impl> impl_;
50
+ };
51
+
52
+ } // namespace coresim