should_use_redux!

This commit is contained in:
Hiro Protagonist 2017-04-18 17:23:59 +12:00
parent e57624454e
commit 73bbc1db92
18 changed files with 2040 additions and 55 deletions

494
#main.js# Executable file
View file

@ -0,0 +1,494 @@
///////////////////////////////////////////////////////////////////////////////////
// Main Module - Communicates with the Server and Orchestrates the other Moules. //
///////////////////////////////////////////////////////////////////////////////////
const sock = require('socket.io-client');
const ffmpeg = require('fluent-ffmpeg');
const http = require('http');
const path = require('path');
const fs = require('fs');
const WMStrm = require(__dirname + '/lib/memWrite.js');
const exec = require('child_process').exec;
const execSync = require('child_process').execSync;
const spawnP = require('child_process').spawn;
/**
* Custom Modules
* Yet to be initialized.
*/
const logger = require('src/logger.js');
///////////////////////////////////////////////////////////////////////////////
// Declarations //
///////////////////////////////////////////////////////////////////////////////
// Force a Stop.
let mustBe = false;
// Restart the stream after it stopped.
let restart = false;
// Central State Variable
// NOTE: Could be done with redux!
let status = {
status: 0,
error: -1
}
// Minor declarations.
let config, source, snapSource, stopTimeout;
let spawn = function() {
source = 'rtsp://' + config.camIP + ':' + config.camPort + '/' + config.camProfile;
ffmpeg.setFfmpegPath(config.ffmpegPath);
delete cmd;
cmd = ffmpeg({
source: source,
stdoutLines: 20
});
if (config.customOutputOptions !== "")
cmd.outputOptions(config.customOutputOptions.replace(/\s+\,\s+/g, ',').replace(/\s+\-/g, ',-').split(','));
if (config.customAudioOptions !== "")
cmd.outputOptions(config.customAudioOptions.replace(/\s+\,\s+/g, ',').replace(/\s+\-/g, ',-').split(','));
else
cmd.AudioCodec('copy');
if (config.customVideoOptions !== "")
cmd.outputOptions(config.customVideoOptions.replace(/\s+\,\s+/g, ',').replace(/\s+\-/g, ',-').split(','));
else
cmd.videoCodec('copy');
cmd.on('start', function(commandLine) {
status.running = 0;
logger.log(logger.importance[4], 'Spawned Ffmpeg with command: ' + commandLine);
})
.on('end', function(o, e) {
imDead('Normal Stop.', e);
})
.on('error', function(err, o, e) {
if (err.message.indexOf(source) > -1)
criticalProblem(0, e, handleDisc, config.camIP, config.camPort);
else if (err.message.indexOf(source + 'Input/output error') > -1 || err.message.indexOf('rtmp://a.rtmp.youtube.com/live2/' + config.key) > -1)
criticalProblem(1, e, handleDisc, 'a.rtmp.youtube.com/live2/', 1935);
else if (err.message.indexOf('spawn') > -1 || err.message.indexOf('niceness') > -1)
criticalProblem(2, e, function() {});
else if (err.message.indexOf('SIGINT') > -1 || err.message.indexOf('SIGKILL') > -1)
imDead('Normal Stop.', e);
else
imDead(err.message, e);
})
.outputFormat('flv')
.outputOptions(['-bufsize 50000k', '-tune film'])
.output('rtmp://a.rtmp.youtube.com/live2/' + config.key);
status.error = -1;
socket.emit('change', {
type: 'startStop',
change: {
running: 0,
error: -1
}
});
cmd.run();
};
let getSnap = function(cb) {
snapSource = 'rtsp://' + config.camIP + ':' + config.camPort + '/' + config.snapProfile;
let picBuff = new WMStrm();
recCmd = ffmpeg(snapSource)
.on('start', function(commandLine) {
logger.log(logger.importance[4], 'Snapshot ' + commandLine);
})
.on('error', function(err, o, e) {})
.outputFormat('mjpeg')
.frames(1)
.stream(picBuff, {
end: true
});
picBuff.on('finish', function() {
try {
cb(picBuff.memStore.toString('base64'));
delete pickBuff;
} catch (e) {
cb(false);
}
});
}
function imDead(why, e = '') {
if(stopTimeout) {
clearTimeout(stopTimeout);
stopTimeout = false;
}
status.running = 1;
socket.emit('change', {
type: 'startStop',
change: {
running: 1
}
});
if (restart) {
spawn();
restart = false;
}
if (!mustBe) {
logger.log(logger.importance[2], 'Crash! ' + e);
setTimeout(function() {
spawn();
}, 1000);
}
mustBe = false;
}
function criticalProblem(err, e, handler, ...args) {
if (!mustBe) {
setTimeout(function() {
status.running = 2;
status.error = err;
logger.log(logger.importance[3], 'Critical Problem: ' + errors[err] + '\n' + e);
socket.emit('change', {
type: 'error',
change: {
running: 2,
error: err
}
});
handler(args)
}, 1000);
}
mustBe = false;
}
function handleDisc(info) {
let [host, port] = info;
isReachable(host, port, is => {
if (is) {
spawn();
} else {
setTimeout(function() {
handleDisc(info);
}, 10000);
}
});
}
function isReachable(host, port, callback) {
http.get({
host: host.split('/')[0],
port: port
}, function(res) {
callback(true);
}).on("error", function(e) {
if (e.message == "socket hang up") {
setTimeout(function() {
callback(true);
}, 1000);
} else
callback(false);
});
}
function stopFFMPEG() {
cmd.kill('SIGINT');
stopTimeout = setTimeout(() => {
logger.log(logger.importance[3], "Force Stop!");
cmd.kill();
}, 3000);
}
var commandHandlers = function commandHandlers(command, cb) {
var handlers = {
startStop: function() {
if(restart)
return;
if (status.running !== 2)
if (status.running === 0) {
logger.log(logger.importance[1], "Stop Command!");
mustBe = true;
stopFFMPEG();
socket.emit('data', {
type: 'message',
data: {
title: 'Success',
type: 'success',
text: 'Stopped!'
}
}, command.sender);
} else {
logger.log(logger.importance[1], "Start Command!");
spawn();
socket.emit('data', {
type: 'message',
data: {
title: 'Success',
type: 'success',
text: 'Started!'
}
}, command.sender);
}
},
snap: function() {
getSnap(snap => {
socket.emit('data', {
type: 'snap',
data: snap,
}, command.sender);
});
},
config: function() {
socket.emit('data', {
type: 'config',
data: config,
}, command.sender);
},
changeSettings: function() {
for (let set in command.data) {
if (typeof config[set] !== 'undefined')
config[set] = command.data[set];
}
let oldConfigured;
if (config.configured === true)
oldConfigured = true;
config.configured = true;
fs.writeFile(__dirname + '/config.js',
JSON.stringify(config,
undefined, 2),
function(err) {
if (err) {
socket.emit('data', {
type: 'message',
data: {
title: 'Error',
type: 'error',
text: 'Can\'t save the Settings!\n' + err.message
}
}, command.sender);
} else {
restartSSH(() => {
socket.emit('data', {
type: 'message',
data: {
title: 'Success',
type: 'success',
text: 'Settings Saved!'
}
}, command.sender);
if (oldConfigured) {
socket.emit('change', {
type: 'settings',
change: {
config: command.data
}
});
stopFFMPEG();
spawn();
} else {
socket.disconnect();
init();
}
});
}
});
},
restart: function() {
if (status.running === 0) {
logger.log(logger.importance[1], "Restart Command!");
mustBe = true;
restart = true;
stopFFMPEG();
} else {
logger.log(logger.importance[1], "Start Command!");
spawn();
}
socket.emit('data', {
type: 'message',
data: {
title: 'Success',
type: 'success',
text: 'Restarted!'
}
}, command.sender);
},
restartSSH: function() {
restartSSH(() => {
socket.emit('data', {
type: 'message',
data: {
title: 'Success',
type: 'success',
text: 'Restarted SSH Tunnels!'
}
}, command.sender);
});
},
getLogs: function() {
logger.query({limit: 100}, function (err, data) {
data = data.file;
if (err) {
data = [];
} else
if (data.length === 0)
data = [];
socket.emit('data', {
type: 'logs',
data: data
}, command.sender);
});
}
};
//call the handler
var call = handlers[command.command];
if (call)
call();
}
function restartSSH(cb) {
exec('forever stop SSH-Serv', () => {
connectSSH(() => {
socket.emit('change', {
change: {
ssh: {
port: status.ssh.port,
camForwardPort: status.ssh.camForwardPort
}
}
});
cb();
});
});
}
function handleKill() {
process.stdin.resume();
logger.log(logger.importance[0], "Received Shutdown Command");
mustBe = true;
cmd.kill();
process.exit(0);
}
process.on('SIGTERM', function() {
handleKill();
});
//let's go
function init() {
config = readConfig();
if (config.configured) {
socket = sock(config.master + '/pi');
initSocket();
status.name = config.name;
if (!cmd)
spawn();
} else {
socket = sock(config.master + '/pi', {
query: "unconfigured=true"
});
status.running = 2;
initSocket();
}
}
function initSSH(cb) {
status.ssh = {
// that Could come from the Master server!
user: config['ssh-user'],
localUser: config['ssh-local-user'],
masterPort: config['ssh-port']
};
checkSSH((alive) => {
if (alive)
cb();
else
connectSSH(cb);
});
}
function connectSSH(cb = function() {
socket.emit('change', {
change: {
ssh: {
port: status.ssh.port,
camForwardPort: status.ssh.camForwardPort
}
}
});
}) {
socket.emit('getSSHPort', ports => {
[status.ssh.port, status.ssh.camForwardPort] = ports;
let ssh = exec(`forever start -a --killSignal=SIGINT --uid SSH-Serv sshManager.js ${status.ssh.port} ${status.ssh.camForwardPort} ${config.camPanelPort}`, {
detached: true,
shell: true,
cwd: __dirname
});
ssh.on('error', (err) => {
socket.emit('data', {
type: 'message',
data: {
title: 'Error',
type: 'error',
text: 'Could not start SSH tunnels!'
}
}, command.sender);
});
cb();
});
}
function checkSSH(cb) {
let m, pid, alive;
let re = /SSH-Serv.*?sshManager\.js\s([0-9]+)\s([0-9]+).*log.*[^STOPPED]+/g;
exec('forever list', (error, stdout, stderr) => {
if (error)
throw error;
let alive = false;
while ((m = re.exec(stdout)) !== null) {
if (m.index === re.lastIndex) {
re.lastIndex++;
}
if (alive) {
exec('forever stop SSH-Serv');
cb(false)
return;
} else {
[, status.ssh.port, status.ssh.camForwardPort] = m;
alive = true;
}
}
cb(alive);
});
}
function initSocket() {
socket.on('connect', function() {
logger.log(logger.importance[0], 'Connected to Master: ' + config.master + '.');
if (config['ssh-user'])
initSSH(err => {
if (err)
throw err;
socket.emit('meta', status);
});
else {
socket.emit('meta', status);
}
});
socket.on('disconnect', function() {
socket.disconnect();
init();
});
socket.on('command', (command, cb) => {
commandHandlers(command, cb);
});
}
function readConfig() {
return JSON.parse(fs.readFileSync(__dirname + '/config.js'));
}
init();

