@appium/coresim 1.5.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 +6 -0
- package/README.md +14 -0
- package/binding.gyp +7 -0
- package/lib/src/commands/video-recording.d.ts +14 -7
- package/lib/src/commands/video-recording.d.ts.map +1 -1
- package/lib/src/commands/video-recording.js +36 -11
- package/lib/src/commands/video-recording.js.map +1 -1
- package/lib/src/commands/video-stream.d.ts +55 -7
- package/lib/src/commands/video-stream.d.ts.map +1 -1
- package/lib/src/commands/video-stream.js +37 -35
- package/lib/src/commands/video-stream.js.map +1 -1
- package/lib/src/native-simctl.js +12 -1
- package/lib/src/native-simctl.js.map +1 -1
- package/lib/src/types.d.ts +72 -10
- 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 +49 -19
- package/src/commands/video-stream.ts +48 -39
- package/src/coresim.mm +547 -118
- 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_video_stream.h +14 -11
- package/src/native/sim_video_stream.mm +27 -383
- package/src/native/video_encoder.h +76 -0
- package/src/native/video_encoder.mm +417 -0
- package/src/native-simctl.ts +12 -1
- package/src/types.ts +84 -11
- package/src/utils/index.ts +1 -1
- package/src/utils/run-catching.ts +11 -0
|
@@ -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
|