Skip to content

Commit

Permalink
Introduce additional RTSP endpoint for video only
Browse files Browse the repository at this point in the history
Add delayed kill handler
Added debug pages to enable/disable logging at runtime
Added debug page for homekit
  • Loading branch information
slyoldfox committed Jun 20, 2024
1 parent 04a9988 commit 8c4bfcd
Show file tree
Hide file tree
Showing 20 changed files with 394 additions and 152 deletions.
6 changes: 2 additions & 4 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
Expand All @@ -11,7 +8,8 @@
"name": "Launch HOMEKIT controller",
"outputCapture": "std",
"env": {
"DEBUG": "rtsp-server:*,rtsp-streaming-server:*,c300x-controller:*"
"DEBUG": "rtsp-server:*,rtsp-streaming-server:*,rtsp-stream:*,c300x-controller:*"
//"DEBUG": "c300x-controller:*"
//"DEBUG": "*"
},
"skipFiles": [
Expand Down
13 changes: 11 additions & 2 deletions base.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
"use strict";
var package_json = require('./package.json');

process.on('uncaughtException', function(e){
console.error('uncaughtException', e);
});

process.on('unhandledRejection', function(e){
console.error('unhandledRejection', e);
});

console.log(`======= c300x-controller ${package_json.version} for use with BTicino plugin 0.0.15 =======`)
const Api = require('./lib/api')
const MulticastListener = require("./lib/multicast-listener");
Expand All @@ -12,9 +20,10 @@ const registry = EndpointRegistry.create()
const api = Api.create(registry)
udpProxy.create( 40004, '0.0.0.0', 4000, '127.0.0.1' )
const eventbus = require('./lib/eventbus').create()
const multicastListener = MulticastListener.create(registry, api, mqtt.create(api), eventbus)
MulticastListener.create(registry, api, mqtt.create(api), eventbus)

module.exports = {
'registry': registry,
'eventbus': eventbus
'eventbus': eventbus,
'api': api
}
28 changes: 22 additions & 6 deletions bundles/homekit-camera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Logger } from './homekit-logger';

