UNPKG

homebridge-loxone-proxy

Version:

Homebridge Dynamic Platform Plugin which exposes a Loxone System to Homekit.

288 lines 11.6 kB
"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