homebridge-eufy-security-mikebcbc
Version:
Control Eufy Security from homebridge.
500 lines • 22.6 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.FFmpegParameters = void 0;
const net_1 = __importDefault(require("net"));
const pick_port_1 = require("pick-port");
class FFmpegParameters {
constructor(options) {
// default parameters
this.hideBanner = true;
this.useWallclockAsTimestamp = true;
this.inputSoure = '-i pipe:';
this.output = 'pipe:1';
this.codec = 'copy';
this.flagsGlobalHeader = false;
this.delaySnapshot = false;
this.progressPort = options.port;
this.isVideo = options.isVideo;
this.isAudio = options.isAudio;
this.isSnapshot = options.isSnapshot;
this.debug = options.debug;
}
static async allocateddPort(type) {
// Call pickPort function and await its result
const port = await (0, pick_port_1.pickPort)({
type: type,
ip: '0.0.0.0',
reserveTimeout: 15,
});
return port; // Return the allocated port
}
static async allocateTCPPort() {
return FFmpegParameters.allocateddPort('tcp');
}
static async allocateUDPPort() {
return FFmpegParameters.allocateddPort('udp');
}
static async create(options) {
var _a;
const port = await FFmpegParameters.allocateTCPPort();
const baseOptions = { port, isVideo: false, isAudio: false, isSnapshot: false, debug: (_a = options.debug) !== null && _a !== void 0 ? _a : false };
switch (options.type) {
case 'audio': {
const params = new FFmpegParameters({ ...baseOptions, isAudio: true });
params.useWallclockAsTimestamp = false;
// The '-flags +global_header' option is crucial for encoding streams, especially when using codecs like 'libfdk_aac'.
// This flag ensures the inclusion of global headers in the output stream, which contain essential codec initialization data.
// It's particularly important for streaming scenarios to ensure the receiving end can properly decode the stream from the start.
// Omitting this flag can lead to errors in stream initialization and decoding failures in certain codecs and streaming contexts.
params.flagsGlobalHeader = true;
return params;
}
case 'video':
return new FFmpegParameters({ ...baseOptions, isVideo: true });
case 'snapshot': {
const params = new FFmpegParameters({ ...baseOptions, isSnapshot: true });
params.useWallclockAsTimestamp = true;
params.numberFrames = 1;
params.format = 'image2';
return params;
}
case 'videoRecording': {
const params = new FFmpegParameters({ ...baseOptions, isVideo: true });
params.useWallclockAsTimestamp = true;
return params;
}
case 'audioRecording':
return new FFmpegParameters({ ...baseOptions, isAudio: true });
default:
throw new Error('Invalid FFmpegParameters type');
}
}
usesStdInAsInput() {
return this.inputSoure === '-i pipe:';
}
setInputSource(value) {
// TODO: check for errors
this.inputSoure = `-i ${value}`;
}
async createServerWithTimeout(handleConnection) {
const port = await FFmpegParameters.allocateTCPPort();
let killTimeout = undefined;
const server = net_1.default.createServer((socket) => {
if (killTimeout) {
clearTimeout(killTimeout);
}
server.close();
socket.on('error', (_) => { });
handleConnection(socket);
});
server.listen(port);
server.on('error', (_) => { });
killTimeout = setTimeout(() => server.close(), 30 * 1000);
return port;
}
async setInputStream(input) {
const port = await this.createServerWithTimeout((socket) => {
input.pipe(socket);
});
this.setInputSource(`tcp://127.0.0.1:${port}`);
}
setDelayedSnapshot() {
this.delaySnapshot = true;
}
setup(cameraConfig, request) {
var _a;
const videoConfig = (_a = cameraConfig.videoConfig) !== null && _a !== void 0 ? _a : (cameraConfig.videoConfig = {});
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 && 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 (this.isAudio) {
const req = request;
let codec = req.audio.codec === "OPUS" /* AudioStreamingCodecType.OPUS */ ? 'libopus' : 'libfdk_aac';
let codecOptions = req.audio.codec === "OPUS" /* AudioStreamingCodecType.OPUS */ ? '-application lowdelay' : '-profile:a aac_eld';
if (videoConfig.acodec && videoConfig.acodec !== '') {
codec = videoConfig.acodec;
codecOptions = '';
}
if (this.flagsGlobalHeader) {
codecOptions += ' -flags +global_header';
}
this.codec = codec;
this.codecOptions = codecOptions;
let samplerate = req.audio.sample_rate;
if (videoConfig.audioSampleRate &&
(videoConfig.audioSampleRate === 8 || videoConfig.audioSampleRate === 16 || videoConfig.audioSampleRate === 24)) {
samplerate = videoConfig.audioSampleRate;
}
this.sampleRate = samplerate;
this.channels = req.audio.channel;
this.bitrate = 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;
}
}
}
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 (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 = configuration.videoCodec.parameters.bitRate;
this.width = configuration.videoCodec.resolution[0];
this.height = configuration.videoCodec.resolution[1];
this.fps = configuration.videoCodec.resolution[2];
}
this.iFrameInterval = configuration.videoCodec.parameters.iFrameInterval;
}
if (this.isAudio) {
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 this.createServerWithTimeout((socket) => {
socket.end(sdpInput);
});
this.setInputSource(`tcp://127.0.0.1:${port}`);
}
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('-thread_queue_size 128');
params.push(this.inputSoure);
params.push(this.isVideo ? '-an -sn -dn' : '');
params.push(this.isAudio ? '-vn -sn -dn' : '');
return params;
}
buildFilterParameters() {
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) {
const resizeFilter = 'scale=' +
`'min(${this.width},iw)':` +
`'min(${this.height},iw)':` +
'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 (this.isSnapshot) {
filters.push('fps=30');
}
}
return filters.length > 0 ? ['-filter:v ' + filters.join(',')] : [];
}
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 : '');
params.concat(this.buildFilterParameters());
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) {
// audio parameters
params.push('-acodec ' + this.codec);
// Adjust your FFmpeg command to handle AAC streams without global headers.
// adding -bsf:a aac_adtstoasc as a bitstream filter for the audio stream, which converts ADTS to ASC (Audio Specific Config).
params.push('-bsf:a aac_adtstoasc');
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' : '');
params.concat(this.buildFilterParameters());
}
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();
params.push('-nostats');
params.push('-fflags', '+discardcorrupt+genpts');
// params.push('-thread_queue_size 128');
// params.push('-probesize 5000000');
// params.push('-analyzeduration 5000000');
// params.push('-max_delay 500000');
// params.push('-r 15');
// video input
params.push('-thread_queue_size 128');
params.push('-f h264');
params.push(parameters[0].inputSoure);
// params.push('-an -sn -dn');
// audio input
if (parameters.length > 1 && parameters[0].inputSoure !== parameters[1].inputSoure) { // don't include extra audio source for rtsp
params.push('-thread_queue_size 128');
params.push('-f aac');
params.push(parameters[1].inputSoure);
// params.push('-vn -sn -dn');
// params.push('-bsf:a aac_adtstoasc');
}
params.push('-map 0:v');
params.push('-vcodec copy');
// params.push('-vsync 1');
// params.push('-filter:v setpts=PTS-STARTPTS');
params.push('-map 1:a');
params.push('-acodec copy');
// params.push('-rtpflags latm');
params.push('-bsf:a aac_adtstoasc');
// params.push('-filter:a asetpts=PTS-STARTPTS');
// params.push(parameters[0].iFrameInterval ? `-force_key_frames expr:gte(t,n_forced*${parameters[0].iFrameInterval / 1000})` : '');
params.push('-reset_timestamps', '1');
// fragmented mp4 options
params.push(parameters[0].movflags ? `-movflags ${parameters[0].movflags}` : '');
// params.push(parameters[0].maxMuxingQueueSize ? `-max_muxing_queue_size ${parameters[0].maxMuxingQueueSize}` : '');
// audio encoding
if (parameters.length > 1) {
// params = params.concat(parameters[1].buildEncodingParameters());
}
// output
params.push('-f mp4');
params.push(parameters[0].output);
// params.push(`/tmp/${this.name}.mp4`);
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';
}
}
exports.FFmpegParameters = FFmpegParameters;
//# sourceMappingURL=ffmpeg-params.js.map