export type VideoConfig = {
source?: string;
stillImageSourceCacheTime?: number
stillImageSource?: string;
returnAudioTarget?: string;
maxStreams?: number;
Expand Down Expand Up @@ -170,7 +171,9 @@ export class StreamingDelegate implements CameraStreamingDelegate {
}

fetchSnapshot(snapFilter?: string): Promise<Buffer> {
const stillImageSourceCacheTime = this.videoConfig.stillImageSourceCacheTime || (5 * 60 * 1000)
this.snapshotPromise = new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error("Operation timed out creating snapshot")), 6000);
const startTime = Date.now();
const ffmpegArgs = (this.videoConfig.stillImageSource || this.videoConfig.source!) + // Still
' -frames:v 1' +
Expand All @@ -179,14 +182,15 @@ export class StreamingDelegate implements CameraStreamingDelegate {
' -hide_banner' +
' -loglevel error';

//this.log.debug('Snapshot command: ' + this.videoProcessor + ' ' + ffmpegArgs, this.cameraName, this.videoConfig.debug);
this.log.debug('Snapshot command: ' + this.videoProcessor + ' ' + ffmpegArgs, this.cameraName, this.videoConfig.debug);
const ffmpeg = spawn(this.videoProcessor, ffmpegArgs.split(/\s+/), { env: process.env });

let snapshotBuffer = Buffer.alloc(0);
ffmpeg.stdout.on('data', (data) => {
snapshotBuffer = Buffer.concat([snapshotBuffer, data]);
});
ffmpeg.on('error', (error: Error) => {
clearTimeout(t)
reject('FFmpeg process creation failed: ' + error.message);
});
ffmpeg.stderr.on('data', (data) => {
Expand All @@ -198,14 +202,16 @@ export class StreamingDelegate implements CameraStreamingDelegate {
});
ffmpeg.on('close', () => {
if (snapshotBuffer.length > 0) {
clearTimeout(t)
resolve(snapshotBuffer);
} else {
clearTimeout(t)
reject('Failed to fetch snapshot.');
}

setTimeout(() => {
this.snapshotPromise = undefined;
}, 5 * 60 * 1000); // Expire cached snapshot after 5 minutes
}, stillImageSourceCacheTime); // Expire cached snapshot after 5 minutes

const runtime = (Date.now() - startTime) / 1000;
let message = 'Fetching snapshot took ' + runtime + ' seconds.';
Expand Down Expand Up @@ -256,7 +262,13 @@ export class StreamingDelegate implements CameraStreamingDelegate {

try {
const cachedSnapshot = !!this.snapshotPromise;


if(request.reason) {
console.log('snapshot requested for reason:', request.reason);
}

const now = Date.now()

//this.log.debug('Snapshot requested: ' + request.width + ' x ' + request.height,
// this.cameraName, this.videoConfig.debug);

Expand All @@ -265,10 +277,14 @@ export class StreamingDelegate implements CameraStreamingDelegate {
//this.log.debug('Sending snapshot: ' + (resolution.width > 0 ? resolution.width : 'native') + ' x ' +
// (resolution.height > 0 ? resolution.height : 'native') +
// (cachedSnapshot ? ' (cached)' : ''), this.cameraName, this.videoConfig.debug);

const resized = await this.resizeSnapshot(snapshot, resolution.resizeFilter);
callback(undefined, resized);

//const resized = await this.resizeSnapshot(snapshot, resolution.resizeFilter);
if(!cachedSnapshot)
this.log.debug("Snapshot took: " + (Date.now() - now) + "ms", this.cameraName, this.videoConfig.debug)
callback(undefined, snapshot);

} catch (err) {
this.snapshotPromise = undefined
this.log.error(err as string, this.cameraName);
callback();
}
Expand Down
6 changes: 4 additions & 2 deletions bundles/homekit-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,6 @@ export class HomekitManager {
category: Categories.BRIDGE,
addIdentifyingMaterial: false
});

this.addDoorbell(videoConfig)
}
addDoorbell(videoConfig: VideoConfig) {
const accessory = new Accessory(videoConfig.displayName, uuid.generate('hap-nodejs:accessories:doorbell:' + videoConfig.displayName));
Expand All @@ -198,6 +196,10 @@ export class HomekitManager {
});

console.log('Camera pairing code: ' + videoConfig.pinCode);
return {
doorbell: accessory,
streamingDelegate
}
}
addLock(id: string, name: string ) {
const lock = new LockAccessory(id, name, this.eventbus);
Expand Down
17 changes: 4 additions & 13 deletions bundles/sip-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,7 @@ export class SipManager {
this.registrarContact = m.headers.contact[0].uri;
}
if( sipOptions.debugSip ) {
console.log("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<")
console.log(stringify( m ));
console.log("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<")
console.log(`<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\r\n${stringify(m)}\r\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<`)
}
},
appendGruu: function( contact, gruuUrn ) {
Expand Down Expand Up @@ -315,7 +313,8 @@ export class SipManager {
}
} else if( m.method == 'ACK' || m.method == 'BYE' ) {
m.headers.to.uri = toWithDomain
m.uri = this.registrarContact
if( this.registrarContact )
m.uri = this.registrarContact
} else if( (m.method == undefined && m.status) && m.headers.cseq ) {
if( m.status == '200' ) {
// Response on invite
Expand All @@ -339,15 +338,7 @@ export class SipManager {
}

if( sipOptions.debugSip ) {
console.log(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
if( m.uri ) {
console.log(stringify( m ));
} else {
m.uri = '';
console.log( stringify( m ) )
}

console.log(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
console.log(`>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\r\n${stringify(m)}\r\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>`);
}
},
},
Expand Down
13 changes: 6 additions & 7 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const model = utils.model()

const global = {
// Use the higher resolution video stream
'highResVideo': model === 'c100x' ? false : true
'highResVideo': model !== 'c100x'
}
const doorUnlock = {
// Default behaviour is device ID 20, if you need more, add them to additionalLocks in config.json
Expand Down Expand Up @@ -61,7 +61,7 @@ const configPaths = [configPath, cwdConfigPath, extraConfigPath]

function overrideAndPrintValue( name, base, overridden ) {
for(const key in overridden) {
if( overridden[key] != undefined && base[key] !== overridden[key] ) {
if( overridden[key] !== undefined && base[key] !== overridden[key] ) {
console.log( name + "." + key + ": " + JSON.stringify( base[key], null, 2) + " -> " + JSON.stringify( overridden[key], null, 2 ))
base[key] = overridden[key]
}
Expand All @@ -76,8 +76,7 @@ function detectConfig() {

const detectedPath = detectConfig()
if( detectedPath ) {
console.log(`FOUND config.json file at '${detectedPath}' and overriding the values from it.`)
console.log("")
console.log(`FOUND config.json file at '${detectedPath}' and overriding the values from it.\r\n`)
const config = JSON.parse( fs.readFileSync(detectedPath) )
overrideAndPrintValue( "global", global, config.global)
overrideAndPrintValue( "doorUnlock", doorUnlock, config.doorUnlock)
Expand All @@ -95,9 +94,9 @@ if( global.highResVideo && utils.model() === 'c100x' ) {
global.highResVideo = false
}

console.log("============================== final config =====================================")
console.log('\x1b[33m'+JSON.stringify( { global, doorUnlock, additionalLocks, mqtt_config, sip }, null, 2 ) +'\x1b[0m' )
console.log("=================================================================================")
console.log(`============================== final config =====================================
\x1b[33m${JSON.stringify( { global, doorUnlock, additionalLocks, mqtt_config, sip }, null, 2 )}\x1b[0m
=================================================================================`)

module.exports = {
doorUnlock, additionalLocks, mqtt_config, global, sip, version
Expand Down
55 changes: 50 additions & 5 deletions controller-homekit.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const BASE_PATH = __dirname
const filestore = jsonstore.create( BASE_PATH + '/config-homekit.json')
const model = utils.model().toLocaleUpperCase()

rtspserver.create(base.registry)
rtspserver.create(base.registry, base.eventbus)

utils.fixMulticast()
utils.verifyFlexisip('webrtc@' + utils.domain()).forEach( (e) => console.error( `* ${e}`) )
Expand Down Expand Up @@ -49,6 +49,7 @@ if( videoConfig.source?.indexOf("tcp://") >= 0 ) {
}

const homekitManager = new homekitBundle.HomekitManager( base.eventbus, BASE_PATH, bridgeConfig, videoConfig, config.version, model, videoConfig)
const {doorbell, streamingDelegate} = homekitManager.addDoorbell(videoConfig)

base.eventbus.on('doorbell:pressed', () => {
console.log('doorbell:pressed')
Expand Down Expand Up @@ -85,9 +86,9 @@ homekitManager.addSwitch('Muted' )
.switchedOff( () => { openwebnet.run("ringerUnmute").then( () => utils.reloadUi() ) } )
.updateState( () => {
return openwebnet.run("ringerStatus").then( (result) => {
if( result == '*#8**33*0##' ) {
if( result === '*#8**33*0##' ) {
return true
} else if( result == '*#8**33*1##' ) {
} else if( result === '*#8**33*1##' ) {
return false
}
} )
Expand All @@ -101,7 +102,7 @@ if( model !== 'C100X' ) {
return openwebnet.run("aswmStatus").then( result => {
const matches = [...result.matchAll(/\*#8\*\*40\*([01])\*([01])\*/gm)]
if( matches && matches.length > 0 && matches[0].length > 0 ) {
return matches[0][1] == '1'
return matches[0][1] === '1'
}
return false
} )
Expand All @@ -110,4 +111,48 @@ if( model !== 'C100X' ) {

openwebnet.run("firmwareVersion").catch( ()=>{} ).then( (result) => {
homekitManager.updateFirmwareVersion(result)
})
})

const homekit = new class Api {
path() {
return "/homekit"
}

description() {
return "Homekit debug page"
}

async handle(request, response, url, q) {
if(!q.raw) {
response.write("<pre>")
response.write("<a href='./homekit?press=true'>Emulate homekit doorbell press</a><br/>")
response.write("<a href='./homekit?thumbnail=true&raw=true'>Video thumbnail (cached)</a><br/>")
response.write("<a href='./homekit?thumbnail=true&raw=true&refresh=true'>Video thumbnail (uncached)</a><br/>")
response.write("</pre>")
}

if(q.press === "true") {
base.eventbus.emit('homekit:pressed')
}
if(q.thumbnail === "true") {
if(!q.raw || q.raw !== "true" ) {
response.write("<br/>call this endpoing with &raw=true")

} else {
const request = {}
if(q.refresh === 'true'){
streamingDelegate.snapshotPromise = undefined
}
streamingDelegate.handleSnapshotRequest(request, (error, image) => {
if(image)
response.write(image)
response.end()
})
}

}
videoConfig.debug = q.enablevideodebug === 'true';
}
}

base.api.apis.set(homekit.path(), homekit )
14 changes: 7 additions & 7 deletions lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,26 @@ const querystring = require('querystring');
const url = require('url')
const fs = require("fs");

const __webpack__enabled = (typeof __webpack_require__ === "function") ? true : false
var normalizedPath = require("path").join(__dirname, "apis");
const __webpack__enabled = (typeof __webpack_require__ === "function")
const normalizedPath = require("path").join(__dirname, "apis");
const apis_path = "./apis/"

class Api {

#apis = new Map()

constructor(registry) {
var req = __webpack__enabled ? require.context( "./apis/" , true, /.js$/) : fs.readdirSync(normalizedPath)
var keys = __webpack__enabled ? req.keys() : req;
const req = __webpack__enabled ? require.context( "./apis/" , true, /.js$/) : fs.readdirSync(normalizedPath)
const keys = __webpack__enabled ? req.keys() : req;
keys.forEach( key => {
const API = __webpack__enabled ? req( /* webpackIgnore: true */ key) : require( /* webpackIgnore: true */ apis_path + key );
const api = new API()
if (api.endpointRegistry) {
api.endpointRegistry(registry)
}
var path = api.path()
let path = api.path();
if (path.length > 0) {
if (path[0] != '/')
if (path[0] !== '/')
path = '/' + path
if (this.#apis[path]) {
console.log("Path already taken by another API")
Expand All @@ -44,7 +44,7 @@ class Api {
response.writeHead(200, { "Content-Type": "text/html" })
response.write('<!doctype html><html lang=en><head><meta charset=utf-8><title>Bticino API</title></head></body>')
response.write(`<p>Version: ${config.version}</p>`)
if (this.#apis.size == 0) {
if (this.#apis.size === 0) {
response.write("No APIs found")
} else {
response.write("<ul>")
Expand Down
2 changes: 1 addition & 1 deletion lib/apis/aswm.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module.exports = class Api {
response.write("<a href='./aswm?enable=false'>Disable</a>")
response.write("</pre>")
if (q.enable) {
if (q.enable == "true") {
if (q.enable === "true") {
openwebnet.run("aswmEnable")
} else {
openwebnet.run("aswmDisable")
Expand Down
Loading

0 comments on commit 8c4bfcd

Please sign in to comment.