@viguza/homebridge-ezviz
Version:
A short description about what your plugin does.
281 lines • 11.2 kB
JavaScript
import { RtpSplitter, reservePorts } from './rtp.js';
import { SwitchTypes } from './enums.js';
import { FfmpegProcess, isFfmpegInstalled, getSnapshot, getCodecsOutput } from './ffmpeg.js';
import { readFile } from 'fs';
import { join } from 'path';
import pathToFfmpeg from 'ffmpeg-for-homebridge';
export class StreamingDelegate {
hap;
log;
videoProcessor;
ffmpegInstalled = true;
ffmpegSupportsLibfdk_acc = true;
deviceData;
cameraConfig;
controller;
// keep track of sessions
pendingSessions = {};
ongoingSessions = {};
constructor(hap, deviceData, log) {
this.hap = hap;
this.log = log;
this.deviceData = deviceData;
this.cameraConfig = deviceData.HBConfig;
this.videoProcessor = pathToFfmpeg || 'ffmpeg';
// Check if ffmpeg is installed
isFfmpegInstalled(this.videoProcessor)
.then((installed) => {
this.ffmpegInstalled = installed;
})
.catch(() => {
// skip
});
// Get the correct video codec
getCodecsOutput(this.videoProcessor)
.then((output) => {
this.ffmpegSupportsLibfdk_acc = output.includes('libfdk_aac');
})
.catch(() => {
// skip
});
}
getOfflineImage(callback) {
const log = this.log;
readFile(join(__dirname, '../images/offline.jpg'), (err, data) => {
if (err) {
log.error(err.message);
callback(err);
}
else {
callback(undefined, data);
}
});
}
getRtspUrl() {
const ip = this.deviceData.Wifi?.address && this.deviceData.Wifi.address !== '0.0.0.0'
? this.deviceData.Wifi.address
: this.deviceData.Connection.localIp;
const port = this.deviceData.Connection.localRtspPort || 554;
const channel = this.deviceData.DeviceInfo.channelNumber || 1;
return `rtsp://${this.cameraConfig.username}:${this.cameraConfig.code}@${ip}:${port}/Streaming/Channels/${channel}/`;
}
handleSnapshotRequest(request, callback) {
const sleepSwitch = this.deviceData.Switches?.find((x) => x.type === SwitchTypes.Sleep);
if (sleepSwitch?.enable) {
this.getOfflineImage(callback);
}
else {
const url = this.getRtspUrl();
getSnapshot(url)
.then((snapshot) => {
callback(undefined, snapshot);
})
.catch((error) => {
this.log.error(`Error fetching snapshot for ${this.deviceData.Name}`);
callback(error);
});
}
}
async prepareStream(request, callback) {
const sessionId = request.sessionID;
const targetAddress = request.targetAddress;
//video setup
const video = request.video;
const videoPort = video.port;
const returnVideoPort = (await reservePorts())[0];
const videoCryptoSuite = video.srtpCryptoSuite;
const videoSrtpKey = video.srtp_key;
const videoSrtpSalt = video.srtp_salt;
const videoSSRC = this.hap.CameraController.generateSynchronisationSource();
//audio setup
const audio = request.audio;
const audioPort = audio.port;
const returnAudioPort = (await reservePorts())[0];
const twoWayAudioPort = (await reservePorts(2))[0];
const audioServerPort = (await reservePorts())[0];
const audioCryptoSuite = video.srtpCryptoSuite;
const audioSrtpKey = audio.srtp_key;
const audioSrtpSalt = audio.srtp_salt;
const audioSSRC = this.hap.CameraController.generateSynchronisationSource();
const sessionInfo = {
address: targetAddress,
videoPort: videoPort,
returnVideoPort: returnVideoPort,
videoCryptoSuite: videoCryptoSuite,
videoSRTP: Buffer.concat([videoSrtpKey, videoSrtpSalt]),
videoSSRC: videoSSRC,
audioPort: audioPort,
returnAudioPort: returnAudioPort,
twoWayAudioPort: twoWayAudioPort,
rtpSplitter: new RtpSplitter(audioServerPort, returnAudioPort, twoWayAudioPort),
audioCryptoSuite: audioCryptoSuite,
audioSRTP: Buffer.concat([audioSrtpKey, audioSrtpSalt]),
audioSSRC: audioSSRC,
};
const response = {
video: {
port: returnVideoPort,
ssrc: videoSSRC,
srtp_key: videoSrtpKey,
srtp_salt: videoSrtpSalt,
},
audio: {
port: audioServerPort,
ssrc: audioSSRC,
srtp_key: audioSrtpKey,
srtp_salt: audioSrtpSalt,
},
};
this.pendingSessions[sessionId] = sessionInfo;
callback(undefined, response);
}
getCommand(videoInfo, audioInfo, sessionId) {
const sessionInfo = this.pendingSessions[sessionId];
const videoPort = sessionInfo.videoPort;
const returnVideoPort = sessionInfo.returnVideoPort;
const videoSsrc = sessionInfo.videoSSRC;
const videoSRTP = sessionInfo.videoSRTP.toString('base64');
const address = sessionInfo.address;
const videoPayloadType = videoInfo.pt;
const mtu = videoInfo.mtu; // maximum transmission unit
const audioPort = sessionInfo.audioPort;
const returnAudioPort = sessionInfo.returnAudioPort;
const audioSsrc = sessionInfo.audioSSRC;
const audioSRTP = sessionInfo.audioSRTP.toString('base64');
const audioPayloadType = audioInfo.pt;
const audioMaxBitrate = audioInfo.max_bit_rate;
const sampleRate = audioInfo.sample_rate;
let command = [
'-rtsp_transport', 'tcp',
'-use_wallclock_as_timestamps', '1',
'-i', this.getRtspUrl(),
'-map', '0:0',
'-c:v', 'copy',
'-pix_fmt', 'yuv420p',
'-an',
'-payload_type', videoPayloadType.toString(),
'-ssrc', videoSsrc.toString(),
'-f', 'rtp',
'-srtp_out_suite', 'AES_CM_128_HMAC_SHA1_80',
'-srtp_out_params', videoSRTP,
`srtp://${address}:${videoPort}?rtcpport=${videoPort}&localrtcpport=${returnVideoPort}&pkt_size=${mtu}`,
];
if (this.ffmpegSupportsLibfdk_acc) {
const audioSwitch = this.deviceData.Switches?.find((x) => x.type === SwitchTypes.Audio);
if (audioSwitch?.enable) {
command = command.concat([
'-map',
'0:1',
'-c:a',
'libfdk_aac',
'-profile:a',
'aac_eld',
'-ac',
'1',
'-vn',
'-af',
'aresample=async=1:min_hard_comp=0.100000:first_pts=0',
'-ar',
`${sampleRate}k`,
'-b:a',
`${audioMaxBitrate}k`,
'-flags',
'+global_header',
'-payload_type',
audioPayloadType.toString(),
'-ssrc',
audioSsrc.toString(),
'-f',
'rtp',
'-srtp_out_suite',
'AES_CM_128_HMAC_SHA1_80',
'-srtp_out_params',
audioSRTP,
`srtp://${address}:${audioPort}?rtcpport=${audioPort}&localrtcpport=${returnAudioPort}&pkt_size=188`,
]);
}
}
else {
this.log.error('This version of FFMPEG does not support the audio codec \'libfdk_aac\'. ' +
'You may need to recompile FFMPEG using \'--enable-libfdk_aac\' and restart homebridge.');
}
const sleepSwitch = this.deviceData.Switches.find((x) => x.type === SwitchTypes.Sleep);
if (sleepSwitch?.enable) {
command = [
'-loop',
'1',
'-i',
join(__dirname, '../images/offline.jpg'),
'-c:v',
'libx264',
'-preset',
'ultrafast',
'-tune',
'stillimage',
'-pix_fmt',
'yuv420p',
'-an',
'-payload_type',
videoPayloadType.toString(),
'-ssrc',
videoSsrc.toString(),
'-f',
'rtp',
'-srtp_out_suite',
'AES_CM_128_HMAC_SHA1_80',
'-srtp_out_params',
videoSRTP,
`srtp://${address}:${videoPort}?rtcpport=${videoPort}&localrtcpport=${returnVideoPort}&pkt_size=${mtu}`,
];
}
return command;
}
handleStreamRequest(request, callback) {
const sessionId = request.sessionID;
switch (request.type) {
case "start" /* StreamRequestTypes.START */:
{
const video = request.video;
const audio = request.audio;
if (!this.ffmpegInstalled) {
this.log.error('FFMPEG is not installed. Please install it and restart homebridge.');
callback(new Error('FFmpeg not installed'));
break;
}
const ffmpegCommand = this.getCommand(video, audio, sessionId);
const ffmpeg = new FfmpegProcess('STREAM', ffmpegCommand, this.log, callback, this, sessionId, false);
this.log.info(`Streaming started for ${this.deviceData.Name}`);
this.ongoingSessions[sessionId] = ffmpeg;
break;
}
case "reconfigure" /* StreamRequestTypes.RECONFIGURE */:
// not implemented
this.log.debug('(Not implemented) Received request to reconfigure to: ' + JSON.stringify(request.video));
callback();
break;
case "stop" /* StreamRequestTypes.STOP */:
this.stopStream(sessionId);
callback();
break;
}
}
stopStream(sessionId) {
try {
if (this.ongoingSessions[sessionId]) {
const ffmpegVideoProcess = this.ongoingSessions[sessionId];
ffmpegVideoProcess?.stop();
this.log.info(`Streaming stopped for ${this.deviceData.Name}`);
}
const sessionInfo = this.pendingSessions[sessionId];
if (sessionInfo) {
sessionInfo.rtpSplitter.close();
}
delete this.pendingSessions[sessionId];
delete this.ongoingSessions[sessionId];
}
catch (error) {
this.log.error('Error occurred terminating the video process!', error);
}
}
}
//# sourceMappingURL=streaming-delegate.js.map