UNPKG

homebridge-foscam-stream

Version:

Library to support streaming for Foscam cameras under Homebridge.

498 lines (413 loc) 17.1 kB
"use strict"; const FoscamStream = require('./FoscamStream'); const Foscam = require('foscam-client'); const FoscamStreamLayer = require('foscam-binary-client').FoscamStreamLayer; class FoscamAccessory { constructor(hap, config, log) { let self = this; const StreamController = hap.StreamController; self.hap = hap; self.log = log; let host = config.host; let username = config.username || 'admin'; let password = config.password || ''; let port = config.port || 88; let gain = config.gain || 0; let maxMainStreams = config.maxMainStreams === undefined ? 2 : config.maxMainStreams; let maxSubStreams = config.maxSubStreams === undefined ? 2 : config.maxSubStreams; let speaker = config.speaker === undefined ? {} : config.speaker; self.streamType = config.streamType === undefined ? 3 : config.streamType; self._motionDetected = false; self._motionDetectedCharacteristic = null; self._foscamClient = new Foscam({ username: username, password: password, host: host, port: port, protocol: 'http' }); self._host = host; self._port = port; self._username = username; self._password = password; self.log('FoscamAccessory configured with', username, host, port, self.streamType); let mainResolutions = [ [1280, 960, 30], [1280, 960, 15], [1280, 720, 30], [1280, 720, 15], [640, 480, 30], [640, 480, 15], [640, 360, 30], [640, 360, 15], [320, 240, 30], [320, 240, 15], [320, 180, 30], [320, 180, 15] ]; let subResolutions = [ [1280, 720, 10], [640, 480, 10], [640, 360, 10], [320, 240, 10], [320, 180, 10] ]; let audioSettings = { codecs: [ { type: 'OPUS', samplerate: 16 } ] }; let videoCodec = { profiles: [StreamController.VideoCodecParamProfileIDTypes.MAIN], levels: [StreamController.VideoCodecParamLevelTypes.TYPE3_1, StreamController.VideoCodecParamLevelTypes.TYPE3_2, StreamController.VideoCodecParamLevelTypes.TYPE4_0] } let mainOptions = { proxy: true, disable_audio_proxy: true, srtp: false, video: { resolutions: mainResolutions, codec: videoCodec }, audio: audioSettings }; let subOptions = { proxy: true, disable_audio_proxy: true, srtp: false, video: { resolutions: subResolutions, codec: videoCodec }, audio: audioSettings }; self.mainSupportedBitRates = [ 4 * 1024 * 1024, 2 * 1024 * 1024, 1 * 1024 * 1024, 512 * 1024, 256 * 1024, 200 * 1024, 128 * 1024, 100 * 1024 ]; self.subSupportedBitRates = [ 512 * 1024, 256 * 1024, 200 * 1024, 128 * 1024, 100 * 1024, 50 * 1024, 20 * 1024 ]; self.services = []; self.streamControllers = []; self.streams = []; self._streamControllerIdx = 0; self._activeDays = []; self._triggerInterval = 5; self._foscamBinaryClient = null; self._motionDetectionEnabled = false; self._motionDetectedTriggerTimeout = false; if(config.motionDetector) { self._syncTime().then(() => { self._configureMotionDetector(config.motionDetector); self.resumePollingForMotion(); }); self._createMotionDetectorService(); } self._infoPromise = Promise.all([self._foscamClient.getDevInfo(), self._foscamClient.getPortInfo()]).then(results => { let info = results[0]; let portInfo = results[1]; let rtspPort = config.rtspPort || portInfo['rtspPort'] || port; let uri = 'rtsp://' + username + ':' + password + '@' + host + ':' + rtspPort + '/'; let mainURI = uri + 'videoMain'; let subURI = uri + 'videoSub'; self.log('Foscam Camera Info:', info, mainURI, subURI); self._createStreamControllers(maxMainStreams, mainURI, gain, speaker, mainOptions, self.setMainOptions.bind(self)); self._createStreamControllers(maxSubStreams, subURI, gain, speaker, subOptions, self.setSubOptions.bind(self)); return info; }); } info() { let self = this; return self._infoPromise; } closestBitRate(list, bitRate) { let closest = null; let closestDiff; for(let rate of list) { let diff = Math.abs(bitRate - rate); if(closest === null || closestDiff > diff) { closest = rate; closestDiff = diff; } } return closest; } setMainOptions(width, height, fps, bitRate) { let self = this; self.log('Requested main options:', width, height, fps, bitRate); return self._foscamClient.setVideoStreamParam({ 'streamType': self.streamType, 'resolution': self.heightToFoscamResolution(height), 'bitRate': self.closestBitRate(self.mainSupportedBitRates, bitRate), 'frameRate': fps, 'GOP': fps, 'isVBR': true }).then(() => { self.log('Set main parameters, requesting set type.'); return self._foscamClient.setMainVideoStreamType(self.streamType); }); } setSubOptions(width, height, fps, bitRate) { let self = this; self.log('Requested sub options:', width, height, fps, bitRate); return self._foscamClient.setSubVideoStreamParam({ 'streamType': self.streamType, 'resolution': self.heightToFoscamResolution(height), 'bitRate': self.closestBitRate(self.subSupportedBitRates, bitRate), 'frameRate': fps, 'GOP': fps, 'isVBR': true }).then(() => { // Work-around for lack of setSubVideoStreamType in foscam-client. self.log('Set sub parameters, requesting set type.'); return self._foscamClient.get('setSubVideoStreamType', {'streamType': self.streamType}); }); } heightToFoscamResolution(height) { switch(height) { case 960: return 6; case 720: return 0; case 480: return 1; case 360: return 3; case 240: return 2; case 180: return 4; } } _createStreamControllers(numStreams, uri, gain, speaker, options, setOptions) { let self = this; for(let i = 0; i < numStreams; i++) { let stream = new FoscamStream(uri, gain, speaker, setOptions, self.log); let streamController = new self.hap.StreamController(self._streamControllerIdx++, options, stream); stream.streamController = streamController; self.services.push(streamController.service); self.streamControllers.push(streamController); self.streams.push(stream); } } handleSnapshotRequest(request, callback) { let self = this; self.log('Foscam-NG: Getting snapshot.'); self._foscamClient.snapPicture2().then(function(data) { self.log('Foscam-NG: Got snapshot.'); callback(null, data); }); } _configureMotionDetector(config) { let self = this; let activeDays; let activeAreas; if(config['schedule']) { activeDays = [0, 0, 0, 0, 0, 0, 0]; let schedule = config['schedule']; let generateBitMapForDay = (config, activeDays, day, idx) => { if(!config[day]) return; let bits = 0; for(let interval of config[day]) { let start = interval[0].split(':'); let stop = interval[1].split(':'); if(start.length != 2) throw {message: 'Bad interval specifier', specifier: start}; if(stop.length != 2) throw {message: 'Bad interval specifier', specifier: stop}; let startBit = parseInt(start[0]) * 2 + (parseInt(start[1]) / 30); if(startBit < 0 || startBit > 48) throw {message: 'Time out of range', specifier: start}; let stopBit = parseInt(stop[0]) * 2 + (parseInt(stop[1]) / 30); if(stopBit < 0 || stopBit > 48) throw {message: 'Time out of range', specifier: stop}; if(startBit == stopBit) throw {message: 'Interval too small', specifier: interval}; for(let i = startBit; i < stopBit; ++i) { if(i >= 32) bits += ((1 << (i - 32)) >>> 0) * 0x100000000; else bits += (1 << i) >>> 0; } } activeDays[idx] = bits; } generateBitMapForDay(schedule, activeDays, 'monday', 0); generateBitMapForDay(schedule, activeDays, 'tuesday', 1); generateBitMapForDay(schedule, activeDays, 'wednesday', 2); generateBitMapForDay(schedule, activeDays, 'thursday', 3); generateBitMapForDay(schedule, activeDays, 'friday', 4); generateBitMapForDay(schedule, activeDays, 'saturday', 5); generateBitMapForDay(schedule, activeDays, 'sunday', 6); } else { let allDay = (((1 << (48 - 32)) >>> 0) * 0x100000000) - 1; activeDays = [allDay, allDay, allDay, allDay, allDay, allDay, allDay]; } if(config['areas']) { activeAreas = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; for(let area of config['areas']) { let topLeft = area[0]; let bottomRight = area[1]; if(topLeft[0] < 0 || topLeft[0] > 9 || topLeft[1] < 0 || topLeft[1] > 9) throw {message: 'Coordinates out of bounds', coordinates: topLeft}; if(topLeft[0] > bottomRight[0] || bottomRight[0] > 9 || topLeft[1] > bottomRight[1] || bottomRight[1] > 9) throw {message: 'Coordinates out of bounds', coordinates: topLeft}; for(let y = topLeft[1]; y <= bottomRight[1]; ++y) { for(let x = topLeft[0]; x <= bottomRight[0]; ++x) { activeAreas[y] = (activeAreas[y] | (1 << x)) >>> 0; } } } } else { let allRow = ((1 << 10) - 1) >>> 0; activeAreas = [allRow, allRow, allRow, allRow, allRow, allRow, allRow, allRow, allRow, allRow]; } let triggerInterval = config.triggerInterval === undefined ? 5 : config.triggerInterval; if(triggerInterval < 5 || triggerInterval > 15) throw {message: 'Trigger interval out of range', triggerInterval: triggerInterval}; self._triggerInterval = triggerInterval; let params = { 'isEnable': true, 'linkage': 0, 'snapInterval': config.snapInterval === undefined ? 0 : config.snapInterval, 'sensitivity': config.sensitivity === undefined ? 1 : config.sensitivity, 'triggerInterval': triggerInterval - 5 }; for(let i = 0; i < 7; ++i) params['schedule' + i.toString()] = activeDays[i]; for(let i = 0; i < 10; ++i) params['area' + i.toString()] = activeAreas[i]; self._activeDays = activeDays; self.log('FoscamAccessory: New motion detect params:', params); return self._foscamClient.setMotionDetectConfig(params); } _isDuringMotionActiveTime(date) { let self = this; // Convert to Foscam's Monday is the begining of the week format. let day = (7 + (date.getDay() - 1)) % 7; let active = self._activeDays[day] || 0; let bit = (date.getHours() * 2) + Math.floor(date.getMinutes() / 30); if(active & (1 << bit)) return true; return false; } _nextMotionActiveTime() { let self = this; let date = new Date(); let loops = 0; // Loop for a maximum of 7 days. let i; let max = 7 * 24 * 2; for(i = 0; i < max; ++i) { if(self._isDuringMotionActiveTime(date)) break; date = new Date(date.getTime() + (30 * 60 * 1000)); date = new Date(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), Math.floor(date.getMinutes() / 30) * 30, 0); } if(i == max) return null; return date; } motionDetected() { let self = this; self._motionDetected = true; if(self._motionDetectedTriggerTimeout) clearTimeout(self._motionDetectedTriggerTimeout); self._motionDetectedTriggerTimeout = setTimeout(() => { self._motionDetected = false; if(self._motionDetectedCharacteristic) self._motionDetectedCharacteristic.setValue(self._motionDetected); }, self._triggerInterval * 1000); if(self._motionDetectedCharacteristic) self._motionDetectedCharacteristic.setValue(self._motionDetected); } stopPollingForMotion() { let self = this; self._motionDetectionEnabled = false; self._foscamBinaryClient.close(); self._foscamBinaryClient = null; } resumePollingForMotion() { let self = this; self._motionDetectionEnabled = true; if(self._foscamBinaryClient) { self._foscamBinaryClient.close(); self._foscamBinaryClient = null; } self._foscamBinaryClient = new FoscamStreamLayer(self._host, self._port, self._username, self._password); self._foscamBinaryClient.on('motion', self.motionDetected.bind(self)); self._foscamBinaryClient.on('error', err => { self.log('FoscamAccessory: Motion sensing connection error:', err); self.resumePollingForMotion(); }); self._foscamBinaryClient.connect(); } _createMotionDetectorService() { let self = this; let service = new self.hap.Service.MotionSensor(); self._motionDetectedCharacteristic = service.getCharacteristic(self.hap.Characteristic.MotionDetected); self._motionDetectedCharacteristic.on('get', callback => { callback(null, self._motionDetected); }); self.services.push(service); } _stdOffset() { let self = this; let date = new Date(); let jan = new Date(date.getFullYear(), 0, 1); let jul = new Date(date.getFullYear(), 6, 1); return Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset()); } _dstOffset() { let self = this; let date = new Date(); let jan = new Date(date.getFullYear(), 0, 1); let jul = new Date(date.getFullYear(), 6, 1); return Math.min(jan.getTimezoneOffset(), jul.getTimezoneOffset()); } _dst() { let self = this; return self._dstOffset() != self._stdOffset(); } _syncTime() { let self = this; return self._foscamClient.getSystemTime().then(time => { let localDate = new Date(); let newParams = { 'timeSource': time['timeSource'], 'ntpServer': time['ntpServer'], 'dateFormat': time['dateFormat'], 'timeFormat': time['timeFormat'], 'timeZone': self._stdOffset() * 60, 'isDst': self._dst() ? 1 : 0, 'dst': (self._stdOffset() - self._dstOffset()) / 2, 'year': localDate.getFullYear(), 'month': localDate.getMonth(), 'day': localDate.getDate(), 'hour': localDate.getHours(), 'min': localDate.getMinutes(), 'sec': localDate.getSeconds() }; self.log('FoscamAccessory: Date Synchronization', time, newParams); return self._foscamClient.get('setSystemTime', newParams); }); } } module.exports = FoscamAccessory;