Skip to content

Commit

Permalink
v2.1.0 (#44)
Browse files Browse the repository at this point in the history
* Update README.md

* Camera Streaming (#43)

* Adapt homebridge-camera-ffmpeg source

* Remove standalone service function

* Add some debugging calls

* Use self for callback

* Add some debugging for ffmpeg

* Add required spawn module

* Use name instead of ID for device name

* Add api check

* Split ArloCameraSource into separate file to handle license better

* Manage options at constructor rather than at stream configuration

* Allow skipping of video transcoding if requested video size matches source

* Add ip and debug modules to package

* Add documentation to README about streaming

* Switch to libopus for audio transcoding due to substantially better performance, also enable comfort noise on stream

* Allow for additional audio commands if user desires

* Some additional documentation and note that libopus is now default audio codec

* Update `ffmpeg` (#41)

* Update `ffmpeg`

Add `ffmpeg-for-homebridge` - static `ffmpeg` binaries for Homebridge with support for audio (`libfdk-aac`) and hardware decoding (`h264_omx`).

* Create config.schema.json

Co-authored-by: Donavan Becker <[email protected]>

* Added option to disable registering cameras (#33)

Arlo now released HomeKit support for Arlo Pro and Pro 2. But the support only provides access to the cameras and motion detection, but not the HomeKit Security System feature. This HomeBridge plugin is still very useful but users might not want to include the cameras, as they would be available twice then.

To support this scenario this commit adds a new optional configuration parameter ‘include_cameras” (default is set to true) which will exclude any camera from being registered via HomeBridge. Another option would have been to add a whitelist or blacklist feature for users, but I didn’t want this to be too complex without knowing the need for this.

Co-authored-by: Charles Powell <[email protected]>
Co-authored-by: David Parry <[email protected]>
Co-authored-by: Kristian Matthews <[email protected]>
Co-authored-by: Andreas Linde <[email protected]>

* update package-lock.json

* Higher Firmware Version (#46)

* Update package.json

* Update package-lock.json

* Update README.md

* Update config.schema.json

* Update config.schema.json

* Update config.schema.json

* add node-arlo v1.1.0

* Update package.json

* Create LICENSE.md

* Update ArloCameraSource.js

* update node-arlo to v1.2.0 (#55)

* Update package.json

* update package-lock.json

* Update package.json

* Update package-lock.json

* Update CHANGELOG.md

Co-authored-by: Charles Powell <[email protected]>
Co-authored-by: David Parry <[email protected]>
Co-authored-by: Kristian Matthews <[email protected]>
Co-authored-by: Andreas Linde <[email protected]>
  • Loading branch information
5 people authored Jun 16, 2020
1 parent b98195c commit 1bb1f0d
Show file tree
Hide file tree
Showing 7 changed files with 587 additions and 116 deletions.
308 changes: 308 additions & 0 deletions ArloCameraSource.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
const EventEmitter = require('events').EventEmitter;
const debug = require('debug')('Homebridge-Arlo:CameraSource');
const debugFFmpeg = require('debug')('ffmpeg');
const crypto = require('crypto');
const ip = require('ip');
const spawn = require('child_process').spawn;

let StreamController, UUIDGen;

class ArloCameraSource extends EventEmitter {
constructor(log, accessory, device, hap, config) {
super();

StreamController = hap.StreamController;
UUIDGen = hap.uuid;

this.log = log;
this.accessory = accessory;
this.device = device;
this.services = [];
this.pendingSessions = {};
this.ongoingSessions = {};
this.streamControllers = [];
this.lastSnapshot = null;

this.videoProcessor = config.videoProcessor || 'ffmpeg';
this.videoDecoder = config.videoDecoder || '';
this.videoEncoder = config.videoEncoder || 'libx264';
this.audioCodec = config.audioEncoder || 'libopus';
this.packetsize = config.packetsize || 1316; //188, 376, 1316
this.fps = 24;
this.maxBitrate = config.maxBitrate || 300;
this.additionalVideoCommands = (config.additionalVideoCommands ? (' ' + config.additionalVideoCommands) : '');
this.additionalAudioCommands = (config.additionalAudioCommands ? (' ' + config.additionalAudioCommands) : '');

let numberOfStreams = config.maxStreams || 2;

let options = {
proxy: false, // Requires RTP/RTCP MUX Proxy
srtp: true, // Supports SRTP AES_CM_128_HMAC_SHA1_80 encryption
video: {
resolutions: [
[1280, 720, 24],
[1280, 720, 15],
[640, 360, 24],
[640, 360, 15],
[320, 240, 24],
[320, 240, 15]
],
codec: {
profiles: [0, 1, 2], //[StreamController.VideoCodecParamProfileIDTypes.MAIN],
levels: [0, 1, 2] //[StreamController.VideoCodecParamLevelTypes.TYPE4_0]
}
},
audio: {
codecs: [
{
type: 'OPUS',
samplerate: 24,
comfort_noise: true
}
]
}
}

this._createStreamControllers(numberOfStreams, options);
debug('Generated Camera Controller');
}

handleCloseConnection(connectionID) {
this.streamControllers.forEach(function(controller) {
controller.handleCloseConnection(connectionID);
});
}

handleSnapshotRequest(request, callback) {
debug('Snapshot requested');

this.log("Snapshot request: Camera %s [%s]", this.accessory.displayName, this.device.id);

this.device.downloadSnapshot(this.device.device.presignedLastImageUrl, function (data) {
this.log("Snapshot downloaded: Camera %s [%s]", this.accessory.displayName, this.device.id);
callback(undefined, data);
}.bind(this));
}

prepareStream(request, callback) {
debug('Prepare stream request');

var self = this;

this.device.getStream(function (streamURL) {
debug('Preparing stream for URL: %s',streamURL);
debug('Prepare Stream request: %O', request);

var sessionInfo = {};
let sessionID = request["sessionID"];
let targetAddress = request["targetAddress"];

sessionInfo["streamURL"] = streamURL;
sessionInfo["address"] = targetAddress;

var response = {};

let videoInfo = request["video"];
if (videoInfo) {
let targetPort = videoInfo["port"];
let srtp_key = videoInfo["srtp_key"];
let srtp_salt = videoInfo["srtp_salt"];

// SSRC is a 32 bit integer that is unique per stream
let ssrcSource = crypto.randomBytes(4);
ssrcSource[0] = 0;
let ssrc = ssrcSource.readInt32BE(0, true);

let videoResponse = {
port: targetPort,
ssrc: ssrc,
srtp_key: srtp_key,
srtp_salt: srtp_salt
};

response["video"] = videoResponse;
sessionInfo["video_port"] = targetPort;
sessionInfo["video_srtp"] = Buffer.concat([srtp_key, srtp_salt]);
sessionInfo["video_ssrc"] = ssrc;
}

let audioInfo = request["audio"];
if (audioInfo) {
let targetPort = audioInfo["port"];
let srtp_key = audioInfo["srtp_key"];
let srtp_salt = audioInfo["srtp_salt"];

// SSRC is a 32 bit integer that is unique per stream
let ssrcSource = crypto.randomBytes(4);
ssrcSource[0] = 0;
let ssrc = ssrcSource.readInt32BE(0, true);

let audioResp = {
port: targetPort,
ssrc: ssrc,
srtp_key: srtp_key,
srtp_salt: srtp_salt
};

response["audio"] = audioResp;

sessionInfo["audio_port"] = targetPort;
sessionInfo["audio_srtp"] = Buffer.concat([srtp_key, srtp_salt]);
sessionInfo["audio_ssrc"] = ssrc;
}

let currentAddress = ip.address();
var addressResp = {
address: currentAddress
};

if (ip.isV4Format(currentAddress)) {
addressResp["type"] = "v4";
} else {
addressResp["type"] = "v6";
}

response["address"] = addressResp;

self.pendingSessions[UUIDGen.unparse(sessionID)] = sessionInfo;

callback(response);
});
}

handleStreamRequest(request) {
debug('Handle Stream request: %O', request);

var sessionID = request["sessionID"];
var requestType = request["type"];
if (sessionID) {
let sessionIdentifier = UUIDGen.unparse(sessionID);

// Start streaming
if (requestType == "start") {
var sessionInfo = this.pendingSessions[sessionIdentifier];
if (sessionInfo) {
var width = 1280;
var height = 720;
var fps = this.fps;
var vbitrate = 1500;
var packetsize = this.packetsize;
var additionalVideoCommands = this.additionalVideoCommands;

var vDecoder, vEncoder, scaleCommand;

let videoInfo = request["video"];
if (videoInfo) {
var width = videoInfo["width"];
var height = videoInfo["height"];

if (width == 1280 && height == 720) {
// No video transcoding required, use copy codec
vDecoder = '';
vEncoder = 'copy';
scaleCommand = '';
debug('No change to video stream size required');
} else {
// Scale video requested, requiring video transcoding
vDecoder = this.videoDecoder ? (' -c:v ' + this.videoDecoder) : '';
vEncoder = this.videoEncoder;
scaleCommand = ' -vf scale=' + width + ':' + height;
}

let expectedFPS = videoInfo["fps"];
if (expectedFPS < fps) {
fps = expectedFPS;
}

if(videoInfo["max_bit_rate"] < vbitrate) {
vbitrate = videoInfo["max_bit_rate"];
}
}

let streamURL = sessionInfo["streamURL"];

let targetAddress = sessionInfo["address"];
let targetVideoPort = sessionInfo["video_port"];
let videoKey = sessionInfo["video_srtp"];
let videoSsrc = sessionInfo["video_ssrc"];

// Video
let ffmpegCommand = '-rtsp_transport tcp' +
vDecoder +
' -re -i ' + streamURL + ' -map 0:0' +
' -c:v ' + vEncoder +
' -pix_fmt yuv420p' +
' -r ' + fps +
' -f rawvideo' +
scaleCommand +
additionalVideoCommands +
' -b:v ' + vbitrate + 'k' +
' -bufsize ' + vbitrate+ 'k' +
' -maxrate '+ vbitrate + 'k' +
' -payload_type 99' +
' -ssrc ' + videoSsrc +
' -f rtp' +
' -srtp_out_suite AES_CM_128_HMAC_SHA1_80' +
' -srtp_out_params ' + videoKey.toString('base64') +
' srtp://' + targetAddress + ':' + targetVideoPort +
'?rtcpport=' + targetVideoPort +
'&localrtcpport=' + targetVideoPort +
'&pkt_size=' + packetsize;

let ffmpeg = spawn(this.videoProcessor, ffmpegCommand.split(' '), {env: process.env});
debugFFmpeg("Start streaming video with " + width + "x" + height + "@" + vbitrate + "kBit");
debugFFmpeg("ffmpeg " + ffmpegCommand);

// Always setup hook on stderr.
// Without this streaming stops within one to two minutes.
ffmpeg.stderr.on('data', function(data) {
// Do not log to the console if debugging is turned off
debugFFmpeg(data.toString());
});

let self = this;
ffmpeg.on('error', function(error){
debugFFmpeg("An error occurs while making stream request");
debugFFmpeg(error);
});

ffmpeg.on('close', (code) => {
if(code == null || code == 0 || code == 255){
debugFFmpeg("Stopped streaming with code %i",code);
} else {
debugFFmpeg("ERROR: FFmpeg exited with code " + code);
for(var i=0; i < self.streamControllers.length; i++){
var controller = self.streamControllers[i];
if(controller.sessionIdentifier === sessionID){
controller.forceStop();
}
}
}
});

// Add to ongoing sessions now that it's been started
this.ongoingSessions[sessionIdentifier] = ffmpeg;
}
// Remove from pending sessions
delete this.pendingSessions[sessionIdentifier];
} else if (requestType == "stop") {
var ffmpegProcess = this.ongoingSessions[sessionIdentifier];
if (ffmpegProcess) {
ffmpegProcess.kill('SIGTERM');
}
}
}
}

_createStreamControllers(numberOfStreams, options) {
let self = this;
for (var i = 0; i < numberOfStreams; i++) {
var streamController = new StreamController(i, options, self);

self.services.push(streamController.service);
self.streamControllers.push(streamController);
}
}
}

module.exports = ArloCameraSource;
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to this project will be documented in this file. This project uses [Semantic Versioning](https://semver.org/).

## v2.1.0 (2020-06-16)

### Changes
* Add Streaming Options to `config.schema.json`.
* Enhanced Camera Streaming. Camera Streaming still `not working` as expected though.

## v2.0.1 (2020-06-12)

### Changes
Expand Down
Loading

0 comments on commit 1bb1f0d

Please sign in to comment.