@0biwank/screen-capture 2.0.1 → 2.0.2
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/package.json +1 -6
- package/prebuilds/SHA256SUMS +2 -2
- package/prebuilds/darwin-arm64/native_capture.node +0 -0
- package/prebuilds/darwin-x64/native_capture.node +0 -0
- package/binding.gyp +0 -58
- package/include/CameraCapturer.h +0 -54
- package/include/DesktopCapturer.h +0 -83
- package/include/HLSMuxer/AudioEncoder.h +0 -75
- package/include/HLSMuxer/FileHLSMuxer.h +0 -63
- package/include/HLSMuxer/HLSMuxer.h +0 -13
- package/include/HLSMuxer/VideoEncoder.h +0 -90
- package/include/MediaPipeline.h +0 -39
- package/include/SourceHelper.h +0 -41
- package/include/SourceHelperWrapper.h +0 -29
- package/include/Types.h +0 -58
- package/include/UploadManager.h +0 -9
- package/include/httplib.h +0 -12065
- package/scripts/build-ffmpeg-vendor.mjs +0 -178
- package/scripts/build.mjs +0 -53
- package/scripts/stage-prebuild.mjs +0 -28
- package/scripts/verify-package.mjs +0 -39
- package/scripts/verify-packlist.mjs +0 -40
- package/scripts/verify-runtime.cjs +0 -28
- package/src/CameraCapturer.mm +0 -154
- package/src/DesktopCapturer.mm +0 -995
- package/src/HLSMuxer/AudioEncoder.cpp +0 -484
- package/src/HLSMuxer/FileHLSMuxer.cpp +0 -345
- package/src/HLSMuxer/HLSMuxer.cpp +0 -0
- package/src/HLSMuxer/VideoEncoder.cpp +0 -462
- package/src/MediaPipeline.cpp +0 -375
- package/src/MediaProcessor.cpp +0 -0
- package/src/SourceHelper.mm +0 -184
- package/src/SourceHelperWrapper.mm +0 -63
- package/src/UploadManager.h +0 -7
- package/src/addon.cpp +0 -347
- package/vendor/ffmpeg/README.md +0 -40
package/src/DesktopCapturer.mm
DELETED
|
@@ -1,995 +0,0 @@
|
|
|
1
|
-
#import <AVFoundation/AVFoundation.h>
|
|
2
|
-
#import <CoreAudio/CoreAudio.h>
|
|
3
|
-
#import <CoreMedia/CoreMedia.h>
|
|
4
|
-
#import <CoreVideo/CVPixelBuffer.h>
|
|
5
|
-
#import <Foundation/Foundation.h>
|
|
6
|
-
#import <ScreenCaptureKit/ScreenCaptureKit.h>
|
|
7
|
-
#import <AudioUnit/AudioUnit.h>
|
|
8
|
-
|
|
9
|
-
#include "DesktopCapturer.h"
|
|
10
|
-
#include "CameraCapturer.h"
|
|
11
|
-
#include "MediaPipeline.h"
|
|
12
|
-
|
|
13
|
-
#include <atomic>
|
|
14
|
-
#include <deque>
|
|
15
|
-
#include <memory>
|
|
16
|
-
#include <mutex>
|
|
17
|
-
#include <iostream>
|
|
18
|
-
#include <vector>
|
|
19
|
-
#include <algorithm>
|
|
20
|
-
#include <cmath>
|
|
21
|
-
#include <cstring>
|
|
22
|
-
#include <mach/mach_time.h>
|
|
23
|
-
|
|
24
|
-
namespace {
|
|
25
|
-
constexpr float kNativeMicGain = 2.0f;
|
|
26
|
-
constexpr size_t kMaxMicBufferFrames = 48000;
|
|
27
|
-
|
|
28
|
-
double host_time_to_seconds(uint64_t host_time) {
|
|
29
|
-
if (host_time == 0) return -1.0;
|
|
30
|
-
static mach_timebase_info_data_t timebase = [] {
|
|
31
|
-
mach_timebase_info_data_t info;
|
|
32
|
-
mach_timebase_info(&info);
|
|
33
|
-
return info;
|
|
34
|
-
}();
|
|
35
|
-
const double nanos =
|
|
36
|
-
static_cast<double>(host_time) *
|
|
37
|
-
static_cast<double>(timebase.numer) /
|
|
38
|
-
static_cast<double>(timebase.denom);
|
|
39
|
-
return nanos / 1'000'000'000.0;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
uint32_t even_down(uint32_t value) {
|
|
43
|
-
return value > 1 ? (value & ~1u) : value;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
float soft_limit(float v) {
|
|
47
|
-
// Smooth saturation keeps transients from breaking the mixed AAC encoder.
|
|
48
|
-
return std::tanh(v * 0.85f);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
bool find_audio_device_by_uid(const std::string& uid, AudioDeviceID* out_device) {
|
|
52
|
-
if (uid.empty() || !out_device) return false;
|
|
53
|
-
|
|
54
|
-
AudioObjectPropertyAddress addr = {
|
|
55
|
-
kAudioHardwarePropertyDevices,
|
|
56
|
-
kAudioObjectPropertyScopeGlobal,
|
|
57
|
-
kAudioObjectPropertyElementMain
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
UInt32 data_size = 0;
|
|
61
|
-
OSStatus status = AudioObjectGetPropertyDataSize(
|
|
62
|
-
kAudioObjectSystemObject, &addr, 0, nullptr, &data_size);
|
|
63
|
-
if (status != noErr || data_size == 0) return false;
|
|
64
|
-
|
|
65
|
-
std::vector<AudioDeviceID> devices(data_size / sizeof(AudioDeviceID));
|
|
66
|
-
status = AudioObjectGetPropertyData(
|
|
67
|
-
kAudioObjectSystemObject, &addr, 0, nullptr, &data_size, devices.data());
|
|
68
|
-
if (status != noErr) return false;
|
|
69
|
-
|
|
70
|
-
AudioObjectPropertyAddress uid_addr = {
|
|
71
|
-
kAudioDevicePropertyDeviceUID,
|
|
72
|
-
kAudioObjectPropertyScopeGlobal,
|
|
73
|
-
kAudioObjectPropertyElementMain
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
for (AudioDeviceID device : devices) {
|
|
77
|
-
CFStringRef cf_uid = nullptr;
|
|
78
|
-
UInt32 uid_size = sizeof(cf_uid);
|
|
79
|
-
status = AudioObjectGetPropertyData(device, &uid_addr, 0, nullptr, &uid_size, &cf_uid);
|
|
80
|
-
if (status != noErr || !cf_uid) continue;
|
|
81
|
-
|
|
82
|
-
char buffer[512] = {0};
|
|
83
|
-
const bool matches =
|
|
84
|
-
CFStringGetCString(cf_uid, buffer, sizeof(buffer), kCFStringEncodingUTF8) &&
|
|
85
|
-
uid == buffer;
|
|
86
|
-
CFRelease(cf_uid);
|
|
87
|
-
|
|
88
|
-
if (matches) {
|
|
89
|
-
*out_device = device;
|
|
90
|
-
return true;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
bool get_default_input_device(AudioDeviceID* out_device) {
|
|
98
|
-
if (!out_device) return false;
|
|
99
|
-
|
|
100
|
-
AudioObjectPropertyAddress addr = {
|
|
101
|
-
kAudioHardwarePropertyDefaultInputDevice,
|
|
102
|
-
kAudioObjectPropertyScopeGlobal,
|
|
103
|
-
kAudioObjectPropertyElementMain
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
UInt32 data_size = sizeof(AudioDeviceID);
|
|
107
|
-
OSStatus status = AudioObjectGetPropertyData(
|
|
108
|
-
kAudioObjectSystemObject, &addr, 0, nullptr, &data_size, out_device);
|
|
109
|
-
return status == noErr && *out_device != kAudioObjectUnknown;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
struct DesktopCapturer::Impl {
|
|
114
|
-
struct AudioSegment {
|
|
115
|
-
int64_t start_frame = 0;
|
|
116
|
-
uint32_t frames = 0;
|
|
117
|
-
std::vector<float> pcm;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
struct MixedAudioChunk {
|
|
121
|
-
int64_t start_frame = 0;
|
|
122
|
-
uint32_t frames = 0;
|
|
123
|
-
std::vector<float> pcm;
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
std::unique_ptr<MediaPipeline> pipeline;
|
|
127
|
-
std::atomic<bool> started{false};
|
|
128
|
-
|
|
129
|
-
std::atomic<bool> paused{false};
|
|
130
|
-
std::atomic<bool> resumePending{false};
|
|
131
|
-
|
|
132
|
-
double last_video_pts_sec = -1.0;
|
|
133
|
-
double pauseStartedVideoPtsSec = -1.0;
|
|
134
|
-
|
|
135
|
-
double totalPausedVideoSec = 0.0;
|
|
136
|
-
bool audioGated = false;
|
|
137
|
-
|
|
138
|
-
std::mutex mix_mutex;
|
|
139
|
-
std::deque<AudioSegment> mic_segments;
|
|
140
|
-
std::deque<AudioSegment> system_segments;
|
|
141
|
-
uint32_t sample_rate = 48000;
|
|
142
|
-
uint32_t channels = 2;
|
|
143
|
-
double video_pts_origin_sec = -1.0;
|
|
144
|
-
double mic_host_origin_sec = -1.0;
|
|
145
|
-
int64_t last_video_pts_tick = -1;
|
|
146
|
-
std::atomic<uint64_t> mic_callbacks{0};
|
|
147
|
-
std::atomic<uint64_t> mic_frames{0};
|
|
148
|
-
std::atomic<uint64_t> dropped_audio_frames{0};
|
|
149
|
-
int64_t last_mixed_audio_frame = 0;
|
|
150
|
-
|
|
151
|
-
void reset_audio_sync() {
|
|
152
|
-
std::lock_guard<std::mutex> lk(mix_mutex);
|
|
153
|
-
mic_segments.clear();
|
|
154
|
-
system_segments.clear();
|
|
155
|
-
mic_host_origin_sec = -1.0;
|
|
156
|
-
last_mixed_audio_frame = 0;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
void append_segment(std::deque<AudioSegment>& segments,
|
|
160
|
-
int64_t start_frame,
|
|
161
|
-
uint32_t frames,
|
|
162
|
-
std::vector<float>&& pcm)
|
|
163
|
-
{
|
|
164
|
-
if (start_frame < 0 || frames == 0 || pcm.empty()) return;
|
|
165
|
-
segments.push_back({start_frame, frames, std::move(pcm)});
|
|
166
|
-
|
|
167
|
-
const int64_t newest_end = segments.back().start_frame + segments.back().frames;
|
|
168
|
-
const int64_t keep_after = newest_end - static_cast<int64_t>(kMaxMicBufferFrames);
|
|
169
|
-
while (!segments.empty() &&
|
|
170
|
-
segments.front().start_frame + segments.front().frames <= keep_after)
|
|
171
|
-
{
|
|
172
|
-
segments.pop_front();
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
std::vector<MixedAudioChunk> collect_ready_locked(bool has_mic)
|
|
177
|
-
{
|
|
178
|
-
std::vector<MixedAudioChunk> chunks;
|
|
179
|
-
const int64_t latest_mic_end = mic_segments.empty()
|
|
180
|
-
? -1
|
|
181
|
-
: mic_segments.back().start_frame + mic_segments.back().frames;
|
|
182
|
-
|
|
183
|
-
while (!system_segments.empty()) {
|
|
184
|
-
AudioSegment& sys = system_segments.front();
|
|
185
|
-
if (sys.start_frame + sys.frames <= last_mixed_audio_frame) {
|
|
186
|
-
system_segments.pop_front();
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (sys.start_frame < last_mixed_audio_frame) {
|
|
191
|
-
const int64_t trim = last_mixed_audio_frame - sys.start_frame;
|
|
192
|
-
if (trim >= sys.frames) {
|
|
193
|
-
system_segments.pop_front();
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
sys.pcm.erase(
|
|
197
|
-
sys.pcm.begin(),
|
|
198
|
-
sys.pcm.begin() + static_cast<ptrdiff_t>(trim * channels));
|
|
199
|
-
sys.start_frame = last_mixed_audio_frame;
|
|
200
|
-
sys.frames -= static_cast<uint32_t>(trim);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const int64_t sys_end = sys.start_frame + sys.frames;
|
|
204
|
-
if (has_mic && latest_mic_end < sys_end) {
|
|
205
|
-
break;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
MixedAudioChunk chunk;
|
|
209
|
-
chunk.start_frame = sys.start_frame;
|
|
210
|
-
chunk.frames = sys.frames;
|
|
211
|
-
chunk.pcm = sys.pcm;
|
|
212
|
-
|
|
213
|
-
if (has_mic) {
|
|
214
|
-
const int64_t end_frame = chunk.start_frame + chunk.frames;
|
|
215
|
-
for (const AudioSegment& segment : mic_segments) {
|
|
216
|
-
const int64_t seg_end = segment.start_frame + segment.frames;
|
|
217
|
-
const int64_t overlap_start = std::max(chunk.start_frame, segment.start_frame);
|
|
218
|
-
const int64_t overlap_end = std::min(end_frame, seg_end);
|
|
219
|
-
if (overlap_start >= overlap_end) continue;
|
|
220
|
-
|
|
221
|
-
const int64_t out_frame_offset = overlap_start - chunk.start_frame;
|
|
222
|
-
const int64_t mic_frame_offset = overlap_start - segment.start_frame;
|
|
223
|
-
const int64_t overlap_frames = overlap_end - overlap_start;
|
|
224
|
-
|
|
225
|
-
for (int64_t f = 0; f < overlap_frames; ++f) {
|
|
226
|
-
for (uint32_t c = 0; c < channels; ++c) {
|
|
227
|
-
const size_t out_i =
|
|
228
|
-
static_cast<size_t>((out_frame_offset + f) * channels + c);
|
|
229
|
-
const size_t mic_i =
|
|
230
|
-
static_cast<size_t>((mic_frame_offset + f) * channels + c);
|
|
231
|
-
chunk.pcm[out_i] = soft_limit(
|
|
232
|
-
chunk.pcm[out_i] + (segment.pcm[mic_i] * kNativeMicGain));
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
last_mixed_audio_frame = sys_end;
|
|
239
|
-
while (!mic_segments.empty() &&
|
|
240
|
-
mic_segments.front().start_frame + mic_segments.front().frames <= last_mixed_audio_frame)
|
|
241
|
-
{
|
|
242
|
-
mic_segments.pop_front();
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
chunks.push_back(std::move(chunk));
|
|
246
|
-
system_segments.pop_front();
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return chunks;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
std::vector<MixedAudioChunk> append_mic_and_collect_ready(
|
|
253
|
-
int64_t start_frame,
|
|
254
|
-
uint32_t frames,
|
|
255
|
-
std::vector<float>&& pcm)
|
|
256
|
-
{
|
|
257
|
-
std::lock_guard<std::mutex> lk(mix_mutex);
|
|
258
|
-
append_segment(mic_segments, start_frame, frames, std::move(pcm));
|
|
259
|
-
return collect_ready_locked(true);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
std::vector<MixedAudioChunk> append_system_and_collect_ready(
|
|
263
|
-
int64_t start_frame,
|
|
264
|
-
uint32_t frames,
|
|
265
|
-
std::vector<float>&& pcm,
|
|
266
|
-
bool has_mic)
|
|
267
|
-
{
|
|
268
|
-
std::lock_guard<std::mutex> lk(mix_mutex);
|
|
269
|
-
append_segment(system_segments, start_frame, frames, std::move(pcm));
|
|
270
|
-
return collect_ready_locked(has_mic);
|
|
271
|
-
}
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
@interface SCKDelegate : NSObject <SCStreamOutput>
|
|
275
|
-
@property (nonatomic, assign) DesktopCapturer* capturer;
|
|
276
|
-
@end
|
|
277
|
-
|
|
278
|
-
@implementation SCKDelegate
|
|
279
|
-
- (void)stream:(SCStream*)stream
|
|
280
|
-
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
|
|
281
|
-
ofType:(SCStreamOutputType)type
|
|
282
|
-
{
|
|
283
|
-
if (!self.capturer) return;
|
|
284
|
-
if (type == SCStreamOutputTypeScreen)
|
|
285
|
-
self.capturer->on_video_sample(static_cast<void*>(sampleBuffer));
|
|
286
|
-
else if (type == SCStreamOutputTypeAudio)
|
|
287
|
-
self.capturer->on_audio_sample(static_cast<void*>(sampleBuffer));
|
|
288
|
-
}
|
|
289
|
-
@end
|
|
290
|
-
|
|
291
|
-
DesktopCapturer::DesktopCapturer()
|
|
292
|
-
: m_impl(std::make_unique<Impl>())
|
|
293
|
-
{}
|
|
294
|
-
|
|
295
|
-
DesktopCapturer::~DesktopCapturer() {
|
|
296
|
-
stopCapture();
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
bool DesktopCapturer::selectSource(CaptureSource source) {
|
|
300
|
-
m_source = source;
|
|
301
|
-
return true;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
bool DesktopCapturer::startCapture(const CaptureOptions& options,
|
|
305
|
-
SegmentReadyCallback segment_ready_callback)
|
|
306
|
-
{
|
|
307
|
-
if (m_capturing) return false;
|
|
308
|
-
m_last_error.clear();
|
|
309
|
-
m_options = options;
|
|
310
|
-
if (m_options.no_screen) {
|
|
311
|
-
// Camera-only capture never starts ScreenCaptureKit, so there is no
|
|
312
|
-
// system-audio callback to drain the system+mic mixer. Force mic-only
|
|
313
|
-
// even if the UI-level desktop-audio toggle is enabled.
|
|
314
|
-
m_options.system_audio = false;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const int ch = static_cast<int>(options.channels > 0 ? options.channels : 2);
|
|
318
|
-
m_impl->sample_rate = options.sample_rate;
|
|
319
|
-
m_impl->channels = static_cast<uint32_t>(ch);
|
|
320
|
-
m_impl->video_pts_origin_sec = -1.0;
|
|
321
|
-
m_impl->mic_host_origin_sec = -1.0;
|
|
322
|
-
m_impl->last_video_pts_tick = -1;
|
|
323
|
-
m_impl->reset_audio_sync();
|
|
324
|
-
|
|
325
|
-
uint32_t output_width = options.width;
|
|
326
|
-
uint32_t output_height = options.height;
|
|
327
|
-
if (options.crop_enabled) {
|
|
328
|
-
const uint32_t crop_x = std::min(options.crop_x, options.width > 1 ? options.width - 1 : 0);
|
|
329
|
-
const uint32_t crop_y = std::min(options.crop_y, options.height > 1 ? options.height - 1 : 0);
|
|
330
|
-
const uint32_t max_width = options.width - crop_x;
|
|
331
|
-
const uint32_t max_height = options.height - crop_y;
|
|
332
|
-
output_width = even_down(std::min(options.crop_width, max_width));
|
|
333
|
-
output_height = even_down(std::min(options.crop_height, max_height));
|
|
334
|
-
|
|
335
|
-
if (output_width < 2 || output_height < 2) {
|
|
336
|
-
m_last_error = "Invalid native crop dimensions";
|
|
337
|
-
return false;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
m_options.crop_x = crop_x;
|
|
341
|
-
m_options.crop_y = crop_y;
|
|
342
|
-
m_options.crop_width = output_width;
|
|
343
|
-
m_options.crop_height = output_height;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (m_options.no_screen && !m_options.camera) {
|
|
347
|
-
m_last_error = "Native no-screen capture requires camera=true";
|
|
348
|
-
return false;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (m_options.no_screen && m_options.camera) {
|
|
352
|
-
CameraCapturer* cam = new CameraCapturer();
|
|
353
|
-
uint32_t req_width = output_width;
|
|
354
|
-
uint32_t req_height = output_height;
|
|
355
|
-
if (!cam->start(m_options.camera_device_uid, req_width, req_height, m_options.fps,
|
|
356
|
-
[this](void* sb) {
|
|
357
|
-
this->on_camera_sample(sb);
|
|
358
|
-
}))
|
|
359
|
-
{
|
|
360
|
-
m_last_error = "Failed to start native camera capture for device '" +
|
|
361
|
-
m_options.camera_device_uid + "' at requested size " +
|
|
362
|
-
std::to_string(output_width) + "x" + std::to_string(output_height);
|
|
363
|
-
delete cam;
|
|
364
|
-
return false;
|
|
365
|
-
}
|
|
366
|
-
m_camera_capturer = cam;
|
|
367
|
-
output_width = req_width;
|
|
368
|
-
output_height = req_height;
|
|
369
|
-
m_options.width = output_width;
|
|
370
|
-
m_options.height = output_height;
|
|
371
|
-
m_options.crop_enabled = false;
|
|
372
|
-
m_options.crop_width = output_width;
|
|
373
|
-
m_options.crop_height = output_height;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
PipelineConfig pcfg;
|
|
377
|
-
pcfg.width = static_cast<int>(output_width);
|
|
378
|
-
pcfg.height = static_cast<int>(output_height);
|
|
379
|
-
pcfg.fps = static_cast<int>(options.fps);
|
|
380
|
-
pcfg.gop_size = static_cast<int>(options.gop_size > 0 ? options.gop_size : options.fps);
|
|
381
|
-
pcfg.video_bitrate = static_cast<int>(options.video_bitrate);
|
|
382
|
-
pcfg.sample_rate = static_cast<int>(options.sample_rate);
|
|
383
|
-
pcfg.channels = ch;
|
|
384
|
-
pcfg.audio_bitrate = static_cast<int>(options.audio_bitrate);
|
|
385
|
-
pcfg.output_dir = options.output_dir;
|
|
386
|
-
pcfg.recording_id = options.recording_id;
|
|
387
|
-
pcfg.segment_time_sec = static_cast<int>(options.segment_time_sec > 0 ? options.segment_time_sec : 4);
|
|
388
|
-
pcfg.segment_ready_callback = std::move(segment_ready_callback);
|
|
389
|
-
|
|
390
|
-
m_impl->pipeline = std::make_unique<MediaPipeline>();
|
|
391
|
-
if (!m_impl->pipeline->init(pcfg))
|
|
392
|
-
{
|
|
393
|
-
m_last_error = "Failed to initialize native media pipeline";
|
|
394
|
-
m_impl->pipeline.reset();
|
|
395
|
-
if (m_camera_capturer) {
|
|
396
|
-
static_cast<CameraCapturer*>(m_camera_capturer)->stop();
|
|
397
|
-
delete static_cast<CameraCapturer*>(m_camera_capturer);
|
|
398
|
-
m_camera_capturer = nullptr;
|
|
399
|
-
}
|
|
400
|
-
return false;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
m_impl->started.store(true);
|
|
404
|
-
|
|
405
|
-
if (options.microphone && !setup_mic_capture()) {
|
|
406
|
-
m_impl->started.store(false);
|
|
407
|
-
m_impl->pipeline.reset();
|
|
408
|
-
if (m_camera_capturer) {
|
|
409
|
-
static_cast<CameraCapturer*>(m_camera_capturer)->stop();
|
|
410
|
-
delete static_cast<CameraCapturer*>(m_camera_capturer);
|
|
411
|
-
m_camera_capturer = nullptr;
|
|
412
|
-
}
|
|
413
|
-
return false;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
if (m_camera_capturer) {
|
|
417
|
-
m_capturing = true;
|
|
418
|
-
return true;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
|
422
|
-
__block bool ok = false;
|
|
423
|
-
DesktopCapturer* self_ptr = this;
|
|
424
|
-
|
|
425
|
-
[SCShareableContent getShareableContentWithCompletionHandler:
|
|
426
|
-
^(SCShareableContent* content, NSError* error)
|
|
427
|
-
{
|
|
428
|
-
if (error || !content) {
|
|
429
|
-
self_ptr->m_last_error = error
|
|
430
|
-
? [[error localizedDescription] UTF8String]
|
|
431
|
-
: "ScreenCaptureKit returned no shareable content";
|
|
432
|
-
dispatch_semaphore_signal(sem);
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
SCContentFilter* filter = nil;
|
|
437
|
-
SCDisplay* display = nil;
|
|
438
|
-
|
|
439
|
-
if (self_ptr->m_options.capture_window && self_ptr->m_options.window_id) {
|
|
440
|
-
for (SCWindow* w in content.windows) {
|
|
441
|
-
if (w.windowID == self_ptr->m_options.window_id) {
|
|
442
|
-
filter = [[SCContentFilter alloc]
|
|
443
|
-
initWithDesktopIndependentWindow:w];
|
|
444
|
-
break;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
if (!filter) {
|
|
448
|
-
self_ptr->m_last_error = "Requested ScreenCaptureKit window was not found";
|
|
449
|
-
dispatch_semaphore_signal(sem);
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
if (!filter) {
|
|
454
|
-
for (SCDisplay* d in content.displays) {
|
|
455
|
-
if (self_ptr->m_options.display_id == 0 ||
|
|
456
|
-
d.displayID == self_ptr->m_options.display_id)
|
|
457
|
-
{ display = d; break; }
|
|
458
|
-
}
|
|
459
|
-
if (!display) display = content.displays.firstObject;
|
|
460
|
-
if (!display) {
|
|
461
|
-
self_ptr->m_last_error = "ScreenCaptureKit returned no displays";
|
|
462
|
-
dispatch_semaphore_signal(sem);
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
filter = [[SCContentFilter alloc]
|
|
466
|
-
initWithDisplay:display excludingWindows:@[]];
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
SCStreamConfiguration* cfg = [[SCStreamConfiguration alloc] init];
|
|
470
|
-
cfg.width = self_ptr->m_options.width;
|
|
471
|
-
cfg.height = self_ptr->m_options.height;
|
|
472
|
-
cfg.pixelFormat = kCVPixelFormatType_32BGRA;
|
|
473
|
-
cfg.showsCursor = YES;
|
|
474
|
-
cfg.minimumFrameInterval = CMTimeMake(1, (int32_t)self_ptr->m_options.fps);
|
|
475
|
-
cfg.capturesAudio = self_ptr->m_options.system_audio ? YES : NO;
|
|
476
|
-
if (cfg.capturesAudio) {
|
|
477
|
-
cfg.sampleRate = self_ptr->m_options.sample_rate;
|
|
478
|
-
cfg.channelCount = self_ptr->m_impl->channels;
|
|
479
|
-
cfg.excludesCurrentProcessAudio = self_ptr->m_options.exclude_app_audio;
|
|
480
|
-
}
|
|
481
|
-
cfg.queueDepth = 3;
|
|
482
|
-
|
|
483
|
-
SCKDelegate* del = [[SCKDelegate alloc] init];
|
|
484
|
-
del.capturer = self_ptr;
|
|
485
|
-
|
|
486
|
-
SCStream* stream = [[SCStream alloc]
|
|
487
|
-
initWithFilter:filter configuration:cfg delegate:nil];
|
|
488
|
-
|
|
489
|
-
NSError* ae = nil;
|
|
490
|
-
[stream addStreamOutput:del
|
|
491
|
-
type:SCStreamOutputTypeScreen
|
|
492
|
-
sampleHandlerQueue:dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE,0)
|
|
493
|
-
error:&ae];
|
|
494
|
-
if (ae) {
|
|
495
|
-
self_ptr->m_last_error = [[ae localizedDescription] UTF8String];
|
|
496
|
-
dispatch_semaphore_signal(sem);
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
if (cfg.capturesAudio)
|
|
500
|
-
[stream addStreamOutput:del
|
|
501
|
-
type:SCStreamOutputTypeAudio
|
|
502
|
-
sampleHandlerQueue:dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE,0)
|
|
503
|
-
error:&ae];
|
|
504
|
-
if (ae) {
|
|
505
|
-
self_ptr->m_last_error = [[ae localizedDescription] UTF8String];
|
|
506
|
-
dispatch_semaphore_signal(sem);
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
self_ptr->m_stream = (__bridge_retained void*)stream;
|
|
511
|
-
self_ptr->m_filter = (__bridge_retained void*)filter;
|
|
512
|
-
self_ptr->m_config = (__bridge_retained void*)cfg;
|
|
513
|
-
self_ptr->m_delegate = (__bridge_retained void*)del;
|
|
514
|
-
|
|
515
|
-
[stream startCaptureWithCompletionHandler:^(NSError* e) {
|
|
516
|
-
ok = (e == nil);
|
|
517
|
-
if (e) self_ptr->m_last_error = [[e localizedDescription] UTF8String];
|
|
518
|
-
dispatch_semaphore_signal(sem);
|
|
519
|
-
}];
|
|
520
|
-
}];
|
|
521
|
-
|
|
522
|
-
dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, 5LL * NSEC_PER_SEC));
|
|
523
|
-
|
|
524
|
-
if (!ok) {
|
|
525
|
-
if (m_last_error.empty()) m_last_error = "Timed out or failed starting ScreenCaptureKit stream";
|
|
526
|
-
m_impl->started.store(false);
|
|
527
|
-
stop_mic_capture();
|
|
528
|
-
m_impl->pipeline.reset();
|
|
529
|
-
return false;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
m_capturing = true;
|
|
533
|
-
return true;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
bool DesktopCapturer::stopCapture() {
|
|
537
|
-
if (!m_capturing && !m_stream && !m_delegate && !m_config && !m_filter && !m_audio_engine && !m_camera_capturer) {
|
|
538
|
-
return true;
|
|
539
|
-
}
|
|
540
|
-
m_capturing = false;
|
|
541
|
-
|
|
542
|
-
stop_mic_capture();
|
|
543
|
-
|
|
544
|
-
if (m_camera_capturer) {
|
|
545
|
-
CameraCapturer* cam = static_cast<CameraCapturer*>(m_camera_capturer);
|
|
546
|
-
cam->stop();
|
|
547
|
-
delete cam;
|
|
548
|
-
m_camera_capturer = nullptr;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
if (m_stream) {
|
|
552
|
-
SCStream* stream = (__bridge SCStream*)m_stream;
|
|
553
|
-
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
|
554
|
-
[stream stopCaptureWithCompletionHandler:^(NSError*) {
|
|
555
|
-
dispatch_semaphore_signal(sem);
|
|
556
|
-
}];
|
|
557
|
-
dispatch_semaphore_wait(sem,
|
|
558
|
-
dispatch_time(DISPATCH_TIME_NOW, 5LL * NSEC_PER_SEC));
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
m_impl->started.store(false);
|
|
562
|
-
|
|
563
|
-
if (m_impl->pipeline) {
|
|
564
|
-
m_impl->pipeline->flush_and_shutdown();
|
|
565
|
-
m_impl->pipeline.reset();
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
if (m_delegate) {
|
|
569
|
-
id del = (__bridge_transfer id)m_delegate;
|
|
570
|
-
(void)del;
|
|
571
|
-
m_delegate = nullptr;
|
|
572
|
-
}
|
|
573
|
-
if (m_stream) {
|
|
574
|
-
id stream = (__bridge_transfer id)m_stream;
|
|
575
|
-
(void)stream;
|
|
576
|
-
m_stream = nullptr;
|
|
577
|
-
}
|
|
578
|
-
if (m_config) {
|
|
579
|
-
id config = (__bridge_transfer id)m_config;
|
|
580
|
-
(void)config;
|
|
581
|
-
m_config = nullptr;
|
|
582
|
-
}
|
|
583
|
-
if (m_filter) {
|
|
584
|
-
id filter = (__bridge_transfer id)m_filter;
|
|
585
|
-
(void)filter;
|
|
586
|
-
m_filter = nullptr;
|
|
587
|
-
}
|
|
588
|
-
m_delegate = nullptr;
|
|
589
|
-
m_stream = nullptr;
|
|
590
|
-
m_config = nullptr;
|
|
591
|
-
m_filter = nullptr;
|
|
592
|
-
return true;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
bool DesktopCapturer::pauseCapture() {
|
|
596
|
-
m_impl->paused.store(true);
|
|
597
|
-
m_impl->pauseStartedVideoPtsSec = m_impl->last_video_pts_sec;
|
|
598
|
-
m_impl->reset_audio_sync();
|
|
599
|
-
std::cout << "[NativeCapture] Pause reset native audio timestamp buffers" << std::endl;
|
|
600
|
-
return true;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
bool DesktopCapturer::resumeCapture() {
|
|
604
|
-
m_impl->paused.store(false);
|
|
605
|
-
m_impl->resumePending.store(true);
|
|
606
|
-
m_impl->audioGated = true;
|
|
607
|
-
m_impl->reset_audio_sync();
|
|
608
|
-
std::cout << "[NativeCapture] Resume pending; audio gated until next video PTS" << std::endl;
|
|
609
|
-
return true;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
void DesktopCapturer::on_camera_sample(void* cm_ref) {
|
|
613
|
-
on_video_sample(cm_ref);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
void DesktopCapturer::on_video_sample(void* cm_ref) {
|
|
617
|
-
if (!m_impl->started.load() || !m_impl->pipeline) return;
|
|
618
|
-
|
|
619
|
-
if (m_impl->paused.load()) return;
|
|
620
|
-
|
|
621
|
-
CMSampleBufferRef sb = static_cast<CMSampleBufferRef>(cm_ref);
|
|
622
|
-
if (!sb || !CMSampleBufferIsValid(sb)) return;
|
|
623
|
-
|
|
624
|
-
CVPixelBufferRef pb = CMSampleBufferGetImageBuffer(sb);
|
|
625
|
-
if (!pb) return;
|
|
626
|
-
|
|
627
|
-
CVPixelBufferLockBaseAddress(pb, kCVPixelBufferLock_ReadOnly);
|
|
628
|
-
const uint8_t* base = static_cast<const uint8_t*>(CVPixelBufferGetBaseAddress(pb));
|
|
629
|
-
int stride = static_cast<int>(CVPixelBufferGetBytesPerRow(pb));
|
|
630
|
-
const uint8_t* frame_base = base;
|
|
631
|
-
if (m_options.crop_enabled && !m_camera_capturer) {
|
|
632
|
-
frame_base = base +
|
|
633
|
-
(static_cast<size_t>(m_options.crop_y) * static_cast<size_t>(stride)) +
|
|
634
|
-
(static_cast<size_t>(m_options.crop_x) * 4u);
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
CMTime pts_cm = CMSampleBufferGetPresentationTimeStamp(sb);
|
|
638
|
-
double pts_sec = CMTIME_IS_VALID(pts_cm) ? CMTimeGetSeconds(pts_cm) : -1.0;
|
|
639
|
-
if (m_camera_capturer) {
|
|
640
|
-
// AVCapture camera sample PTS can advance on a media clock that does
|
|
641
|
-
// not match elapsed recording time. Camera-only mic timestamps are
|
|
642
|
-
// host-time based, so drive camera video from host time too.
|
|
643
|
-
pts_sec = host_time_to_seconds(mach_absolute_time());
|
|
644
|
-
}
|
|
645
|
-
int64_t pts_ticks = -1;
|
|
646
|
-
bool is_keyframe_hint = false;
|
|
647
|
-
|
|
648
|
-
if (pts_sec >= 0.0) {
|
|
649
|
-
m_impl->last_video_pts_sec = pts_sec;
|
|
650
|
-
|
|
651
|
-
if (m_impl->resumePending.exchange(false)) {
|
|
652
|
-
if (m_impl->pauseStartedVideoPtsSec >= 0.0) {
|
|
653
|
-
m_impl->totalPausedVideoSec += (pts_sec - m_impl->pauseStartedVideoPtsSec);
|
|
654
|
-
}
|
|
655
|
-
m_impl->audioGated = false;
|
|
656
|
-
m_impl->reset_audio_sync();
|
|
657
|
-
is_keyframe_hint = true;
|
|
658
|
-
std::cout << "[NativeCapture] Resume aligned at videoPtsSec="
|
|
659
|
-
<< pts_sec
|
|
660
|
-
<< " totalPausedSec=" << m_impl->totalPausedVideoSec
|
|
661
|
-
<< std::endl;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
if (m_impl->video_pts_origin_sec < 0.0) {
|
|
665
|
-
m_impl->video_pts_origin_sec = pts_sec;
|
|
666
|
-
std::cout << "[NativeCapture] Video timestamp origin sec="
|
|
667
|
-
<< m_impl->video_pts_origin_sec << std::endl;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
const double relative_pts_sec = std::max(0.0, pts_sec - m_impl->video_pts_origin_sec - m_impl->totalPausedVideoSec);
|
|
671
|
-
pts_ticks = static_cast<int64_t>(std::llround(relative_pts_sec * m_options.fps));
|
|
672
|
-
|
|
673
|
-
if (pts_ticks <= m_impl->last_video_pts_tick) {
|
|
674
|
-
pts_ticks = m_impl->last_video_pts_tick + 1;
|
|
675
|
-
}
|
|
676
|
-
m_impl->last_video_pts_tick = pts_ticks;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
m_impl->pipeline->push_video_frame(frame_base, stride, pts_ticks, is_keyframe_hint);
|
|
680
|
-
|
|
681
|
-
CVPixelBufferUnlockBaseAddress(pb, kCVPixelBufferLock_ReadOnly);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
void DesktopCapturer::on_audio_sample(void* cm_ref) {
|
|
685
|
-
if (!m_impl->started.load() || !m_impl->pipeline) return;
|
|
686
|
-
|
|
687
|
-
if (m_impl->paused.load() || m_impl->audioGated) return;
|
|
688
|
-
|
|
689
|
-
CMSampleBufferRef sb = static_cast<CMSampleBufferRef>(cm_ref);
|
|
690
|
-
if (!sb || !CMSampleBufferIsValid(sb) || !CMSampleBufferDataIsReady(sb)) return;
|
|
691
|
-
|
|
692
|
-
if (m_impl->video_pts_origin_sec < 0.0) {
|
|
693
|
-
m_impl->dropped_audio_frames.fetch_add(
|
|
694
|
-
static_cast<uint64_t>(CMSampleBufferGetNumSamples(sb)));
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
CMTime pts_cm = CMSampleBufferGetPresentationTimeStamp(sb);
|
|
699
|
-
if (!CMTIME_IS_VALID(pts_cm)) {
|
|
700
|
-
m_last_error = "System audio sample did not include a valid timestamp";
|
|
701
|
-
return;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
const double pts_sec = CMTimeGetSeconds(pts_cm);
|
|
705
|
-
const double relative_pts_sec =
|
|
706
|
-
pts_sec - m_impl->video_pts_origin_sec - m_impl->totalPausedVideoSec;
|
|
707
|
-
if (relative_pts_sec < 0.0) {
|
|
708
|
-
m_impl->dropped_audio_frames.fetch_add(
|
|
709
|
-
static_cast<uint64_t>(CMSampleBufferGetNumSamples(sb)));
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
const int64_t start_frame = static_cast<int64_t>(
|
|
714
|
-
std::llround(relative_pts_sec * static_cast<double>(m_impl->sample_rate)));
|
|
715
|
-
|
|
716
|
-
CMAudioFormatDescriptionRef fmtDesc =
|
|
717
|
-
CMSampleBufferGetFormatDescription(sb);
|
|
718
|
-
if (!fmtDesc) return;
|
|
719
|
-
|
|
720
|
-
const AudioStreamBasicDescription* asbd =
|
|
721
|
-
CMAudioFormatDescriptionGetStreamBasicDescription(fmtDesc);
|
|
722
|
-
if (!asbd) return;
|
|
723
|
-
|
|
724
|
-
const uint32_t in_ch = static_cast<uint32_t>(asbd->mChannelsPerFrame);
|
|
725
|
-
const size_t frames = static_cast<size_t>(CMSampleBufferGetNumSamples(sb));
|
|
726
|
-
if (!frames || !in_ch) return;
|
|
727
|
-
|
|
728
|
-
std::vector<float> pcm(frames * in_ch);
|
|
729
|
-
const bool non_il = (asbd->mFormatFlags & kAudioFormatFlagIsNonInterleaved) != 0;
|
|
730
|
-
|
|
731
|
-
if (non_il) {
|
|
732
|
-
std::vector<uint8_t> abl_storage(
|
|
733
|
-
sizeof(AudioBufferList) + sizeof(AudioBuffer) * (in_ch - 1));
|
|
734
|
-
AudioBufferList* abl =
|
|
735
|
-
reinterpret_cast<AudioBufferList*>(abl_storage.data());
|
|
736
|
-
abl->mNumberBuffers = in_ch;
|
|
737
|
-
std::vector<std::vector<float>> ch(in_ch, std::vector<float>(frames));
|
|
738
|
-
for (uint32_t c = 0; c < in_ch; ++c) {
|
|
739
|
-
abl->mBuffers[c].mNumberChannels = 1;
|
|
740
|
-
abl->mBuffers[c].mDataByteSize =
|
|
741
|
-
static_cast<uint32_t>(frames * sizeof(float));
|
|
742
|
-
abl->mBuffers[c].mData = ch[c].data();
|
|
743
|
-
}
|
|
744
|
-
CMSampleBufferCopyPCMDataIntoAudioBufferList(
|
|
745
|
-
sb, 0, static_cast<int32_t>(frames), abl);
|
|
746
|
-
for (uint32_t c = 0; c < in_ch; ++c)
|
|
747
|
-
for (size_t f = 0; f < frames; ++f)
|
|
748
|
-
pcm[f * in_ch + c] = ch[c][f];
|
|
749
|
-
} else {
|
|
750
|
-
CMBlockBufferRef bb = CMSampleBufferGetDataBuffer(sb);
|
|
751
|
-
if (!bb) return;
|
|
752
|
-
size_t tot = CMBlockBufferGetDataLength(bb);
|
|
753
|
-
std::vector<uint8_t> raw(tot);
|
|
754
|
-
CMBlockBufferCopyDataBytes(bb, 0, tot, raw.data());
|
|
755
|
-
const float* src = reinterpret_cast<const float*>(raw.data());
|
|
756
|
-
std::copy(src, src + frames * in_ch, pcm.begin());
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
const uint32_t tgt_ch = m_impl->channels;
|
|
760
|
-
std::vector<float> sys_pcm(frames * tgt_ch);
|
|
761
|
-
if (in_ch == tgt_ch) {
|
|
762
|
-
sys_pcm = pcm;
|
|
763
|
-
} else {
|
|
764
|
-
for (size_t f = 0; f < frames; ++f)
|
|
765
|
-
for (uint32_t c = 0; c < tgt_ch; ++c)
|
|
766
|
-
sys_pcm[f * tgt_ch + c] = pcm[f * in_ch + (c % in_ch)];
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
std::vector<DesktopCapturer::Impl::MixedAudioChunk> chunks =
|
|
770
|
-
m_impl->append_system_and_collect_ready(
|
|
771
|
-
start_frame,
|
|
772
|
-
static_cast<uint32_t>(frames),
|
|
773
|
-
std::move(sys_pcm),
|
|
774
|
-
m_options.microphone);
|
|
775
|
-
|
|
776
|
-
for (const auto& chunk : chunks) {
|
|
777
|
-
std::vector<int16_t> s16(chunk.pcm.size());
|
|
778
|
-
for (size_t i = 0; i < chunk.pcm.size(); ++i) {
|
|
779
|
-
float v = chunk.pcm[i];
|
|
780
|
-
v = v > 1.f ? 1.f : (v < -1.f ? -1.f : v);
|
|
781
|
-
s16[i] = static_cast<int16_t>(v * 32767.f);
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
m_impl->pipeline->push_audio_s16(
|
|
785
|
-
s16.data(), static_cast<int>(chunk.frames), chunk.start_frame);
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
void DesktopCapturer::on_mic_pcm(const float* data, uint32_t frames,
|
|
790
|
-
uint32_t sr, uint32_t ch,
|
|
791
|
-
double host_time_sec)
|
|
792
|
-
{
|
|
793
|
-
if (!m_impl->started.load() || !data || !frames || !ch) return;
|
|
794
|
-
|
|
795
|
-
if (m_impl->paused.load() || m_impl->audioGated) return;
|
|
796
|
-
|
|
797
|
-
if (m_impl->last_video_pts_tick < 0 || host_time_sec <= 0.0) {
|
|
798
|
-
m_impl->dropped_audio_frames.fetch_add(frames);
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
const uint64_t callbacks = m_impl->mic_callbacks.fetch_add(1) + 1;
|
|
803
|
-
const uint64_t total_frames = m_impl->mic_frames.fetch_add(frames) + frames;
|
|
804
|
-
if (callbacks == 1 || callbacks == 50) {
|
|
805
|
-
std::cout << "[NativeCapture] Mic PCM received callbacks="
|
|
806
|
-
<< callbacks
|
|
807
|
-
<< " frames=" << total_frames
|
|
808
|
-
<< " sampleRate=" << sr
|
|
809
|
-
<< " channels=" << ch
|
|
810
|
-
<< " systemAudio=" << (m_options.system_audio ? "true" : "false")
|
|
811
|
-
<< std::endl;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
const uint32_t target_rate = m_impl->sample_rate ? m_impl->sample_rate : 48000;
|
|
815
|
-
const uint32_t target_ch = m_impl->channels ? m_impl->channels : 2;
|
|
816
|
-
const uint32_t input_rate = sr ? sr : target_rate;
|
|
817
|
-
const uint32_t out_frames = std::max<uint32_t>(
|
|
818
|
-
1,
|
|
819
|
-
static_cast<uint32_t>(std::llround(
|
|
820
|
-
static_cast<double>(frames) * static_cast<double>(target_rate) /
|
|
821
|
-
static_cast<double>(input_rate))));
|
|
822
|
-
|
|
823
|
-
std::vector<float> normalized(out_frames * target_ch);
|
|
824
|
-
const double src_step = static_cast<double>(input_rate) /
|
|
825
|
-
static_cast<double>(target_rate);
|
|
826
|
-
|
|
827
|
-
for (uint32_t f = 0; f < out_frames; ++f) {
|
|
828
|
-
const double src_pos = static_cast<double>(f) * src_step;
|
|
829
|
-
const uint32_t i0 = std::min<uint32_t>(
|
|
830
|
-
static_cast<uint32_t>(std::floor(src_pos)), frames - 1);
|
|
831
|
-
const uint32_t i1 = std::min<uint32_t>(i0 + 1, frames - 1);
|
|
832
|
-
const float frac = static_cast<float>(src_pos - static_cast<double>(i0));
|
|
833
|
-
|
|
834
|
-
for (uint32_t c = 0; c < target_ch; ++c) {
|
|
835
|
-
const uint32_t src_c = ch == 1 ? 0 : std::min<uint32_t>(c, ch - 1);
|
|
836
|
-
const float a = data[(i0 * ch) + src_c];
|
|
837
|
-
const float b = data[(i1 * ch) + src_c];
|
|
838
|
-
normalized[(f * target_ch) + c] = a + ((b - a) * frac);
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
if (m_impl->mic_host_origin_sec < 0.0) {
|
|
843
|
-
const double current_recording_sec =
|
|
844
|
-
static_cast<double>(m_impl->last_video_pts_tick) /
|
|
845
|
-
static_cast<double>(m_options.fps ? m_options.fps : 30);
|
|
846
|
-
m_impl->mic_host_origin_sec = host_time_sec - current_recording_sec;
|
|
847
|
-
std::cout << "[NativeCapture] Mic timestamp origin hostSec="
|
|
848
|
-
<< m_impl->mic_host_origin_sec
|
|
849
|
-
<< " currentRecordingSec=" << current_recording_sec
|
|
850
|
-
<< std::endl;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
const double relative_sec = host_time_sec - m_impl->mic_host_origin_sec;
|
|
854
|
-
int64_t start_frame = static_cast<int64_t>(
|
|
855
|
-
std::llround(relative_sec * static_cast<double>(target_rate)));
|
|
856
|
-
uint32_t frames_to_use = out_frames;
|
|
857
|
-
size_t sample_offset = 0;
|
|
858
|
-
|
|
859
|
-
if (start_frame < 0) {
|
|
860
|
-
const int64_t trim = -start_frame;
|
|
861
|
-
if (trim >= static_cast<int64_t>(frames_to_use)) {
|
|
862
|
-
m_impl->dropped_audio_frames.fetch_add(frames);
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
sample_offset = static_cast<size_t>(trim) * target_ch;
|
|
866
|
-
frames_to_use -= static_cast<uint32_t>(trim);
|
|
867
|
-
start_frame = 0;
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
std::vector<float> aligned(
|
|
871
|
-
normalized.begin() + static_cast<ptrdiff_t>(sample_offset),
|
|
872
|
-
normalized.end());
|
|
873
|
-
|
|
874
|
-
if (!m_options.system_audio) {
|
|
875
|
-
std::vector<int16_t> s16(static_cast<size_t>(frames_to_use) * target_ch);
|
|
876
|
-
for (size_t i = 0; i < s16.size(); ++i) {
|
|
877
|
-
float v = soft_limit(aligned[i] * kNativeMicGain);
|
|
878
|
-
v = v > 1.f ? 1.f : (v < -1.f ? -1.f : v);
|
|
879
|
-
s16[i] = static_cast<int16_t>(v * 32767.f);
|
|
880
|
-
}
|
|
881
|
-
if (m_impl->pipeline) {
|
|
882
|
-
m_impl->pipeline->push_audio_s16(
|
|
883
|
-
s16.data(), static_cast<int>(frames_to_use), start_frame);
|
|
884
|
-
}
|
|
885
|
-
return;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
std::vector<DesktopCapturer::Impl::MixedAudioChunk> chunks =
|
|
889
|
-
m_impl->append_mic_and_collect_ready(start_frame, frames_to_use, std::move(aligned));
|
|
890
|
-
|
|
891
|
-
for (const auto& chunk : chunks) {
|
|
892
|
-
std::vector<int16_t> s16(chunk.pcm.size());
|
|
893
|
-
for (size_t i = 0; i < chunk.pcm.size(); ++i) {
|
|
894
|
-
float v = chunk.pcm[i];
|
|
895
|
-
v = v > 1.f ? 1.f : (v < -1.f ? -1.f : v);
|
|
896
|
-
s16[i] = static_cast<int16_t>(v * 32767.f);
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
if (m_impl->pipeline) {
|
|
900
|
-
m_impl->pipeline->push_audio_s16(
|
|
901
|
-
s16.data(), static_cast<int>(chunk.frames), chunk.start_frame);
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
bool DesktopCapturer::setup_mic_capture() {
|
|
907
|
-
DesktopCapturer* self = this;
|
|
908
|
-
|
|
909
|
-
AVAudioEngine* engine = [[AVAudioEngine alloc] init];
|
|
910
|
-
AVAudioInputNode* node = engine.inputNode;
|
|
911
|
-
|
|
912
|
-
if (!m_options.mic_device_uid.empty()) {
|
|
913
|
-
AudioDeviceID mic_device = kAudioObjectUnknown;
|
|
914
|
-
if (!find_audio_device_by_uid(m_options.mic_device_uid, &mic_device)) {
|
|
915
|
-
m_last_error = "Selected native microphone was not found";
|
|
916
|
-
return false;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
AudioDeviceID default_device = kAudioObjectUnknown;
|
|
920
|
-
const bool is_default_device =
|
|
921
|
-
get_default_input_device(&default_device) && default_device == mic_device;
|
|
922
|
-
|
|
923
|
-
if (!is_default_device) {
|
|
924
|
-
AudioUnit input_unit = node.audioUnit;
|
|
925
|
-
OSStatus status = AudioUnitSetProperty(
|
|
926
|
-
input_unit,
|
|
927
|
-
kAudioOutputUnitProperty_CurrentDevice,
|
|
928
|
-
kAudioUnitScope_Global,
|
|
929
|
-
0,
|
|
930
|
-
&mic_device,
|
|
931
|
-
sizeof(mic_device));
|
|
932
|
-
if (status != noErr) {
|
|
933
|
-
m_last_error = "Failed to select native microphone device";
|
|
934
|
-
return false;
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
std::cout << "[NativeCapture] Selected native microphone uid="
|
|
939
|
-
<< m_options.mic_device_uid
|
|
940
|
-
<< " audioDeviceId=" << mic_device
|
|
941
|
-
<< " default=" << (is_default_device ? "true" : "false")
|
|
942
|
-
<< std::endl;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
AVAudioFormat* fmt = [node inputFormatForBus:0];
|
|
946
|
-
std::cout << "[NativeCapture] Starting native microphone tap sampleRate="
|
|
947
|
-
<< (uint32_t)fmt.sampleRate
|
|
948
|
-
<< " channels=" << (uint32_t)fmt.channelCount
|
|
949
|
-
<< std::endl;
|
|
950
|
-
|
|
951
|
-
@try {
|
|
952
|
-
[node installTapOnBus:0
|
|
953
|
-
bufferSize:512
|
|
954
|
-
format:nil
|
|
955
|
-
block:^(AVAudioPCMBuffer* buf, AVAudioTime* when)
|
|
956
|
-
{
|
|
957
|
-
if (!buf.floatChannelData) return;
|
|
958
|
-
uint32_t nf = buf.frameLength;
|
|
959
|
-
uint32_t nc = (uint32_t)buf.format.channelCount;
|
|
960
|
-
std::vector<float> il(nf * nc);
|
|
961
|
-
for (uint32_t c = 0; c < nc; ++c) {
|
|
962
|
-
const float* src = buf.floatChannelData[c];
|
|
963
|
-
for (uint32_t f = 0; f < nf; ++f)
|
|
964
|
-
il[f * nc + c] = src[f];
|
|
965
|
-
}
|
|
966
|
-
const double host_time_sec = host_time_to_seconds([when hostTime]);
|
|
967
|
-
self->on_mic_pcm(il.data(), nf,
|
|
968
|
-
(uint32_t)buf.format.sampleRate, nc, host_time_sec);
|
|
969
|
-
}];
|
|
970
|
-
} @catch (NSException* ex) {
|
|
971
|
-
m_last_error = std::string("Failed to install native microphone tap: ") +
|
|
972
|
-
[[ex reason] UTF8String];
|
|
973
|
-
return false;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
NSError* err = nil;
|
|
977
|
-
[engine startAndReturnError:&err];
|
|
978
|
-
if (err) {
|
|
979
|
-
m_last_error = [[err localizedDescription] UTF8String];
|
|
980
|
-
[node removeTapOnBus:0];
|
|
981
|
-
return false;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
m_audio_engine = (__bridge_retained void*)engine;
|
|
985
|
-
std::cout << "[NativeCapture] Native microphone engine started" << std::endl;
|
|
986
|
-
return true;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
void DesktopCapturer::stop_mic_capture() {
|
|
990
|
-
if (!m_audio_engine) return;
|
|
991
|
-
AVAudioEngine* engine = (__bridge_transfer AVAudioEngine*)m_audio_engine;
|
|
992
|
-
[engine.inputNode removeTapOnBus:0];
|
|
993
|
-
[engine stop];
|
|
994
|
-
m_audio_engine = nullptr;
|
|
995
|
-
}
|