scalra
Version:
node.js framework to prototype and scale rapidly
292 lines (217 loc) • 7.98 kB
JavaScript
//
// stream.js
//
// a stream object that allows incoming & outgoing streams to be handled simutaneously
//
// history:
// 2013-10-08 adapted from source sample 'stream-server.js' at jsmpeg
// see: https://github.com/phoboslab/jsmpeg
// src: https://raw.github.com/phoboslab/jsmpeg/master/stream-server.js
/*
public methods:
StreamServer(config)
*/
//
// local reference
//
/*
config: {
secret: 'string',
IN_PORT: 'number',
OUT_PORT: 'number',
type: 'string'
}
type: 'ws' (websocket server)
'socketio' (socket.io server)
*/
var StreamServer = function (config) {
// check parameters
if ((typeof config.IN_PORT !== 'number' &&
typeof config.OUT_PORT !== 'number')) {
LOG.warn('config needs to define parameters: IN_PORT or OUT_PORT');
return;
}
var server_type = config.type || 'MPEG';
var STREAM_SECRET = config.secret;
IN_PORT = config.IN_PORT || 8082,
OUT_PORT = config.OUT_PORT || 8084,
STREAM_MAGSR_BYTES = 'jsmp'; // Must be 4 bytes
// channel info
var l_channels = {};
var socketServer = undefined;
var streamServer = undefined;
this.start = function () {
// Websocket Server
socketServer = new (require('ws').Server)({port: OUT_PORT});
// process incoming connection of subscribers
socketServer.on('connection', function (socket) {
// generate unique clientID
var clientID = UTIL.createUUID();
var clientData = socketServer.clients[socketServer.clients.length - 1];
var channel = undefined;
LOG.sys( 'New WebSocket Connection (' + socketServer.clients.length + ' total) id: ' + clientID );
// we start to handle after receiving channel name to subscribe
// NOTE: the incoming content is not string type
socket.on('message', function (name) {
try {
/*
var obj = JSON.parse(name.data);
LOG.warn(obj);
*/
// extract info
if (l_channels.hasOwnProperty(name) === false) {
var msg = 'no stream registered for channel [' + name + ']';
LOG.warn(msg);
socket.send(msg);
return;
}
// record channel name
channel = name;
// get parameters
var params = l_channels[channel].para;
// NOTE: we convert string to numbers with | 0
var width = (params[0] || 320) | 0;
var height = (params[1] || 240) | 0;
LOG.warn('params:');
LOG.warn(params);
LOG.warn('client to receive channel: ' + channel + ' w: ' + width + ' h: ' + height);
// record this user's reference as part of the channel subscribers
// NOTE: we record clientData, not socket
l_channels[channel].users[clientID] = clientData;
LOG.warn('channel [' + channel + '] has ' + Object.keys(l_channels[channel].users).length + ' users', 'message');
// Send back magic bytes and video size to the newly connected socket
// struct { char magic[4]; unsigned short width, height;}
var streamHeader = new Buffer(8);
streamHeader.write(STREAM_MAGSR_BYTES);
streamHeader.writeUInt16BE(width, 4);
streamHeader.writeUInt16BE(height, 6);
socket.send(streamHeader, {binary:true});
}
catch(e) {
LOG.error(e);
}
});
// handles closing in-coming connection
socket.on('close', function (code, message) {
LOG.sys( 'Disconnected WebSocket (' + socketServer.clients.length + ' total)' );
// remove user record (i.e., socket) from channel
if (l_channels.hasOwnProperty(channel)) {
delete l_channels[channel].users[clientID];
LOG.warn('client [' + clientID + '] unsubscribe from channel [' + channel + ']');
}
});
});
socketServer.broadcast = function (channel, data, opts) {
// go over all clients for a given channel
if (l_channels.hasOwnProperty(channel) === false) {
LOG.warn('channel [' + channel + '] not registered, cannot broadcast');
return;
}
//LOG.warn('channel [' + channel + '] has ' + Object.keys(l_channels[channel].users).length + ' users', 'broadcast');
var clients = l_channels[channel].users;
for (var id in clients) {
try {
//this.clients[i].send(data, opts);
clients[id].send(data, opts);
}
catch(e) {
LOG.warn('client[' + id + '] fails to send to channel [' + channel + ']');
}
}
};
// HTTP Server to accept incoming MPEG Stream
streamServer = require('http').createServer(function (request, response) {
var params = request.url.substr(1).split('/');
var channel = params[0];
if (channel) {
LOG.sys(
'Stream Connected: ' + request.socket.remoteAddress +
':' + request.socket.remotePort + ' channel: ' + channel
);
//' size: ' + width + 'x' + height
// remove first element (channel name) and store the rest as parameters
params.splice(0, 1);
l_channels[channel] = {
para: params, // app-specific parameters in string array format
users: {} // user clients conneted awating message updates
}
request.on('data', function(data) {
if (socketServer)
socketServer.broadcast(channel, data, {binary:true});
});
}
else {
LOG.sys(
'failed connection: '+ request.socket.remoteAddress +
request.socket.remotePort + ' - no channel specified.'
);
response.end();
}
}).listen(IN_PORT);
LOG.warn('Listening for stream on http://'+ SR.Settings.SERVER_INFO.IP +':'+IN_PORT+'/<channel>/<para1>/<para2>/<para3>/...', 'SR.Stream');
LOG.warn('Awaiting WebSocket connections on ws://' + SR.Settings.SERVER_INFO.IP + ':'+OUT_PORT+'/', 'SR.Stream');
}
this.stop = function () {
if (socketServer) {
socketServer.close();
}
if (streamServer) {
streamServer.close();
}
}
} // end StreamServer
// export socket object to outside
exports.icStream = StreamServer;
//var l_logChannels = {};
// create a new message queue
var l_msgqueue = new SR.MsgQueue();
// record a registered user of execution output
exports.subscribe = function (channel, conn, last) {
var count = SR.Comm.count(channel);
return (SR.Comm.subscribe(conn.connID, channel, conn) !== count);
/*
// if we want the last X messages
if (typeof info.last === 'number' && info.last > 0) {
// get message from queue
var msg_list = l_msgqueue.get(msgqueue_para, event.data.channel);
event.done('SR_MSGLIST', {channel: event.data.channel, msgs: msg_list});
}
*/
/*
// NOTE: replace with pub/sub?
if (l_logChannels.hasOwnProperty(channel) === false)
l_logChannels[channel] = {};
// check for redundency
if (l_logChannels[channel].hasOwnProperty(conn.connID) === false) {
l_logChannels[channel][conn.connID] = conn;
LOG.warn('conn ' + conn.connID + ' subscribe channel [' + channel + ']');
return true;
}
else {
LOG.warn('connection object already subcribed for channel [' + channel + ']');
return false;
}
*/
}
exports.unsubscribe = function (channel, conn) {
return SR.Comm.unsubscribe(conn.connID, channel);
}
exports.publish = function (channel, msg) {
LOG.sys('publish to channel [' + channel + ']:', 'SR.Stream');
LOG.sys(msg);
// store to queue
l_msgqueue.add(msg, channel);
return SR.Comm.publish(channel, msg, 'LOG');
/*
if (l_logChannels.hasOwnProperty(channel) === false)
return false;
var connections = l_logChannels[channel];
var list = [];
// convert map to array
for (var connID in connections) {
list.push(connections[connID]);
}
SR.EventManager.send('LOG', msg, list);
return true;
*/
}