@0biwank/screen-capture 1.0.1 → 2.0.1
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.
Potentially problematic release.
This version of @0biwank/screen-capture might be problematic. Click here for more details.
- package/LICENSE +339 -21
- package/LICENSES/CPP-HTTPLIB-MIT.txt +21 -0
- package/LICENSES/FFMPEG-GPLv2.txt +340 -0
- package/LICENSES/PROJECT-MIT.txt +21 -0
- package/LICENSES/X264-COPYING.txt +341 -0
- package/README.md +53 -64
- package/SOURCE.md +35 -0
- package/THIRD_PARTY_NOTICES.md +28 -0
- package/binding.gyp +58 -0
- package/include/CameraCapturer.h +54 -0
- package/include/DesktopCapturer.h +83 -0
- package/include/HLSMuxer/AudioEncoder.h +75 -0
- package/include/HLSMuxer/FileHLSMuxer.h +63 -0
- package/include/HLSMuxer/HLSMuxer.h +13 -0
- package/include/HLSMuxer/VideoEncoder.h +90 -0
- package/include/MediaPipeline.h +39 -0
- package/include/SourceHelper.h +41 -0
- package/include/SourceHelperWrapper.h +29 -0
- package/include/Types.h +58 -0
- package/include/UploadManager.h +9 -0
- package/include/httplib.h +12065 -0
- package/index.d.ts +99 -0
- package/index.js +105 -14
- package/package.json +31 -17
- package/prebuilds/SHA256SUMS +2 -0
- package/prebuilds/darwin-arm64/native_capture.node +0 -0
- package/prebuilds/darwin-x64/native_capture.node +0 -0
- package/scripts/build-ffmpeg-vendor.mjs +178 -0
- package/scripts/build.mjs +53 -0
- package/scripts/stage-prebuild.mjs +28 -0
- package/scripts/verify-package.mjs +39 -0
- package/scripts/verify-packlist.mjs +40 -0
- package/scripts/verify-runtime.cjs +28 -0
- package/src/CameraCapturer.mm +154 -0
- package/src/DesktopCapturer.mm +995 -0
- package/src/HLSMuxer/AudioEncoder.cpp +484 -0
- package/src/HLSMuxer/FileHLSMuxer.cpp +345 -0
- package/src/HLSMuxer/HLSMuxer.cpp +0 -0
- package/src/HLSMuxer/VideoEncoder.cpp +462 -0
- package/src/MediaPipeline.cpp +375 -0
- package/src/MediaProcessor.cpp +0 -0
- package/src/SourceHelper.mm +184 -0
- package/src/SourceHelperWrapper.mm +63 -0
- package/src/UploadManager.h +7 -0
- package/src/addon.cpp +347 -0
- package/vendor/ffmpeg/README.md +40 -0
- package/build/Release/media_processor.node +0 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
#include "HLSMuxer/FileHLSMuxer.h"
|
|
2
|
+
|
|
3
|
+
#include <algorithm>
|
|
4
|
+
#include <cmath>
|
|
5
|
+
#include <cstring>
|
|
6
|
+
#include <dirent.h>
|
|
7
|
+
#include <iostream>
|
|
8
|
+
#include <sys/stat.h>
|
|
9
|
+
#include <utility>
|
|
10
|
+
|
|
11
|
+
extern "C" {
|
|
12
|
+
#include <libavcodec/avcodec.h>
|
|
13
|
+
#include <libavcodec/bsf.h>
|
|
14
|
+
#include <libavcodec/packet.h>
|
|
15
|
+
#include <libavformat/avformat.h>
|
|
16
|
+
#include <libavutil/avutil.h>
|
|
17
|
+
#include <libavutil/error.h>
|
|
18
|
+
#include <libavutil/opt.h>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
namespace {
|
|
22
|
+
std::string av_error(int err) {
|
|
23
|
+
char buf[AV_ERROR_MAX_STRING_SIZE];
|
|
24
|
+
av_strerror(err, buf, sizeof(buf));
|
|
25
|
+
return std::string(buf);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
bool is_avcc_h264(const AVCodecParameters* par) {
|
|
29
|
+
return par &&
|
|
30
|
+
par->codec_id == AV_CODEC_ID_H264 &&
|
|
31
|
+
par->extradata &&
|
|
32
|
+
par->extradata_size > 0 &&
|
|
33
|
+
static_cast<uint8_t>(par->extradata[0]) == 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
FileHLSMuxer::FileHLSMuxer() = default;
|
|
38
|
+
|
|
39
|
+
FileHLSMuxer::~FileHLSMuxer() {
|
|
40
|
+
shutdown();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
bool FileHLSMuxer::init(const std::string& output_dir,
|
|
44
|
+
const std::string& recording_id,
|
|
45
|
+
int segment_time_sec,
|
|
46
|
+
const AVCodecContext* video_codec_ctx,
|
|
47
|
+
const AVCodecContext* audio_codec_ctx)
|
|
48
|
+
{
|
|
49
|
+
if (m_initialized) return true;
|
|
50
|
+
if (!video_codec_ctx || !audio_codec_ctx) {
|
|
51
|
+
std::cerr << "[FileHLSMuxer] video/audio codec context missing\n";
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (output_dir.empty() || recording_id.empty()) {
|
|
55
|
+
std::cerr << "[FileHLSMuxer] outputDir/recordingId required\n";
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
m_output_dir = output_dir;
|
|
60
|
+
m_recording_id = recording_id;
|
|
61
|
+
m_playlist_path = output_dir + "/playlist.m3u8";
|
|
62
|
+
m_fps = video_codec_ctx->framerate.num > 0 ? video_codec_ctx->framerate.num : 30;
|
|
63
|
+
m_sample_rate = audio_codec_ctx->sample_rate > 0 ? audio_codec_ctx->sample_rate : 48000;
|
|
64
|
+
m_audio_frame_samples = audio_codec_ctx->frame_size > 0 ? audio_codec_ctx->frame_size : 1024;
|
|
65
|
+
m_video_tb_num = video_codec_ctx->time_base.num > 0 ? video_codec_ctx->time_base.num : 1;
|
|
66
|
+
m_video_tb_den = video_codec_ctx->time_base.den > 0 ? video_codec_ctx->time_base.den : m_fps;
|
|
67
|
+
m_audio_tb_num = audio_codec_ctx->time_base.num > 0 ? audio_codec_ctx->time_base.num : 1;
|
|
68
|
+
m_audio_tb_den = audio_codec_ctx->time_base.den > 0 ? audio_codec_ctx->time_base.den : m_sample_rate;
|
|
69
|
+
|
|
70
|
+
int ret = avformat_alloc_output_context2(&m_fmt_ctx, nullptr, "hls", m_playlist_path.c_str());
|
|
71
|
+
if (ret < 0 || !m_fmt_ctx) {
|
|
72
|
+
std::cerr << "[FileHLSMuxer] alloc hls output failed: " << av_error(ret) << "\n";
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
m_video_stream = avformat_new_stream(m_fmt_ctx, nullptr);
|
|
77
|
+
if (!m_video_stream) {
|
|
78
|
+
std::cerr << "[FileHLSMuxer] failed to create video stream\n";
|
|
79
|
+
shutdown();
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
ret = avcodec_parameters_from_context(m_video_stream->codecpar, video_codec_ctx);
|
|
83
|
+
if (ret < 0) {
|
|
84
|
+
std::cerr << "[FileHLSMuxer] copy video codec params failed: " << av_error(ret) << "\n";
|
|
85
|
+
shutdown();
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
m_video_stream->time_base = video_codec_ctx->time_base;
|
|
89
|
+
|
|
90
|
+
m_audio_stream = avformat_new_stream(m_fmt_ctx, nullptr);
|
|
91
|
+
if (!m_audio_stream) {
|
|
92
|
+
std::cerr << "[FileHLSMuxer] failed to create audio stream\n";
|
|
93
|
+
shutdown();
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
ret = avcodec_parameters_from_context(m_audio_stream->codecpar, audio_codec_ctx);
|
|
97
|
+
if (ret < 0) {
|
|
98
|
+
std::cerr << "[FileHLSMuxer] copy audio codec params failed: " << av_error(ret) << "\n";
|
|
99
|
+
shutdown();
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
m_audio_stream->time_base = audio_codec_ctx->time_base;
|
|
103
|
+
|
|
104
|
+
if (!(m_fmt_ctx->oformat->flags & AVFMT_NOFILE)) {
|
|
105
|
+
ret = avio_open(&m_fmt_ctx->pb, m_playlist_path.c_str(), AVIO_FLAG_WRITE);
|
|
106
|
+
if (ret < 0) {
|
|
107
|
+
std::cerr << "[FileHLSMuxer] open playlist failed: " << av_error(ret)
|
|
108
|
+
<< " path=" << m_playlist_path << "\n";
|
|
109
|
+
shutdown();
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
AVDictionary* opts = nullptr;
|
|
115
|
+
const int safe_segment_time = std::max(1, segment_time_sec);
|
|
116
|
+
const std::string segment_pattern = output_dir + "/" + recording_id + "-video-%04d.ts";
|
|
117
|
+
av_dict_set(&opts, "hls_time", std::to_string(safe_segment_time).c_str(), 0);
|
|
118
|
+
av_dict_set(&opts, "hls_segment_filename", segment_pattern.c_str(), 0);
|
|
119
|
+
av_dict_set(&opts, "hls_list_size", "0", 0);
|
|
120
|
+
av_dict_set(&opts, "hls_playlist_type", "vod", 0);
|
|
121
|
+
av_dict_set(&opts, "hls_flags", "independent_segments+temp_file", 0);
|
|
122
|
+
av_dict_set(&opts, "hls_segment_type", "mpegts", 0);
|
|
123
|
+
|
|
124
|
+
ret = avformat_write_header(m_fmt_ctx, &opts);
|
|
125
|
+
av_dict_free(&opts);
|
|
126
|
+
if (ret < 0) {
|
|
127
|
+
std::cerr << "[FileHLSMuxer] write header failed: " << av_error(ret) << "\n";
|
|
128
|
+
shutdown();
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!init_h264_bsf_if_needed()) {
|
|
133
|
+
shutdown();
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
m_initialized = true;
|
|
138
|
+
std::cerr << "[FileHLSMuxer] initialized playlist=" << m_playlist_path
|
|
139
|
+
<< " segmentPattern=" << segment_pattern
|
|
140
|
+
<< " hlsTime=" << safe_segment_time
|
|
141
|
+
<< " fps=" << m_fps
|
|
142
|
+
<< " sampleRate=" << m_sample_rate
|
|
143
|
+
<< "\n";
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
void FileHLSMuxer::set_expected_segment_duration_ms(double duration_ms) {
|
|
148
|
+
m_expected_segment_duration_ms = duration_ms > 0.0 ? duration_ms : 0.0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
void FileHLSMuxer::set_segment_ready_callback(std::function<void(const std::string&)> callback) {
|
|
152
|
+
m_segment_ready_callback = std::move(callback);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
bool FileHLSMuxer::init_h264_bsf_if_needed() {
|
|
156
|
+
if (!is_avcc_h264(m_video_stream ? m_video_stream->codecpar : nullptr)) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const AVBitStreamFilter* bsf = av_bsf_get_by_name("h264_mp4toannexb");
|
|
161
|
+
if (!bsf) {
|
|
162
|
+
std::cerr << "[FileHLSMuxer] h264_mp4toannexb filter not found\n";
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
int ret = av_bsf_alloc(bsf, &m_h264_bsf_ctx);
|
|
167
|
+
if (ret < 0) {
|
|
168
|
+
std::cerr << "[FileHLSMuxer] bsf alloc failed: " << av_error(ret) << "\n";
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
ret = avcodec_parameters_copy(m_h264_bsf_ctx->par_in, m_video_stream->codecpar);
|
|
173
|
+
if (ret < 0) {
|
|
174
|
+
std::cerr << "[FileHLSMuxer] bsf parameter copy failed: " << av_error(ret) << "\n";
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
m_h264_bsf_ctx->time_base_in = m_video_stream->time_base;
|
|
178
|
+
|
|
179
|
+
ret = av_bsf_init(m_h264_bsf_ctx);
|
|
180
|
+
if (ret < 0) {
|
|
181
|
+
std::cerr << "[FileHLSMuxer] bsf init failed: " << av_error(ret) << "\n";
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
std::cerr << "[FileHLSMuxer] enabled h264_mp4toannexb\n";
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
void FileHLSMuxer::push_video(AVPacket* pkt) {
|
|
190
|
+
if (!pkt) return;
|
|
191
|
+
write_packet(pkt, true);
|
|
192
|
+
av_packet_free(&pkt);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
void FileHLSMuxer::push_audio(AVPacket* pkt) {
|
|
196
|
+
if (!pkt) return;
|
|
197
|
+
write_packet(pkt, false);
|
|
198
|
+
av_packet_free(&pkt);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
int FileHLSMuxer::write_packet(AVPacket* pkt, bool is_video) {
|
|
202
|
+
if (!m_initialized || !pkt) return AVERROR(EINVAL);
|
|
203
|
+
|
|
204
|
+
AVStream* stream = is_video ? m_video_stream : m_audio_stream;
|
|
205
|
+
if (!stream) return AVERROR(EINVAL);
|
|
206
|
+
|
|
207
|
+
if (pkt->duration <= 0) {
|
|
208
|
+
pkt->duration = is_video ? 1 : m_audio_frame_samples;
|
|
209
|
+
}
|
|
210
|
+
if (pkt->dts == AV_NOPTS_VALUE) {
|
|
211
|
+
pkt->dts = pkt->pts;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
pkt->stream_index = stream->index;
|
|
215
|
+
av_packet_rescale_ts(
|
|
216
|
+
pkt,
|
|
217
|
+
is_video ? AVRational{m_video_tb_num, m_video_tb_den}
|
|
218
|
+
: AVRational{m_audio_tb_num, m_audio_tb_den},
|
|
219
|
+
stream->time_base);
|
|
220
|
+
|
|
221
|
+
if (is_video) {
|
|
222
|
+
log_overlong_segment_if_needed(pkt);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (is_video && m_h264_bsf_ctx) {
|
|
226
|
+
int ret = av_bsf_send_packet(m_h264_bsf_ctx, pkt);
|
|
227
|
+
if (ret < 0) {
|
|
228
|
+
std::cerr << "[FileHLSMuxer] bsf send failed: " << av_error(ret) << "\n";
|
|
229
|
+
return ret;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
while ((ret = av_bsf_receive_packet(m_h264_bsf_ctx, pkt)) == 0) {
|
|
233
|
+
pkt->stream_index = stream->index;
|
|
234
|
+
ret = av_interleaved_write_frame(m_fmt_ctx, pkt);
|
|
235
|
+
av_packet_unref(pkt);
|
|
236
|
+
if (ret < 0) {
|
|
237
|
+
std::cerr << "[FileHLSMuxer] write video packet failed: " << av_error(ret) << "\n";
|
|
238
|
+
return ret;
|
|
239
|
+
}
|
|
240
|
+
notify_ready_segments();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
246
|
+
std::cerr << "[FileHLSMuxer] bsf receive failed: " << av_error(ret) << "\n";
|
|
247
|
+
return ret;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
int ret = av_interleaved_write_frame(m_fmt_ctx, pkt);
|
|
251
|
+
if (ret < 0) {
|
|
252
|
+
std::cerr << "[FileHLSMuxer] write " << (is_video ? "video" : "audio")
|
|
253
|
+
<< " packet failed: " << av_error(ret) << "\n";
|
|
254
|
+
} else if (is_video) {
|
|
255
|
+
notify_ready_segments();
|
|
256
|
+
}
|
|
257
|
+
return ret;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
void FileHLSMuxer::notify_ready_segments() {
|
|
261
|
+
if (!m_segment_ready_callback || m_output_dir.empty() || m_recording_id.empty()) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
DIR* dir = opendir(m_output_dir.c_str());
|
|
266
|
+
if (!dir) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const std::string prefix = m_recording_id + "-video-";
|
|
271
|
+
const std::string suffix = ".ts";
|
|
272
|
+
while (dirent* entry = readdir(dir)) {
|
|
273
|
+
const std::string name = entry->d_name;
|
|
274
|
+
if (name.size() <= prefix.size() + suffix.size()) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (name.rfind(prefix, 0) != 0 ||
|
|
278
|
+
name.compare(name.size() - suffix.size(), suffix.size(), suffix) != 0) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const std::string full_path = m_output_dir + "/" + name;
|
|
283
|
+
struct stat st {};
|
|
284
|
+
if (stat(full_path.c_str(), &st) != 0 || st.st_size <= 0) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (m_notified_segments.insert(full_path).second) {
|
|
289
|
+
std::cerr << "[FileHLSMuxer] segment ready path=" << full_path
|
|
290
|
+
<< " bytes=" << st.st_size << "\n";
|
|
291
|
+
m_segment_ready_callback(full_path);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
closedir(dir);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
void FileHLSMuxer::log_overlong_segment_if_needed(AVPacket* pkt) {
|
|
298
|
+
if (!pkt || !(pkt->flags & AV_PKT_FLAG_KEY) || pkt->pts == AV_NOPTS_VALUE) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (m_last_segment_key_pts >= 0 && m_expected_segment_duration_ms > 0.0) {
|
|
303
|
+
const double dur_ms =
|
|
304
|
+
(pkt->pts - m_last_segment_key_pts) * av_q2d(m_video_stream->time_base) * 1000.0;
|
|
305
|
+
const double one_frame_ms = 1000.0 / std::max(1, m_fps);
|
|
306
|
+
if (dur_ms > m_expected_segment_duration_ms + one_frame_ms) {
|
|
307
|
+
std::cerr << "[FileHLSMuxer] Overlong native HLS segment cadence"
|
|
308
|
+
<< " durationMs=" << dur_ms
|
|
309
|
+
<< " expectedMs=" << m_expected_segment_duration_ms
|
|
310
|
+
<< " previousKeyPts=" << m_last_segment_key_pts
|
|
311
|
+
<< " currentKeyPts=" << pkt->pts
|
|
312
|
+
<< "\n";
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
m_last_segment_key_pts = pkt->pts;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
void FileHLSMuxer::shutdown() {
|
|
319
|
+
if (m_h264_bsf_ctx) {
|
|
320
|
+
av_bsf_free(&m_h264_bsf_ctx);
|
|
321
|
+
m_h264_bsf_ctx = nullptr;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (m_fmt_ctx) {
|
|
325
|
+
if (m_initialized) {
|
|
326
|
+
int ret = av_write_trailer(m_fmt_ctx);
|
|
327
|
+
if (ret < 0) {
|
|
328
|
+
std::cerr << "[FileHLSMuxer] write trailer failed: " << av_error(ret) << "\n";
|
|
329
|
+
} else {
|
|
330
|
+
std::cerr << "[FileHLSMuxer] finalized playlist=" << m_playlist_path << "\n";
|
|
331
|
+
notify_ready_segments();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (!(m_fmt_ctx->oformat->flags & AVFMT_NOFILE)) {
|
|
336
|
+
avio_closep(&m_fmt_ctx->pb);
|
|
337
|
+
}
|
|
338
|
+
avformat_free_context(m_fmt_ctx);
|
|
339
|
+
m_fmt_ctx = nullptr;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
m_video_stream = nullptr;
|
|
343
|
+
m_audio_stream = nullptr;
|
|
344
|
+
m_initialized = false;
|
|
345
|
+
}
|
|
File without changes
|