screencapturekit
Version:
A nodejs wrapper over a swift CLI program which is a wrapper over ScreenCaptureKit module with HDR and microphone support
380 lines (377 loc) • 12.5 kB
JavaScript
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined") return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
// src/index.ts
import os from "node:os";
import path2 from "node:path";
import fs from "node:fs";
import { v4 as uuidv4 } from "uuid";
import * as macosVersion from "macos-version";
import { execa } from "execa";
// src/utils/packagePaths.ts
import path from "path";
import { fileURLToPath } from "url";
var getPackageRoot = () => {
if (process.env.NODE_ENV === "test") {
return path.join(__dirname, "..", "..", "dist");
}
try {
const app = __require("electron").app;
const packageMainPath = __require.resolve("screencapturekit");
if (typeof process.resourcesPath === "string" && app?.isPackaged) {
const finalPath2 = path.join(process.resourcesPath);
return finalPath2;
}
const finalPath = path.dirname(packageMainPath);
return finalPath;
} catch (e) {
const __filename = fileURLToPath(import.meta.url);
const finalPath = path.join(path.dirname(__filename));
console.log("finalPath : ESM", finalPath);
return finalPath;
}
};
var resolvePackagePath = (...segments) => path.join(getPackageRoot(), ...segments);
// src/index.ts
var BIN = resolvePackagePath("./screencapturekit");
var getRandomId = () => Math.random().toString(36).slice(2, 15);
var supportsHevcHardwareEncoding = (() => {
const cpuModel = os.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 = 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 = fs.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 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 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 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 execa(BIN, ["list", "screens"]);
try {
return JSON.parse(stderr);
} catch {
throw new ScreenCaptureKitError(`Failed to retrieve screens: ${stderr}`);
}
};
var audioDevices = async () => {
const { stderr } = await 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 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 = os.tmpdir();
const randomId = uuidv4();
const extension = options.extension ? `.${options.extension}` : "";
const tempFilePath = path2.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}`;
}
export {
ScreenCaptureKit,
ScreenCaptureKitError,
audioDevices,
index_default as default,
microphoneDevices,
screens,
supportsHDRCapture,
videoCodecs
};
//# sourceMappingURL=index.js.map