homebridge-eufy-security
Version:
Control Eufy Security from homebridge.
863 lines • 36.1 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FFmpeg = exports.FFmpegParameters = void 0;
const child_process_1 = require("child_process");
const net_1 = __importDefault(require("net"));
const os_1 = __importDefault(require("os"));
const ffmpeg_for_homebridge_1 = __importDefault(require("ffmpeg-for-homebridge"));
const events_1 = __importDefault(require("events"));
const utils_1 = require("./utils");
const camera_utils_1 = require("@homebridge/camera-utils");
class FFmpegProgress extends events_1.default {
server;
started = false;
constructor(port) {
super();
let killTimeout = undefined;
this.server = net_1.default.createServer((socket) => {
if (killTimeout) {
clearTimeout(killTimeout);
}
this.server.close(); // close server and terminate after connection is released
socket.on('data', this.analyzeProgress.bind(this));
socket.on('error', () => { }); // ignore since this is handled elsewhere
});
killTimeout = setTimeout(() => {
this.server.close();
}, 30 * 1000); // TBC for variable
this.server.on('close', () => {
this.emit('progress stopped');
});
this.server.on('error', () => { }); // ignore since this is handled elsewhere
this.server.listen(port);
}
analyzeProgress(progressData) {
const progress = new Map();
progressData.toString().split(/\r?\n/).forEach((line) => {
const split = line.split('=', 2);
if (split.length !== 2) {
return;
}
progress.set(split[0], split[1]);
});
if (!this.started) {
if (progress.get('progress') !== undefined) {
this.started = true;
this.emit('progress started');
}
}
}
}
class FFmpegParameters {
progressPort;
debug;
// default parameters
processor;
hideBanner = true;
useWallclockAsTimestamp = true;
inputSoure = '-i pipe:';
protocolWhitelist;
inputCodec;
inputFormat;
output = 'pipe:1';
isVideo;
isAudio;
isSnapshot;
// generic options
analyzeDuration;
probeSize;
stimeout;
readrate;
codec = 'copy';
codecOptions;
bitrate;
// output options
payloadType;
ssrc;
srtpSuite;
srtpParams;
format;
// video options
fps;
pixFormat;
colorRange;
filters;
width;
height;
bufsize;
maxrate;
crop = false;
// audio options
sampleRate;
channels;
flagsGlobalHeader = false;
// snapshot options
numberFrames;
delaySnapshot = false;
// recording options / fragmented mp4
movflags;
maxMuxingQueueSize;
iFrameInterval;
processAudio = true;
constructor(port, isVideo, isAudio, isSnapshot, debug = false) {
this.progressPort = port;
this.isVideo = isVideo;
this.isAudio = isAudio;
this.isSnapshot = isSnapshot;
this.debug = debug;
}
static async forAudio(debug = false) {
const [port] = await (0, camera_utils_1.reservePorts)({ type: 'tcp', count: 1 });
const ffmpeg = new FFmpegParameters(port, false, true, false, debug);
ffmpeg.useWallclockAsTimestamp = false;
ffmpeg.flagsGlobalHeader = true;
return ffmpeg;
}
static async forVideo(debug = false) {
const [port] = await (0, camera_utils_1.reservePorts)({ type: 'tcp', count: 1 });
return new FFmpegParameters(port, true, false, false, debug);
}
static async forSnapshot(debug = false) {
const [port] = await (0, camera_utils_1.reservePorts)({ type: 'tcp', count: 1 });
const ffmpeg = new FFmpegParameters(port, false, false, true, debug);
ffmpeg.useWallclockAsTimestamp = false;
ffmpeg.numberFrames = 1;
ffmpeg.format = 'image2';
return ffmpeg;
}
static async forVideoRecording(debug = false) {
const [port] = await (0, camera_utils_1.reservePorts)({ type: 'tcp', count: 1 });
const ffmpeg = new FFmpegParameters(port, true, false, false, debug);
ffmpeg.useWallclockAsTimestamp = true;
return ffmpeg;
}
static async forAudioRecording(debug = false) {
const [port] = await (0, camera_utils_1.reservePorts)({ type: 'tcp', count: 1 });
const ffmpeg = new FFmpegParameters(port, false, true, false, debug);
return ffmpeg;
}
setResolution(width, height) {
this.width = width;
this.height = height;
}
usesStdInAsInput() {
return this.inputSoure === '-i pipe:';
}
setInputSource(value) {
this.inputSoure = `-i ${value}`;
}
async setInputStream(input) {
const [port] = await (0, camera_utils_1.reservePorts)({ type: 'tcp', count: 1 });
let killTimeout = undefined;
const server = net_1.default.createServer((socket) => {
if (killTimeout) {
clearTimeout(killTimeout);
}
server.close();
socket.on('error', () => { }); // ignore since this is handled elsewhere
input.pipe(socket);
});
server.listen(port);
server.on('error', () => { }); // ignore since this is handled elsewhere
killTimeout = setTimeout(() => {
server.close();
}, 30 * 1000);
this.setInputSource(`tcp://127.0.0.1:${port}`);
}
setDelayedSnapshot() {
this.delaySnapshot = true;
}
setup(cameraConfig, request) {
const videoConfig = cameraConfig.videoConfig ??= {};
if (videoConfig.videoProcessor && videoConfig.videoProcessor !== '') {
this.processor = videoConfig.videoProcessor;
}
if (videoConfig.readRate) {
this.readrate = videoConfig.readRate;
}
if (videoConfig.stimeout) {
this.stimeout = videoConfig.stimeout;
}
if (videoConfig.probeSize) {
this.probeSize = videoConfig.probeSize;
}
if (videoConfig.analyzeDuration) {
this.analyzeDuration = videoConfig.analyzeDuration;
}
if (this.isVideo) {
const req = request;
let codec = 'libx264';
if (videoConfig.vcodec && videoConfig.vcodec !== '') {
codec = videoConfig.vcodec;
}
this.codec = codec;
if (codec !== 'copy') {
const fps = videoConfig.maxFPS ? videoConfig.maxFPS : req.video.fps;
this.fps = fps;
const bitrate = videoConfig.maxBitrate ? videoConfig.maxBitrate : req.video.max_bit_rate;
this.bitrate = bitrate;
this.bufsize = bitrate * 2;
this.maxrate = bitrate;
let encoderOptions = codec === 'libx264' ? '-preset ultrafast -tune zerolatency' : '';
if (videoConfig.encoderOptions) {
encoderOptions = videoConfig.encoderOptions;
}
this.codecOptions = encoderOptions;
this.pixFormat = 'yuv420p';
this.colorRange = 'mpeg';
let width = req.video.width;
if (videoConfig.maxWidth && videoConfig.maxWidth < width) {
width = videoConfig.maxWidth;
}
let height = req.video.height;
if (videoConfig.maxHeight && videoConfig.maxHeight < height) {
height = videoConfig.maxHeight;
}
this.width = width;
this.height = height;
if (videoConfig.videoFilter && videoConfig.videoFilter !== '') {
this.filters = videoConfig.videoFilter;
}
if (videoConfig.crop) {
this.crop = videoConfig.crop;
}
}
}
if (this.isAudio) {
const req = request;
let codec = 'libfdk_aac';
let codecOptions = '-profile:a aac_eld';
switch (req.audio.codec) {
case "OPUS" /* AudioStreamingCodecType.OPUS */:
codec = 'libopus';
codecOptions = '-application lowdelay';
break;
default:
codec = 'libfdk_aac';
codecOptions = '-profile:a aac_eld';
break;
}
if (videoConfig.acodec && videoConfig.acodec !== '') {
codec = videoConfig.acodec;
codecOptions = '';
}
if (videoConfig.acodecOptions !== undefined) {
codecOptions = videoConfig.acodecOptions;
}
if (this.flagsGlobalHeader) {
if (codecOptions !== '') {
codecOptions += ' ';
}
codecOptions += '-flags +global_header';
}
this.codec = codec;
this.codecOptions = codecOptions;
if (this.codec !== ' copy') {
this.sampleRate = req.audio.sample_rate;
this.channels = req.audio.channel;
this.bitrate = videoConfig.audioBitrate ? videoConfig.audioBitrate : req.audio.max_bit_rate;
}
}
if (this.isSnapshot) {
const req = request;
let width = req.width;
if (videoConfig.maxWidth && videoConfig.maxWidth < width) {
width = videoConfig.maxWidth;
}
let height = req.height;
if (videoConfig.maxHeight && videoConfig.maxHeight < height) {
height = videoConfig.maxHeight;
}
this.width = width;
this.height = height;
if (videoConfig.videoFilter && videoConfig.videoFilter !== '') {
this.filters = videoConfig.videoFilter;
}
if (videoConfig.crop) {
this.crop = videoConfig.crop;
}
}
}
setRTPTarget(sessionInfo, request) {
if (this.isVideo) {
this.payloadType = request.video.pt;
this.ssrc = sessionInfo.videoSSRC;
this.srtpParams = sessionInfo.videoSRTP.toString('base64');
this.srtpSuite = 'AES_CM_128_HMAC_SHA1_80';
this.format = 'rtp';
this.output = `srtp://${sessionInfo.address}:${sessionInfo.videoPort}?rtcpport=${sessionInfo.videoPort}&pkt_size=1128`;
}
if (this.isAudio) {
this.payloadType = request.audio.pt;
this.ssrc = sessionInfo.audioSSRC;
this.srtpParams = sessionInfo.audioSRTP.toString('base64');
this.srtpSuite = 'AES_CM_128_HMAC_SHA1_80';
this.format = 'rtp';
this.output = `srtp://${sessionInfo.address}:${sessionInfo.audioPort}?rtcpport=${sessionInfo.audioPort}&pkt_size=188`;
}
}
setOutput(output) {
this.output = output;
}
setupForRecording(videoConfig, configuration) {
this.movflags = 'frag_keyframe+empty_moov+default_base_moof';
this.maxMuxingQueueSize = 1024;
if (videoConfig.videoProcessor && videoConfig.videoProcessor !== '') {
this.processor = videoConfig.videoProcessor;
}
if (this.isVideo) {
if (videoConfig.vcodec && videoConfig.vcodec !== '') {
this.codec = videoConfig.vcodec;
}
else {
this.codec = 'libx264';
}
if (this.codec === 'libx264') {
this.pixFormat = 'yuv420p';
const profile = configuration.videoCodec.parameters.profile === 2 /* H264Profile.HIGH */
? 'high'
: configuration.videoCodec.parameters.profile === 1 /* H264Profile.MAIN */
? 'main'
: 'baseline';
const level = configuration.videoCodec.parameters.level === 2 /* H264Level.LEVEL4_0 */
? '4.0'
: configuration.videoCodec.parameters.level === 1 /* H264Level.LEVEL3_2 */
? '3.2'
: '3.1';
this.codecOptions = `-profile:v ${profile} -level:v ${level}`;
}
if (this.codec !== 'copy') {
this.bitrate = videoConfig.maxBitrate ?? configuration.videoCodec.parameters.bitRate;
this.width = configuration.videoCodec.resolution[0];
this.height = configuration.videoCodec.resolution[1];
this.fps = videoConfig.maxFPS ?? configuration.videoCodec.resolution[2];
this.crop = (videoConfig.crop !== false); // only false if 'crop: false' was specifically set
}
this.iFrameInterval = configuration.videoCodec.parameters.iFrameInterval;
}
if (this.isAudio) {
if (videoConfig.audio === false) {
this.processAudio = false;
}
if (videoConfig.acodec && videoConfig.acodec !== '') {
this.codec = videoConfig.acodec;
}
else {
this.codec = 'libfdk_aac';
}
if (this.codec === 'libfdk_aac' || this.codec === 'aac') {
this.codecOptions = (configuration.audioCodec.type === 0 /* AudioRecordingCodecType.AAC_LC */)
? '-profile:a aac_low'
: '-profile:a aac_eld';
this.codecOptions += ' -flags +global_header';
}
if (this.codec !== 'copy') {
let samplerate;
switch (configuration.audioCodec.samplerate) {
case 0 /* AudioRecordingSamplerate.KHZ_8 */:
samplerate = '8';
break;
case 1 /* AudioRecordingSamplerate.KHZ_16 */:
samplerate = '16';
break;
case 2 /* AudioRecordingSamplerate.KHZ_24 */:
samplerate = '24';
break;
case 3 /* AudioRecordingSamplerate.KHZ_32 */:
samplerate = '32';
break;
case 4 /* AudioRecordingSamplerate.KHZ_44_1 */:
samplerate = '44.1';
break;
case 5 /* AudioRecordingSamplerate.KHZ_48 */:
samplerate = '48';
break;
default:
throw new Error(`Unsupported audio samplerate: ${configuration.audioCodec.samplerate}`);
}
this.sampleRate = samplerate;
this.bitrate = configuration.audioCodec.bitrate;
this.channels = configuration.audioCodec.audioChannels;
}
}
}
async setTalkbackInput(sessionInfo) {
this.useWallclockAsTimestamp = false;
this.protocolWhitelist = 'pipe,udp,rtp,file,crypto,tcp';
this.inputFormat = 'sdp';
this.inputCodec = 'libfdk_aac';
this.codec = 'libfdk_aac';
this.sampleRate = 16;
this.channels = 1;
this.bitrate = 20;
this.format = 'adts';
const ipVer = sessionInfo.ipv6 ? 'IP6' : 'IP4';
const sdpInput = 'v=0\r\n' +
'o=- 0 0 IN ' + ipVer + ' ' + sessionInfo.address + '\r\n' +
's=Talk\r\n' +
'c=IN ' + ipVer + ' ' + sessionInfo.address + '\r\n' +
't=0 0\r\n' +
'm=audio ' + sessionInfo.audioReturnPort + ' RTP/AVP 110\r\n' +
'b=AS:24\r\n' +
'a=rtpmap:110 MPEG4-GENERIC/16000/1\r\n' +
'a=rtcp-mux\r\n' + // FFmpeg ignores this, but might as well
'a=fmtp:110 ' +
'profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; ' +
'config=F8F0212C00BC00\r\n' +
'a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:' + sessionInfo.audioSRTP.toString('base64') + '\r\n';
const [port] = await (0, camera_utils_1.reservePorts)({ type: 'tcp', count: 1 });
let killTimeout = undefined;
const server = net_1.default.createServer((socket) => {
if (killTimeout) {
clearTimeout(killTimeout);
}
server.close();
socket.on('error', () => { }); // ignore since this is handled elsewhere
socket.end(sdpInput);
});
server.listen(port);
server.on('error', () => { }); // ignore since this is handled elsewhere
killTimeout = setTimeout(() => {
server.close();
}, 30 * 1000);
this.setInputSource(`tcp://127.0.0.1:${port}`);
}
setTalkbackChannels(channels) {
this.channels = channels;
}
buildGenericParameters() {
const params = [];
params.push(this.hideBanner ? '-hide_banner' : '');
params.push('-loglevel level+verbose'); // default log to stderr
params.push(this.useWallclockAsTimestamp ? '-use_wallclock_as_timestamps 1' : '');
return params;
}
buildInputParamters() {
const params = [];
// input
params.push(this.analyzeDuration ? `-analyzeduration ${this.analyzeDuration}` : '');
params.push(this.probeSize ? `-probesize ${this.probeSize}` : '');
params.push(this.stimeout ? `-stimeout ${this.stimeout * 10000000}` : '');
params.push(this.readrate ? '-re' : '');
params.push(this.protocolWhitelist ? `-protocol_whitelist ${this.protocolWhitelist}` : '');
params.push(this.inputFormat ? `-f ${this.inputFormat}` : '');
params.push(this.inputCodec ? `-c:a ${this.inputCodec}` : '');
params.push(this.inputSoure);
params.push(this.isVideo ? '-an -sn -dn' : '');
params.push(this.isAudio ? '-vn -sn -dn' : '');
return params;
}
buildEncodingParameters() {
const params = [];
if (this.isVideo) {
params.push(this.fps ? '-r ' + this.fps : '');
params.push('-vcodec ' + this.codec);
params.push(this.pixFormat ? '-pix_fmt ' + this.pixFormat : '');
params.push(this.colorRange ? '-color_range ' + this.colorRange : '');
params.push(this.codecOptions ? this.codecOptions : '');
// video filters
const filters = this.filters ? this.filters.split(',') : [];
const noneFilter = filters.indexOf('none');
if (noneFilter >= 0) {
filters.splice(noneFilter, 1);
}
if (noneFilter < 0 && this.width && this.height) {
if (this.crop) {
const resizeFilter = `scale=${this.width}:${this.height}:force_original_aspect_ratio=increase`;
filters.push(resizeFilter);
filters.push(`crop=${this.width}:${this.height}`);
filters.push(`scale='trunc(${this.width}/2)*2:trunc(${this.height}/2)*2'`); // Force to fit encoder restrictions
}
else {
const resizeFilter = 'scale=' +
'\'min(' + this.width + ',iw)\'' +
':' +
'\'min(' + this.height + ',ih)\'' +
':force_original_aspect_ratio=decrease';
filters.push(resizeFilter);
filters.push('scale=\'trunc(iw/2)*2:trunc(ih/2)*2\''); // Force to fit encoder restrictions
}
}
if (filters.length > 0) {
params.push('-filter:v ' + filters.join(','));
}
params.push(this.bitrate ? '-b:v ' + this.bitrate + 'k' : '');
params.push(this.bufsize ? '-bufsize ' + this.bufsize + 'k' : '');
params.push(this.maxrate ? `-maxrate ${this.maxrate}k` : '');
}
if (this.isAudio && this.processAudio) {
// audio parameters
params.push('-acodec ' + this.codec);
params.push(this.codecOptions ? this.codecOptions : '');
params.push(this.bitrate ? `-b:a ${this.bitrate}k` : '');
params.push(this.sampleRate ? `-ar ${this.sampleRate}k` : '');
params.push(this.bitrate ? `-ac ${this.channels}` : '');
}
if (this.isSnapshot) {
params.push(this.numberFrames ? `-frames:v ${this.numberFrames}` : '');
params.push(this.delaySnapshot ? '-ss 00:00:00.500' : '');
const filters = this.filters ? this.filters.split(',') : [];
const noneFilter = filters.indexOf('none');
if (noneFilter >= 0) {
filters.splice(noneFilter, 1);
}
if (noneFilter < 0 && this.width && this.height) {
if (this.crop) {
const resizeFilter = `scale=${this.width}:${this.height}:force_original_aspect_ratio=increase`;
filters.push(resizeFilter);
filters.push(`crop=${this.width}:${this.height}`);
filters.push(`scale='trunc(${this.width}/2)*2:trunc(${this.height}/2)*2'`); // Force to fit encoder restrictions
}
else {
const resizeFilter = 'scale=' +
'\'min(' + this.width + ',iw)\'' +
':' +
'\'min(' + this.height + ',ih)\'' +
':force_original_aspect_ratio=decrease';
filters.push(resizeFilter);
filters.push('scale=\'trunc(iw/2)*2:trunc(ih/2)*2\''); // Force to fit encoder restrictions
}
}
if (filters.length > 0) {
params.push('-filter:v ' + filters.join(','));
}
}
return params;
}
buildOutputParameters() {
const params = [];
// output
params.push(this.payloadType ? `-payload_type ${this.payloadType}` : '');
params.push(this.ssrc ? `-ssrc ${this.ssrc}` : '');
params.push(this.format ? `-f ${this.format}` : '');
params.push(this.srtpSuite ? `-srtp_out_suite ${this.srtpSuite}` : '');
params.push(this.srtpParams ? `-srtp_out_params ${this.srtpParams}` : '');
params.push(this.output);
return params;
}
buildParameters() {
let params = [];
params = this.buildGenericParameters();
params = params.concat(this.buildInputParamters());
params = params.concat(this.buildEncodingParameters());
params = params.concat(this.buildOutputParameters());
params.push(`-progress tcp://127.0.0.1:${this.progressPort}`);
params = params.filter(x => x !== '');
return params;
}
getProcessArguments() {
return this.buildParameters();
}
static getRecordingArguments(parameters) {
let params = [];
if (parameters.length === 0) {
return params;
}
params = parameters[0].buildGenericParameters();
// input
params.push(parameters[0].inputSoure);
if (parameters.length > 1 && parameters[0].inputSoure !== parameters[1].inputSoure) { // don't include extra audio source for rtsp
if (parameters[1].processAudio) {
params.push(parameters[1].inputSoure);
}
else {
params.push('-f lavfi -i anullsrc -shortest');
}
}
if (parameters.length === 1) {
params.push('-an');
}
params.push('-sn -dn');
// video encoding
params = params.concat(parameters[0].buildEncodingParameters());
params.push(parameters[0].iFrameInterval ? `-force_key_frames expr:gte(t,n_forced*${parameters[0].iFrameInterval / 1000})` : '');
// audio encoding
if (parameters.length > 1) {
if (parameters[1].processAudio) {
params.push('-bsf:a aac_adtstoasc');
}
params = params.concat(parameters[1].buildEncodingParameters());
}
// fragmented mp4 options
params.push(parameters[0].movflags ? `-movflags ${parameters[0].movflags}` : '');
params.push(parameters[0].maxMuxingQueueSize ? `-max_muxing_queue_size ${parameters[0].maxMuxingQueueSize}` : '');
// output
params.push('-f mp4');
params.push(parameters[0].output);
params.push(`-progress tcp://127.0.0.1:${parameters[0].progressPort}`);
params = params.filter(x => x !== '');
return params;
}
static getCombinedArguments(parameters) {
let params = [];
if (parameters.length === 0) {
return params;
}
params = parameters[0].buildGenericParameters();
parameters.forEach((p) => {
params = params.concat(p.buildInputParamters());
params = params.concat(p.buildEncodingParameters());
params = params.concat(p.buildOutputParameters());
});
params.push(`-progress tcp://127.0.0.1:${parameters[0].progressPort}`);
params = params.filter(x => x !== '');
return params;
}
getStreamStartText() {
let message = '';
if (this.isVideo) {
message = this.codec === 'copy' ? 'native' : `${this.width}x${this.height}, ${this.fps} fps, ${this.bitrate} kbps`;
return `Starting video stream: ${message}`;
}
if (this.isAudio) {
message = this.codec === 'copy' ? 'native' : `${this.sampleRate} kHz, ${this.bitrate} kbps, codec: ${this.codec}`;
return `Starting audio stream: ${message}`;
}
return 'Starting unknown stream';
}
hasCustomFfmpeg() {
return (this.processor !== undefined);
}
getCustomFfmpeg() {
return (this.hasCustomFfmpeg()) ? this.processor : '';
}
}
exports.FFmpegParameters = FFmpegParameters;
class FFmpeg extends events_1.default {
process;
name;
progress;
parameters;
ffmpegExec = ffmpeg_for_homebridge_1.default || 'ffmpeg';
stdin;
stdout;
starttime;
killTimeout;
constructor(name, parameters) {
super();
this.name = name;
if (Array.isArray(parameters)) {
if (parameters.length === 0) {
throw new Error('No ffmpeg parameters found.');
}
this.parameters = parameters;
}
else {
this.parameters = [parameters];
}
if (this.parameters[0].hasCustomFfmpeg()) {
this.ffmpegExec = this.parameters[0].getCustomFfmpeg();
}
}
start() {
this.starttime = Date.now();
this.progress = new FFmpegProgress(this.parameters[0].progressPort);
this.progress.on('progress started', this.onProgressStarted.bind(this));
const processArgs = FFmpegParameters.getCombinedArguments(this.parameters);
utils_1.ffmpegLogger.debug(this.name, 'Stream command: ' + this.ffmpegExec + ' ' + processArgs.join(' '));
this.parameters.forEach((p) => {
utils_1.ffmpegLogger.info(this.name, p.getStreamStartText());
});
this.process = (0, child_process_1.spawn)(this.ffmpegExec, processArgs.join(' ').split(/\s+/), { env: process.env });
this.stdin = this.process.stdin;
this.stdout = this.process.stdout;
this.process.stderr.on('data', (chunk) => {
const isError = chunk.toString().indexOf('[panic]') !== -1 ||
chunk.toString().indexOf('[error]') !== -1 ||
chunk.toString().indexOf('[fatal]') !== -1;
if (this.parameters[0].debug && !isError) {
utils_1.ffmpegLogger.debug(this.name, 'ffmpeg log message:\n' + chunk.toString());
}
else if (isError) {
utils_1.ffmpegLogger.error(this.name, 'ffmpeg log message:\n' + chunk.toString());
}
});
this.process.on('error', this.onProcessError.bind(this));
this.process.on('exit', this.onProcessExit.bind(this));
}
async getResult(input) {
this.starttime = Date.now();
this.progress = new FFmpegProgress(this.parameters[0].progressPort);
this.progress.on('progress started', this.onProgressStarted.bind(this));
const processArgs = FFmpegParameters.getCombinedArguments(this.parameters);
utils_1.ffmpegLogger.debug(this.name, 'Process command: ' + this.ffmpegExec + ' ' + processArgs.join(' '));
return new Promise((resolve, reject) => {
this.process = (0, child_process_1.spawn)(this.ffmpegExec, processArgs.join(' ').split(/\s+/), { env: process.env });
this.process.stderr.on('data', (chunk) => {
const isError = chunk.toString().indexOf('[panic]') !== -1 ||
chunk.toString().indexOf('[error]') !== -1 ||
chunk.toString().indexOf('[fatal]') !== -1;
if (this.parameters[0].debug && !isError) {
utils_1.ffmpegLogger.debug(this.name, 'ffmpeg log message:\n' + chunk.toString());
}
else if (isError) {
utils_1.ffmpegLogger.error(this.name, 'ffmpeg log message:\n' + chunk.toString());
}
});
const killTimeout = setTimeout(() => {
this.stop();
reject('ffmpeg process timed out.');
}, 15 * 1000);
this.process.on('error', (error) => {
reject(error);
this.onProcessError(error);
});
let resultBuffer = Buffer.alloc(0);
this.process.stdout.on('data', (data) => {
resultBuffer = Buffer.concat([resultBuffer, data]);
});
this.process.on('exit', () => {
if (killTimeout) {
clearTimeout(killTimeout);
}
if (resultBuffer.length > 0) {
resolve(resultBuffer);
}
else {
reject('Failed to fetch data.');
}
});
if (input) {
this.process.stdin.end(input);
}
});
}
async startFragmentedMP4Session() {
this.starttime = Date.now();
this.progress = new FFmpegProgress(this.parameters[0].progressPort);
this.progress.on('progress started', this.onProgressStarted.bind(this));
const [port] = await (0, camera_utils_1.reservePorts)({ type: 'tcp', count: 1 });
return new Promise((resolve) => {
const server = net_1.default.createServer((socket) => {
server.close();
resolve({
socket: socket,
process: this.process,
generator: this.parseFragmentedMP4(socket),
});
});
server.listen(port, () => {
this.parameters[0].setOutput(`tcp://127.0.0.1:${port}`);
const processArgs = FFmpegParameters.getRecordingArguments(this.parameters);
utils_1.ffmpegLogger.debug(this.name, 'Stream command: ' + this.ffmpegExec + ' ' + processArgs.join(' '));
this.parameters.forEach((p) => {
utils_1.ffmpegLogger.info(this.name, p.getStreamStartText());
});
this.process = (0, child_process_1.spawn)(this.ffmpegExec, processArgs.join(' ').split(/\s+/), { env: process.env });
this.stdin = this.process.stdin;
this.stdout = this.process.stdout;
this.process.stderr.on('data', (chunk) => {
const isError = chunk.toString().indexOf('[panic]') !== -1 ||
chunk.toString().indexOf('[error]') !== -1 ||
chunk.toString().indexOf('[fatal]') !== -1;
if (this.parameters[0].debug && !isError) {
utils_1.ffmpegLogger.debug(this.name, 'ffmpeg log message:\n' + chunk.toString());
}
else if (isError) {
utils_1.ffmpegLogger.error(this.name, 'ffmpeg log message:\n' + chunk.toString());
}
});
this.process.on('error', this.onProcessError.bind(this));
this.process.on('exit', this.onProcessExit.bind(this));
});
});
}
async *parseFragmentedMP4(socket) {
while (true) {
const header = await this.readLength(socket, 8);
const length = header.readInt32BE(0) - 8;
const type = header.slice(4).toString();
const data = await this.readLength(socket, length);
yield {
header,
length,
type,
data,
};
}
}
async readLength(socket, length) {
if (length <= 0) {
return Buffer.alloc(0);
}
const value = socket.read(length);
if (value) {
return value;
}
return new Promise((resolve, reject) => {
const readHandler = () => {
const value = socket.read(length);
if (value) {
cleanup();
resolve(value);
}
};
const endHandler = () => {
cleanup();
reject(new Error(`FFMPEG socket closed during read for ${length} bytes!`));
};
const cleanup = () => {
socket.removeListener('readable', readHandler);
socket.removeListener('close', endHandler);
};
if (!socket) {
throw new Error('FFMPEG socket is closed now!');
}
socket.on('readable', readHandler);
socket.on('close', endHandler);
});
}
stop() {
let usesStdIn = false;
this.parameters.forEach(p => {
if (p.usesStdInAsInput()) {
usesStdIn = true;
}
});
if (usesStdIn) {
this.process?.stdin.destroy();
this.process?.kill('SIGTERM');
}
else {
this.process?.stdin.write('q' + os_1.default.EOL);
}
this.killTimeout = setTimeout(() => {
this.process?.kill('SIGKILL');
}, 2 * 1000);
}
onProgressStarted() {
this.emit('started');
const runtime = this.starttime ? (Date.now() - this.starttime) / 1000 : undefined;
utils_1.ffmpegLogger.debug(this.name, `process started. Getting the first response took ${runtime} seconds.`);
}
onProcessError(error) {
this.emit('error', error);
}
onProcessExit(code, signal) {
this.emit('exit');
if (this.killTimeout) {
clearTimeout(this.killTimeout);
}
const message = 'FFmpeg exited with code: ' + code + ' and signal: ' + signal;
if (this.killTimeout && code === 0) {
utils_1.ffmpegLogger.info(this.name, message + ' (Expected)');
}
else if (code === null || code === 255) {
if (this.process?.killed) {
utils_1.ffmpegLogger.info(this.name, message + ' (Forced)');
}
else {
utils_1.ffmpegLogger.error(this.name, message + ' (Unexpected)');
}
}
else {
this.emit('error', message + ' (Error)');
// ffmpegLogger.error(this.name, message + ' (Error)');
}
}
}
exports.FFmpeg = FFmpeg;
//# sourceMappingURL=ffmpeg.js.map