@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
package/src/addon.cpp
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#include "DesktopCapturer.h"
|
|
2
|
+
#include "CameraCapturer.h"
|
|
3
|
+
#include "SourceHelperWrapper.h"
|
|
4
|
+
#include <napi.h>
|
|
5
|
+
#include <atomic>
|
|
6
|
+
#include <memory>
|
|
7
|
+
#include <vector>
|
|
8
|
+
|
|
9
|
+
#ifdef __APPLE__
|
|
10
|
+
#include <CoreAudio/CoreAudio.h>
|
|
11
|
+
#include <CoreFoundation/CoreFoundation.h>
|
|
12
|
+
#endif
|
|
13
|
+
|
|
14
|
+
static std::unique_ptr<DesktopCapturer> g_capturer;
|
|
15
|
+
static std::string g_last_error;
|
|
16
|
+
static Napi::ThreadSafeFunction g_segment_ready_tsfn;
|
|
17
|
+
static std::atomic<bool> g_has_segment_ready_tsfn{false};
|
|
18
|
+
|
|
19
|
+
static void releaseSegmentReadyCallback()
|
|
20
|
+
{
|
|
21
|
+
if (g_has_segment_ready_tsfn.exchange(false))
|
|
22
|
+
{
|
|
23
|
+
g_segment_ready_tsfn.Release();
|
|
24
|
+
g_segment_ready_tsfn = Napi::ThreadSafeFunction();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#ifdef __APPLE__
|
|
29
|
+
static std::string cfStringToUtf8(CFStringRef value)
|
|
30
|
+
{
|
|
31
|
+
if (!value)
|
|
32
|
+
return {};
|
|
33
|
+
char buffer[1024] = {0};
|
|
34
|
+
if (CFStringGetCString(value, buffer, sizeof(buffer), kCFStringEncodingUTF8))
|
|
35
|
+
{
|
|
36
|
+
return buffer;
|
|
37
|
+
}
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static std::string getDeviceString(AudioDeviceID device, AudioObjectPropertySelector selector)
|
|
42
|
+
{
|
|
43
|
+
AudioObjectPropertyAddress addr = {
|
|
44
|
+
selector,
|
|
45
|
+
kAudioObjectPropertyScopeGlobal,
|
|
46
|
+
kAudioObjectPropertyElementMain};
|
|
47
|
+
CFStringRef value = nullptr;
|
|
48
|
+
UInt32 size = sizeof(value);
|
|
49
|
+
OSStatus status = AudioObjectGetPropertyData(device, &addr, 0, nullptr, &size, &value);
|
|
50
|
+
if (status != noErr || !value)
|
|
51
|
+
return {};
|
|
52
|
+
std::string out = cfStringToUtf8(value);
|
|
53
|
+
CFRelease(value);
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static bool hasInputChannels(AudioDeviceID device)
|
|
58
|
+
{
|
|
59
|
+
AudioObjectPropertyAddress addr = {
|
|
60
|
+
kAudioDevicePropertyStreamConfiguration,
|
|
61
|
+
kAudioObjectPropertyScopeInput,
|
|
62
|
+
kAudioObjectPropertyElementMain};
|
|
63
|
+
|
|
64
|
+
UInt32 size = 0;
|
|
65
|
+
OSStatus status = AudioObjectGetPropertyDataSize(device, &addr, 0, nullptr, &size);
|
|
66
|
+
if (status != noErr || size == 0)
|
|
67
|
+
return false;
|
|
68
|
+
|
|
69
|
+
std::vector<uint8_t> storage(size);
|
|
70
|
+
auto *buffers = reinterpret_cast<AudioBufferList *>(storage.data());
|
|
71
|
+
status = AudioObjectGetPropertyData(device, &addr, 0, nullptr, &size, buffers);
|
|
72
|
+
if (status != noErr)
|
|
73
|
+
return false;
|
|
74
|
+
|
|
75
|
+
for (UInt32 i = 0; i < buffers->mNumberBuffers; ++i)
|
|
76
|
+
{
|
|
77
|
+
if (buffers->mBuffers[i].mNumberChannels > 0)
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
#endif
|
|
83
|
+
|
|
84
|
+
Napi::Value startCapture(const Napi::CallbackInfo &info)
|
|
85
|
+
{
|
|
86
|
+
Napi::Env env = info.Env();
|
|
87
|
+
|
|
88
|
+
CaptureOptions opts;
|
|
89
|
+
|
|
90
|
+
if (info.Length() >= 1 && info[0].IsObject())
|
|
91
|
+
{
|
|
92
|
+
Napi::Object o = info[0].As<Napi::Object>();
|
|
93
|
+
auto get_u32 = [&](const char *key, uint32_t def) -> uint32_t
|
|
94
|
+
{
|
|
95
|
+
if (o.Has(key) && o.Get(key).IsNumber())
|
|
96
|
+
return o.Get(key).As<Napi::Number>().Uint32Value();
|
|
97
|
+
return def;
|
|
98
|
+
};
|
|
99
|
+
auto get_bool = [&](const char *key, bool def) -> bool
|
|
100
|
+
{
|
|
101
|
+
if (o.Has(key) && o.Get(key).IsBoolean())
|
|
102
|
+
return o.Get(key).As<Napi::Boolean>().Value();
|
|
103
|
+
return def;
|
|
104
|
+
};
|
|
105
|
+
auto get_str = [&](const char *key) -> std::string
|
|
106
|
+
{
|
|
107
|
+
if (o.Has(key) && o.Get(key).IsString())
|
|
108
|
+
return o.Get(key).As<Napi::String>().Utf8Value();
|
|
109
|
+
return {};
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
opts.display_id = get_u32("displayId", 0);
|
|
113
|
+
opts.capture_window = get_bool("captureWindow", false);
|
|
114
|
+
opts.window_id = get_u32("windowId", 0);
|
|
115
|
+
opts.no_screen = get_bool("noScreen", false);
|
|
116
|
+
|
|
117
|
+
opts.camera = get_bool("camera", false);
|
|
118
|
+
opts.camera_device_uid = get_str("cameraDeviceUid");
|
|
119
|
+
|
|
120
|
+
opts.width = get_u32("width", 1920);
|
|
121
|
+
opts.height = get_u32("height", 1080);
|
|
122
|
+
opts.fps = get_u32("fps", 30);
|
|
123
|
+
opts.gop_size = get_u32("gopSize", opts.fps);
|
|
124
|
+
opts.video_bitrate = get_u32("videoBitrate", 9'000'000);
|
|
125
|
+
opts.crop_x = get_u32("cropX", 0);
|
|
126
|
+
opts.crop_y = get_u32("cropY", 0);
|
|
127
|
+
opts.crop_width = get_u32("cropWidth", 0);
|
|
128
|
+
opts.crop_height = get_u32("cropHeight", 0);
|
|
129
|
+
opts.crop_enabled = opts.crop_width > 0 && opts.crop_height > 0;
|
|
130
|
+
|
|
131
|
+
opts.system_audio = get_bool("systemAudio", true);
|
|
132
|
+
opts.microphone = get_bool("microphone", false);
|
|
133
|
+
opts.mic_device_uid = get_str("micDeviceUid");
|
|
134
|
+
opts.sample_rate = get_u32("sampleRate", 48000);
|
|
135
|
+
opts.audio_bitrate = get_u32("audioBitrate", 128'000);
|
|
136
|
+
opts.channels = 2;
|
|
137
|
+
|
|
138
|
+
opts.output_dir = get_str("outputDir");
|
|
139
|
+
opts.recording_id = get_str("recordingId");
|
|
140
|
+
opts.segment_time_sec = get_u32("segmentTime", 4);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (g_capturer)
|
|
144
|
+
{
|
|
145
|
+
g_capturer->stopCapture();
|
|
146
|
+
g_capturer.reset();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
g_capturer = std::make_unique<DesktopCapturer>();
|
|
150
|
+
|
|
151
|
+
g_last_error.clear();
|
|
152
|
+
|
|
153
|
+
bool ok = g_capturer->startCapture(opts, [](const std::string &full_path) {
|
|
154
|
+
if (!g_has_segment_ready_tsfn.load())
|
|
155
|
+
return;
|
|
156
|
+
|
|
157
|
+
auto *payload = new std::string(full_path);
|
|
158
|
+
napi_status status = g_segment_ready_tsfn.NonBlockingCall(
|
|
159
|
+
payload,
|
|
160
|
+
[](Napi::Env env, Napi::Function callback, std::string *path) {
|
|
161
|
+
callback.Call({Napi::String::New(env, *path)});
|
|
162
|
+
delete path;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (status != napi_ok)
|
|
166
|
+
{
|
|
167
|
+
delete payload;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (!ok)
|
|
172
|
+
{
|
|
173
|
+
g_last_error = g_capturer->lastError();
|
|
174
|
+
g_capturer.reset();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return Napi::Boolean::New(env, ok);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
Napi::Value listVideoInputDevices(const Napi::CallbackInfo &info)
|
|
181
|
+
{
|
|
182
|
+
Napi::Env env = info.Env();
|
|
183
|
+
auto cameras = CameraCapturer::listDevices();
|
|
184
|
+
Napi::Array arr = Napi::Array::New(env, cameras.size());
|
|
185
|
+
for (size_t i = 0; i < cameras.size(); ++i)
|
|
186
|
+
{
|
|
187
|
+
Napi::Object c = Napi::Object::New(env);
|
|
188
|
+
c.Set("uid", Napi::String::New(env, cameras[i].uid));
|
|
189
|
+
c.Set("name", Napi::String::New(env, cameras[i].name));
|
|
190
|
+
arr.Set(static_cast<uint32_t>(i), c);
|
|
191
|
+
}
|
|
192
|
+
return arr;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
Napi::Value getLastError(const Napi::CallbackInfo &info)
|
|
196
|
+
{
|
|
197
|
+
return Napi::String::New(info.Env(), g_last_error);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
Napi::Value setSegmentReadyCallback(const Napi::CallbackInfo &info)
|
|
201
|
+
{
|
|
202
|
+
Napi::Env env = info.Env();
|
|
203
|
+
releaseSegmentReadyCallback();
|
|
204
|
+
|
|
205
|
+
if (info.Length() < 1 || info[0].IsNull() || info[0].IsUndefined())
|
|
206
|
+
{
|
|
207
|
+
return env.Undefined();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!info[0].IsFunction())
|
|
211
|
+
{
|
|
212
|
+
Napi::TypeError::New(env, "setSegmentReadyCallback expects a function").ThrowAsJavaScriptException();
|
|
213
|
+
return env.Undefined();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
g_segment_ready_tsfn = Napi::ThreadSafeFunction::New(
|
|
217
|
+
env,
|
|
218
|
+
info[0].As<Napi::Function>(),
|
|
219
|
+
"NativeSegmentReadyCallback",
|
|
220
|
+
256,
|
|
221
|
+
1);
|
|
222
|
+
g_has_segment_ready_tsfn.store(true);
|
|
223
|
+
|
|
224
|
+
return env.Undefined();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
Napi::Value pauseCapture(const Napi::CallbackInfo &info)
|
|
228
|
+
{
|
|
229
|
+
Napi::Env env = info.Env();
|
|
230
|
+
if (!g_capturer) return Napi::Boolean::New(env, false);
|
|
231
|
+
return Napi::Boolean::New(env, g_capturer->pauseCapture());
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
Napi::Value resumeCapture(const Napi::CallbackInfo &info)
|
|
235
|
+
{
|
|
236
|
+
Napi::Env env = info.Env();
|
|
237
|
+
if (!g_capturer) return Napi::Boolean::New(env, false);
|
|
238
|
+
return Napi::Boolean::New(env, g_capturer->resumeCapture());
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
Napi::Value stopCapture(const Napi::CallbackInfo &info)
|
|
242
|
+
{
|
|
243
|
+
Napi::Env env = info.Env();
|
|
244
|
+
if (!g_capturer) return Napi::Boolean::New(env, false);
|
|
245
|
+
bool ok = g_capturer->stopCapture();
|
|
246
|
+
g_capturer.reset();
|
|
247
|
+
releaseSegmentReadyCallback();
|
|
248
|
+
return Napi::Boolean::New(env, ok);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
Napi::Value listDisplaySources(const Napi::CallbackInfo &info)
|
|
252
|
+
{
|
|
253
|
+
Napi::Env env = info.Env();
|
|
254
|
+
auto displays = SourceHelperWrapper::getDisplaySources();
|
|
255
|
+
|
|
256
|
+
Napi::Array arr = Napi::Array::New(env, displays.size());
|
|
257
|
+
for (size_t i = 0; i < displays.size(); ++i)
|
|
258
|
+
{
|
|
259
|
+
Napi::Object d = Napi::Object::New(env);
|
|
260
|
+
d.Set("id", Napi::Number::New(env, displays[i].id));
|
|
261
|
+
d.Set("name", Napi::String::New(env, std::to_string(displays[i].id)));
|
|
262
|
+
d.Set("isMain", Napi::Boolean::New(env, displays[i].isMain));
|
|
263
|
+
d.Set("width", Napi::Number::New(env, displays[i].width));
|
|
264
|
+
d.Set("height", Napi::Number::New(env, displays[i].height));
|
|
265
|
+
arr.Set(static_cast<uint32_t>(i), d);
|
|
266
|
+
}
|
|
267
|
+
return arr;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
Napi::Value listWindowSources(const Napi::CallbackInfo &info)
|
|
271
|
+
{
|
|
272
|
+
Napi::Env env = info.Env();
|
|
273
|
+
auto windows = SourceHelperWrapper::getWindowSources();
|
|
274
|
+
|
|
275
|
+
Napi::Array arr = Napi::Array::New(env, windows.size());
|
|
276
|
+
for (size_t i = 0; i < windows.size(); ++i)
|
|
277
|
+
{
|
|
278
|
+
Napi::Object w = Napi::Object::New(env);
|
|
279
|
+
w.Set("id", Napi::Number::New(env, windows[i].id));
|
|
280
|
+
w.Set("name", Napi::String::New(env, windows[i].title));
|
|
281
|
+
arr.Set(static_cast<uint32_t>(i), w);
|
|
282
|
+
}
|
|
283
|
+
return arr;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
Napi::Value listAudioInputDevices(const Napi::CallbackInfo &info)
|
|
287
|
+
{
|
|
288
|
+
Napi::Env env = info.Env();
|
|
289
|
+
Napi::Array arr = Napi::Array::New(env);
|
|
290
|
+
|
|
291
|
+
#ifdef __APPLE__
|
|
292
|
+
AudioObjectPropertyAddress addr = {
|
|
293
|
+
kAudioHardwarePropertyDevices,
|
|
294
|
+
kAudioObjectPropertyScopeGlobal,
|
|
295
|
+
kAudioObjectPropertyElementMain};
|
|
296
|
+
|
|
297
|
+
UInt32 data_size = 0;
|
|
298
|
+
OSStatus status = AudioObjectGetPropertyDataSize(
|
|
299
|
+
kAudioObjectSystemObject, &addr, 0, nullptr, &data_size);
|
|
300
|
+
if (status != noErr || data_size == 0)
|
|
301
|
+
return arr;
|
|
302
|
+
|
|
303
|
+
std::vector<AudioDeviceID> devices(data_size / sizeof(AudioDeviceID));
|
|
304
|
+
status = AudioObjectGetPropertyData(
|
|
305
|
+
kAudioObjectSystemObject, &addr, 0, nullptr, &data_size, devices.data());
|
|
306
|
+
if (status != noErr)
|
|
307
|
+
return arr;
|
|
308
|
+
|
|
309
|
+
uint32_t index = 0;
|
|
310
|
+
for (AudioDeviceID device : devices)
|
|
311
|
+
{
|
|
312
|
+
if (!hasInputChannels(device))
|
|
313
|
+
continue;
|
|
314
|
+
|
|
315
|
+
const std::string uid = getDeviceString(device, kAudioDevicePropertyDeviceUID);
|
|
316
|
+
if (uid.empty())
|
|
317
|
+
continue;
|
|
318
|
+
|
|
319
|
+
Napi::Object d = Napi::Object::New(env);
|
|
320
|
+
d.Set("uid", Napi::String::New(env, uid));
|
|
321
|
+
d.Set("name", Napi::String::New(env, getDeviceString(device, kAudioObjectPropertyName)));
|
|
322
|
+
d.Set(
|
|
323
|
+
"manufacturer",
|
|
324
|
+
Napi::String::New(env, getDeviceString(device, kAudioObjectPropertyManufacturer)));
|
|
325
|
+
arr.Set(index++, d);
|
|
326
|
+
}
|
|
327
|
+
#endif
|
|
328
|
+
|
|
329
|
+
return arr;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
Napi::Object Init(Napi::Env env, Napi::Object exports)
|
|
333
|
+
{
|
|
334
|
+
exports.Set("startCapture", Napi::Function::New(env, startCapture));
|
|
335
|
+
exports.Set("stopCapture", Napi::Function::New(env, stopCapture));
|
|
336
|
+
exports.Set("setSegmentReadyCallback", Napi::Function::New(env, setSegmentReadyCallback));
|
|
337
|
+
exports.Set("getLastError", Napi::Function::New(env, getLastError));
|
|
338
|
+
exports.Set("listDisplaySources", Napi::Function::New(env, listDisplaySources));
|
|
339
|
+
exports.Set("listWindowSources", Napi::Function::New(env, listWindowSources));
|
|
340
|
+
exports.Set("listAudioInputDevices", Napi::Function::New(env, listAudioInputDevices));
|
|
341
|
+
exports.Set("listVideoInputDevices", Napi::Function::New(env, listVideoInputDevices));
|
|
342
|
+
exports.Set("pauseCapture", Napi::Function::New(env, pauseCapture));
|
|
343
|
+
exports.Set("resumeCapture", Napi::Function::New(env, resumeCapture));
|
|
344
|
+
return exports;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
NODE_API_MODULE(native_capture, Init)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Vendored FFmpeg Static Libraries
|
|
2
|
+
|
|
3
|
+
This directory contains static macOS FFmpeg libraries used by the native
|
|
4
|
+
capture addon. The addon links these archives directly so packaged builds do not
|
|
5
|
+
depend on Homebrew FFmpeg dylibs being present on the user's machine.
|
|
6
|
+
|
|
7
|
+
Required libraries linked by the addon:
|
|
8
|
+
|
|
9
|
+
- `darwin-<arch>/lib/libavformat.a`
|
|
10
|
+
- `darwin-<arch>/lib/libavcodec.a`
|
|
11
|
+
- `darwin-<arch>/lib/libavutil.a`
|
|
12
|
+
- `darwin-<arch>/lib/libswscale.a`
|
|
13
|
+
- `darwin-<arch>/lib/libswresample.a`
|
|
14
|
+
- `darwin-<arch>/lib/libx264.a`
|
|
15
|
+
|
|
16
|
+
The generated FFmpeg install may also contain additional static archives such as
|
|
17
|
+
`libavfilter.a` and `libavdevice.a`; those are not linked by `binding.gyp`.
|
|
18
|
+
|
|
19
|
+
The generated `darwin-*` folders are ignored by git. Rebuild them with:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm run build:vendor
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
From the app repo root, the equivalent convenience command is
|
|
26
|
+
`npm run build:native-vendor`.
|
|
27
|
+
|
|
28
|
+
`npm run build` inside this package also runs the vendor build automatically when
|
|
29
|
+
the required static archives are missing, unless `SKIP_NATIVE_FFMPEG_VENDOR_BUILD=1`
|
|
30
|
+
is set. The app repo's `npm run build:native-capture` delegates to this package
|
|
31
|
+
build.
|
|
32
|
+
|
|
33
|
+
The FFmpeg archive is configured for the native recorder's MPEG-TS HLS output.
|
|
34
|
+
The vendored `libavformat.a` disables fMP4 HLS at compile time to avoid pulling
|
|
35
|
+
in the MOV/MP4 muxer symbols that are not needed by the app's HLS pipeline.
|
|
36
|
+
|
|
37
|
+
The x264 archive is built locally for arm64 or x64 with
|
|
38
|
+
`MACOSX_DEPLOYMENT_TARGET=14.0`. Set `NATIVE_CAPTURE_ARCH=arm64` or
|
|
39
|
+
`NATIVE_CAPTURE_ARCH=x64` before running the vendor or addon build. Optimized
|
|
40
|
+
x64 builds require NASM.
|
|
Binary file
|