@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/MediaPipeline.cpp
DELETED
|
@@ -1,375 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* MediaPipeline.cpp — pure C++ file.
|
|
3
|
-
*
|
|
4
|
-
* All FFmpeg includes are here. No Apple/ObjC headers.
|
|
5
|
-
* The AVMediaType clash is avoided because this file never includes AVFoundation.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
#include "MediaPipeline.h"
|
|
9
|
-
#include "HLSMuxer/VideoEncoder.h"
|
|
10
|
-
#include "HLSMuxer/AudioEncoder.h"
|
|
11
|
-
#include "HLSMuxer/FileHLSMuxer.h"
|
|
12
|
-
#include <mutex>
|
|
13
|
-
#include <deque>
|
|
14
|
-
#include <vector>
|
|
15
|
-
#include <algorithm>
|
|
16
|
-
#include <iostream>
|
|
17
|
-
|
|
18
|
-
extern "C" {
|
|
19
|
-
#include <libavcodec/avcodec.h>
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
struct MediaPipeline::Impl {
|
|
23
|
-
struct PendingAudio {
|
|
24
|
-
int64_t startFrame = 0;
|
|
25
|
-
int frames = 0;
|
|
26
|
-
std::vector<int16_t> samples;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
VideoEncoder video_enc;
|
|
30
|
-
AudioEncoder audio_enc;
|
|
31
|
-
FileHLSMuxer file_muxer;
|
|
32
|
-
std::mutex pipeline_mutex;
|
|
33
|
-
int channels = 2;
|
|
34
|
-
int sampleRate = 48000;
|
|
35
|
-
int fps = 30;
|
|
36
|
-
bool forceNextKeyframe = false;
|
|
37
|
-
int64_t keyframeIntervalTicks = 30;
|
|
38
|
-
int64_t nextForcedKeyframePts = 30;
|
|
39
|
-
int64_t currentSegmentStartPtsTicks = -1;
|
|
40
|
-
int64_t lastKeyframePtsTicks = -1;
|
|
41
|
-
bool awaitingForcedKeyframe = false;
|
|
42
|
-
bool warnedOverlongSegment = false;
|
|
43
|
-
bool warnedDoubleOverlongSegment = false;
|
|
44
|
-
bool pendingResumeKeyframe = false;
|
|
45
|
-
uint64_t forcedKeyframeCount = 0;
|
|
46
|
-
int64_t lastVideoPtsTicks = -1;
|
|
47
|
-
int64_t lastAudioEndFrame = 0;
|
|
48
|
-
std::deque<PendingAudio> pendingAudio;
|
|
49
|
-
|
|
50
|
-
int64_t maxAudioEndFrame() const {
|
|
51
|
-
if (lastVideoPtsTicks < 0 || fps <= 0 || sampleRate <= 0) {
|
|
52
|
-
return 0;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return ((lastVideoPtsTicks + 1) * static_cast<int64_t>(sampleRate)) /
|
|
56
|
-
static_cast<int64_t>(fps);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
void drainAudioPackets() {
|
|
60
|
-
while (audio_enc.hasPackets()) {
|
|
61
|
-
AVPacket* pkt = audio_enc.getNextPacket();
|
|
62
|
-
if (pkt) {
|
|
63
|
-
file_muxer.push_audio(pkt);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
void encodeAudioFrames(const int16_t* data, int frames, int64_t pts) {
|
|
69
|
-
if (!data || frames <= 0) {
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
audio_enc.encodeSamples(
|
|
74
|
-
reinterpret_cast<const uint8_t*>(data),
|
|
75
|
-
frames,
|
|
76
|
-
pts);
|
|
77
|
-
lastAudioEndFrame = pts + frames;
|
|
78
|
-
drainAudioPackets();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
void encodeSilence(int frames, int64_t pts) {
|
|
82
|
-
if (frames <= 0) {
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
std::vector<int16_t> silence(static_cast<size_t>(frames) * channels, 0);
|
|
87
|
-
encodeAudioFrames(silence.data(), frames, pts);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
void drainPendingAudioToVideo() {
|
|
91
|
-
const int64_t maxEndFrame = maxAudioEndFrame();
|
|
92
|
-
if (maxEndFrame <= 0) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
while (!pendingAudio.empty() && lastAudioEndFrame < maxEndFrame) {
|
|
97
|
-
PendingAudio audio = std::move(pendingAudio.front());
|
|
98
|
-
pendingAudio.pop_front();
|
|
99
|
-
|
|
100
|
-
if (audio.frames <= 0 || audio.samples.empty()) {
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const int64_t audioEndFrame = audio.startFrame + audio.frames;
|
|
105
|
-
if (audioEndFrame <= lastAudioEndFrame) {
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (audio.startFrame > lastAudioEndFrame) {
|
|
110
|
-
const int silenceFrames = static_cast<int>(
|
|
111
|
-
std::min<int64_t>(
|
|
112
|
-
audio.startFrame - lastAudioEndFrame,
|
|
113
|
-
maxEndFrame - lastAudioEndFrame));
|
|
114
|
-
encodeSilence(silenceFrames, lastAudioEndFrame);
|
|
115
|
-
if (lastAudioEndFrame >= maxEndFrame) {
|
|
116
|
-
pendingAudio.push_front(std::move(audio));
|
|
117
|
-
break;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
int64_t sampleOffsetFrames = 0;
|
|
122
|
-
if (audio.startFrame < lastAudioEndFrame) {
|
|
123
|
-
sampleOffsetFrames = lastAudioEndFrame - audio.startFrame;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (sampleOffsetFrames >= audio.frames) {
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const int readyFrames = static_cast<int>(
|
|
131
|
-
std::min<int64_t>(
|
|
132
|
-
audio.frames - sampleOffsetFrames,
|
|
133
|
-
maxEndFrame - lastAudioEndFrame));
|
|
134
|
-
|
|
135
|
-
if (readyFrames <= 0) {
|
|
136
|
-
pendingAudio.push_front(std::move(audio));
|
|
137
|
-
break;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const int16_t* samples = audio.samples.data() +
|
|
141
|
-
(sampleOffsetFrames * channels);
|
|
142
|
-
encodeAudioFrames(samples, readyFrames, lastAudioEndFrame);
|
|
143
|
-
|
|
144
|
-
const int64_t consumedFrames = sampleOffsetFrames + readyFrames;
|
|
145
|
-
const int remainingFrames = static_cast<int>(audio.frames - consumedFrames);
|
|
146
|
-
if (remainingFrames > 0) {
|
|
147
|
-
PendingAudio rest;
|
|
148
|
-
rest.startFrame = audio.startFrame + consumedFrames;
|
|
149
|
-
rest.frames = remainingFrames;
|
|
150
|
-
const int16_t* restData = audio.samples.data() +
|
|
151
|
-
(consumedFrames * channels);
|
|
152
|
-
rest.samples.assign(
|
|
153
|
-
restData,
|
|
154
|
-
restData + (static_cast<int64_t>(remainingFrames) * channels));
|
|
155
|
-
pendingAudio.push_front(std::move(rest));
|
|
156
|
-
break;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
void requestKeyframe(int64_t pts_ticks, const char* reason) {
|
|
162
|
-
forceNextKeyframe = true;
|
|
163
|
-
awaitingForcedKeyframe = true;
|
|
164
|
-
++forcedKeyframeCount;
|
|
165
|
-
std::cerr << "[MediaPipeline] Requesting native keyframe"
|
|
166
|
-
<< " ptsTicks=" << pts_ticks
|
|
167
|
-
<< " reason=" << reason
|
|
168
|
-
<< " forcedCount=" << forcedKeyframeCount
|
|
169
|
-
<< "\n";
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
void checkSegmentCadenceBeforeEncode(int64_t pts_ticks) {
|
|
173
|
-
if (pts_ticks < 0 || keyframeIntervalTicks <= 0) {
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (currentSegmentStartPtsTicks < 0) {
|
|
178
|
-
currentSegmentStartPtsTicks = pts_ticks;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const int64_t segmentAgeTicks = pts_ticks - currentSegmentStartPtsTicks;
|
|
182
|
-
if (pts_ticks >= nextForcedKeyframePts) {
|
|
183
|
-
requestKeyframe(pts_ticks, "scheduled-boundary");
|
|
184
|
-
while (pts_ticks >= nextForcedKeyframePts) {
|
|
185
|
-
nextForcedKeyframePts += keyframeIntervalTicks;
|
|
186
|
-
}
|
|
187
|
-
} else if (awaitingForcedKeyframe && segmentAgeTicks >= keyframeIntervalTicks) {
|
|
188
|
-
requestKeyframe(pts_ticks, "awaiting-encoded-keyframe");
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (segmentAgeTicks > keyframeIntervalTicks + 1 && !warnedOverlongSegment) {
|
|
192
|
-
warnedOverlongSegment = true;
|
|
193
|
-
std::cerr << "[MediaPipeline] Native segment exceeded expected cadence"
|
|
194
|
-
<< " segmentStartPts=" << currentSegmentStartPtsTicks
|
|
195
|
-
<< " currentPts=" << pts_ticks
|
|
196
|
-
<< " lastKeyframePts=" << lastKeyframePtsTicks
|
|
197
|
-
<< " expectedTicks=" << keyframeIntervalTicks
|
|
198
|
-
<< " forcedCount=" << forcedKeyframeCount
|
|
199
|
-
<< "\n";
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (segmentAgeTicks > (keyframeIntervalTicks * 2) && !warnedDoubleOverlongSegment) {
|
|
203
|
-
warnedDoubleOverlongSegment = true;
|
|
204
|
-
std::cerr << "[MediaPipeline] Native segment exceeded double expected cadence"
|
|
205
|
-
<< " segmentStartPts=" << currentSegmentStartPtsTicks
|
|
206
|
-
<< " currentPts=" << pts_ticks
|
|
207
|
-
<< " lastKeyframePts=" << lastKeyframePtsTicks
|
|
208
|
-
<< " expectedTicks=" << keyframeIntervalTicks
|
|
209
|
-
<< " forcedCount=" << forcedKeyframeCount
|
|
210
|
-
<< "\n";
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
void markEncodedKeyframe(int64_t pkt_pts) {
|
|
215
|
-
if (pkt_pts < 0) {
|
|
216
|
-
pkt_pts = lastVideoPtsTicks;
|
|
217
|
-
}
|
|
218
|
-
if (pkt_pts < 0) {
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const bool wasAwaitingForcedKeyframe = awaitingForcedKeyframe;
|
|
223
|
-
lastKeyframePtsTicks = pkt_pts;
|
|
224
|
-
currentSegmentStartPtsTicks = pkt_pts;
|
|
225
|
-
awaitingForcedKeyframe = false;
|
|
226
|
-
warnedOverlongSegment = false;
|
|
227
|
-
warnedDoubleOverlongSegment = false;
|
|
228
|
-
|
|
229
|
-
while (nextForcedKeyframePts <= pkt_pts) {
|
|
230
|
-
nextForcedKeyframePts += keyframeIntervalTicks;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (pendingResumeKeyframe || wasAwaitingForcedKeyframe) {
|
|
234
|
-
std::cerr << "[MediaPipeline] Encoded native keyframe"
|
|
235
|
-
<< " ptsTicks=" << pkt_pts
|
|
236
|
-
<< " resume=" << (pendingResumeKeyframe ? "true" : "false")
|
|
237
|
-
<< " forcedCount=" << forcedKeyframeCount
|
|
238
|
-
<< "\n";
|
|
239
|
-
}
|
|
240
|
-
pendingResumeKeyframe = false;
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
MediaPipeline::MediaPipeline()
|
|
245
|
-
: m_impl(std::make_unique<Impl>())
|
|
246
|
-
{}
|
|
247
|
-
|
|
248
|
-
MediaPipeline::~MediaPipeline() {
|
|
249
|
-
// flush_and_shutdown should be called explicitly; this is a safety net
|
|
250
|
-
if (m_impl) {
|
|
251
|
-
m_impl->file_muxer.shutdown();
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
bool MediaPipeline::init(const PipelineConfig& cfg) {
|
|
256
|
-
m_impl->channels = cfg.channels;
|
|
257
|
-
m_impl->sampleRate = cfg.sample_rate > 0 ? cfg.sample_rate : 48000;
|
|
258
|
-
m_impl->fps = cfg.fps > 0 ? cfg.fps : 30;
|
|
259
|
-
m_impl->keyframeIntervalTicks = cfg.gop_size > 0 ? cfg.gop_size : cfg.fps;
|
|
260
|
-
m_impl->nextForcedKeyframePts = m_impl->keyframeIntervalTicks;
|
|
261
|
-
|
|
262
|
-
if (!m_impl->video_enc.initialize(cfg.width, cfg.height, cfg.fps,
|
|
263
|
-
cfg.video_bitrate, OutputMode::MUXED,
|
|
264
|
-
cfg.gop_size))
|
|
265
|
-
return false;
|
|
266
|
-
|
|
267
|
-
if (!m_impl->audio_enc.initialize(cfg.sample_rate, cfg.channels,
|
|
268
|
-
cfg.audio_bitrate, OutputMode::MUXED))
|
|
269
|
-
return false;
|
|
270
|
-
|
|
271
|
-
const double expectedSegmentMs =
|
|
272
|
-
(static_cast<double>(m_impl->keyframeIntervalTicks) * 1000.0) /
|
|
273
|
-
static_cast<double>(m_impl->fps);
|
|
274
|
-
|
|
275
|
-
if (!m_impl->file_muxer.init(cfg.output_dir,
|
|
276
|
-
cfg.recording_id,
|
|
277
|
-
cfg.segment_time_sec,
|
|
278
|
-
m_impl->video_enc.getCodecContext(),
|
|
279
|
-
m_impl->audio_enc.getCodecContext()))
|
|
280
|
-
return false;
|
|
281
|
-
m_impl->file_muxer.set_expected_segment_duration_ms(expectedSegmentMs);
|
|
282
|
-
m_impl->file_muxer.set_segment_ready_callback(cfg.segment_ready_callback);
|
|
283
|
-
std::cerr << "[MediaPipeline] file-backed native HLS enabled"
|
|
284
|
-
<< " outputDir=" << cfg.output_dir
|
|
285
|
-
<< " recordingId=" << cfg.recording_id
|
|
286
|
-
<< " segmentTime=" << cfg.segment_time_sec
|
|
287
|
-
<< "\n";
|
|
288
|
-
return true;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
void MediaPipeline::push_video_frame(const uint8_t* data, int stride,
|
|
292
|
-
int64_t pts_ticks, bool is_keyframe_hint)
|
|
293
|
-
{
|
|
294
|
-
std::lock_guard<std::mutex> lock(m_impl->pipeline_mutex);
|
|
295
|
-
|
|
296
|
-
if(is_keyframe_hint){
|
|
297
|
-
m_impl->pendingResumeKeyframe = true;
|
|
298
|
-
m_impl->requestKeyframe(pts_ticks, "explicit-hint");
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
m_impl->checkSegmentCadenceBeforeEncode(pts_ticks);
|
|
302
|
-
|
|
303
|
-
if(m_impl->forceNextKeyframe){
|
|
304
|
-
m_impl->video_enc.forceKeyFrame();
|
|
305
|
-
m_impl->forceNextKeyframe = false;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (pts_ticks >= 0) {
|
|
309
|
-
m_impl->lastVideoPtsTicks = std::max(m_impl->lastVideoPtsTicks, pts_ticks);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// VideoEncoder takes non-const uint8_t* (legacy API)
|
|
313
|
-
m_impl->video_enc.encodeFrame(const_cast<uint8_t*>(data), stride, pts_ticks);
|
|
314
|
-
|
|
315
|
-
while (m_impl->video_enc.hasPackets()) {
|
|
316
|
-
AVPacket* pkt = m_impl->video_enc.getNextPacket();
|
|
317
|
-
if (!pkt) break;
|
|
318
|
-
bool is_key = (pkt->flags & AV_PKT_FLAG_KEY) != 0;
|
|
319
|
-
if (is_key) {
|
|
320
|
-
m_impl->markEncodedKeyframe(pkt->pts);
|
|
321
|
-
}
|
|
322
|
-
m_impl->file_muxer.push_video(pkt);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
m_impl->drainPendingAudioToVideo();
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
void MediaPipeline::push_audio_s16(const int16_t* data, int num_frames, int64_t pts_samples) {
|
|
329
|
-
std::lock_guard<std::mutex> lock(m_impl->pipeline_mutex);
|
|
330
|
-
|
|
331
|
-
if (num_frames <= 0 || pts_samples < 0) {
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
Impl::PendingAudio pending;
|
|
336
|
-
pending.startFrame = pts_samples;
|
|
337
|
-
pending.frames = num_frames;
|
|
338
|
-
pending.samples.assign(data, data + (num_frames * m_impl->channels));
|
|
339
|
-
m_impl->pendingAudio.push_back(std::move(pending));
|
|
340
|
-
|
|
341
|
-
const int64_t keepAfter = std::max<int64_t>(
|
|
342
|
-
0,
|
|
343
|
-
m_impl->lastAudioEndFrame - static_cast<int64_t>(m_impl->sampleRate));
|
|
344
|
-
while (!m_impl->pendingAudio.empty() &&
|
|
345
|
-
m_impl->pendingAudio.front().startFrame +
|
|
346
|
-
m_impl->pendingAudio.front().frames <= keepAfter)
|
|
347
|
-
{
|
|
348
|
-
m_impl->pendingAudio.pop_front();
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
void MediaPipeline::flush_and_shutdown() {
|
|
353
|
-
std::lock_guard<std::mutex> lock(m_impl->pipeline_mutex);
|
|
354
|
-
|
|
355
|
-
m_impl->video_enc.cleanup();
|
|
356
|
-
while (m_impl->video_enc.hasPackets()) {
|
|
357
|
-
AVPacket* pkt = m_impl->video_enc.getNextPacket();
|
|
358
|
-
if (pkt) {
|
|
359
|
-
m_impl->file_muxer.push_video(pkt);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
m_impl->drainPendingAudioToVideo();
|
|
364
|
-
m_impl->pendingAudio.clear();
|
|
365
|
-
|
|
366
|
-
m_impl->audio_enc.cleanup();
|
|
367
|
-
while (m_impl->audio_enc.hasPackets()) {
|
|
368
|
-
AVPacket* pkt = m_impl->audio_enc.getNextPacket();
|
|
369
|
-
if (pkt) {
|
|
370
|
-
m_impl->file_muxer.push_audio(pkt);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
m_impl->file_muxer.shutdown();
|
|
375
|
-
}
|
package/src/MediaProcessor.cpp
DELETED
|
File without changes
|
package/src/SourceHelper.mm
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
#include "SourceHelper.h"
|
|
2
|
-
#include <ApplicationServices/ApplicationServices.h>
|
|
3
|
-
#include <CoreFoundation/CFBase.h>
|
|
4
|
-
#include <CoreFoundation/CFDictionary.h>
|
|
5
|
-
#include <CoreGraphics/CGDirectDisplay.h>
|
|
6
|
-
#include <CoreGraphics/CGWindow.h>
|
|
7
|
-
#include <Foundation/Foundation.h>
|
|
8
|
-
#include <Foundation/NSObjCRuntime.h>
|
|
9
|
-
#include <ScreenCaptureKit/ScreenCaptureKit.h>
|
|
10
|
-
#include <cstdint>
|
|
11
|
-
#include <iostream>
|
|
12
|
-
|
|
13
|
-
CGDirectDisplayID CGDisplayIDFromNSScreen(NSScreen *screen) {
|
|
14
|
-
NSDictionary *deviceDescription = [screen deviceDescription];
|
|
15
|
-
NSNumber *screenNumber = [deviceDescription objectForKey:@"NSScreenNumber"];
|
|
16
|
-
return [screenNumber unsignedIntValue];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
std::vector<SourceHelper::Displays> SourceHelper::getDisplaySources() {
|
|
20
|
-
CGDirectDisplayID displays[16];
|
|
21
|
-
std::vector<SourceHelper::Displays> displayList;
|
|
22
|
-
CGDisplayCount availableDisplays;
|
|
23
|
-
CGError err = CGGetActiveDisplayList(16, displays, &availableDisplays);
|
|
24
|
-
|
|
25
|
-
for (int i = 0; i < availableDisplays; i++) {
|
|
26
|
-
CGDirectDisplayID display = displays[i];
|
|
27
|
-
CGDisplayModeRef mode = CGDisplayCopyDisplayMode(display);
|
|
28
|
-
uint32_t width = 0;
|
|
29
|
-
uint32_t height = 0;
|
|
30
|
-
|
|
31
|
-
if (mode) {
|
|
32
|
-
// This gets the actual pixel dimensions
|
|
33
|
-
width = CGDisplayModeGetPixelWidth(mode);
|
|
34
|
-
height = CGDisplayModeGetPixelHeight(mode);
|
|
35
|
-
|
|
36
|
-
// Some macOS versions need this approach
|
|
37
|
-
if (width == 0 || height == 0) {
|
|
38
|
-
// Use NSScreen for accurate pixel resolution
|
|
39
|
-
NSScreen *screen = nil;
|
|
40
|
-
for (NSScreen *s in [NSScreen screens]) {
|
|
41
|
-
if (CGDisplayIDFromNSScreen(s) == display) {
|
|
42
|
-
screen = s;
|
|
43
|
-
break;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
if (screen) {
|
|
47
|
-
NSRect backing = [screen convertRectToBacking:[screen frame]];
|
|
48
|
-
width = (uint32_t)backing.size.width;
|
|
49
|
-
height = (uint32_t)backing.size.height;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
CGDisplayModeRelease(mode);
|
|
53
|
-
}
|
|
54
|
-
CGRect bounds = CGDisplayBounds(display);
|
|
55
|
-
NSString *title = [NSString stringWithFormat:@"Display %u", display];
|
|
56
|
-
std::string titleStr = std::string([title UTF8String]);
|
|
57
|
-
|
|
58
|
-
bool isMain = (display == CGMainDisplayID());
|
|
59
|
-
displayList.push_back({display, width, height, titleStr, isMain});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return displayList;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
std::vector<SourceHelper::Windows> SourceHelper::getWindowSources() {
|
|
66
|
-
CFArrayRef windows = CGWindowListCopyWindowInfo(
|
|
67
|
-
kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
|
|
68
|
-
std::vector<SourceHelper::Windows> windowList;
|
|
69
|
-
if (windows) {
|
|
70
|
-
CFIndex count = CFArrayGetCount(windows);
|
|
71
|
-
for (CFIndex i = 0; i < count; i++) {
|
|
72
|
-
CFDictionaryRef windowInfo =
|
|
73
|
-
(CFDictionaryRef)CFArrayGetValueAtIndex(windows, i);
|
|
74
|
-
CFNumberRef windowIDRef =
|
|
75
|
-
(CFNumberRef)CFDictionaryGetValue(windowInfo, kCGWindowNumber);
|
|
76
|
-
|
|
77
|
-
if (windowIDRef) {
|
|
78
|
-
CGWindowID windowID;
|
|
79
|
-
CFNumberGetValue(windowIDRef, kCFNumberIntType, &windowID);
|
|
80
|
-
bool isCaptureable =
|
|
81
|
-
isCaptureableWindow((__bridge NSDictionary *)windowInfo);
|
|
82
|
-
if (isCaptureable) {
|
|
83
|
-
// Debug output
|
|
84
|
-
CFStringRef windowNameRef =
|
|
85
|
-
(CFStringRef)CFDictionaryGetValue(windowInfo, kCGWindowName);
|
|
86
|
-
if (windowNameRef) {
|
|
87
|
-
std::cout << "Window ID: " << windowID << " - ";
|
|
88
|
-
CFDictionaryRef boundsRef = (CFDictionaryRef)CFDictionaryGetValue(
|
|
89
|
-
windowInfo, kCGWindowBounds);
|
|
90
|
-
uint32_t width = 0;
|
|
91
|
-
uint32_t height = 0;
|
|
92
|
-
if (boundsRef) {
|
|
93
|
-
CGRect bounds;
|
|
94
|
-
if (CGRectMakeWithDictionaryRepresentation(boundsRef, &bounds)) {
|
|
95
|
-
width = (uint32_t)bounds.size.width;
|
|
96
|
-
height = (uint32_t)bounds.size.height;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
std::cout << "Width: " << width << ", Height: " << height
|
|
100
|
-
<< std::endl;
|
|
101
|
-
CFStringRef ownerNameRef = (CFStringRef)CFDictionaryGetValue(
|
|
102
|
-
windowInfo, kCGWindowOwnerName);
|
|
103
|
-
char ownerName[256];
|
|
104
|
-
if (ownerNameRef) {
|
|
105
|
-
if (CFStringGetCString(ownerNameRef, ownerName, sizeof(ownerName),
|
|
106
|
-
kCFStringEncodingUTF8)) {
|
|
107
|
-
std::cout << "Owner: " << ownerName << " - ";
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
char windowName[256];
|
|
111
|
-
if (CFStringGetCString(windowNameRef, windowName,
|
|
112
|
-
sizeof(windowName), kCFStringEncodingUTF8)) {
|
|
113
|
-
std::cout << windowName << std::endl;
|
|
114
|
-
}
|
|
115
|
-
windowList.push_back(
|
|
116
|
-
{windowID, width, height,
|
|
117
|
-
std::string(ownerName) + " - " + std::string(windowName)});
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
CFRelease(windows);
|
|
123
|
-
}
|
|
124
|
-
return windowList;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
bool SourceHelper::isCaptureableWindow(const NSDictionary *windowInfo) {
|
|
128
|
-
NSNumber *isOnscreen =
|
|
129
|
-
[windowInfo objectForKey:(__bridge NSString *)kCGWindowIsOnscreen];
|
|
130
|
-
if (!isOnscreen || ![isOnscreen boolValue]) {
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Filter out windows with no owner (e.g., desktop elements)
|
|
135
|
-
NSNumber *ownerPID =
|
|
136
|
-
[windowInfo objectForKey:(__bridge NSString *)kCGWindowOwnerPID];
|
|
137
|
-
if (!ownerPID || [ownerPID intValue] == 0) {
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Filter out windows with layer != 0 (normal windows are at layer 0)
|
|
142
|
-
NSNumber *windowLayer =
|
|
143
|
-
[windowInfo objectForKey:(__bridge NSString *)kCGWindowLayer];
|
|
144
|
-
if (windowLayer && [windowLayer intValue] != 0) {
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Filter out windows with no bounds or zero size
|
|
149
|
-
NSDictionary *boundsDict =
|
|
150
|
-
[windowInfo objectForKey:(__bridge NSString *)kCGWindowBounds];
|
|
151
|
-
if (boundsDict) {
|
|
152
|
-
CGRect bounds;
|
|
153
|
-
if (CGRectMakeWithDictionaryRepresentation(
|
|
154
|
-
(__bridge CFDictionaryRef)boundsDict, &bounds)) {
|
|
155
|
-
if (bounds.size.width <= 1 || bounds.size.height <= 1) {
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Filter out special window types
|
|
162
|
-
NSString *windowName =
|
|
163
|
-
[windowInfo objectForKey:(__bridge NSString *)kCGWindowName];
|
|
164
|
-
NSString *ownerName =
|
|
165
|
-
[windowInfo objectForKey:(__bridge NSString *)kCGWindowOwnerName];
|
|
166
|
-
|
|
167
|
-
// Exclude system UI elements and known non-capturable windows
|
|
168
|
-
if (ownerName) {
|
|
169
|
-
std::string owner = std::string([ownerName UTF8String]);
|
|
170
|
-
if (owner == "Window Server" || owner == "SystemUIServer" ||
|
|
171
|
-
owner == "Dock" || owner == "Control Center" ||
|
|
172
|
-
owner == "NotificationCenter" || owner == "Spotlight") {
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (windowName && [windowName length] > 0) {
|
|
178
|
-
std::string name = std::string([windowName UTF8String]);
|
|
179
|
-
if (name == "Dock" || name == "Menubar" || name.empty()) {
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return true;
|
|
184
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
#include "SourceHelperWrapper.h"
|
|
2
|
-
#include "SourceHelper.h"
|
|
3
|
-
#include <Foundation/Foundation.h>
|
|
4
|
-
|
|
5
|
-
// Implementation of wrapper functions
|
|
6
|
-
std::vector<SourceHelperWrapper::Display>
|
|
7
|
-
SourceHelperWrapper::getDisplaySources() {
|
|
8
|
-
std::vector<SourceHelperWrapper::Display> result;
|
|
9
|
-
std::vector<SourceHelper::Displays> displays =
|
|
10
|
-
SourceHelper::getDisplaySources();
|
|
11
|
-
|
|
12
|
-
for (const auto &display : displays) {
|
|
13
|
-
result.push_back({static_cast<uint32_t>(display.id),
|
|
14
|
-
static_cast<uint32_t>(display.width),
|
|
15
|
-
static_cast<uint32_t>(display.height), display.title,
|
|
16
|
-
display.isMain});
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return result;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
std::vector<SourceHelperWrapper::Window>
|
|
23
|
-
SourceHelperWrapper::getWindowSources() {
|
|
24
|
-
std::vector<SourceHelperWrapper::Window> result;
|
|
25
|
-
std::vector<SourceHelper::Windows> windows = SourceHelper::getWindowSources();
|
|
26
|
-
|
|
27
|
-
for (const auto &window : windows) {
|
|
28
|
-
result.push_back({static_cast<uint32_t>(window.id),
|
|
29
|
-
static_cast<uint32_t>(window.width),
|
|
30
|
-
static_cast<uint32_t>(window.height),
|
|
31
|
-
window.title.empty() ? "" : window.title});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return result;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
bool SourceHelperWrapper::isCaptureableWindow(uint32_t windowID) {
|
|
38
|
-
CFArrayRef windows =
|
|
39
|
-
CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
|
|
40
|
-
if (!windows)
|
|
41
|
-
return false;
|
|
42
|
-
|
|
43
|
-
CFIndex count = CFArrayGetCount(windows);
|
|
44
|
-
for (CFIndex i = 0; i < count; i++) {
|
|
45
|
-
CFDictionaryRef windowInfo =
|
|
46
|
-
(CFDictionaryRef)CFArrayGetValueAtIndex(windows, i);
|
|
47
|
-
CFNumberRef windowIDRef =
|
|
48
|
-
(CFNumberRef)CFDictionaryGetValue(windowInfo, kCGWindowNumber);
|
|
49
|
-
|
|
50
|
-
if (windowIDRef) {
|
|
51
|
-
CGWindowID currentID;
|
|
52
|
-
CFNumberGetValue(windowIDRef, kCFNumberIntType, ¤tID);
|
|
53
|
-
if (currentID == windowID) {
|
|
54
|
-
bool result = SourceHelper::isCaptureableWindow(
|
|
55
|
-
(__bridge NSDictionary *)windowInfo);
|
|
56
|
-
CFRelease(windows);
|
|
57
|
-
return result;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
CFRelease(windows);
|
|
62
|
-
return false;
|
|
63
|
-
}
|