1
.#main.js Symbolic link
View file

@ -0,0 +1 @@
hiro@ArLeenUX.4902:1492242088

9
.idea/codeStyleSettings.xml generated Normal file
View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectCodeStyleSettingsManager">
<option name="PER_PROJECT_SETTINGS">
<value />
</option>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</component>
</project>

12
.idea/doccam-pi.iml generated Normal file
View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/doccam-pi.iml" filepath="$PROJECT_DIR$/.idea/doccam-pi.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

295
.idea/workspace.xml generated Normal file
View file

@ -0,0 +1,295 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="BookmarkManager">
<bookmark url="file://$PROJECT_DIR$" />
</component>
<component name="ChangeListManager">
<list default="true" id="969ea136-ca9e-4b96-a835-7f68ccc264a6" name="Default" comment="">
<change type="MODIFICATION" beforePath="$PROJECT_DIR$/main.js" afterPath="$PROJECT_DIR$/main.js" />
</list>
<ignored path="$PROJECT_DIR$/.tmp/" />
<ignored path="$PROJECT_DIR$/temp/" />
<ignored path="$PROJECT_DIR$/tmp/" />
<option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" />
<option name="TRACKING_ENABLED" value="true" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="ExecutionTargetManager" SELECTED_TARGET="default_target" />
<component name="FavoritesManager">
<favorites_list name="doccam-pi" />
</component>
<component name="FileEditorManager">
<leaf SIDE_TABS_SIZE_LIMIT_KEY="300">
<file leaf-file-name="main.js" pinned="false" current-in-tab="true">
<entry file="file://$PROJECT_DIR$/main.js">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="36">
<caret line="5" column="21" lean-forward="true" selection-start-line="5" selection-start-column="21" selection-end-line="5" selection-end-column="21" />
<folding />
</state>
</provider>
</entry>
</file>
</leaf>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="JsBuildToolGruntFileManager" detection-done="true" sorting="DEFINITION_ORDER" />
<component name="JsBuildToolPackageJson" detection-done="true" sorting="DEFINITION_ORDER">
<package-json value="$PROJECT_DIR$/package.json" />
</component>
<component name="JsGulpfileManager">
<detection-done>true</detection-done>
<sorting>DEFINITION_ORDER</sorting>
</component>
<component name="NodeModulesDirectoryManager">
<handled-path value="$PROJECT_DIR$/node_modules" />
</component>
<component name="ProjectFrameBounds">
<option name="width" value="1920" />
<option name="height" value="1044" />
</component>
<component name="ProjectView">
<navigator currentView="ProjectPane" proportions="" version="1">
<flattenPackages />
<showMembers />
<showModules />
<showLibraryContents />
<hideEmptyPackages />
<abbreviatePackageNames />
<autoscrollToSource />
<autoscrollFromSource />
<sortByType />
<manualOrder />
<foldersAlwaysOnTop value="true" />
</navigator>
<panes>
<pane id="ProjectPane">
<subPane>
<PATH>
<PATH_ELEMENT>
<option name="myItemId" value="doccam-pi" />
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.ProjectViewProjectNode" />
</PATH_ELEMENT>
<PATH_ELEMENT>
<option name="myItemId" value="doccam-pi" />
<option name="myItemType" value="com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode" />
</PATH_ELEMENT>
</PATH>
</subPane>
</pane>
<pane id="Scope" />
<pane id="Scratches" />
</panes>
</component>
<component name="PropertiesComponent">
<property name="WebServerToolWindowFactoryState" value="false" />
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
<property name="HbShouldOpenHtmlAsHb" value="" />
<property name="nodejs_interpreter_path" value="/usr/bin/node" />
<property name="settings.editor.selected.configurable" value="preferences.pluginManager" />
</component>
<component name="RunDashboard">
<option name="ruleStates">
<list>
<RuleState>
<option name="name" value="ConfigurationTypeDashboardGroupingRule" />
</RuleState>
<RuleState>
<option name="name" value="StatusDashboardGroupingRule" />
</RuleState>
</list>
</option>
</component>
<component name="RunManager" selected="Node.js.main.js">
<configuration default="true" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application">
<method />
</configuration>
<configuration default="true" type="DartTestRunConfigurationType" factoryName="Dart Test">
<method />
</configuration>
<configuration default="true" type="JavaScriptTestRunnerJest" factoryName="Jest">
<node-interpreter value="project" />
<working-dir value="" />
<envs />
<scope-kind value="ALL" />
<method />
</configuration>
<configuration default="true" type="JavaScriptTestRunnerKarma" factoryName="Karma">
<config-file value="" />
<node-interpreter value="project" />
<envs />
<method />
</configuration>
<configuration default="true" type="JavaScriptTestRunnerProtractor" factoryName="Protractor">
<config-file value="" />
<node-interpreter value="project" />
<envs />
<method />
</configuration>
<configuration default="true" type="JavascriptDebugType" factoryName="JavaScript Debug">
<method />
</configuration>
<configuration default="true" type="NodeJSConfigurationType" factoryName="Node.js" path-to-node="project" working-dir="">
<method />
</configuration>
<configuration default="true" type="cucumber.js" factoryName="Cucumber.js">
<option name="cucumberJsArguments" value="" />
<option name="executablePath" />
<option name="filePath" />
<method />
</configuration>
<configuration default="true" type="js.build_tools.gulp" factoryName="Gulp.js">
<node-interpreter>project</node-interpreter>
<node-options />
<gulpfile />
<tasks />
<arguments />
<envs />
<method />
</configuration>
<configuration default="true" type="js.build_tools.npm" factoryName="npm">
<command value="run" />
<scripts />
<node-interpreter value="project" />
<envs />
<method />
</configuration>
<configuration default="true" type="mocha-javascript-test-runner" factoryName="Mocha">
<node-interpreter>project</node-interpreter>
<node-options />
<working-directory />
<pass-parent-env>true</pass-parent-env>
<envs />
<ui />
<extra-mocha-options />
<test-kind>DIRECTORY</test-kind>
<test-directory />
<recursive>false</recursive>
<method />
</configuration>
<configuration default="false" name="main.js" type="NodeJSConfigurationType" factoryName="Node.js" path-to-node="project" path-to-js-file="main.js" working-dir="$PROJECT_DIR$">
<EXTENSION ID="com.jetbrains.nodejs.remote.docker.NodeJSDockerRunConfigurationExtension">
<option name="envVars">
<list />
</option>
<option name="extraHosts">
<list />
</option>
<option name="links">
<list />
</option>
<option name="networkDisabled" value="false" />
<option name="networkMode" value="bridge" />
<option name="portBindings">
<list />
</option>
<option name="publishAllPorts" value="false" />
<option name="version" value="1" />
<option name="volumeBindings">
<list />
</option>
</EXTENSION>
<method />
</configuration>
<list size="1">
<item index="0" class="java.lang.String" itemvalue="Node.js.main.js" />
</list>
</component>
<component name="ShelveChangesManager" show_recycled="false">
<option name="remove_strategy" value="false" />
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="969ea136-ca9e-4b96-a835-7f68ccc264a6" name="Default" comment="" />
<created>1492326299503</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1492326299503</updated>
<workItem from="1492326301278" duration="521000" />
<workItem from="1492326839089" duration="529000" />
</task>
<servers />
</component>
<component name="TimeTrackingManager">
<option name="totallyTimeSpent" value="1050000" />
</component>
<component name="TodoView">
<todo-panel id="selected-file">
<is-autoscroll-to-source value="true" />
</todo-panel>
<todo-panel id="all">
<are-packages-shown value="true" />
<is-autoscroll-to-source value="true" />
</todo-panel>
</component>
<component name="ToolWindowManager">
<frame x="0" y="0" width="1920" height="1044" extended-state="6" />
<layout>
<window_info id="Project" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" show_stripe_button="true" weight="0.18125" sideWeight="0.5" order="0" side_tool="false" content_ui="combo" />
<window_info id="TODO" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="6" side_tool="false" content_ui="tabs" />
<window_info id="Event Log" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="7" side_tool="true" content_ui="tabs" />
<window_info id="Find" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
<window_info id="Version Control" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="7" side_tool="false" content_ui="tabs" />
<window_info id="Run" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="2" side_tool="false" content_ui="tabs" />
<window_info id="npm" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="2" side_tool="true" content_ui="tabs" />
<window_info id="Structure" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
<window_info id="Terminal" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="7" side_tool="false" content_ui="tabs" />
<window_info id="Favorites" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="2" side_tool="true" content_ui="tabs" />
<window_info id="Debug" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.4" sideWeight="0.5" order="3" side_tool="false" content_ui="tabs" />
<window_info id="Cvs" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="4" side_tool="false" content_ui="tabs" />
<window_info id="Message" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="0" side_tool="false" content_ui="tabs" />
<window_info id="Commander" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.4" sideWeight="0.5" order="0" side_tool="false" content_ui="tabs" />
<window_info id="Inspection" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.4" sideWeight="0.5" order="5" side_tool="false" content_ui="tabs" />
<window_info id="Hierarchy" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="2" side_tool="false" content_ui="combo" />
<window_info id="Ant Build" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
</layout>
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="processedProjectFiles" value="true" />
</component>
<component name="VcsContentAnnotationSettings">
<option name="myLimit" value="2678400000" />
</component>
<component name="XDebuggerManager">
<breakpoint-manager />
<watches-manager />
</component>
<component name="editorHistoryManager">
<entry file="file://$PROJECT_DIR$/main.js">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="0">
<caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" />
<folding />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/main.js">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="36">
<caret line="5" column="21" lean-forward="true" selection-start-line="5" selection-start-column="21" selection-end-line="5" selection-end-column="21" />
<folding />
</state>
</provider>
</entry>
</component>
<component name="masterDetails">
<states>
<state key="ScopeChooserConfigurable.UI">
<settings>
<splitter-proportions>
<option name="proportions">
<list>
<option value="0.2" />
</list>
</option>
</splitter-proportions>
</settings>
</state>
</states>
</component>
</project>

