homebridge-ezviz
Version:
EZVIZ plugin for homebridge: https://homebridge.io/
289 lines • 12.2 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.StreamingDelegate = void 0;
const endpoints_1 = require("./ezviz/endpoints");
const rtp_1 = require("./util/rtp");
const camera_1 = require("./ezviz/models/camera");
const ffmpeg_1 = require("./ffmpeg");
const fs_1 = require("fs");
const path_1 = require("path");
const ffmpeg_for_homebridge_1 = __importDefault(require("ffmpeg-for-homebridge"));
class StreamingDelegate {
constructor(hap, camera, config, log) {
var _a;
this.ffmpegInstalled = true;
this.ffmpegSupportsLibfdk_acc = true;
this.pendingSessions = {};
this.ongoingSessions = {};
this.hap = hap;
this.log = log;
this.camera = camera;
this.customFfmpeg = (_a = config.options) === null || _a === void 0 ? void 0 : _a.pathToFfmpeg;
this.videoProcessor = this.customFfmpeg || ffmpeg_for_homebridge_1.default || 'ffmpeg';
ffmpeg_1.isFfmpegInstalled(this.videoProcessor)
.then((installed) => {
this.ffmpegInstalled = installed;
})
.catch(() => {
});
ffmpeg_1.getCodecsOutput(this.videoProcessor)
.then((output) => {
this.ffmpegSupportsLibfdk_acc = output.includes('libfdk_aac');
})
.catch(() => {
});
}
getOfflineImage(callback) {
const log = this.log;
fs_1.readFile(path_1.join(__dirname, `../images/offline.jpg`), (err, data) => {
if (err) {
log.error(err.message);
callback(err);
}
else {
callback(undefined, data);
}
});
}
handleSnapshotRequest(request, callback) {
const sleepSwitch = this.camera.info.switch.find((x) => x.type === 21);
if (sleepSwitch && sleepSwitch.enable) {
this.getOfflineImage(callback);
}
else {
const url = `rtsp://admin:${this.camera.info.code}@${this.camera.info.connection.localIp}/Streaming/Channels/${this.camera.info.channelNumber}/`;
ffmpeg_1.getSnapshot(url, this.customFfmpeg)
.then((snapshot) => {
callback(undefined, snapshot);
})
.catch((error) => {
endpoints_1.handleError(this.log, error, `Error fetching snapshot for ${this.camera.info.name}`);
callback(error);
});
}
}
prepareStream(request, callback) {
return __awaiter(this, void 0, void 0, function* () {
const sessionId = request.sessionID;
const targetAddress = request.targetAddress;
const video = request.video;
const videoPort = video.port;
const returnVideoPort = (yield rtp_1.reservePorts())[0];
const videoCryptoSuite = video.srtpCryptoSuite;
const videoSrtpKey = video.srtp_key;
const videoSrtpSalt = video.srtp_salt;
const videoSSRC = this.hap.CameraController.generateSynchronisationSource();
const audio = request.audio;
const audioPort = audio.port;
const returnAudioPort = (yield rtp_1.reservePorts())[0];
const twoWayAudioPort = (yield rtp_1.reservePorts(2))[0];
const audioServerPort = (yield rtp_1.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 rtp_1.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 bitrate = videoInfo.max_bit_rate * 4;
const videoPayloadType = videoInfo.pt;
const mtu = videoInfo.mtu;
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 = [
'-i',
`rtsp://admin:${this.camera.info.code}@${this.camera.info.connection.localIp}/Streaming/Channels/${this.camera.info.channelNumber}/`,
'-map',
'0:0',
'-c:v',
'copy',
'-b:v',
`${bitrate}k`,
'-bufsize',
`${bitrate}k`,
'-maxrate',
`${2 * bitrate}k`,
'-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.camera.info.switch.find((x) => x.type === camera_1.SwitchTypes.Audio);
if (audioSwitch === null || audioSwitch === void 0 ? void 0 : audioSwitch.enable) {
command = command.concat([
'-map',
'0:1',
'-c:a',
'libfdk_aac',
'-profile:a',
'aac_eld',
'-ac',
'1',
'-vn',
'-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.camera.info.switch.find((x) => x.type === 21);
if (sleepSwitch && sleepSwitch.enable) {
command = [
'-loop',
'1',
'-i',
path_1.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":
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 ffmpeg_1.FfmpegProcess('STREAM', ffmpegCommand, this.log, callback, this, sessionId, false, this.customFfmpeg);
this.log.info(`Streaming started for ${this.camera.info.name}`);
this.ongoingSessions[sessionId] = ffmpeg;
break;
case "reconfigure":
this.log.debug('(Not implemented) Received request to reconfigure to: ' + JSON.stringify(request.video));
callback();
break;
case "stop":
this.stopStream(sessionId);
callback();
break;
}
}
stopStream(sessionId) {
try {
if (this.ongoingSessions[sessionId]) {
const ffmpegVideoProcess = this.ongoingSessions[sessionId];
ffmpegVideoProcess === null || ffmpegVideoProcess === void 0 ? void 0 : ffmpegVideoProcess.stop();
this.log.info(`Streaming stopped for ${this.camera.info.name}`);
}
const sessionInfo = this.pendingSessions[sessionId];
if (sessionInfo) {
sessionInfo.rtpSplitter.close();
}
delete this.pendingSessions[sessionId];
delete this.ongoingSessions[sessionId];
}
catch (e) {
this.log.error('Error occurred terminating the video process!');
this.log.error(e);
}
}
}
exports.StreamingDelegate = StreamingDelegate;
//# sourceMappingURL=streaming-delegate.js.map