homebridge-loxone-proxy
Version:
Homebridge Dynamic Platform Plugin which exposes a Loxone System to Homekit.
288 lines • 11.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.streamingDelegate = void 0;
const camera_utils_1 = require("@homebridge/camera-utils");
const child_process_1 = require("child_process");
const dgram_1 = require("dgram");
const FfmpegStreamingProcess_1 = require("./FfmpegStreamingProcess");
const RecordingDelegate_1 = require("./RecordingDelegate");
class streamingDelegate {
constructor(platform, streamUrl, base64auth) {
this.platform = platform;
this.pendingSessions = {};
this.ongoingSessions = {};
this.hap = this.platform.api.hap;
this.streamUrl = streamUrl;
const ipAddressRegex = /http:\/\/([\d.]+)/;
const match = streamUrl.match(ipAddressRegex);
if (match && match[1]) {
this.ip = match[1];
}
this.base64auth = base64auth ||
Buffer.from(`${this.platform.config.username}:${this.platform.config.password}`, 'utf8').toString('base64');
this.recordingDelegate = new RecordingDelegate_1.RecordingDelegate(this.platform, this.streamUrl);
const resolutions = [
[320, 180, 30],
[320, 240, 15],
[320, 240, 30],
[480, 270, 30],
[480, 360, 30],
[640, 360, 30],
[640, 480, 30],
[1024, 768, 30],
[1280, 720, 30],
];
const streamingOptions = {
supportedCryptoSuites: [0],
video: {
codec: {
profiles: [0, 1, 2],
levels: [0, 1, 2],
},
resolutions: resolutions,
},
audio: {
twoWayAudio: false,
codecs: [
{
type: "AAC-eld",
samplerate: 16,
},
],
},
};
const recordingOptions = {
overrideEventTriggerOptions: [
1,
2,
],
prebufferLength: 4 * 1000,
mediaContainerConfiguration: [
{
type: 0,
fragmentLength: 4000,
},
],
video: {
parameters: {
profiles: [
0,
1,
2,
],
levels: [
0,
1,
2,
],
},
resolutions: resolutions,
type: 0,
},
audio: {
codecs: [
{
samplerate: 3,
type: 0,
},
],
},
};
const options = {
cameraStreamCount: 5,
delegate: this,
streamingOptions: streamingOptions,
recording: {
options: recordingOptions,
delegate: this.recordingDelegate,
},
};
this.controller = new this.hap.CameraController(options);
}
stopStream(sessionId) {
var _a, _b, _c;
const session = this.ongoingSessions[sessionId];
if (session) {
if (session.timeout) {
clearTimeout(session.timeout);
}
try {
(_a = session.socket) === null || _a === void 0 ? void 0 : _a.close();
}
catch (error) {
this.platform.log.error(`Error occurred closing socket: ${error}`, this.ip, 'Homebridge');
}
try {
(_b = session.mainProcess) === null || _b === void 0 ? void 0 : _b.stop();
}
catch (error) {
this.platform.log.error(`Error occurred terminating main FFmpeg process: ${error}`, this.ip, 'Homebridge');
}
try {
(_c = session.returnProcess) === null || _c === void 0 ? void 0 : _c.stop();
}
catch (error) {
this.platform.log.error(`Error occurred terminating two-way FFmpeg process: ${error}`, this.ip, 'Homebridge');
}
delete this.ongoingSessions[sessionId];
this.platform.log.info('Stopped video stream.', this.ip);
}
}
forceStopStream(sessionId) {
this.controller.forceStopStreamingSession(sessionId);
}
async handleSnapshotRequest(request, callback) {
this.platform.log.debug(`Snapshot requested: ${request.width} x ${request.height}`, this.ip);
const ffmpeg = (0, child_process_1.spawn)('ffmpeg', [
'-re',
'-headers', `Authorization: Basic ${this.base64auth}\r\n`,
'-i', `${this.streamUrl}`,
'-frames:v', '1',
'-loglevel', 'info',
'-f', 'image2',
'-',
], { env: process.env });
const snapshotBuffers = [];
ffmpeg.stdout.on('data', data => snapshotBuffers.push(data));
ffmpeg.stderr.on('data', data => {
this.platform.log.info('SNAPSHOT: ' + String(data));
});
ffmpeg.on('exit', (code, signal) => {
if (signal) {
this.platform.log.debug('Snapshot process was killed with signal: ' + signal);
callback(new Error('killed with signal ' + signal));
}
else if (code === 0) {
this.platform.log.debug(`Successfully captured snapshot at ${request.width}x${request.height}`);
callback(undefined, Buffer.concat(snapshotBuffers));
}
else {
this.platform.log.debug('Snapshot process exited with code ' + code);
callback(new Error('Snapshot process exited with code ' + code));
}
});
}
async prepareStream(request, callback) {
const videoIncomingPort = await (0, camera_utils_1.reservePorts)({
count: 1,
});
const videoSSRC = this.hap.CameraController.generateSynchronisationSource();
const audioIncomingPort = await (0, camera_utils_1.reservePorts)({
count: 1,
});
const audioSSRC = this.hap.CameraController.generateSynchronisationSource();
const sessionInfo = {
address: request.targetAddress,
addressVersion: request.addressVersion,
audioCryptoSuite: request.audio.srtpCryptoSuite,
audioPort: request.audio.port,
audioSRTP: Buffer.concat([request.audio.srtp_key, request.audio.srtp_salt]),
audioSSRC: audioSSRC,
audioIncomingPort: audioIncomingPort[0],
videoCryptoSuite: request.video.srtpCryptoSuite,
videoPort: request.video.port,
videoSRTP: Buffer.concat([request.video.srtp_key, request.video.srtp_salt]),
videoSSRC: videoSSRC,
videoIncomingPort: videoIncomingPort[0],
};
const response = {
video: {
port: sessionInfo.videoIncomingPort,
ssrc: videoSSRC,
srtp_key: request.video.srtp_key,
srtp_salt: request.video.srtp_salt,
},
audio: {
port: sessionInfo.audioIncomingPort,
ssrc: audioSSRC,
srtp_key: request.audio.srtp_key,
srtp_salt: request.audio.srtp_salt,
},
};
this.pendingSessions[request.sessionID] = sessionInfo;
callback(undefined, response);
}
async handleStreamRequest(request, callback) {
switch (request.type) {
case "start": {
this.platform.log.debug(`Start stream requested:
${request.video.width}x${request.video.height}, ${request.video.fps} fps, ${request.video.max_bit_rate} kbps`, this.ip);
await this.startStream(request, callback);
break;
}
case "reconfigure": {
this.platform.log.debug(`Reconfigure stream requested:
${request.video.width}x${request.video.height}, ${request.video.fps} fps, ${request.video.max_bit_rate} kbps (Ignored)`, this.ip);
callback();
break;
}
case "stop": {
this.platform.log.debug('Stop stream requested', this.ip);
this.stopStream(request.sessionID);
callback();
break;
}
}
}
async startStream(request, callback) {
const sessionInfo = this.pendingSessions[request.sessionID];
if (!sessionInfo) {
this.platform.log.error('Error finding session information.', this.ip);
callback(new Error('Error finding session information'));
}
const mtu = 1316;
const ffmpegArgs = [
'-headers', `Authorization: Basic ${this.base64auth}\r\n`,
'-use_wallclock_as_timestamps', '1',
'-probesize', '32',
'-analyzeduration', '0',
'-fflags', 'nobuffer',
'-flags', 'low_delay',
'-max_delay', '0',
'-re',
'-i', `${this.streamUrl}`,
'-an',
'-sn',
'-dn',
'-codec:v', 'libx264',
'-pix_fmt', 'yuv420p',
'-color_range', 'mpeg',
'-r', '25',
'-f', 'rawvideo',
'-preset', 'ultrafast',
'-tune', 'zerolatency',
'-crf', '22',
'-filter:v', 'scale=\'min(1280,iw)\':\'min(720,ih)\':force_original_aspect_ratio=decrease,scale=trunc(iw/2)*2:trunc(ih/2)*2',
'-b:v', '299k',
'-payload_type', '99',
'-ssrc', `${sessionInfo.videoSSRC}`,
'-f', 'rtp',
'-srtp_out_suite', 'AES_CM_128_HMAC_SHA1_80',
'-srtp_out_params', sessionInfo.videoSRTP.toString('base64'),
`srtp://${sessionInfo.address}:${sessionInfo.videoPort}?rtcpport=${sessionInfo.videoPort}&pkt_size=${mtu}`,
];
ffmpegArgs.push('-progress', 'pipe:1');
const activeSession = {};
activeSession.socket = (0, dgram_1.createSocket)(sessionInfo.addressVersion === 'ipv6' ? 'udp6' : 'udp4');
activeSession.socket.on('error', (err) => {
this.platform.log.error('Socket error: ' + err.message, this.ip);
this.stopStream(request.sessionID);
});
activeSession.socket.on('message', () => {
if (activeSession.timeout) {
clearTimeout(activeSession.timeout);
}
activeSession.timeout = setTimeout(() => {
this.platform.log.info('Device appears to be inactive. Stopping stream.', this.ip);
this.controller.forceStopStreamingSession(request.sessionID);
this.stopStream(request.sessionID);
}, request.video.rtcp_interval * 5 * 1000);
});
activeSession.socket.bind(sessionInfo.videoIncomingPort);
activeSession.mainProcess = new FfmpegStreamingProcess_1.FfmpegStreamingProcess(this.ip, request.sessionID, camera_utils_1.defaultFfmpegPath, ffmpegArgs, this.platform.log, true, this, callback);
this.ongoingSessions[request.sessionID] = activeSession;
delete this.pendingSessions[request.sessionID];
}
}
exports.streamingDelegate = streamingDelegate;
//# sourceMappingURL=StreamingDelegate.js.map