screencapturekit
Version:
A nodejs wrapper over a swift CLI program which is a wrapper over ScreenCaptureKit module with HDR and microphone support
415 lines (412 loc) • 14.5 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
ScreenCaptureKit: () => ScreenCaptureKit,
ScreenCaptureKitError: () => ScreenCaptureKitError,
audioDevices: () => audioDevices,
default: () => index_default,
microphoneDevices: () => microphoneDevices,
screens: () => screens,
supportsHDRCapture: () => supportsHDRCapture,
videoCodecs: () => videoCodecs
});
module.exports = __toCommonJS(index_exports);
var import_node_os = __toESM(require("os"), 1);
var import_node_path = __toESM(require("path"), 1);
var import_node_fs = __toESM(require("fs"), 1);
var import_uuid = require("uuid");
var macosVersion = __toESM(require("macos-version"), 1);
var import_execa = require("execa");
// src/utils/packagePaths.ts
var import_path = __toESM(require("path"), 1);
var import_url = require("url");
var import_meta = {};
var getPackageRoot = () => {
if (process.env.NODE_ENV === "test") {
return import_path.default.join(__dirname, "..", "..", "dist");
}
try {
const app = require("electron").app;
const packageMainPath = require.resolve("screencapturekit");
if (typeof process.resourcesPath === "string" && app?.isPackaged) {
const finalPath2 = import_path.default.join(process.resourcesPath);
return finalPath2;
}
const finalPath = import_path.default.dirname(packageMainPath);
return finalPath;
} catch (e) {
const __filename = (0, import_url.fileURLToPath)(import_meta.url);
const finalPath = import_path.default.join(import_path.default.dirname(__filename));
console.log("finalPath : ESM", finalPath);
return finalPath;
}
};
var resolvePackagePath = (...segments) => import_path.default.join(getPackageRoot(), ...segments);
// src/index.ts
var BIN = resolvePackagePath("./screencapturekit");
var getRandomId = () => Math.random().toString(36).slice(2, 15);
var supportsHevcHardwareEncoding = (() => {
const cpuModel = import_node_os.default.cpus()[0].model;
if (cpuModel.startsWith("Apple ")) {
return true;
}
const result = /Intel.*Core.*i\d+-(\d)/.exec(cpuModel);
return result && Number.parseInt(result[1], 10) >= 6;
})();
var supportsHDR = (() => {
return macosVersion.isMacOSVersionGreaterThanOrEqualTo("13.0");
})();
var supportsDirectRecordingAPI = (() => {
return macosVersion.isMacOSVersionGreaterThanOrEqualTo("15.0");
})();
var supportsMicrophoneCapture = (() => {
return macosVersion.isMacOSVersionGreaterThanOrEqualTo("15.0");
})();
var ScreenCaptureKit = class {
/** Path to the output video file. */
videoPath = null;
/** The ongoing recording process. */
recorder;
/** Unique identifier of the recording process. */
processId = null;
/** Options used for recording */
currentOptions;
/** Path to the final processed video file */
processedVideoPath = null;
/**
* Creates a new instance of ScreenCaptureKit.
* Checks that the macOS version is compatible (10.13+).
* @throws {Error} If the macOS version is not supported.
*/
constructor() {
macosVersion.assertMacOSVersionGreaterThanOrEqualTo("10.13");
}
/**
* Checks that recording has been started.
* @throws {Error} If recording has not been started.
* @private
*/
throwIfNotStarted() {
if (this.recorder === void 0) {
throw new Error("Call `.startRecording()` first");
}
}
/**
* Starts screen recording.
* @param {Partial<RecordingOptions>} options - Recording options.
* @param {number} [options.fps=30] - Frames per second.
* @param {CropArea} [options.cropArea] - Area of the screen to capture.
* @param {boolean} [options.showCursor=true] - Show the cursor.
* @param {boolean} [options.highlightClicks=false] - Highlight mouse clicks.
* @param {number} [options.screenId=0] - Screen ID to capture.
* @param {string} [options.audioDeviceId] - System audio device ID.
* @param {string} [options.microphoneDeviceId] - Microphone device ID.
* @param {string} [options.videoCodec="h264"] - Video codec to use.
* @param {boolean} [options.enableHDR=false] - Enable HDR recording.
* @param {boolean} [options.recordToFile=false] - Use the direct recording API.
* @returns {Promise<void>} A promise that resolves when recording starts.
* @throws {Error} If recording is already in progress or if the options are invalid.
*/
async startRecording({
fps = 30,
cropArea = void 0,
showCursor = true,
highlightClicks = false,
screenId = 0,
audioDeviceId = void 0,
microphoneDeviceId = void 0,
videoCodec = "h264",
enableHDR = false,
recordToFile = false,
audioOnly = false
} = {}) {
this.processId = getRandomId();
this.currentOptions = {
fps,
cropArea,
showCursor,
highlightClicks,
screenId,
audioDeviceId,
microphoneDeviceId,
videoCodec,
enableHDR,
recordToFile,
audioOnly
};
return new Promise((resolve, reject) => {
if (this.recorder !== void 0) {
reject(new Error("Call `.stopRecording()` first"));
return;
}
this.videoPath = createTempFile({ extension: "mp4" });
console.log(this.videoPath);
const recorderOptions = {
destination: fileUrlFromPath(this.videoPath),
framesPerSecond: fps,
showCursor,
highlightClicks,
screenId,
audioDeviceId
};
if (highlightClicks === true) {
showCursor = true;
}
if (typeof cropArea === "object" && (typeof cropArea.x !== "number" || typeof cropArea.y !== "number" || typeof cropArea.width !== "number" || typeof cropArea.height !== "number")) {
reject(new Error("Invalid `cropArea` option object"));
return;
}
if (videoCodec) {
if (!videoCodecs.has(videoCodec)) {
throw new Error(`Unsupported video codec specified: ${videoCodec}`);
}
recorderOptions.videoCodec = videoCodecs.get(videoCodec);
}
if (enableHDR) {
if (!supportsHDR) {
console.warn(
"HDR requested but not supported on this macOS version. Falling back to SDR."
);
} else {
recorderOptions.enableHDR = true;
}
}
if (microphoneDeviceId) {
if (!supportsMicrophoneCapture) {
console.warn(
"Microphone capture requested but requires macOS 15.0+. This feature will be ignored."
);
} else {
recorderOptions.microphoneDeviceId = microphoneDeviceId;
}
}
if (recordToFile) {
if (!supportsDirectRecordingAPI) {
console.warn(
"Direct recording API requested but requires macOS 15.0+. Falling back to manual recording."
);
} else {
recorderOptions.useDirectRecordingAPI = true;
}
}
if (cropArea) {
recorderOptions.cropRect = [
[cropArea.x, cropArea.y],
[cropArea.width, cropArea.height]
];
}
const timeout = setTimeout(resolve, 1e3);
this.recorder = (0, import_execa.execa)(BIN, ["record", JSON.stringify(recorderOptions)]);
this.recorder?.catch((error) => {
clearTimeout(timeout);
delete this.recorder;
reject(error);
});
this.recorder?.stdout?.setEncoding("utf8");
this.recorder?.stdout?.on("data", (data) => {
console.log("From swift executable: ", data);
});
});
}
/**
* Stops the ongoing recording and processes the video to merge audio tracks if needed.
* @returns {Promise<string|null>} A promise that resolves with the path to the processed video file.
* @throws {Error} If recording has not been started.
*/
async stopRecording() {
this.throwIfNotStarted();
console.log("Arr\xEAt de l'enregistrement");
this.recorder?.kill();
await this.recorder;
console.log("Enregistrement arr\xEAt\xE9");
this.recorder = void 0;
if (!this.videoPath) {
return null;
}
await new Promise((resolve) => setTimeout(resolve, 1e3));
let currentFile = this.videoPath;
try {
const stats = import_node_fs.default.statSync(currentFile);
if (stats.size === 0) {
console.error("Le fichier d'enregistrement est vide");
return null;
}
} catch (error) {
console.error("Erreur lors de la v\xE9rification du fichier d'enregistrement:", error);
return null;
}
const hasMultipleAudioTracks = !!(this.currentOptions?.audioDeviceId && this.currentOptions?.microphoneDeviceId);
if (hasMultipleAudioTracks) {
try {
console.log("Fusion des pistes audio avec ffmpeg");
this.processedVideoPath = createTempFile({ extension: "mp4" });
const { stdout: probeOutput } = await (0, import_execa.execa)("ffprobe", [
"-v",
"error",
"-show_entries",
"stream=index,codec_type",
"-of",
"json",
currentFile
]);
const probeResult = JSON.parse(probeOutput);
const streams = probeResult.streams || [];
const audioStreams = streams.filter((stream) => stream.codec_type === "audio").map((stream) => stream.index);
const videoStream = streams.find((stream) => stream.codec_type === "video")?.index;
if (audioStreams.length < 2 || videoStream === void 0) {
console.log("Pas assez de pistes audio pour fusionner ou pas de piste vid\xE9o");
} else {
const systemAudioIndex = audioStreams[0];
const microphoneIndex = audioStreams[1];
const filterComplex = `[0:${systemAudioIndex}]volume=1[a1];[0:${microphoneIndex}]volume=3[a2];[a1][a2]amerge=inputs=2[aout]`;
await (0, import_execa.execa)("ffmpeg", [
"-i",
currentFile,
"-filter_complex",
filterComplex,
"-map",
"[aout]",
"-map",
`0:${videoStream}`,
"-c:v",
"copy",
"-c:a",
"aac",
"-b:a",
"256k",
"-ac",
"2",
"-y",
this.processedVideoPath
]);
currentFile = this.processedVideoPath;
}
} catch (error) {
console.error("Erreur lors de la fusion des pistes audio:", error);
}
}
if (this.currentOptions?.audioOnly) {
try {
console.log("Conversion en MP3");
const audioPath = createTempFile({ extension: "mp3" });
await (0, import_execa.execa)("ffmpeg", [
"-i",
currentFile,
"-vn",
"-c:a",
"libmp3lame",
"-b:a",
"192k",
"-y",
audioPath
]);
return audioPath;
} catch (error) {
console.error("Erreur lors de la conversion en MP3:", error);
return currentFile;
}
}
return currentFile;
}
};
function index_default() {
return new ScreenCaptureKit();
}
function getCodecs() {
const codecs = /* @__PURE__ */ new Map([
["h264", "H264"],
["hevc", "HEVC"],
["proRes422", "Apple ProRes 422"],
["proRes4444", "Apple ProRes 4444"]
]);
if (!supportsHevcHardwareEncoding) {
codecs.delete("hevc");
}
return codecs;
}
var ScreenCaptureKitError = class extends Error {
constructor(message) {
super(message);
this.name = "ScreenCaptureKitError";
}
};
var screens = async () => {
const { stderr } = await (0, import_execa.execa)(BIN, ["list", "screens"]);
try {
return JSON.parse(stderr);
} catch {
throw new ScreenCaptureKitError(`Failed to retrieve screens: ${stderr}`);
}
};
var audioDevices = async () => {
const { stderr } = await (0, import_execa.execa)(BIN, ["list", "audio-devices"]);
try {
return JSON.parse(stderr);
} catch {
throw new ScreenCaptureKitError(`Failed to retrieve audio devices: ${stderr}`);
}
};
var microphoneDevices = async () => {
const { stderr } = await (0, import_execa.execa)(BIN, ["list", "microphone-devices"]);
try {
return JSON.parse(stderr);
} catch {
throw new ScreenCaptureKitError(`Failed to retrieve microphones: ${stderr}`);
}
};
var supportsHDRCapture = supportsHDR;
var videoCodecs = getCodecs();
function createTempFile(options = {}) {
const tempDir = import_node_os.default.tmpdir();
const randomId = (0, import_uuid.v4)();
const extension = options.extension ? `.${options.extension}` : "";
const tempFilePath = import_node_path.default.join(tempDir, `${randomId}${extension}`);
return tempFilePath;
}
function fileUrlFromPath(filePath) {
let pathName = filePath.replace(/\\/g, "/");
if (pathName[0] !== "/") {
pathName = "/" + pathName;
}
pathName = encodeURI(pathName).replace(/#/g, "%23").replace(/\?/g, "%3F");
return `file://${pathName}`;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ScreenCaptureKit,
ScreenCaptureKitError,
audioDevices,
microphoneDevices,
screens,
supportsHDRCapture,
videoCodecs
});
//# sourceMappingURL=index.cjs.map