1
.tern-port Normal file
View file

@ -0,0 +1 @@
36411

7
.tern-project Normal file
View file

@ -0,0 +1,7 @@
{
"plugins": {
"node": {},
"lint": {},
"node-extension": {}
}
}

89
main.js
View file

@ -1,3 +1,7 @@
///////////////////////////////////////////////////////////////////////////////////
// Main Module - Communicates with the Server and Orchestrates the other Moules. //
///////////////////////////////////////////////////////////////////////////////////
const sock = require('socket.io-client'); const sock = require('socket.io-client');
const ffmpeg = require('fluent-ffmpeg'); const ffmpeg = require('fluent-ffmpeg');
const http = require('http'); const http = require('http');
@ -8,56 +12,31 @@ const exec = require('child_process').exec;
const execSync = require('child_process').execSync; const execSync = require('child_process').execSync;
const spawnP = require('child_process').spawn; const spawnP = require('child_process').spawn;
/**
* Custom Modules
* Yet to be initialized.
*/
const logger = require('src/logger.js');
///////////////////////////////////////////////////////////////////////////////
// Declarations //
///////////////////////////////////////////////////////////////////////////////
// Force a Stop.
let mustBe = false; let mustBe = false;
// Restart the stream after it stopped.
let restart = false; let restart = false;
let config, source, snapSource, stopTimeout;
const importance = ['normal', 'info', 'warning', 'danger', 'success']; // Central State Variable
var customLevels = { // NOTE: Could be done with redux!
levels: {
normal: 0,
info: 1,
warning: 2,
danger: 3,
success: 4
},
colors: {
normal: 'white',
info: 'blue',
warning: 'orange',
danger: 'red',
success: 'green'
}
};
let winston = require('winston');
let logger = new(winston.Logger)({
levels: customLevels.levels,
transports: [
new(winston.transports.Console)({
level: 'success'
}),
new(winston.transports.File)({
filename: __dirname + '/process.log',
colorize: true,
timestamp: true,
level: 'success',
json: true,
maxsize: 500000,
maxFiles: 10
})
]
});
winston.addColors(customLevels.colors);
let dir = '/home';
let status = { let status = {
status: 0, status: 0,
error: -1 error: -1
} }
let errors = ['Camera Disconnected', 'YoutTube Disconnected', 'Wrong ffmpeg executable.']; // Minor declarations.
let cmd; let config, source, snapSource, stopTimeout;
let spawn = function() { let spawn = function() {
source = 'rtsp://' + config.camIP + ':' + config.camPort + '/' + config.camProfile; source = 'rtsp://' + config.camIP + ':' + config.camPort + '/' + config.camProfile;
@ -80,7 +59,7 @@ let spawn = function() {
cmd.on('start', function(commandLine) { cmd.on('start', function(commandLine) {
status.running = 0; status.running = 0;
logger.log(importance[4], 'Spawned Ffmpeg with command: ' + commandLine); logger.log(logger.importance[4], 'Spawned Ffmpeg with command: ' + commandLine);
}) })
.on('end', function(o, e) { .on('end', function(o, e) {
imDead('Normal Stop.', e); imDead('Normal Stop.', e);
@ -110,14 +89,14 @@ let spawn = function() {
} }
}); });
cmd.run(); cmd.run();
} };
let getSnap = function(cb) { let getSnap = function(cb) {
snapSource = 'rtsp://' + config.camIP + ':' + config.camPort + '/' + config.snapProfile; snapSource = 'rtsp://' + config.camIP + ':' + config.camPort + '/' + config.snapProfile;
let picBuff = new WMStrm(); let picBuff = new WMStrm();
recCmd = ffmpeg(snapSource) recCmd = ffmpeg(snapSource)
.on('start', function(commandLine) { .on('start', function(commandLine) {
logger.log(importance[4], 'Snapshot ' + commandLine); logger.log(logger.importance[4], 'Snapshot ' + commandLine);
}) })
.on('error', function(err, o, e) {}) .on('error', function(err, o, e) {})
.outputFormat('mjpeg') .outputFormat('mjpeg')
@ -153,7 +132,7 @@ function imDead(why, e = '') {
restart = false; restart = false;
} }
if (!mustBe) { if (!mustBe) {
logger.log(importance[2], 'Crash! ' + e); logger.log(logger.importance[2], 'Crash! ' + e);
setTimeout(function() { setTimeout(function() {
spawn(); spawn();
}, 1000); }, 1000);
@ -166,7 +145,7 @@ function criticalProblem(err, e, handler, ...args) {
setTimeout(function() { setTimeout(function() {
status.running = 2; status.running = 2;
status.error = err; status.error = err;
logger.log(importance[3], 'Critical Problem: ' + errors[err] + '\n' + e); logger.log(logger.importance[3], 'Critical Problem: ' + errors[err] + '\n' + e);
socket.emit('change', { socket.emit('change', {
type: 'error', type: 'error',
change: { change: {
@ -187,7 +166,7 @@ function handleDisc(info) {
spawn(); spawn();
} else { } else {
setTimeout(function() { setTimeout(function() {
handleDisc(info) handleDisc(info);
}, 10000); }, 10000);
} }
}); });
@ -212,7 +191,7 @@ function isReachable(host, port, callback) {
function stopFFMPEG() { function stopFFMPEG() {
cmd.kill('SIGINT'); cmd.kill('SIGINT');
stopTimeout = setTimeout(() => { stopTimeout = setTimeout(() => {
logger.log(importance[3], "Force Stop!"); logger.log(logger.importance[3], "Force Stop!");
cmd.kill(); cmd.kill();
}, 3000); }, 3000);
} }
@ -225,7 +204,7 @@ var commandHandlers = function commandHandlers(command, cb) {
if (status.running !== 2) if (status.running !== 2)
if (status.running === 0) { if (status.running === 0) {
logger.log(importance[1], "Stop Command!"); logger.log(logger.importance[1], "Stop Command!");
mustBe = true; mustBe = true;
stopFFMPEG(); stopFFMPEG();
socket.emit('data', { socket.emit('data', {
@ -237,7 +216,7 @@ var commandHandlers = function commandHandlers(command, cb) {
} }
}, command.sender); }, command.sender);
} else { } else {
logger.log(importance[1], "Start Command!"); logger.log(logger.importance[1], "Start Command!");
spawn(); spawn();
socket.emit('data', { socket.emit('data', {
type: 'message', type: 'message',
@ -314,12 +293,12 @@ var commandHandlers = function commandHandlers(command, cb) {
}, },
restart: function() { restart: function() {
if (status.running === 0) { if (status.running === 0) {
logger.log(importance[1], "Restart Command!"); logger.log(logger.importance[1], "Restart Command!");
mustBe = true; mustBe = true;
restart = true; restart = true;
stopFFMPEG(); stopFFMPEG();
} else { } else {
logger.log(importance[1], "Start Command!"); logger.log(logger.importance[1], "Start Command!");
spawn(); spawn();
} }
socket.emit('data', { socket.emit('data', {
@ -383,7 +362,7 @@ function restartSSH(cb) {
function handleKill() { function handleKill() {
process.stdin.resume(); process.stdin.resume();
logger.log(importance[0], "Received Shutdown Command"); logger.log(logger.importance[0], "Received Shutdown Command");
mustBe = true; mustBe = true;
cmd.kill(); cmd.kill();
process.exit(0); process.exit(0);
@ -486,7 +465,7 @@ function checkSSH(cb) {
function initSocket() { function initSocket() {
socket.on('connect', function() { socket.on('connect', function() {
logger.log(importance[0], 'Connected to Master: ' + config.master + '.'); logger.log(logger.importance[0], 'Connected to Master: ' + config.master + '.');
if (config['ssh-user']) if (config['ssh-user'])
initSSH(err => { initSSH(err => {
if (err) if (err)

488
main.js.orig Executable file
View file

@ -0,0 +1,488 @@
const sock = require('socket.io-client');
const ffmpeg = require('fluent-ffmpeg');
const http = require('http');
const path = require('path');
const fs = require('fs');
const WMStrm = require(__dirname + '/lib/memWrite.js');
const exec = require('child_process').exec;
const execSync = require('child_process').execSync;
const spawnP = require('child_process').spawn;
const logger = require('src/logger.js');
/**
* Global Variables
*/
// Force a Stop.
let mustBe = false;
// Restart the stream after it stopped.
let restart = false;
// Central State Variable
// NOTE: Could be done with redux!
let status = {
status: 0,
error: -1
}
// Minor declarations.
let config, source, snapSource, stopTimeout;
let spawn = function() {
source = 'rtsp://' + config.camIP + ':' + config.camPort + '/' + config.camProfile;
ffmpeg.setFfmpegPath(config.ffmpegPath);
delete cmd;
cmd = ffmpeg({
source: source,
stdoutLines: 20
});
if (config.customOutputOptions !== "")
cmd.outputOptions(config.customOutputOptions.replace(/\s+\,\s+/g, ',').replace(/\s+\-/g, ',-').split(','));
if (config.customAudioOptions !== "")
cmd.outputOptions(config.customAudioOptions.replace(/\s+\,\s+/g, ',').replace(/\s+\-/g, ',-').split(','));
else
cmd.AudioCodec('copy');
if (config.customVideoOptions !== "")
cmd.outputOptions(config.customVideoOptions.replace(/\s+\,\s+/g, ',').replace(/\s+\-/g, ',-').split(','));
else
cmd.videoCodec('copy');
cmd.on('start', function(commandLine) {
status.running = 0;
logger.log(logger.importance[4], 'Spawned Ffmpeg with command: ' + commandLine);
})
.on('end', function(o, e) {
imDead('Normal Stop.', e);
})
.on('error', function(err, o, e) {
if (err.message.indexOf(source) > -1)
criticalProblem(0, e, handleDisc, config.camIP, config.camPort);
else if (err.message.indexOf(source + 'Input/output error') > -1 || err.message.indexOf('rtmp://a.rtmp.youtube.com/live2/' + config.key) > -1)
criticalProblem(1, e, handleDisc, 'a.rtmp.youtube.com/live2/', 1935);
else if (err.message.indexOf('spawn') > -1 || err.message.indexOf('niceness') > -1)
criticalProblem(2, e, function() {});
else if (err.message.indexOf('SIGINT') > -1 || err.message.indexOf('SIGKILL') > -1)
imDead('Normal Stop.', e);
else
imDead(err.message, e);
})
.outputFormat('flv')
.outputOptions(['-bufsize 50000k', '-tune film'])
.output('rtmp://a.rtmp.youtube.com/live2/' + config.key);
status.error = -1;
socket.emit('change', {
type: 'startStop',
change: {
running: 0,
error: -1
}
});
cmd.run();
};
let getSnap = function(cb) {
snapSource = 'rtsp://' + config.camIP + ':' + config.camPort + '/' + config.snapProfile;
let picBuff = new WMStrm();
recCmd = ffmpeg(snapSource)
.on('start', function(commandLine) {
logger.log(logger.importance[4], 'Snapshot ' + commandLine);
})
.on('error', function(err, o, e) {})
.outputFormat('mjpeg')
.frames(1)
.stream(picBuff, {
end: true
});
picBuff.on('finish', function() {
try {
cb(picBuff.memStore.toString('base64'));
delete pickBuff;
} catch (e) {
cb(false);
}
});
}
function imDead(why, e = '') {
if(stopTimeout) {
clearTimeout(stopTimeout);
stopTimeout = false;
}
status.running = 1;
socket.emit('change', {
type: 'startStop',
change: {
running: 1
}
});
if (restart) {
spawn();
restart = false;
}
if (!mustBe) {
logger.log(logger.importance[2], 'Crash! ' + e);
setTimeout(function() {
spawn();
}, 1000);
}
mustBe = false;
}
function criticalProblem(err, e, handler, ...args) {
if (!mustBe) {
setTimeout(function() {
status.running = 2;
status.error = err;
logger.log(logger.importance[3], 'Critical Problem: ' + errors[err] + '\n' + e);
socket.emit('change', {
type: 'error',
change: {
running: 2,
error: err
}
});
handler(args)
}, 1000);
}
mustBe = false;
}
function handleDisc(info) {
let [host, port] = info;
isReachable(host, port, is => {
if (is) {
spawn();
} else {
setTimeout(function() {
handleDisc(info);
}, 10000);
}
});
}
function isReachable(host, port, callback) {
http.get({
host: host.split('/')[0],
port: port
}, function(res) {
callback(true);
}).on("error", function(e) {
if (e.message == "socket hang up") {
setTimeout(function() {
callback(true);
}, 1000);
} else
callback(false);
});
}
function stopFFMPEG() {
cmd.kill('SIGINT');
stopTimeout = setTimeout(() => {
logger.log(logger.importance[3], "Force Stop!");
cmd.kill();
}, 3000);
}
var commandHandlers = function commandHandlers(command, cb) {
var handlers = {
startStop: function() {
if(restart)
return;
if (status.running !== 2)
if (status.running === 0) {
logger.log(logger.importance[1], "Stop Command!");
mustBe = true;
stopFFMPEG();
socket.emit('data', {
type: 'message',
data: {
title: 'Success',
type: 'success',
text: 'Stopped!'
}
}, command.sender);
} else {
logger.log(logger.importance[1], "Start Command!");
spawn();
socket.emit('data', {
type: 'message',
data: {
title: 'Success',
type: 'success',
text: 'Started!'
}
}, command.sender);
}
},
snap: function() {
getSnap(snap => {
socket.emit('data', {
type: 'snap',
data: snap,
}, command.sender);
});
},
config: function() {
socket.emit('data', {
type: 'config',
data: config,
}, command.sender);
},
changeSettings: function() {
for (let set in command.data) {
if (typeof config[set] !== 'undefined')
config[set] = command.data[set];
}
let oldConfigured;
if (config.configured === true)
oldConfigured = true;
config.configured = true;
fs.writeFile(__dirname + '/config.js',
JSON.stringify(config,
undefined, 2),
function(err) {
if (err) {
socket.emit('data', {
type: 'message',
data: {
title: 'Error',
type: 'error',
text: 'Can\'t save the Settings!\n' + err.message
}
}, command.sender);
} else {
restartSSH(() => {
socket.emit('data', {
type: 'message',
data: {
title: 'Success',
type: 'success',
text: 'Settings Saved!'
}
}, command.sender);
if (oldConfigured) {
socket.emit('change', {
type: 'settings',
change: {
config: command.data
}
});
stopFFMPEG();
spawn();
} else {
socket.disconnect();
init();
}
});
}
});
},
restart: function() {
if (status.running === 0) {
logger.log(logger.importance[1], "Restart Command!");
mustBe = true;
restart = true;
stopFFMPEG();
} else {
logger.log(logger.importance[1], "Start Command!");
spawn();
}
socket.emit('data', {
type: 'message',
data: {
title: 'Success',
type: 'success',
text: 'Restarted!'
}
}, command.sender);
},
restartSSH: function() {
restartSSH(() => {
socket.emit('data', {
type: 'message',
data: {
title: 'Success',
type: 'success',
text: 'Restarted SSH Tunnels!'
}
}, command.sender);
});
},
getLogs: function() {
logger.query({limit: 100}, function (err, data) {
data = data.file;
if (err) {
data = [];
} else
if (data.length === 0)
data = [];
socket.emit('data', {
type: 'logs',
data: data
}, command.sender);
});
}
};
//call the handler
var call = handlers[command.command];
if (call)
call();
}
function restartSSH(cb) {
exec('forever stop SSH-Serv', () => {
connectSSH(() => {
socket.emit('change', {
change: {
ssh: {
port: status.ssh.port,
camForwardPort: status.ssh.camForwardPort
}
}
});
cb();
});
});
}
function handleKill() {
process.stdin.resume();
logger.log(logger.importance[0], "Received Shutdown Command");
mustBe = true;
cmd.kill();
process.exit(0);
}
process.on('SIGTERM', function() {
handleKill();
});
//let's go
function init() {
config = readConfig();
if (config.configured) {
socket = sock(config.master + '/pi');
initSocket();
status.name = config.name;
if (!cmd)
spawn();
} else {
socket = sock(config.master + '/pi', {
query: "unconfigured=true"
});
status.running = 2;
initSocket();
}
}
function initSSH(cb) {
status.ssh = {
// that Could come from the Master server!
user: config['ssh-user'],
localUser: config['ssh-local-user'],
masterPort: config['ssh-port']
};
checkSSH((alive) => {
if (alive)
cb();
else
connectSSH(cb);
});
}
function connectSSH(cb = function() {
socket.emit('change', {
change: {
ssh: {
port: status.ssh.port,
camForwardPort: status.ssh.camForwardPort
}
}
});
}) {
socket.emit('getSSHPort', ports => {
[status.ssh.port, status.ssh.camForwardPort] = ports;
let ssh = exec(`forever start -a --killSignal=SIGINT --uid SSH-Serv sshManager.js ${status.ssh.port} ${status.ssh.camForwardPort} ${config.camPanelPort}`, {
detached: true,
shell: true,
cwd: __dirname
});
ssh.on('error', (err) => {
socket.emit('data', {
type: 'message',
data: {
title: 'Error',
type: 'error',
text: 'Could not start SSH tunnels!'
}
}, command.sender);
});
cb();
});
}
function checkSSH(cb) {
let m, pid, alive;
let re = /SSH-Serv.*?sshManager\.js\s([0-9]+)\s([0-9]+).*log.*[^STOPPED]+/g;
exec('forever list', (error, stdout, stderr) => {
if (error)
throw error;
let alive = false;
while ((m = re.exec(stdout)) !== null) {
if (m.index === re.lastIndex) {
re.lastIndex++;
}
if (alive) {
exec('forever stop SSH-Serv');
cb(false)
return;
} else {
[, status.ssh.port, status.ssh.camForwardPort] = m;
alive = true;
}
}
cb(alive);
});
}
function initSocket() {
socket.on('connect', function() {
logger.log(logger.importance[0], 'Connected to Master: ' + config.master + '.');
if (config['ssh-user'])
initSSH(err => {
if (err)
throw err;
socket.emit('meta', status);
});
else {
socket.emit('meta', status);
}
});
socket.on('disconnect', function() {
socket.disconnect();
init();
});
socket.on('command', (command, cb) => {
commandHandlers(command, cb);
});
}
function readConfig() {
return JSON.parse(fs.readFileSync(__dirname + '/config.js'));
}
init();

1
src/.tern-port Normal file
View file

@ -0,0 +1 @@
44205

251
src/ffmpegCommand.js Normal file
View file

@ -0,0 +1,251 @@
///////////////////////////////////////////////////////////////////////////////
// A Wrapper for Fluent-FFMPEG with a custom command. //
///////////////////////////////////////////////////////////////////////////////
const ffmpeg = require('fluent-ffmpeg');
const http = require('http');
///////////////////////////////////////////////////////////////////////////////
// Declarations //
///////////////////////////////////////////////////////////////////////////////
// Reference to itself. (Object oriented this. Only used to call public methods.)
let self = false;
// The Variable for the FFMpeg command.
let cmd = ffmpeg({
stdoutLines: 20
});
// The Config, Logger and a handle for the kill timeout.
let config, logger, stopHandle = false, connectHandle = false;
// True if stream should be restarted.
let restart = false;
// Error Texts
let errorDescriptions = ['Camera Disconnected',
'YoutTube Disconnected',
'Wrong ffmpeg executable.',
'Unknown Error - Restarting'];
// Internal Status
let status = {
streaming: false,
error: false
};
// The stream source url. Yet to be set.
let source = "";
///////////////////////////////////////////////////////////////////////////////
// Code //
///////////////////////////////////////////////////////////////////////////////
/**
* Interface to the ffmpeg process. Uses fluent-ffmpeg.
* @param {Object} _config Configuration for the stream. @see config.js.example
* @param {Object} _logger Logger Instance from main module.
*/
let command = function(_config, _logger){
// singleton
if(self)
return self;
// TODO: Better Error Checking
if(!_config)
throw new Error("Invalid Config");
if(!_logger)
throw new Error("Invalid Logger");
config = _config;
self = this;
// (Re)Create the ffmpeg command and configure it.
let createCommand = function() {
// Clear inputs
cmd._inputs = [];
// TODO: Multi Protocol
source = 'rtsp://' + config.camIP + ':' + config.camPort + '/' + config.camProfile;
cmd.input(source);
// Custom config if any.
if (config.customOutputOptions !== "")
cmd.outputOptions(config.customOutputOptions.replace(/\s+\,\s+/g, ',').replace(/\s+\-/g, ',-').split(','));
if (config.customAudioOptions !== "")
cmd.outputOptions(config.customAudioOptions.replace(/\s+\,\s+/g, ',').replace(/\s+\-/g, ',-').split(','));
else
cmd.AudioCodec('copy');
if (config.customVideoOptions !== "")
cmd.outputOptions(config.customVideoOptions.replace(/\s+\,\s+/g, ',').replace(/\s+\-/g, ',-').split(','));
else
cmd.videoCodec('copy');
// Output Options.
cmd.outputFormat('flv')
.outputOptions(['-bufsize 50000k', '-tune film'])
.output('rtmp://a.rtmp.youtube.com/live2/' + config.key);
// Register events.
cmd.on('start', started);
cmd.on('end', stopped);
cmd.on('error', crashed);
};
let ffmpegCommand = function() {
return cmd;
};
// NOTE: Maybe better error resolving strategy.
// Start streaming.
let start = function() {
// Ignore if we try to reconnect.
if(connectHandle)
return;
cmd.run();
};
// Restart the stream;
let restart = function() {
if(status.streaming) {
restart = true; // NOTE: not very nice
this.stop();
} else
this.start();
};
// Stop streaming.
let stop = function() {
cmd.kill('SIGINT');
stopHandle = setTimeout(() => {
logger.log(logger.importance[3], "Force Stop!");
cmd.kill();
}, 3000);
};
let setConfig = function(_conf) {
config = _conf;
};
};
/**
* Utilities
*/
// Handle Stop and Restart
function stopped() {
status.streaming = false;
// Clear force kill Timeout
if(stopTimeout) {
clearTimeout(stopTimeout);
stopTimeout = false;
}
// Restart the stream;
if (restart) {
self.start();
}
cmd.emit('stopped');
}
// TODO: Restart = false in stopped?
// Hande Stat
function started() {
cmd.emit(restart ? 'restarted' : 'started');
restart = false;
status.error = false;
status.streaming = true;
}
/**
* Error Handling
*/
/**
* Log and handle crashes. Notify the main module.
* @param { Object } error - Error object from the crash.
* @param { String } stdout
* @param { String } stderr
*/
function crashed(error, stdout, stderr){
// Can't connect to the
if (err.message.indexOf(source) > -1)
status.error = 0;
// Can't connect to the Internet / YouTube
else if (err.message.indexOf(source + 'Input/output error') > -1 || err.message.indexOf('rtmp://a.rtmp.youtube.com/live2/' + config.key) > -1)
status.error = 1;
// Wrong FFMPEG Executable
else if (err.message.indexOf('spawn') > -1 || err.message.indexOf('niceness') > -1)
status.error = 2;
// Stopped by us - SIGINT Shouldn't lead to a crash.
else if (err.message.indexOf('SIGINT') > -1 || err.message.indexOf('SIGKILL') > -1){
stopped();
return;
}
// Some unknown Problem, just try to restart.
else {
status.error = 3;
// Just restart in a Second
setTimeout(function(){
self.start();
}, 1000);
}
logger.log(logger.importance[2], `Crashed: ${erro}\nSTDERR: ${stderr}`);
}
/**
* Probe the connection to the host on the port and restart the stream afterwards.
* @param { string } host
* @param { number } port
*/
function tryReconnect( host, port ){
if (!host || !port)
return;
http.get({
host: host.split('/')[0],
port: port
}, function(res) {
// We have a response! Connection works.
setTimeout(function() {
// NOTE: Ugly!
// We have a response! Connection works.
connectHandle = false;
self.start();
}, 1000);
}).on("error", function(e) {
if (e.message == "socket hang up") {
setTimeout(function() {
// NOTE: Ugly!
// We have a response! Connection works.
connectHandle = false;
self.start();
}, 1000);
} else {
// try again
connectHandle = setTimeout(function(){
tryReconnect(host, port);
}, 1000);
}
});
}

246
src/ffmpegCommand.js~ Normal file
View file

@ -0,0 +1,246 @@
///////////////////////////////////////////////////////////////////////////////
// A Wrapper for Fluent-FFMPEG with a custom command. //
///////////////////////////////////////////////////////////////////////////////
const ffmpeg = require('fluent-ffmpeg');
const http = require('http');
///////////////////////////////////////////////////////////////////////////////
// Declarations //
///////////////////////////////////////////////////////////////////////////////
// This
let self;
// The Variable for the FFMpeg command.
let cmd = ffmpeg({
stdoutLines: 20
});
// The Config, Logger and a handle for the kill timeout.
let config, logger, stopHandle = false, connectHandle = false;
// True if stream should be restarted.
let restart = false;
// Error Texts
let errorDescriptions = ['Camera Disconnected',
'YoutTube Disconnected',
'Wrong ffmpeg executable.',
'Unknown Error - Restarting'];
// Internal Status
let status = {
streaming: false,
error: false
};
// The stream source url. Yet to be set.
let source = "";
///////////////////////////////////////////////////////////////////////////////
// Code //
///////////////////////////////////////////////////////////////////////////////
/**
* Interface to the ffmpeg process. Uses fluent-ffmpeg.
* @param {Object} _config Configuration for the stream. @see config.js.example
* @param {Object} _logger Logger Instance from main module.
*/
let command = function(_config, _logger){
// TODO: Better Error Checking
if(!_config)
throw new Error("Invalid Config");
if(!_logger)
throw new Error("Invalid Logger");
config = _config;
self = this;
// (Re)Create the ffmpeg command and configure it.
let createCommand = function() {
// Clear inputs
cmd._inputs = [];
// TODO: Multi Protocol
source = 'rtsp://' + config.camIP + ':' + config.camPort + '/' + config.camProfile;
cmd.input(source);
// Custom config if any.
if (config.customOutputOptions !== "")
cmd.outputOptions(config.customOutputOptions.replace(/\s+\,\s+/g, ',').replace(/\s+\-/g, ',-').split(','));
if (config.customAudioOptions !== "")
cmd.outputOptions(config.customAudioOptions.replace(/\s+\,\s+/g, ',').replace(/\s+\-/g, ',-').split(','));
else
cmd.AudioCodec('copy');
if (config.customVideoOptions !== "")
cmd.outputOptions(config.customVideoOptions.replace(/\s+\,\s+/g, ',').replace(/\s+\-/g, ',-').split(','));
else
cmd.videoCodec('copy');
// Output Options.
cmd.outputFormat('flv')
.outputOptions(['-bufsize 50000k', '-tune film'])
.output('rtmp://a.rtmp.youtube.com/live2/' + config.key);
// Register events.
cmd.on('start', started);
cmd.on('end', stopped);
cmd.on('error', crashed);
};
let ffmpegCommand = function() {
return cmd;
};
// NOTE: Maybe better error resolving strategy.
// Start streaming.
let start = function() {
// Ignore if we try to reconnect.
if(connectHandle)
return;
cmd.run();
};
// Restart the stream;
let restart = function() {
if(status.streaming) {
restart = true; // NOTE: not very nice
this.stop();
} else
this.start();
};
// Stop streaming.
let stop = function() {
cmd.kill('SIGINT');
stopHandle = setTimeout(() => {
logger.log(logger.importance[3], "Force Stop!");
cmd.kill();
}, 3000);
};
let setConfig = function(_conf) {
config = _conf;
};
};
/**
* Utilities
*/
// Handle Stop and Restart
function stopped() {
status.streaming = false;
// Clear force kill Timeout
if(stopTimeout) {
clearTimeout(stopTimeout);
stopTimeout = false;
}
// Restart the stream;
if (restart) {
self.start();
}
cmd.emit('stopped');
}
// TODO: Restart = false in stopped?
// Hande Stat
function started() {
cmd.emit(restart ? 'restarted' : 'started');
restart = false;
status.error = false;
status.streaming = true;
}
/**
* Error Handling
*/
/**
* Log and handle crashes. Notify the main module.
* @param { Object } error - Error object from the crash.
* @param { String } stdout
* @param { String } stderr
*/
function crashed(error, stdout, stderr){
// Can't connect to the
if (err.message.indexOf(source) > -1)
status.error = 0;
// Can't connect to the Internet / YouTube
else if (err.message.indexOf(source + 'Input/output error') > -1 || err.message.indexOf('rtmp://a.rtmp.youtube.com/live2/' + config.key) > -1)
status.error = 1;
// Wrong FFMPEG Executable
else if (err.message.indexOf('spawn') > -1 || err.message.indexOf('niceness') > -1)
status.error = 2;
// Stopped by us - SIGINT Shouldn't lead to a crash.
else if (err.message.indexOf('SIGINT') > -1 || err.message.indexOf('SIGKILL') > -1){
stopped();
return;
}
// Some unknown Problem, just try to restart.
else {
status.error = 3;
// Just restart in a Second
setTimeout(function(){
self.start();
}, 1000);
}
logger.log(logger.importance[2], `Crashed: ${erro}\nSTDERR: ${stderr}`);
}
/**
* Probe the connection to the host on the port and restart the stream afterwards.
* @param { string } host
* @param { number } port
*/
function tryReconnect( host, port ){
if (!host || !port)
return;
http.get({
host: host.split('/')[0],
port: port
}, function(res) {
// We have a response! Connection works.
setTimeout(function() {
// NOTE: Ugly!
// We have a response! Connection works.
connectHandle = false;
self.start();
}, 1000);
}).on("error", function(e) {
if (e.message == "socket hang up") {
setTimeout(function() {
// NOTE: Ugly!
// We have a response! Connection works.
connectHandle = false;
self.start();
}, 1000);
} else {
// try again
connectHandle = setTimeout(function(){
tryReconnect(host, port);
}, 1000);
}
});
}

49
src/logger.js Normal file
View file

@ -0,0 +1,49 @@
///////////////////////////////////////////////////////////////////////////////
// Winston Logger Wrapper with Custom Colors and Transports //
///////////////////////////////////////////////////////////////////////////////
let winston = require('winston');
const customLevels = {
levels: {
normal: 0,
info: 1,
warning: 2,
danger: 3,
success: 4
},
colors: {
normal: 'white',
info: 'blue',
warning: 'orange',
danger: 'red',
success: 'green'
}
};
// Set the Colors
winston.addColors(customLevels.colors);
let logger = new(winston.Logger)({
levels: customLevels.levels,
transports: [
new(winston.transports.Console)({
level: 'success'
}),
new(winston.transports.File)({
filename: __dirname + '/process.log',
colorize: true,
timestamp: true,
level: 'success',
json: true,
maxsize: 500000,
maxFiles: 10
})
]
});
logger.importance = ['normal', 'info', 'warning', 'danger', 'success'];
// Export the Logger
module.exports = logger;

2
src/logger.js~ Normal file
View file

@ -0,0 +1,2 @@

117
src/status.js Normal file
View file

@ -0,0 +1,117 @@
///////////////////////////////////////////////////////////////////////////////
// Redux wrapper to hold the status object and the master server about changes. //
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
// Declarations //
///////////////////////////////////////////////////////////////////////////////
// Reference to itself.
let self = false;
// The socket.io connection from the main-module.
let _socket;
// The Properties of the status.
let _properties;
// Allowed values for the properties.
let _allowed;
// Action names for the properties.
let _actions;
///////////////////////////////////////////////////////////////////////////////
// Code //
///////////////////////////////////////////////////////////////////////////////
/**
* Wrapper to hold the status object and the master server about changes.
*
* The state is a shallow object which contains non-object values.
* @param { Object } socket Socket.io instance from the main module..
* @param { Object } properties The state properties to be set and read.
* @param { Array } properties.name.allowed Array of allowed values. The first value is the default value.
* @param { String } properties.name.action Name of the action to send to the server.
*/
let state = function( socket, properties ){
// singleton
if(self)
return self;
self = this;
_socket = socket;
// Initialize the Properties
for(let prop in properties){
// When already there or invalid, skip.
if(this[prop] || !(properties[prop] instanceof Array))
continue;
// Set prop and allowed;
_properties[prop] = properties[prop][0];
_allowed[prop] = Object.assign([], properties[prop]);
// Getter function.
(
function(prop){
Object.defineProperty(self, prop, {
get: function(){
return this.get(prop);
}
});
}
)(prop);
}
return this;
};
module.exports = state;
/**
* Get a state value.
* @param {String} prop Name of the state property.
* @return { * } The properties value or false if it is not found.x
*/
state.prototype.get = function(prop){
if(_properties[prop])
return _properties[prop];
else
return false;
};
/**
* Set a state value.
* @param {String} prop Name of the state property.
* @param { * } value Value to set the property to.
* @return { * } The properties new value or false if not allowed or not found.
*
* /**
* Set state values.
* @param { Object } properties An object with the properties to be changed.
* @return { * } The properties new value or false if not allowed or not found.
*/
state.prototype.set = function(prop, value){
let change;
if(!value){
if(typeof prop !== 'object')
return false;
} else {
if(typeof value == 'Object')
}
if(!_properties[prop] || !_properties[prop].allowed[value])
return false;
// NOTE: Maybe unmutable!
_properties[prop].value = value;
return _properties[prop].value;
};
/**
* Utilities
*/

19
src/status.js~ Normal file
View file

@ -0,0 +1,19 @@
///////////////////////////////////////////////////////////////////////////////
// Wrapper to hold the status object and the master server about changes. //
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
// Declarations //
///////////////////////////////////////////////////////////////////////////////
let _socket;
///////////////////////////////////////////////////////////////////////////////
// Code //
///////////////////////////////////////////////////////////////////////////////
/**
* Wrapper to hold the status object and the master server about changes.
* @param {Object} _socket - Socket.io instance from the main module..
*/
module.exports = function( _socket ){};