Skip to content

Commit

Permalink
Fix motionDoorbell & port pick, better quit ffmpeg
Browse files Browse the repository at this point in the history
  • Loading branch information
Sunoo committed Aug 25, 2021
1 parent b299428 commit 6fa51ce
Show file tree
Hide file tree
Showing 8 changed files with 993 additions and 1,399 deletions.
9 changes: 2 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
# Homebridge Camera FFmpeg

[![npm](https://badgen.net/npm/v/homebridge-camera-ffmpeg) ![npm](https://badgen.net/npm/dt/homebridge-camera-ffmpeg)](https://www.npmjs.com/package/homebridge-camera-ffmpeg) [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)
[![npm](https://badgen.net/npm/v/homebridge-camera-ffmpeg) ![npm](https://badgen.net/npm/dt/homebridge-camera-ffmpeg)](https://www.npmjs.com/package/homebridge-camera-ffmpeg) [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) [![certified-hoobs-plugin](https://badgen.net/badge/HOOBS/certified/yellow)](https://plugins.hoobs.org/plugin/homebridge-camera-ffmpeg)

[Homebridge](https://homebridge.io) Plugin Providing [FFmpeg](https://www.ffmpeg.org)-based Camera Support

## Installation

Before installing this plugin, you should install Homebridge using the [official instructions](https://github.com/homebridge/homebridge/wiki).

### Install via Homebridge Config UI X

1. Search for `Camera FFmpeg` on the Plugins tab of [Config UI X](https://www.npmjs.com/package/homebridge-config-ui-x).
2. Install the `Homebridge Camera FFmpeg` plugin and use the form to enter your camera configurations.
This plugin is supported under both [Homebridge](https://homebridge.io) and [HOOBS](https://hoobs.org/). It is highly recommended that you use either [Homebridge Config UI X](https://www.npmjs.com/package/homebridge-config-ui-x) or the HOOBS UI to install and configure this plugin.

### Manual Installation

Expand Down
4 changes: 2 additions & 2 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"portmqtt": {
"title": "MQTT Port",
"type": "integer",
"placeholder": "1883",
"placeholder": 1883,
"description": "The port of the MQTT broker."
},
"tlsmqtt": {
Expand All @@ -47,7 +47,7 @@
"porthttp": {
"title": "HTTP Port",
"type": "integer",
"placeholder": "8080",
"placeholder": 8080,
"description": "The port to listen on for HTTP-based automation. If not set, HTTP support is not started."
},
"localhttp": {
Expand Down
2,283 changes: 919 additions & 1,364 deletions package-lock.json

Large diffs are not rendered by default.

19 changes: 10 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"displayName": "Homebridge Camera FFmpeg",
"name": "homebridge-camera-ffmpeg",
"version": "3.1.2",
"version": "3.1.3",
"description": "Homebridge Plugin Providing FFmpeg-based Camera Support",
"main": "dist/index.js",
"license": "ISC",
Expand Down Expand Up @@ -59,17 +59,18 @@
"README.md"
],
"devDependencies": {
"@types/node": "^14.14.31",
"@typescript-eslint/eslint-plugin": "^4.16.1",
"@typescript-eslint/parser": "^4.16.1",
"eslint": "^7.21.0",
"homebridge": "^1.3.2",
"@types/node": "^16.7.1",
"@types/ws": "^7.4.7",
"@typescript-eslint/eslint-plugin": "^4.29.3",
"@typescript-eslint/parser": "^4.29.3",
"eslint": "^7.32.0",
"homebridge": "^1.3.4",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
},
"dependencies": {
"ffmpeg-for-homebridge": "^0.0.9",
"get-port": "^5.1.1",
"mqtt": "^4.2.6"
"mqtt": "^4.2.8",
"pick-port": "^1.0.0"
}
}
25 changes: 16 additions & 9 deletions src/ffmpeg.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ChildProcessWithoutNullStreams, spawn } from 'child_process';
import { StreamRequestCallback } from 'homebridge';
import os from 'os';
import readline from 'readline';
import { Writable } from 'stream';
import { Logger } from './logger';
Expand All @@ -21,6 +22,8 @@ type FfmpegProgress = {

export class FfmpegProcess {
private readonly process: ChildProcessWithoutNullStreams;
private killTimeout?: NodeJS.Timeout;
readonly stdin: Writable;

constructor(cameraName: string, sessionId: string, videoProcessor: string, ffmpegArgs: string, log: Logger,
debug = false, delegate: StreamingDelegate, callback?: StreamRequestCallback) {
Expand All @@ -29,6 +32,7 @@ export class FfmpegProcess {
let started = false;
const startTime = Date.now();
this.process = spawn(videoProcessor, ffmpegArgs.split(/\s+/), { env: process.env });
this.stdin = this.process.stdin;

this.process.stdout.on('data', (data) => {
const progress = this.parseProgress(data);
Expand Down Expand Up @@ -56,7 +60,7 @@ export class FfmpegProcess {
callback();
callback = undefined;
}
if (line.match(/\[(panic|fatal|error)\]/)) {
if (debug && line.match(/\[(panic|fatal|error)\]/)) { // For now only write anything out when debug is set
log.error(line, cameraName);
} else if (debug) {
log.debug(line, cameraName, true);
Expand All @@ -70,13 +74,17 @@ export class FfmpegProcess {
delegate.stopStream(sessionId);
});
this.process.on('exit', (code: number, signal: NodeJS.Signals) => {
if (this.killTimeout) {
clearTimeout(this.killTimeout);
}

const message = 'FFmpeg exited with code: ' + code + ' and signal: ' + signal;

if (code == 0) {
log.debug(message + ' (Graceful)', cameraName, debug);
if (this.killTimeout && code === 0) {
log.debug(message + ' (Expected)', cameraName, debug);
} else if (code == null || code === 255) {
if (this.process.killed) {
log.debug(message + ' (Expected)', cameraName, debug);
log.debug(message + ' (Forced)', cameraName, debug);
} else {
log.error(message + ' (Unexpected)', cameraName);
}
Expand Down Expand Up @@ -125,10 +133,9 @@ export class FfmpegProcess {
}

public stop(): void {
this.process.stdin.write("q\r\n");
}

public getStdin(): Writable {
return this.process.stdin;
this.process.stdin.write('q' + os.EOL);
this.killTimeout = setTimeout(() => {
this.process.kill('SIGKILL');
}, 2 * 1000);
}
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ class FfmpegPlatform implements DynamicPlatformPlugin {
if (motionTrigger) {
motionTrigger.updateCharacteristic(hap.Characteristic.On, true);
}
if (config?.motionDoorbell) {
if (!timeout && config?.motionDoorbell) {
this.doorbellHandler(accessory, true);
}
let timeoutConfig = config?.motionTimeout ?? 1;
Expand Down
31 changes: 31 additions & 0 deletions src/pick-port.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
declare module 'pick-port' {
namespace pickPort {
type pickPortOptions = {
type?: ('udp' | 'tcp');
ip?: string;
minPort?: number;
maxPort?: number;
reserveTimeout?: number;
};

type reserveOptions = {
type: ('udp' | 'tcp');
ip: string;
port: number;
reserveTimeout: number;
};

type isReservedOptions = {
type: ('udp' | 'tcp');
ip: string;
port: number;
};

function reserve(options: reserveOptions): void;
function isReserved(options: isReservedOptions): boolean;
}

function pickPort(options: pickPort.pickPortOptions): Promise<number>;

export = pickPort;
}
19 changes: 12 additions & 7 deletions src/streamingDelegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
import { spawn } from 'child_process';
import { createSocket, Socket } from 'dgram';
import ffmpegPath from 'ffmpeg-for-homebridge';
import getPort from 'get-port';
import pickPort, { pickPortOptions } from 'pick-port';
import { CameraConfig, VideoConfig } from './configTypes';
import { FfmpegProcess } from './ffmpeg';
import { Logger } from './logger';
Expand Down Expand Up @@ -189,7 +189,7 @@ export class StreamingDelegate implements CameraStreamingDelegate {
});
ffmpeg.stderr.on('data', (data) => {
data.toString().split('\n').forEach((line: string) => {
if (line.length > 0) {
if (this.videoConfig.debug && line.length > 0) { // For now only write anything out when debug is set
this.log.error(line, this.cameraName + '] [Snapshot');
}
});
Expand Down Expand Up @@ -273,13 +273,18 @@ export class StreamingDelegate implements CameraStreamingDelegate {
}

async prepareStream(request: PrepareStreamRequest, callback: PrepareStreamCallback): Promise<void> {
const videoReturnPort = await getPort();
const ipv6 = request.addressVersion === 'ipv6';

const options: pickPortOptions = {
type: 'udp',
ip: ipv6 ? '::' : '0.0.0.0',
reserveTimeout: 15
};
const videoReturnPort = await pickPort(options);
const videoSSRC = this.hap.CameraController.generateSynchronisationSource();
const audioReturnPort = await getPort();
const audioReturnPort = await pickPort(options);
const audioSSRC = this.hap.CameraController.generateSynchronisationSource();

const ipv6 = request.addressVersion === 'ipv6';

const sessionInfo: SessionInfo = {
address: request.targetAddress,
ipv6: ipv6,
Expand Down Expand Up @@ -455,7 +460,7 @@ export class StreamingDelegate implements CameraStreamingDelegate {
'a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:' + sessionInfo.audioSRTP.toString('base64') + '\r\n';
activeSession.returnProcess = new FfmpegProcess(this.cameraName + '] [Two-way', request.sessionID,
this.videoProcessor, ffmpegReturnArgs, this.log, this.videoConfig.debugReturn, this);
activeSession.returnProcess.getStdin().end(sdpReturnAudio);
activeSession.returnProcess.stdin.end(sdpReturnAudio);
}

this.ongoingSessions.set(request.sessionID, activeSession);
Expand Down

0 comments on commit 6fa51ce

Please sign in to comment.