vidclip
Version:
A highly customizable, lightweight screen recorder.
91 lines (90 loc) • 4.73 kB
JavaScript
// if there is an error while recording make sure the
// ffmpeg process is terminated by checking task manager.
import fs from 'fs';
import os from 'os';
import path from 'path';
import ffmpeg from '@ffmpeg-installer/ffmpeg';
import { spawn } from 'child_process';
const ffmpegPath = ffmpeg.path;
export class Recorder {
constructor({ resolution, frameRate, fileFormat, audioSource, outputFile, replaceExisting, verbose, rateControl, codec, preset, pixelFormat, } = {}) {
this.ffmpegProcess = null;
this.isRecording = false;
const format = fileFormat !== null && fileFormat !== void 0 ? fileFormat : 'mp4';
// config object
this.config = {
resolution: resolution !== null && resolution !== void 0 ? resolution : '1920x1080',
frameRate: frameRate !== null && frameRate !== void 0 ? frameRate : 30,
fileFormat: format,
audioSource,
outputFile: outputFile !== null && outputFile !== void 0 ? outputFile : `recordings/recording.${format}`,
replaceExisting: replaceExisting !== null && replaceExisting !== void 0 ? replaceExisting : true,
verbose: verbose !== null && verbose !== void 0 ? verbose : false,
rateControl: rateControl !== null && rateControl !== void 0 ? rateControl : { mode: 'crf', value: 18 },
codec: codec !== null && codec !== void 0 ? codec : 'libx264',
preset: preset !== null && preset !== void 0 ? preset : 'fast',
pixelFormat: pixelFormat !== null && pixelFormat !== void 0 ? pixelFormat : 'yuv420p',
};
const dir = path.dirname(this.config.outputFile);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
// get functions
get getConfig() {
return { ...this.config };
}
// public functions
start({ stopAfter } = {}) {
const platform = os.platform();
// prettier-ignore
const ffmpegArgs = [
...(this.config.replaceExisting ? ['-y'] : ['-n']),
'-thread_queue_size', '512',
'-f', platform === 'win32' ? 'gdigrab' : platform === 'darwin' ? 'avfoundation' : 'x11grab',
'-framerate', String(this.config.frameRate),
'-video_size', this.config.resolution,
'-i', platform === 'darwin' ? '1' : platform === 'win32' ? 'desktop' : ':0.0',
// AUDIO INPUT
...(this.config.audioSource ? platform === 'win32' ? ['-f', 'dshow', '-i', `audio=${this.config.audioSource}`] : platform === 'darwin' ? ['-f', 'avfoundation', '-i', this.config.audioSource] : ['-f', 'pulse', '-i', 'default'] : []),
// VIDEO ENCODING
'-c:v', this.config.codec,
...(this.config.preset ? ['-preset', this.config.preset] : []),
...(this.config.pixelFormat ? ['-pix_fmt', this.config.pixelFormat] : []),
// RATE CONTROL
...(this.config.rateControl ? [this.config.rateControl.mode === 'crf' ? '-crf' : this.config.rateControl.mode === 'cq' ? '-cq' : this.config.rateControl.mode === 'bitrate' ? '-b:v' : this.config.rateControl.mode === 'qp' ? '-qp' : '', this.config.rateControl.mode === 'bitrate' ? `${this.config.rateControl.value}k` : String(this.config.rateControl.value)] : []),
...(stopAfter ? ['-t', String(stopAfter)] : []),
this.config.outputFile
];
try {
this.ffmpegProcess = spawn(ffmpegPath, ffmpegArgs, {
stdio: this.config.verbose ? ['pipe', 'inherit', 'inherit'] : ['pipe', 'ignore', 'ignore'],
});
this.isRecording = true;
this.ffmpegProcess.on('error', (error) => {
var _a;
console.error(`ffmpegProcess error: ${(_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error}`);
return this.stop(true);
});
}
catch (error) {
this.stop(true);
throw error;
}
}
stop(force) {
var _a, _b;
this.isRecording = false;
if (this.ffmpegProcess) {
// perform multiple checks so ffmpegProcess gets terminated
force ? this.ffmpegProcess.kill('SIGKILL') : this.ffmpegProcess.kill('SIGINT');
(_a = this.ffmpegProcess.stdin) === null || _a === void 0 ? void 0 : _a.write('q');
(_b = this.ffmpegProcess.stdin) === null || _b === void 0 ? void 0 : _b.end();
this.ffmpegProcess = null;
console.log('ffmpegProcess successfully terminated.');
}
else {
console.warn('ffmpegProcess is nullish.');
}
}
}