UNPKG

@viguza/homebridge-ezviz

Version:

A short description about what your plugin does.

281 lines 11.2 kB
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