@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/index.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export interface CaptureOptions {
|
|
2
|
+
/** CGDirectDisplayID — 0 means primary display */
|
|
3
|
+
displayId?: number;
|
|
4
|
+
/** Capture one ScreenCaptureKit window instead of the complete display. */
|
|
5
|
+
captureWindow?: boolean;
|
|
6
|
+
/** CGWindowID used when captureWindow is true. */
|
|
7
|
+
windowId?: number;
|
|
8
|
+
/** If true, don't start ScreenCaptureKit (useful for camera-only) */
|
|
9
|
+
noScreen?: boolean;
|
|
10
|
+
/** If true, capture from native camera */
|
|
11
|
+
camera?: boolean;
|
|
12
|
+
/** Unique ID of the camera device; empty = default */
|
|
13
|
+
cameraDeviceUid?: string;
|
|
14
|
+
|
|
15
|
+
width?: number;
|
|
16
|
+
height?: number;
|
|
17
|
+
fps?: number;
|
|
18
|
+
/** Encoder GOP size in frames. Use segmentTime * fps for HLS-aligned chunks. */
|
|
19
|
+
gopSize?: number;
|
|
20
|
+
/** Video bitrate in bps (default 9 000 000) */
|
|
21
|
+
videoBitrate?: number;
|
|
22
|
+
cropX?: number;
|
|
23
|
+
cropY?: number;
|
|
24
|
+
cropWidth?: number;
|
|
25
|
+
cropHeight?: number;
|
|
26
|
+
/** Capture system audio via ScreenCaptureKit (default true) */
|
|
27
|
+
systemAudio?: boolean;
|
|
28
|
+
/** Mix microphone audio via AVAudioEngine (default false) */
|
|
29
|
+
microphone?: boolean;
|
|
30
|
+
/** CoreAudio device UID for the microphone; empty = OS default */
|
|
31
|
+
micDeviceUid?: string;
|
|
32
|
+
sampleRate?: number;
|
|
33
|
+
/** Audio bitrate in bps (default 128 000) */
|
|
34
|
+
audioBitrate?: number;
|
|
35
|
+
/** Directory where native file-backed HLS should write playlist.m3u8 and .ts files. */
|
|
36
|
+
outputDir?: string;
|
|
37
|
+
/** Recording ID used in segment filenames: <recordingId>-video-%04d.ts. */
|
|
38
|
+
recordingId?: string;
|
|
39
|
+
/** HLS segment duration in seconds. */
|
|
40
|
+
segmentTime?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DisplaySource {
|
|
44
|
+
id: number;
|
|
45
|
+
name: string;
|
|
46
|
+
isMain: boolean;
|
|
47
|
+
width: number;
|
|
48
|
+
height: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface WindowSource {
|
|
52
|
+
id: number;
|
|
53
|
+
name: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface AudioInputDevice {
|
|
57
|
+
uid: string;
|
|
58
|
+
name: string;
|
|
59
|
+
manufacturer: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface VideoInputDevice {
|
|
63
|
+
uid: string;
|
|
64
|
+
name: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type SegmentReadyCallback = (fullPath: string) => void;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Start native screen + audio capture.
|
|
71
|
+
* Returns false if capture could not be started.
|
|
72
|
+
*/
|
|
73
|
+
export function startCapture(options?: CaptureOptions): boolean;
|
|
74
|
+
|
|
75
|
+
export function setSegmentReadyCallback(callback: SegmentReadyCallback | null | undefined): void;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Stop capture and flush any remaining buffered data.
|
|
79
|
+
*/
|
|
80
|
+
export function stopCapture(): boolean;
|
|
81
|
+
|
|
82
|
+
/** Return the last native capture error message, if any. */
|
|
83
|
+
export function getLastError(): string;
|
|
84
|
+
|
|
85
|
+
/** List available display sources. */
|
|
86
|
+
export function listDisplaySources(): DisplaySource[];
|
|
87
|
+
|
|
88
|
+
/** List available window sources. */
|
|
89
|
+
export function listWindowSources(): WindowSource[];
|
|
90
|
+
|
|
91
|
+
/** List native CoreAudio input devices. */
|
|
92
|
+
export function listAudioInputDevices(): AudioInputDevice[];
|
|
93
|
+
|
|
94
|
+
/** List available native camera devices. */
|
|
95
|
+
export function listVideoInputDevices(): VideoInputDevice[];
|
|
96
|
+
|
|
97
|
+
export function resumeCapture(): boolean;
|
|
98
|
+
|
|
99
|
+
export function pauseCapture(): boolean;
|
package/index.js
CHANGED
|
@@ -1,16 +1,107 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
if (process.platform !== 'darwin') {
|
|
7
|
+
throw new Error(`@0biwank/screen-capture supports macOS only; received ${process.platform}`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (!['arm64', 'x64'].includes(process.arch)) {
|
|
11
|
+
throw new Error(`@0biwank/screen-capture does not support macOS architecture ${process.arch}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const prebuiltPath = path.join(
|
|
15
|
+
__dirname,
|
|
16
|
+
'prebuilds',
|
|
17
|
+
`darwin-${process.arch}`,
|
|
18
|
+
'native_capture.node',
|
|
19
|
+
);
|
|
20
|
+
const developmentPath = path.join(__dirname, 'build', 'Release', 'native_capture.node');
|
|
21
|
+
const addonPath = fs.existsSync(prebuiltPath) ? prebuiltPath : developmentPath;
|
|
22
|
+
|
|
23
|
+
if (!fs.existsSync(addonPath)) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Missing @0biwank/screen-capture native binary for darwin-${process.arch}. ` +
|
|
26
|
+
`Expected ${prebuiltPath}`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const addon = require(addonPath);
|
|
2
31
|
|
|
3
32
|
module.exports = {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
33
|
+
/**
|
|
34
|
+
* Start capturing.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} [options]
|
|
37
|
+
* @param {number} [options.displayId=0] CGDirectDisplayID; 0 = primary
|
|
38
|
+
* @param {number} [options.width=1920]
|
|
39
|
+
* @param {number} [options.height=1080]
|
|
40
|
+
* @param {number} [options.fps=30]
|
|
41
|
+
* @param {number} [options.videoBitrate=9000000] bps
|
|
42
|
+
* @param {number} [options.cropX=0]
|
|
43
|
+
* @param {number} [options.cropY=0]
|
|
44
|
+
* @param {number} [options.cropWidth=0]
|
|
45
|
+
* @param {number} [options.cropHeight=0]
|
|
46
|
+
* @param {boolean} [options.systemAudio=true]
|
|
47
|
+
* @param {boolean} [options.microphone=false]
|
|
48
|
+
* @param {string} [options.micDeviceUid=''] empty = default input device
|
|
49
|
+
* @param {number} [options.sampleRate=48000]
|
|
50
|
+
* @param {number} [options.audioBitrate=128000] bps
|
|
51
|
+
* @param {string} [options.outputDir]
|
|
52
|
+
* @param {string} [options.recordingId]
|
|
53
|
+
* @param {number} [options.segmentTime=4]
|
|
54
|
+
* @returns {boolean}
|
|
55
|
+
*/
|
|
56
|
+
startCapture: addon.startCapture.bind(addon),
|
|
57
|
+
|
|
58
|
+
setSegmentReadyCallback: addon.setSegmentReadyCallback.bind(addon),
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Stop capturing and flush remaining data.
|
|
62
|
+
* @returns {boolean}
|
|
63
|
+
*/
|
|
64
|
+
stopCapture: addon.stopCapture.bind(addon),
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Last native capture startup/runtime error.
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
getLastError: addon.getLastError.bind(addon),
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* List available displays.
|
|
74
|
+
* @returns {Array<{id: number, name: string, isMain: boolean, width: number, height: number}>}
|
|
75
|
+
*/
|
|
76
|
+
listDisplaySources: addon.listDisplaySources.bind(addon),
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* List available windows.
|
|
80
|
+
* @returns {Array<{id: number, name: string}>}
|
|
81
|
+
*/
|
|
82
|
+
listWindowSources: addon.listWindowSources.bind(addon),
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* List native CoreAudio input devices.
|
|
86
|
+
* @returns {Array<{ uid: string, name: string, manufacturer: string }>}
|
|
87
|
+
*/
|
|
88
|
+
listAudioInputDevices: addon.listAudioInputDevices.bind(addon),
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* List native camera devices.
|
|
92
|
+
* @returns {Array<{ uid: string, name: string }>}
|
|
93
|
+
*/
|
|
94
|
+
listVideoInputDevices: addon.listVideoInputDevices.bind(addon),
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resume capturing.
|
|
98
|
+
* @returns {boolean}
|
|
99
|
+
*/
|
|
100
|
+
resumeCapture: addon.resumeCapture.bind(addon),
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Pause capturing.
|
|
104
|
+
* @returns {boolean}
|
|
105
|
+
*/
|
|
106
|
+
pauseCapture: addon.pauseCapture.bind(addon),
|
|
107
|
+
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0biwank/screen-capture",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Native screen capture
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Native screen + audio capture for macOS with file-backed HLS output.",
|
|
5
5
|
"main": "index.js",
|
|
6
|
-
"gypfile":
|
|
6
|
+
"gypfile": false,
|
|
7
7
|
"scripts": {
|
|
8
|
-
"build": "node
|
|
8
|
+
"build": "node scripts/build.mjs",
|
|
9
|
+
"build:vendor": "node scripts/build-ffmpeg-vendor.mjs",
|
|
10
|
+
"build:addon": "node-gyp configure build",
|
|
9
11
|
"rebuild": "node-gyp rebuild",
|
|
12
|
+
"stage:prebuild": "node scripts/stage-prebuild.mjs",
|
|
13
|
+
"verify:runtime": "node scripts/verify-runtime.cjs",
|
|
14
|
+
"verify:package": "node scripts/verify-package.mjs",
|
|
15
|
+
"verify:packlist": "node scripts/verify-packlist.mjs",
|
|
10
16
|
"test": "node test.js"
|
|
11
17
|
},
|
|
12
18
|
"keywords": [
|
|
@@ -14,35 +20,43 @@
|
|
|
14
20
|
"desktop-capture",
|
|
15
21
|
"native-addon",
|
|
16
22
|
"napi",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
23
|
+
"macos",
|
|
24
|
+
"mpegts"
|
|
19
25
|
],
|
|
20
26
|
"author": "Kunal Dubey",
|
|
21
|
-
"license": "
|
|
27
|
+
"license": "GPL-2.0-or-later",
|
|
22
28
|
"repository": {
|
|
23
29
|
"type": "git",
|
|
24
|
-
"url": "https://github.com/xakep8/screenCaptureNAPI"
|
|
30
|
+
"url": "git+https://github.com/xakep8/screenCaptureNAPI.git"
|
|
25
31
|
},
|
|
26
32
|
"homepage": "https://github.com/xakep8/screenCaptureNAPI#readme",
|
|
27
33
|
"bugs": {
|
|
28
34
|
"url": "https://github.com/xakep8/screenCaptureNAPI/issues"
|
|
29
35
|
},
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
33
39
|
"files": [
|
|
34
40
|
"index.js",
|
|
35
|
-
"
|
|
41
|
+
"index.d.ts",
|
|
42
|
+
"prebuilds/**/*.node",
|
|
43
|
+
"prebuilds/SHA256SUMS",
|
|
44
|
+
"binding.gyp",
|
|
45
|
+
"include/**/*",
|
|
46
|
+
"src/**/*",
|
|
47
|
+
"scripts/**/*",
|
|
48
|
+
"vendor/ffmpeg/README.md",
|
|
36
49
|
"README.md",
|
|
37
|
-
"
|
|
50
|
+
"SOURCE.md",
|
|
51
|
+
"THIRD_PARTY_NOTICES.md",
|
|
52
|
+
"LICENSE",
|
|
53
|
+
"LICENSES/**/*"
|
|
38
54
|
],
|
|
39
|
-
"dependencies": {
|
|
40
|
-
"node-addon-api": "^8.5.0"
|
|
41
|
-
},
|
|
42
55
|
"devDependencies": {
|
|
56
|
+
"node-addon-api": "^8.5.0",
|
|
43
57
|
"node-gyp": "^11.4.2"
|
|
44
58
|
},
|
|
45
59
|
"engines": {
|
|
46
|
-
"node": ">=
|
|
60
|
+
"node": ">=18.0.0"
|
|
47
61
|
}
|
|
48
62
|
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, copyFileSync} from 'fs';
|
|
2
|
+
import {fileURLToPath} from 'url';
|
|
3
|
+
import {join, resolve} from 'path';
|
|
4
|
+
import {spawnSync} from 'child_process';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import {createHash} from 'crypto';
|
|
7
|
+
|
|
8
|
+
const FFMPEG_VERSION = '8.1';
|
|
9
|
+
const FFMPEG_SHA256 = 'b072aed6871998cce9b36e7774033105ca29e33632be5b6347f3206898e0756a';
|
|
10
|
+
const X264_REF = 'b35605ace3ddf7c1a5d67a2eb553f034aef41d55';
|
|
11
|
+
const X264_SHA256 = '6eeb82934e69fd51e043bd8c5b0d152839638d1ce7aa4eea65a3fedcf83ff224';
|
|
12
|
+
const MACOS_DEPLOYMENT_TARGET = '14.0';
|
|
13
|
+
|
|
14
|
+
const packageRoot = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
15
|
+
const requestedArch =
|
|
16
|
+
process.env.NATIVE_CAPTURE_ARCH ||
|
|
17
|
+
process.env.TARGET_ARCH ||
|
|
18
|
+
process.env.npm_config_arch ||
|
|
19
|
+
process.arch;
|
|
20
|
+
const targetArch = requestedArch === 'x86_64' ? 'x64' : requestedArch;
|
|
21
|
+
const vendorRoot = resolve(packageRoot, 'vendor', 'ffmpeg');
|
|
22
|
+
const vendorDir = join(vendorRoot, `darwin-${targetArch}`);
|
|
23
|
+
const workRoot = join(os.tmpdir(), `freeboom-native-ffmpeg-${targetArch}`);
|
|
24
|
+
const ffmpegArchive = join(workRoot, `ffmpeg-${FFMPEG_VERSION}.tar.xz`);
|
|
25
|
+
const x264Archive = join(workRoot, `x264-${X264_REF}.tar.bz2`);
|
|
26
|
+
const ffmpegSrc = join(workRoot, 'ffmpeg-src');
|
|
27
|
+
const x264Src = join(workRoot, 'x264-src');
|
|
28
|
+
const x264Install = join(workRoot, 'x264-install');
|
|
29
|
+
|
|
30
|
+
function run(command, args, options = {}) {
|
|
31
|
+
console.log(`[NativeVendor] ${command} ${args.join(' ')}`);
|
|
32
|
+
const result = spawnSync(command, args, {
|
|
33
|
+
stdio: 'inherit',
|
|
34
|
+
shell: process.platform === 'win32',
|
|
35
|
+
...options,
|
|
36
|
+
env: {
|
|
37
|
+
...process.env,
|
|
38
|
+
MACOSX_DEPLOYMENT_TARGET: MACOS_DEPLOYMENT_TARGET,
|
|
39
|
+
...options.env,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (result.status !== 0) {
|
|
44
|
+
process.exit(result.status ?? 1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sha256(file) {
|
|
49
|
+
return createHash('sha256').update(readFileSync(file)).digest('hex');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function download(url, output, expectedSha256) {
|
|
53
|
+
if (existsSync(output) && sha256(output) === expectedSha256) {
|
|
54
|
+
console.log(`[NativeVendor] Reusing ${output}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
rmSync(output, {force: true});
|
|
59
|
+
run('curl', ['-fL', '--retry', '3', '--retry-delay', '2', url, '-o', output]);
|
|
60
|
+
const actualSha256 = sha256(output);
|
|
61
|
+
if (actualSha256 !== expectedSha256) {
|
|
62
|
+
rmSync(output, {force: true});
|
|
63
|
+
throw new Error(
|
|
64
|
+
`[NativeVendor] SHA-256 mismatch for ${url}: expected ${expectedSha256}, received ${actualSha256}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function patchFfmpegHlsMuxer() {
|
|
70
|
+
const hlsencPath = join(ffmpegSrc, 'libavformat', 'hlsenc.c');
|
|
71
|
+
const source = readFileSync(hlsencPath, 'utf8');
|
|
72
|
+
const needle = ` if (hls->segment_type == SEGMENT_TYPE_FMP4) {
|
|
73
|
+
EXTERN const FFOutputFormat ff_mp4_muxer;
|
|
74
|
+
vs->oformat = &ff_mp4_muxer.p;
|
|
75
|
+
} else {
|
|
76
|
+
EXTERN const FFOutputFormat ff_mpegts_muxer;
|
|
77
|
+
vs->oformat = &ff_mpegts_muxer.p;
|
|
78
|
+
}`;
|
|
79
|
+
const replacement = ` if (hls->segment_type == SEGMENT_TYPE_FMP4) {
|
|
80
|
+
av_log(s, AV_LOG_ERROR, "fmp4 HLS segments are disabled in this static build\\n");
|
|
81
|
+
return AVERROR_PATCHWELCOME;
|
|
82
|
+
} else {
|
|
83
|
+
EXTERN const FFOutputFormat ff_mpegts_muxer;
|
|
84
|
+
vs->oformat = &ff_mpegts_muxer.p;
|
|
85
|
+
}`;
|
|
86
|
+
|
|
87
|
+
if (!source.includes(needle)) {
|
|
88
|
+
throw new Error('Unable to patch FFmpeg HLS muxer; expected fMP4 branch was not found');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
writeFileSync(hlsencPath, source.replace(needle, replacement));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (process.platform !== 'darwin') {
|
|
95
|
+
console.log('[NativeVendor] Skipping FFmpeg static vendor build on', process.platform);
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!['arm64', 'x64'].includes(targetArch)) {
|
|
100
|
+
throw new Error(`[NativeVendor] Unsupported macOS architecture: ${targetArch}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const compilerArch = targetArch === 'x64' ? 'x86_64' : 'arm64';
|
|
104
|
+
const host = targetArch === 'x64' ? 'x86_64-apple-darwin' : 'aarch64-apple-darwin';
|
|
105
|
+
const architectureFlags = `-arch ${compilerArch} -mmacosx-version-min=${MACOS_DEPLOYMENT_TARGET}`;
|
|
106
|
+
|
|
107
|
+
rmSync(ffmpegSrc, {recursive: true, force: true});
|
|
108
|
+
rmSync(x264Src, {recursive: true, force: true});
|
|
109
|
+
rmSync(x264Install, {recursive: true, force: true});
|
|
110
|
+
rmSync(vendorDir, {recursive: true, force: true});
|
|
111
|
+
mkdirSync(workRoot, {recursive: true});
|
|
112
|
+
mkdirSync(ffmpegSrc, {recursive: true});
|
|
113
|
+
mkdirSync(x264Src, {recursive: true});
|
|
114
|
+
mkdirSync(x264Install, {recursive: true});
|
|
115
|
+
mkdirSync(vendorDir, {recursive: true});
|
|
116
|
+
|
|
117
|
+
download(
|
|
118
|
+
`https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz`,
|
|
119
|
+
ffmpegArchive,
|
|
120
|
+
FFMPEG_SHA256,
|
|
121
|
+
);
|
|
122
|
+
download(
|
|
123
|
+
`https://code.videolan.org/videolan/x264/-/archive/${X264_REF}/x264-${X264_REF}.tar.bz2`,
|
|
124
|
+
x264Archive,
|
|
125
|
+
X264_SHA256,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
run('tar', ['-xJf', ffmpegArchive, '-C', ffmpegSrc, '--strip-components=1']);
|
|
129
|
+
run('tar', ['-xjf', x264Archive, '-C', x264Src, '--strip-components=1']);
|
|
130
|
+
|
|
131
|
+
run(
|
|
132
|
+
'./configure',
|
|
133
|
+
[
|
|
134
|
+
`--prefix=${x264Install}`,
|
|
135
|
+
'--enable-static',
|
|
136
|
+
'--disable-cli',
|
|
137
|
+
'--disable-opencl',
|
|
138
|
+
`--host=${host}`,
|
|
139
|
+
`--extra-cflags=${architectureFlags}`,
|
|
140
|
+
`--extra-ldflags=${architectureFlags}`,
|
|
141
|
+
],
|
|
142
|
+
{cwd: x264Src},
|
|
143
|
+
);
|
|
144
|
+
run('make', [`-j${os.cpus().length}`, 'install'], {cwd: x264Src});
|
|
145
|
+
|
|
146
|
+
patchFfmpegHlsMuxer();
|
|
147
|
+
|
|
148
|
+
run(
|
|
149
|
+
'./configure',
|
|
150
|
+
[
|
|
151
|
+
`--prefix=${vendorDir}`,
|
|
152
|
+
'--disable-shared',
|
|
153
|
+
'--enable-static',
|
|
154
|
+
'--disable-programs',
|
|
155
|
+
'--disable-doc',
|
|
156
|
+
'--disable-debug',
|
|
157
|
+
'--disable-autodetect',
|
|
158
|
+
'--disable-network',
|
|
159
|
+
'--enable-gpl',
|
|
160
|
+
'--enable-libx264',
|
|
161
|
+
'--target-os=darwin',
|
|
162
|
+
`--arch=${compilerArch}`,
|
|
163
|
+
'--cc=clang',
|
|
164
|
+
`--extra-cflags=-I${join(x264Install, 'include')} ${architectureFlags}`,
|
|
165
|
+
`--extra-ldflags=-L${join(x264Install, 'lib')} ${architectureFlags}`,
|
|
166
|
+
`--pkg-config-flags=--static`,
|
|
167
|
+
],
|
|
168
|
+
{
|
|
169
|
+
cwd: ffmpegSrc,
|
|
170
|
+
env: {
|
|
171
|
+
PKG_CONFIG_PATH: join(x264Install, 'lib', 'pkgconfig'),
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
run('make', [`-j${os.cpus().length}`, 'install'], {cwd: ffmpegSrc});
|
|
176
|
+
|
|
177
|
+
copyFileSync(join(x264Install, 'lib', 'libx264.a'), join(vendorDir, 'lib', 'libx264.a'));
|
|
178
|
+
console.log(`[NativeVendor] Static FFmpeg vendor bundle ready: ${vendorDir}`);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {existsSync} from 'fs';
|
|
2
|
+
import {fileURLToPath} from 'url';
|
|
3
|
+
import {join, resolve} from 'path';
|
|
4
|
+
import {spawnSync} from 'child_process';
|
|
5
|
+
|
|
6
|
+
if (process.platform !== 'darwin') {
|
|
7
|
+
console.log('[NativeCapture] Skipping macOS native capture build on', process.platform);
|
|
8
|
+
process.exit(0);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const packageRoot = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
12
|
+
const requestedArch =
|
|
13
|
+
process.env.NATIVE_CAPTURE_ARCH ||
|
|
14
|
+
process.env.TARGET_ARCH ||
|
|
15
|
+
process.env.npm_config_arch ||
|
|
16
|
+
process.arch;
|
|
17
|
+
const targetArch = requestedArch === 'x86_64' ? 'x64' : requestedArch;
|
|
18
|
+
const vendorLibDir = join(packageRoot, 'vendor', 'ffmpeg', `darwin-${targetArch}`, 'lib');
|
|
19
|
+
const requiredVendorLibs = [
|
|
20
|
+
'libavformat.a',
|
|
21
|
+
'libavcodec.a',
|
|
22
|
+
'libswscale.a',
|
|
23
|
+
'libswresample.a',
|
|
24
|
+
'libavutil.a',
|
|
25
|
+
'libx264.a',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function run(command, args, options = {}) {
|
|
29
|
+
const result = spawnSync(command, args, {
|
|
30
|
+
cwd: packageRoot,
|
|
31
|
+
stdio: 'inherit',
|
|
32
|
+
shell: process.platform === 'win32',
|
|
33
|
+
...options,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (result.status !== 0) {
|
|
37
|
+
process.exit(result.status ?? 1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const hasVendorBundle = requiredVendorLibs.every(lib => existsSync(join(vendorLibDir, lib)));
|
|
42
|
+
if (!hasVendorBundle && process.env.SKIP_NATIVE_FFMPEG_VENDOR_BUILD !== '1') {
|
|
43
|
+
console.log('[NativeCapture] Static FFmpeg vendor bundle missing; building it now');
|
|
44
|
+
run('node', ['scripts/build-ffmpeg-vendor.mjs'], {
|
|
45
|
+
env: {
|
|
46
|
+
...process.env,
|
|
47
|
+
NATIVE_CAPTURE_ARCH: targetArch,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log('[NativeCapture] Building native addon');
|
|
53
|
+
run('node-gyp', ['configure', 'build', `--arch=${targetArch}`]);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {chmodSync, copyFileSync, existsSync, mkdirSync} from 'fs';
|
|
2
|
+
import {join, resolve} from 'path';
|
|
3
|
+
import {fileURLToPath} from 'url';
|
|
4
|
+
|
|
5
|
+
const packageRoot = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
6
|
+
const requestedArch =
|
|
7
|
+
process.env.NATIVE_CAPTURE_ARCH ||
|
|
8
|
+
process.env.TARGET_ARCH ||
|
|
9
|
+
process.env.npm_config_arch ||
|
|
10
|
+
process.arch;
|
|
11
|
+
const arch = requestedArch === 'x86_64' ? 'x64' : requestedArch;
|
|
12
|
+
|
|
13
|
+
if (!['arm64', 'x64'].includes(arch)) {
|
|
14
|
+
throw new Error(`Unsupported prebuild architecture: ${arch}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const source = join(packageRoot, 'build', 'Release', 'native_capture.node');
|
|
18
|
+
const destinationDir = join(packageRoot, 'prebuilds', `darwin-${arch}`);
|
|
19
|
+
const destination = join(destinationDir, 'native_capture.node');
|
|
20
|
+
|
|
21
|
+
if (!existsSync(source)) {
|
|
22
|
+
throw new Error(`Native addon has not been built: ${source}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
mkdirSync(destinationDir, {recursive: true});
|
|
26
|
+
copyFileSync(source, destination);
|
|
27
|
+
chmodSync(destination, 0o755);
|
|
28
|
+
console.log(`[NativeCapture] Staged darwin-${arch} prebuild at ${destination}`);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {createHash} from 'crypto';
|
|
2
|
+
import {readdirSync, readFileSync, statSync} from 'fs';
|
|
3
|
+
import {join, resolve} from 'path';
|
|
4
|
+
import {spawnSync} from 'child_process';
|
|
5
|
+
import {fileURLToPath} from 'url';
|
|
6
|
+
|
|
7
|
+
const packageRoot = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
8
|
+
const expected = [
|
|
9
|
+
{arch: 'arm64', machine: 'arm64'},
|
|
10
|
+
{arch: 'x64', machine: 'x86_64'},
|
|
11
|
+
];
|
|
12
|
+
const checksumLines = [];
|
|
13
|
+
|
|
14
|
+
for (const {arch, machine} of expected) {
|
|
15
|
+
const relativePath = `darwin-${arch}/native_capture.node`;
|
|
16
|
+
const filePath = join(packageRoot, 'prebuilds', relativePath);
|
|
17
|
+
const size = statSync(filePath).size;
|
|
18
|
+
if (size < 1024 * 1024 || size > 64 * 1024 * 1024) {
|
|
19
|
+
throw new Error(`Unexpected prebuild size for ${relativePath}: ${size}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const inspection = spawnSync('file', [filePath], {encoding: 'utf8'});
|
|
23
|
+
if (inspection.status !== 0 || !inspection.stdout.includes(machine)) {
|
|
24
|
+
throw new Error(`Invalid architecture for ${relativePath}: ${inspection.stdout}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const checksum = createHash('sha256').update(readFileSync(filePath)).digest('hex');
|
|
28
|
+
checksumLines.push(`${checksum} ${relativePath}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const directories = readdirSync(join(packageRoot, 'prebuilds'), {withFileTypes: true})
|
|
32
|
+
.filter(entry => entry.isDirectory())
|
|
33
|
+
.map(entry => entry.name)
|
|
34
|
+
.sort();
|
|
35
|
+
if (directories.join(',') !== 'darwin-arm64,darwin-x64') {
|
|
36
|
+
throw new Error(`Unexpected prebuild directories: ${directories.join(', ')}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
process.stdout.write(`${checksumLines.join('\n')}\n`);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {readFileSync} from 'fs';
|
|
2
|
+
|
|
3
|
+
const reportPath = process.argv[2];
|
|
4
|
+
if (!reportPath) throw new Error('Usage: node scripts/verify-packlist.mjs <npm-pack-report.json>');
|
|
5
|
+
|
|
6
|
+
const [report] = JSON.parse(readFileSync(reportPath, 'utf8'));
|
|
7
|
+
if (!report?.files) throw new Error('npm pack report does not contain a file list');
|
|
8
|
+
|
|
9
|
+
const paths = report.files.map(file => file.path);
|
|
10
|
+
const required = [
|
|
11
|
+
'index.js',
|
|
12
|
+
'index.d.ts',
|
|
13
|
+
'LICENSE',
|
|
14
|
+
'SOURCE.md',
|
|
15
|
+
'THIRD_PARTY_NOTICES.md',
|
|
16
|
+
'prebuilds/SHA256SUMS',
|
|
17
|
+
'prebuilds/darwin-arm64/native_capture.node',
|
|
18
|
+
'prebuilds/darwin-x64/native_capture.node',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
for (const path of required) {
|
|
22
|
+
if (!paths.includes(path)) throw new Error(`Required package file is missing: ${path}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const forbidden = paths.filter(
|
|
26
|
+
path =>
|
|
27
|
+
path.startsWith('build/') ||
|
|
28
|
+
path.startsWith('node_modules/') ||
|
|
29
|
+
path.includes('/vendor/ffmpeg/darwin-') ||
|
|
30
|
+
/\.(a|o|dylib)$/.test(path),
|
|
31
|
+
);
|
|
32
|
+
if (forbidden.length > 0) {
|
|
33
|
+
throw new Error(`Forbidden package files found:\n${forbidden.join('\n')}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (report.unpackedSize > 64 * 1024 * 1024) {
|
|
37
|
+
throw new Error(`Unpacked package is unexpectedly large: ${report.unpackedSize} bytes`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(`Verified npm pack contents: ${paths.length} files, ${report.unpackedSize} bytes`);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const capture = require('../index.js');
|
|
4
|
+
|
|
5
|
+
const expectedFunctions = [
|
|
6
|
+
'startCapture',
|
|
7
|
+
'setSegmentReadyCallback',
|
|
8
|
+
'stopCapture',
|
|
9
|
+
'getLastError',
|
|
10
|
+
'listDisplaySources',
|
|
11
|
+
'listWindowSources',
|
|
12
|
+
'listAudioInputDevices',
|
|
13
|
+
'listVideoInputDevices',
|
|
14
|
+
'pauseCapture',
|
|
15
|
+
'resumeCapture',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
for (const name of expectedFunctions) {
|
|
19
|
+
if (typeof capture[name] !== 'function') {
|
|
20
|
+
throw new Error(`Missing native export: ${name}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (typeof capture.getLastError() !== 'string') {
|
|
25
|
+
throw new Error('getLastError() must return a string');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log(`Verified ${expectedFunctions.length} native exports on darwin-${process.arch}`);
|