├── .gitignore ├── README.md ├── loggers ├── hls │ ├── package.json │ └── stream_logger_hls.js └── rtmp │ ├── package.json │ └── stream_logger_rtmp.js └── node ├── .bowerrc ├── .npmignore ├── Gruntfile.js ├── app.js ├── auth_roles ├── connect_roles.js └── dataCheck.js ├── bower.json ├── conf.js ├── fakeAuth.js ├── models ├── clients.js ├── index.js ├── persons.js ├── servers.js └── streams.js ├── package.json ├── routes ├── client.js └── stream.js └── winstonConf.js /.gitignore: -------------------------------------------------------------------------------- 1 | loggers/node_modules/ 2 | loggers/*/node_modules/ 3 | node/node_modules/ 4 | node/models_* 5 | node/routes_* 6 | *sublime* 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | ``rtmp-logger`` is a logger for streaming services hosted on nginx, written in Node.js. 3 | It supports rtmp and hls together with multiple servers networks (push/pull). 4 | The logs are stored in PostgreSQL through Sequelize.js 5 | 6 | # Folders 7 | 8 | ``node/``: the main logger server 9 | 10 | ``loggers/hls/``: script that parses nginx access.log and send the logs to the main server 11 | 12 | ``loggers/rtmp/``: script that parses the nginx-rtmp stats page and send the logs to the main server. This script can be put remotely, as long as the stat page for the specified server is accessible 13 | 14 | # API Schema 15 | 16 | The logger server exposes ``private`` and ``public`` APIs. 17 | The response data is always in JSON format. 18 | 19 | ### Private API 20 | The private APIs are used for pushing the logs to the server and shoould not be accessible by external clients. 21 | 22 | #### rtmp Start/Stop events 23 | 24 | GET:/Stream/Start 25 | GET:/Stream/Done 26 | 27 | Used for stopping and starting a stream, both play and publish. The publishing/playing client information are passed as GET parameters and are compatible with nginx ``on_start``/``on_stop`` directives (https://github.com/arut/nginx-rtmp-module/wiki/Directives), so these APIs should be called directly by nginx. 28 | 29 | #### Update Stream 30 | 31 | GET:/Stream/Update 32 | 33 | Used for updating stream datas. Optionally information about single clients can be put in the body of the HTTP packet. Should be called by the hls and the rtmp scripts 34 | 35 | ### Public APIs 36 | 37 | Public APIs can be called by logged users. 38 | 39 | #### Register new Stream 40 | 41 | POST:/Person/:personid/Stream/:streamname 42 | 43 | Used by a logged person to register a new stream. Publishing/playing are performed in a second moment. 44 | 45 | #### Get Stream Stats 46 | GET:/Person/:personid/Stream/:streamname/Stats 47 | GET:/Person/:personid/Streams/Stats 48 | GET:/Person/:personid/Streams/Stats/:interval 49 | 50 | Gather statistics for a single stream (first line), or for all streams of a person. 51 | Interval can be {Day,Week,Month}. If set only the streams of the selected interval are included. 52 | Should be called by logged users. Normal users are allowed to access only their own streams. 53 | 54 | 55 | 56 | 57 | #Installation 58 | 59 | Clone the repo or a specific folder, as needed, i.e.: 60 | 61 | $ git clone https://bitbucket.org/m3l7/rtmp-logger 62 | 63 | Install the dependencies on every folder with npm (make sure to have node installed first): 64 | 65 | $ cd node && npm install 66 | $ cd loggers/rtmp && npm install 67 | $ cd loggers/hls && npm install 68 | 69 | Make sure to have PostgreSQL installed on the server 70 | 71 | # Configuration 72 | 73 | ### Main Server 74 | 75 | Configs are located in ``node/conf.js``. In particular, it is possible to: 76 | 77 | * Choose a timeout for hls clients, so that they're set to idle state automatically if they have not communicated with the server for a while. 78 | * Configure the DB connection parameters 79 | * Enable filling the DB with test data (see ``howto stream`` section) 80 | * choose the error logging mode (file/console) and levels 81 | * choose start/stop API format (post/get, input data in headers/body) 82 | 83 | ### nginx 84 | 85 | Build nginx with rtmp support and configure the directives on_play/done to point to the server API /Stream/Start, /Stream/Done. Example: 86 | 87 | on_publish http://localhost:3000/Stream/Start; 88 | on_play http://localhost:3000/Stream/Start; 89 | on_publish_done http://localhost:3000/Stream/Done; 90 | on_play_done http://localhost:3000/Stream/Done; 91 | 92 | In this way, every time a client start or stop streaming, nginx calls the logger server which will update the db. 93 | 94 | ### rtmp script 95 | Edit the script directly (``stream_logger_rtmp.js``). You can choose to select which app or stream to log (or everything by setting the proper array to []). 96 | Also, it can be chosen an array of streaming servers and the url of the main logger server. 97 | If ``include_clients`` is set, additional information on the clients which are not passed with nginx directives (i.e. drop/av sync) are included. 98 | 99 | ### hls script 100 | 101 | Edit the script directly (``stream_logger_hls.js``). As for the rtmp script, you can choose an array of streams to fetch (set to [] to fetch all streams). 102 | The position of the ``accesso.log`` of nginx should be set. Nginx must be configured with the default log format. Make sure that the script has the correct permissions to access the log. 103 | Additionally, you should provide the url of the main logger server. 104 | 105 | 106 | 107 | # Run 108 | 109 | ### Run the logger server 110 | 111 | just run ``app.js`` with node: 112 | 113 | $ node node/app.js 114 | 115 | ### Run nginx 116 | 117 | $ nginx 118 | 119 | ### rtmp Script 120 | 121 | Run the rtmp script when needed (i.e. with a cron job): 122 | 123 | $ node loggers/rtmp/stream_logger_rtmp.js 124 | 125 | the script will pass to the logger server information about the stream (and optionally additional information about the clients, see ``Configuration`` section). 126 | The script can be put everywhere, as long as the streaming servers stat page is accessible 127 | 128 | ### hls script 129 | 130 | Run the script in each streaming server. 131 | 132 | $ node loggers/hls/stream_logger_hls.js 133 | 134 | Make sure that the script can read the nginx access.log 135 | 136 | # Howto Stream 137 | 138 | A good starting point is to enable ``enableTestData`` in ``conf.js`` of the server. Example data will be filled in the DB. They can be used as a reference. 139 | 140 | #### Create a Person and a Server 141 | 142 | Create manually (or with external software) a person and a server in the db, table Persons and Servers 143 | 144 | #### Register a new stream 145 | 146 | Register a new stream using the proper public API, using the name of the stream and the person id as parameters 147 | 148 | #### Publish the stream 149 | 150 | Start streaming with rtmp (make sure to have nginx properly configured, see ``Configuration`` section. 151 | 152 | #### Play/Pull/Push the stream 153 | 154 | Play or pull/push the stream with rtmp or hls. 155 | 156 | 157 | # Advanced Topics: server code 158 | 159 | The server uses the following libraries: 160 | 161 | express.js 162 | passport.js for authentication (need to be configured) 163 | connect-roles for authorization 164 | sequelize.js as ORM 165 | postgreSQL for the db 166 | winston for error logging 167 | 168 | When an API request is performed, a typical routing is the following: 169 | 170 | API -> authentication -> authorization -> dataCheck -> resource 171 | 172 | every module is implemented as express middleware. 173 | 174 | ### Authentication 175 | 176 | Authentication should be performed by ``passport.js``. At the moment, a dummy module (``FakeAuth``) is used instead, which just authenticates without checking for passwords/tokens 177 | 178 | ### Authorization 179 | 180 | authorization choses if a logged user has the privilege to access the resource. It is implemented with connect-roles. 181 | The rules are located in ``auth_roles/connect_roles.js``. Right now, the only rule coded is the one for stats resource (an user, unless it is an admin, can access only it own stats). 182 | 183 | ### dataCheck 184 | 185 | The dataCheck middleware makes sure that the input data is compatible with the resource. 186 | For example, if someone tries to update a stream which does not exist, dataCheck will block the execution of the API. 187 | If the middleware succedes, it will inject into the request object the proper sequelize objects (i.e. stream/server/person objects) 188 | 189 | ### Routing 190 | 191 | The actual resource is handled by the two routes (``streams.js`` and ``clients.js``). 192 | 193 | ### DB Schema 194 | 195 | The sequelize schema is stored in ``models/`` folder. -------------------------------------------------------------------------------- /loggers/hls/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node app" 7 | }, 8 | "dependencies": { 9 | "url-template": "~2.0.4", 10 | "line-by-line": "~0.1.1", 11 | "useragent": "~2.0.8" 12 | }, 13 | "devDependencies": {} 14 | } 15 | -------------------------------------------------------------------------------- /loggers/hls/stream_logger_hls.js: -------------------------------------------------------------------------------- 1 | //LOG COLLECTOR FOR HLS 2 | //Collect global data for a stream generated by HLS clients 3 | //AND 4 | //infos on single HLS clients connected 5 | 6 | //NB: nginx MUST have the default access log format ('combined') 7 | 8 | var http = require('http') 9 | , fs = require('fs') 10 | , template = require('url-template') 11 | , LineByLineReader = require('line-by-line') 12 | , Useragent = require('useragent') 13 | 14 | //list of streams to log / set to [] to fetch all 15 | var stream_list = [] 16 | 17 | //logger url 18 | var logger_server = { 19 | host: '94.23.55.74', 20 | port: 3000, 21 | url: '/Stream/Update' 22 | } 23 | 24 | //in this file we put the timestamp of the last log analysis 25 | var timestamp_file = '/tmp/hls_log_timestamp.txt' 26 | 27 | var log_file = '/var/log/nginx/access.log' 28 | 29 | var last_timestamp = 0 30 | fs.readFile(timestamp_file,'utf8',function(err,data){ 31 | 32 | //check last log analysis timestamp 33 | if (!!err) { 34 | // console.log('Error reading from timestamp file: '+err) 35 | } 36 | else{ 37 | if (!!parseInt(data)) last_timestamp = parseInt(data) 38 | } 39 | 40 | var clients_list = [] 41 | var streams_list = [] 42 | 43 | var lr = new LineByLineReader(log_file); 44 | 45 | lr.on('error', function (err) { 46 | console.log('Error reading from log file: '+err) 47 | }); 48 | 49 | lr.on('line', function (line) { 50 | 51 | 52 | var date_exp = /\[(.*)\]/g 53 | var date = date_exp.exec(line)[1] 54 | if (!!date){ 55 | //check if the line has already been parsed in the past 56 | timestamp = timestampFromDate(date) 57 | 58 | if (timestamp>last_timestamp){ 59 | 60 | //check if the stream 61 | var stream_reg = /GET.*\/(.*)-[0-9]*\./g 62 | if (stream_reg.test(line)){ 63 | 64 | var stream_reg = /(.*)\s-(.*)\s-\s\[(.*)\]\s"GET\s(.*\..*)\sH.*"\s([0-9]*).*\s([0-9]*)\s".*"\s"(.*)"/g 65 | var params = stream_reg.exec(line) 66 | if (!!params){ 67 | var ip = params[1] 68 | var url = params[4] 69 | 70 | var name_reg = /.*\/(.*)-[0-9]+.*/g 71 | var streamname = name_reg.exec(url) 72 | 73 | url = url.replace(/-[0-9]*/g,'') 74 | 75 | if (!!streamname) streamname = streamname[1] 76 | 77 | var httpcode = params[5] 78 | var bytes = params[6] 79 | var useragent = params[7] 80 | 81 | //check if the http code is OK 82 | if (((httpcode=='200') || (httpcode=='206')) && ((stream_list.indexOf(streamname)!=-1) || (!stream_list.length))) { 83 | var clientid = jenkins_hash(ip+useragent,99999) 84 | 85 | // var os_reg = /;\s([^\)]+)/g 86 | // var os = os_reg.exec(useragent) 87 | 88 | var agent = Useragent.parse(useragent); 89 | var os = agent.os.family 90 | var browser = agent.family 91 | 92 | //update client data 93 | if (!clients_list[clientid]) clients_list[clientid] = {} 94 | clients_list[clientid].os = os 95 | clients_list[clientid].url = url 96 | clients_list[clientid].browser = browser 97 | clients_list[clientid].id = clientid 98 | clients_list[clientid].timestamp = timestamp 99 | 100 | 101 | //update stream data 102 | if (!streams_list[streamname]) streams_list[streamname] = { 103 | dataout: 0, 104 | clients_id: [], 105 | clients: [], 106 | name: '', 107 | timestamp: 0 108 | } 109 | streams_list[streamname].dataout += parseInt(bytes) 110 | streams_list[streamname].name = streamname 111 | streams_list[streamname].timestamp = last_timestamp 112 | if (streams_list[streamname].clients_id.indexOf(clientid)==-1) 113 | streams_list[streamname].clients_id.push(clientid) 114 | 115 | 116 | } 117 | } 118 | 119 | 120 | 121 | 122 | } 123 | 124 | } 125 | 126 | } 127 | }); 128 | 129 | lr.on('end', function () { 130 | //build the data object 131 | var streams = [] 132 | for (var stream in streams_list){ 133 | var clients_id = streams_list[stream].clients_id 134 | for (var i = 0; i < clients_id.length; i++) { 135 | streams_list[stream].clients.push(clients_list[clients_id[i]]) 136 | }; 137 | streams.push(streams_list[stream]) 138 | } 139 | 140 | for (var i = 0; i < streams.length; i++) { 141 | 142 | //send to the logger 143 | var clientstring = JSON.stringify({clients: streams[i].clients}); 144 | 145 | var headers = { 146 | 'Content-Type': 'application/json', 147 | 'Content-Length': clientstring.length 148 | }; 149 | 150 | var params = { 151 | path : '?dataout='+streams[i].dataout+'&name='+streams[i].name+'×tamp='+streams[i].timestamp, 152 | headers: headers 153 | } 154 | params.host = logger_server.host 155 | params.path = logger_server.url+params.path 156 | params.port = logger_server.port 157 | 158 | var request = http.request(params) 159 | request.on('error', function(err){console.log("HTTP error: "+err)}) 160 | request.write(clientstring) 161 | request.end() 162 | }; 163 | 164 | //update timestamp on file 165 | fs.writeFile(timestamp_file,timestamp,function(err){ 166 | if (!!err){ 167 | console.log('Error writing to timestamp file: '+err) 168 | } 169 | }) 170 | 171 | 172 | 173 | }); 174 | 175 | 176 | }) 177 | 178 | var timestampFromDate = function(date){ 179 | date = date.replace(':',' ') 180 | return +(new Date(date)) 181 | } 182 | var jenkins_hash = function(key, interval_size) { 183 | var hash = 0; 184 | for (var i=0; i> 6); 188 | } 189 | hash += (hash << 3); 190 | hash ^= (hash >> 11); 191 | hash += (hash << 15); 192 | // make unsigned and modulo interval_size 193 | return (hash >>> 0) % interval_size; 194 | } 195 | -------------------------------------------------------------------------------- /loggers/rtmp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node app" 7 | }, 8 | "dependencies": { 9 | "xml2js": "~0.4.2", 10 | "url-template": "~2.0.4" 11 | }, 12 | "devDependencies": {} 13 | } 14 | -------------------------------------------------------------------------------- /loggers/rtmp/stream_logger_rtmp.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | , xml2js = require('xml2js') 3 | , fs = require('fs') 4 | , template = require('url-template') 5 | 6 | //list of apps/streams to log / set to [] to fetch all 7 | var app_list = [] 8 | var stream_list = [] 9 | 10 | //logger config 11 | var logger_server = { 12 | host: '94.23.55.74', 13 | port: 3000, 14 | url: '/Stream/Update' 15 | } 16 | 17 | //url of the streaming servers --- PLEASE USE LOCALHOST ONLY IF STREAMER AND SERVER ARE IN THE SAME MACHINE! 18 | // ---- USE WAN/LAN IP IN THE OTHER CASES 19 | var stream_servers = [{ 20 | host: '94.23.55.74', 21 | port: 2080, 22 | path: '/stat', 23 | method: 'GET'}] 24 | 25 | //include clients data, mainly for av/drop/duration infos 26 | var include_clients = true 27 | 28 | 29 | 30 | //BEGIN OF CODE 31 | for (var l = 0; l < stream_servers.length; l++) { 32 | var stream_server = stream_servers[l] 33 | 34 | var req = http.request(stream_server, function(res) { 35 | 36 | var streamip = res.req.connection.remoteAddress 37 | // save the data 38 | var xml = ''; 39 | res.on('data', function(chunk) { 40 | xml += chunk; 41 | }); 42 | 43 | res.on('end', function() { 44 | //xml ---> js object 45 | 46 | xml2js.parseString(xml,function(err,item){ 47 | //ANALYZE STREAMS 48 | if (!!err) { 49 | console.log('Error fetching the log: check your connection: ') 50 | console.log(err) 51 | } 52 | else if (!!item.rtmp) for (var i = 0; i < item.rtmp.server[0].application.length; i++) { 53 | var app = item.rtmp.server[0].application[i] 54 | var app_pos = app_list.indexOf(app.name[0]) 55 | 56 | if ((app_pos!=-1) || (!app_list.length)) { 57 | //we have a good app, loop through its streams 58 | 59 | if ((!!app.live) && (app.live[0].stream!=undefined)) for (var i = 0; i < app.live[0].stream.length; i++) { 60 | var stream = app.live[0].stream[i] 61 | 62 | var stream_pos = stream_list.indexOf(stream.name[0]) 63 | if ((stream_pos!=-1) || (!stream_list.length)) { 64 | 65 | //BUILD STREAM INFO 66 | var params = { 67 | vcodec : stream.meta[0].video[0].codec[0], 68 | vbit : stream.bw_video[0], 69 | sizew : stream.meta[0].video[0].width[0], 70 | sizeh : stream.meta[0].video[0].height[0], 71 | fps : stream.meta[0].video[0].frame_rate[0], 72 | acodec : stream.meta[0].audio[0].codec[0], 73 | abit : stream.bw_audio[0], 74 | freq : stream.meta[0].audio[0].sample_rate[0], 75 | chan : stream.meta[0].audio[0].channels[0], 76 | datain : stream.bytes_in[0], 77 | dataout : stream.bytes_out[0], 78 | bandin : stream.bw_in[0], 79 | bandout : stream.bw_out[0], 80 | nclients : stream.nclients[0], 81 | duration : stream.time[0], 82 | name : stream.name[0], 83 | ip: streamip, 84 | 85 | url : '?ip={ip}&vcodec={vcodec}&vbit={vbit}&sizew={sizew}&sizeh={sizeh}&fps={fps}&acodec={acodec}&abit={abit}&freq={freq}&chan={chan}&datain={datain}&dataout={dataout}&bandin={bandin}&bandout={bandout}&nclients={nclients}&name={name}&duration={duration}', 86 | } 87 | params.host = logger_server.host 88 | params.url = logger_server.url+params.url 89 | params.port = logger_server.port 90 | 91 | 92 | //INCLUDE CLIENTS DATA IF NEEDED 93 | if (include_clients){ 94 | var clients = stream.client 95 | var clients_arr = [] 96 | 97 | for (var c = 0; c < clients.length; c++) { 98 | var client = {} 99 | 100 | //populate client object 101 | client.av = clients[c].avsync[0] 102 | client.drop = clients[c].dropped[0] 103 | client.duration = clients[c].time[0] 104 | client.id = clients[c].id[0] 105 | client.flash = clients[c].flashver[0] 106 | client.timestamp = +(new Date()) 107 | clients_arr.push(client) 108 | }; 109 | 110 | var clientstring = JSON.stringify({clients: clients_arr}); 111 | 112 | params.headers={ 113 | 'Content-Type': 'application/json', 114 | 'Content-Length': clientstring.length 115 | } 116 | 117 | } 118 | 119 | //SEND DATA TO THE LOGGER 120 | params.path = template.parse(params.url).expand(params) 121 | var request = http.request(params) 122 | request.on('error', function(err){console.log("HTTP error: "+err)}) 123 | if (include_clients) request.write(clientstring) 124 | request.end() 125 | 126 | stream_list.splice(stream_pos,1) 127 | } 128 | 129 | app_list.splice(app_pos,1) 130 | }; 131 | } 132 | }; 133 | 134 | //if the stream does not exist in the logs, push the idle state 135 | for (var i = 0; i < stream_list.length; i++) { 136 | var params = { 137 | path: logger_url+'?idle=true&name='+stream_list[i], 138 | port: logger_port 139 | } 140 | var request = http.request(params) 141 | request.on('error', function(err){console.log("Error sending to Logger: "+err)}) 142 | request.end() 143 | }; 144 | 145 | 146 | }) //end xml 147 | }); 148 | 149 | }); 150 | 151 | req.end() 152 | 153 | req.on('error', function(err) { 154 | console.log('Error fetching nginx stats: '+err) 155 | }); 156 | 157 | } 158 | 159 | -------------------------------------------------------------------------------- /node/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "public/components", 3 | "json": "bower.json" 4 | } 5 | -------------------------------------------------------------------------------- /node/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/components 3 | .sass-cache -------------------------------------------------------------------------------- /node/Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var request = require('request'); 4 | 5 | module.exports = function (grunt) { 6 | // show elapsed time at the end 7 | require('time-grunt')(grunt); 8 | // load all grunt tasks 9 | require('load-grunt-tasks')(grunt); 10 | 11 | var reloadPort = 35729, files; 12 | 13 | grunt.initConfig({ 14 | pkg: grunt.file.readJSON('package.json'), 15 | develop: { 16 | server: { 17 | file: 'app.js' 18 | } 19 | }, 20 | watch: { 21 | options: { 22 | nospawn: true, 23 | livereload: reloadPort 24 | }, 25 | server: { 26 | files: [ 27 | 'app.js', 28 | 'routes/*.js' 29 | ], 30 | tasks: ['develop', 'delayed-livereload'] 31 | }, 32 | js: { 33 | files: ['public/js/*.js'], 34 | options: { 35 | livereload: reloadPort 36 | } 37 | }, 38 | css: { 39 | files: ['public/css/*.css'], 40 | options: { 41 | livereload: reloadPort 42 | } 43 | }, 44 | jade: { 45 | files: ['views/*.jade'], 46 | options: { 47 | livereload: reloadPort 48 | } 49 | } 50 | } 51 | }); 52 | 53 | grunt.config.requires('watch.server.files'); 54 | files = grunt.config('watch.server.files'); 55 | files = grunt.file.expand(files); 56 | 57 | grunt.registerTask('delayed-livereload', 'Live reload after the node server has restarted.', function () { 58 | var done = this.async(); 59 | setTimeout(function () { 60 | request.get('http://localhost:' + reloadPort + '/changed?files=' + files.join(','), function (err, res) { 61 | var reloaded = !err && res.statusCode === 200; 62 | if (reloaded) { 63 | grunt.log.ok('Delayed live reload successful.'); 64 | } else { 65 | grunt.log.error('Unable to make a delayed live reload.'); 66 | } 67 | done(reloaded); 68 | }); 69 | }, 500); 70 | }); 71 | 72 | grunt.registerTask('default', ['develop', 'watch']); 73 | }; 74 | -------------------------------------------------------------------------------- /node/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | , bodyParser = require('body-parser') 3 | , methodOverride = require('method-override') 4 | , http = require('http') 5 | , path = require('path') 6 | , db = require('./models') 7 | , stream = require('./routes/stream') 8 | , client = require('./routes/client') 9 | , passport = require('passport') 10 | , fakeAuth = require('./fakeAuth') 11 | , user = require('./auth_roles/connect_roles') 12 | , dataCheck = require('./auth_roles/dataCheck') 13 | , conf = require('./conf') 14 | , app = express() 15 | , winstonConf = require('./winstonConf.js')(app) //log/error management (through winston) 16 | 17 | 18 | app.set('port', process.env.PORT || 3000); 19 | app.use(bodyParser()); 20 | app.use(methodOverride()); 21 | 22 | //HTTP LOGGING 23 | if (winstonConf.transports.length) app.use(winstonConf.expressWinston.logger({ 24 | transports: winstonConf.transports, 25 | meta: false, // optional: control whether you want to log the meta data about the request (default to true) 26 | msg: "HTTP {{req.method}} {{req.url}} {{res.responseTime}}ms {{res.statusCode}}" // optional: customize the default logging message. E.g. "{{res.statusCode}} {{req.method}} {{res.responseTime}}ms {{req.url}}" 27 | })); 28 | 29 | 30 | //PASSPORT CONFIG 31 | // require('./passport-config') 32 | 33 | //SERVER API -- PRIVATE 34 | app[conf.api.onstartMethod]('/Stream/Start',dataCheck.EditStream('start'), stream.onStart) //start publish/play -- call with nginx on_* 35 | app[conf.api.ondoneMethod]('/Stream/Done',dataCheck.EditStream('done'),stream.onDone) //stop publish/play -- call with nginx on_* 36 | app.get('/Stream/Update',dataCheck.EditStream('update'),stream.onUpdate) //update stream -- call with stream_logger_rtmp or hls 37 | 38 | 39 | 40 | //SERVER API -- PUBLIC 41 | 42 | //register new stream 43 | app.post('/Person/:personid/Stream/:streamname',dataCheck.createNewStream(),stream.createNew) //register new stream 44 | 45 | //stats for a particular stream 46 | app.get('/Person/:personid/Stream/:streamname/Stats', 47 | fakeAuth.fakeAuth, user.can('access stats'),dataCheck.StreamStats(), 48 | client.streamStats); 49 | 50 | //stats for all times 51 | app.get('/Person/:personid/Streams/Stats', 52 | fakeAuth.fakeAuth, user.can('access stats'),dataCheck.StreamStats(), 53 | client.streamsStats); 54 | 55 | //interval: {Day,Week,Month} 56 | app.get('/Person/:personid/Streams/Stats/:interval', 57 | fakeAuth.fakeAuth, user.can('access stats'),dataCheck.StreamStats(), 58 | client.streamsStats); 59 | 60 | 61 | //error logging/management 62 | app.use(winstonConf.errorMiddleware) 63 | 64 | //INITIALIZE THE DB 65 | db 66 | .sequelize 67 | .sync({ force: conf.force_db_sync }) 68 | .complete(function(err) { 69 | if (err) { 70 | throw err 71 | } else { 72 | 73 | 74 | //TEST DATA ----- REMOVE ME? 75 | if (conf.enableTestData){ 76 | db.Server.findOrCreate({ 77 | id: 13, 78 | ip: "127.0.0.1", 79 | status: 'new', 80 | provider: 'provider1', 81 | geo: [0.001,0.32222] 82 | }) 83 | db.Server.findOrCreate({ 84 | id: 16, 85 | ip: "192.168.10.6", 86 | status: 'new', 87 | provider: 'provider1', 88 | geo: [0.04,0.2] 89 | }) 90 | db.Server.findOrCreate({ 91 | id: 15, 92 | ip: '94.23.55.74', 93 | status: 'new', 94 | provider: 'provider1', 95 | geo: [0.04,0.2] 96 | }) 97 | db.Person.findOrCreate({ 98 | username: 'topolino', 99 | password: '123456', 100 | email: 'topolino@paperopoli.it', 101 | plan: 'plan1', 102 | rank: 'user', 103 | lastlogin: "2011-11-05", 104 | }) 105 | } 106 | 107 | 108 | 109 | http.createServer(app).listen(app.get('port'), function(){ 110 | console.log('Express server listening on port ' + app.get('port')) 111 | 112 | 113 | }) 114 | } 115 | }) 116 | 117 | -------------------------------------------------------------------------------- /node/auth_roles/connect_roles.js: -------------------------------------------------------------------------------- 1 | var ConnectRoles = require('connect-roles') 2 | , user = new ConnectRoles() 3 | 4 | user.use('access stats',function(req){ 5 | if (req.user.rank=='admin') return true 6 | else if ((req.user.rank=='user') && (req.user.id==req.params.personid)) return true 7 | else return false 8 | }) 9 | 10 | module.exports = user -------------------------------------------------------------------------------- /node/auth_roles/dataCheck.js: -------------------------------------------------------------------------------- 1 | var db = require('../models') 2 | var conf = require('../conf') 3 | 4 | //VALIDATE INPUT 5 | //servers and streams are put in req.server, req.stream, req.streamserver 6 | 7 | 8 | exports.createNewStream = function(){ 9 | 10 | return function(req,res,next){ 11 | //mandatory 12 | var personid = req.params.personid 13 | var streamname = req.params.streamname 14 | 15 | if ((!personid) || (!streamname)) next(new Error('PersonId or StreamName missing')) 16 | 17 | else{ 18 | //Check if person exists 19 | db.Person.find({id:personid}) 20 | .success(function(person_item){ 21 | if (!person_item) next(new Error("Person does not exist!")) 22 | else{ 23 | db.Stream.find({where: {name:streamname}}) 24 | .success(function(stream_item){ 25 | if (!!stream_item) next(new Error('Stream already exists')) 26 | else { 27 | req.person = person_item 28 | next() 29 | } 30 | }) 31 | .error(function(err){next(new Error(JSON.stringify(err)))}) 32 | } 33 | 34 | }) 35 | .error(function(err){next(new Error(JSON.stringify(err)))}) 36 | } 37 | 38 | } 39 | 40 | } 41 | 42 | exports.EditStream = function(action){ 43 | return function(req,res,next){ 44 | 45 | //choose where to get the input data (from headers or from body) 46 | if ((action=='start') || (action=='done')){ 47 | if (conf.api.dataContainer=='body') req.query = req.body 48 | } 49 | 50 | 51 | //CHECK THE CONSISTENCY OF INPUT DATAS 52 | var params_ok = false 53 | if (!req.query.name) next(new Error('Stream Name missing')) 54 | else if (action=='start'){ 55 | if ((req.query.call!='play') && (req.query.call!='publish')) next(new Error('Wrong action')) 56 | else if (!req.query.clientid) next(new Error('nginxclientid missing!')) 57 | else params_ok = true 58 | } 59 | else if (action=='done'){ 60 | if ((req.query.call!='publish_done') && (req.query.call!='play_done')) manage_error(res,'OnDone: Wrong action') 61 | else if (!req.query.clientid) manage_error(res,'OnDone: nginxclientid missing!') 62 | else params_ok = true 63 | } 64 | else if (action=='update') params_ok = true 65 | else next(new Error('Wrong Action')) 66 | 67 | if (params_ok){ 68 | //CHECK IF THE DB IS DB (STREAM/SERVER) IS CONSISTENT 69 | var remoteAddress = req.query.ip || req.connection.remoteAddress 70 | var streamname = req.query.name 71 | 72 | //CHECK SERVER 73 | db.Server.find({where:{ip:remoteAddress}}) 74 | .success(function(server_item){ 75 | if (!server_item) next(new Error('Server does not exist')) 76 | else{ 77 | req.server = server_item 78 | var serverid = server_item.id 79 | 80 | //CHECK IF STREAM IS REGISTERED 81 | db.Stream.find({where:{name:streamname,ServerId:null}}) 82 | .success(function(stream_item){ 83 | if (!stream_item) next(new Error('Stream is not registered')) 84 | else{ 85 | req.stream = stream_item 86 | 87 | //CHECK OR CREATE STREAM/SERVER 88 | db.Stream.find({where:{ 89 | name: streamname, 90 | ServerId: serverid 91 | },include:[db.Client]}) 92 | .success(function(ss_item){ 93 | if (!ss_item) { 94 | db.Stream.create({ 95 | name: streamname, 96 | ServerId: serverid 97 | }) 98 | .success(function(ss_item){ 99 | if (!ss_item) next(new Error('Stream/Server does not exist')) 100 | else { 101 | req.streamserver = ss_item 102 | next() 103 | } 104 | }) 105 | .error(function(err){next(new Error(JSON.stringify(err)))}) 106 | } 107 | else{ 108 | req.streamserver = ss_item 109 | next() 110 | } 111 | }) 112 | .error(function(err){next(new Error(JSON.stringify(err)))}) 113 | 114 | } 115 | }) 116 | .error(function(err){next(new Error(JSON.stringify(err)))}) 117 | } 118 | }) 119 | .error(function(err){next(new Error(JSON.stringify(err)))}) 120 | } 121 | 122 | } 123 | } 124 | 125 | exports.StreamStats = function(){ 126 | return function(req,res,next){ 127 | 128 | var query = {PersonId:req.params.personid} 129 | if (!!req.params.streamname) query.name = req.params.streamname 130 | 131 | db.Stream.findAll({where:query}) 132 | .success(function(streams_item){ 133 | if ((!streams_item) && (!!req.params.streamname)) next(new Error('Stream not found')) 134 | else{ 135 | req.streams = streams_item 136 | next() 137 | } 138 | }) 139 | .error(function(err){next(new Error(JSON.stringify(err)))}) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /node/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node", 3 | "version": "0.0.1", 4 | "ignore": [ 5 | "**/.*", 6 | "node_modules", 7 | "components" 8 | ], 9 | "dependencies": {} 10 | } 11 | -------------------------------------------------------------------------------- /node/conf.js: -------------------------------------------------------------------------------- 1 | var conf = { 2 | force_db_sync: true, //force rebuilding the DB, set to false in production! 3 | disable_pushpull: false, //if true, the servers are seen as publisher/player rather than pusher/pullers 4 | client_timeout: 600000, //timeout for not updated clients (in ms) 5 | enableTestData: true, //test data, see app.js 6 | stats:{ 7 | include_clients: false 8 | }, 9 | debug: { 10 | console:{ 11 | enabled: true, 12 | options:{ 13 | colorize: true, 14 | level: 'verbose' //silly,debug,verbose,info,warn,error 15 | } 16 | }, 17 | file:{ 18 | enabled: true, 19 | options:{ 20 | filename: 'rtmp_logger.txt', 21 | level: 'verbose' //silly,debug,verbose,info,warn,error 22 | } 23 | } 24 | }, 25 | db:{ 26 | database: 'test', 27 | user: 'testuser', 28 | password: 'test' 29 | }, 30 | api:{ 31 | onstartMethod: 'post', //{get,post} 32 | ondoneMethod: 'post', //{get,post} 33 | dataContainer: 'body' //{query,body} 34 | } 35 | } 36 | 37 | 38 | module.exports = conf 39 | -------------------------------------------------------------------------------- /node/fakeAuth.js: -------------------------------------------------------------------------------- 1 | var db = require('./models') 2 | 3 | exports.fakeAuth = function(req,res,next){ 4 | var personid = req.params.personid 5 | db.Person.find({where:{id:personid}}) 6 | .success(function(person_item){ 7 | if (!!user_item){ 8 | req.user = user_item 9 | delete req.user.password 10 | next() 11 | } 12 | else next(new Error('User not found')) 13 | }) 14 | .error(function(err){next(new Error(err))}) 15 | } -------------------------------------------------------------------------------- /node/models/clients.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sequelize, DataTypes) { 2 | var Client = sequelize.define('Client', { 3 | ClientId: {type: DataTypes.INTEGER, primaryKey: true}, 4 | StreamId: {type: DataTypes.INTEGER, primaryKey: true}, 5 | status: {type: DataTypes.ENUM('playing', 'publishing', 'pushing', 'pulling','idle'), allowNull: true}, 6 | os: DataTypes.STRING, 7 | flash: DataTypes.STRING, 8 | browser: DataTypes.STRING, 9 | url: DataTypes.STRING, 10 | swf: DataTypes.STRING, 11 | drop: DataTypes.STRING, 12 | av: DataTypes.STRING, 13 | duration: DataTypes.INTEGER //fixme? 14 | }, { 15 | classMethods: { 16 | associate: function(models) { 17 | Client.belongsTo(models.Stream,{foreignKey: 'StreamId'}) 18 | } 19 | } 20 | }) 21 | 22 | return Client 23 | 24 | } 25 | -------------------------------------------------------------------------------- /node/models/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | , path = require('path') 3 | , Sequelize = require('sequelize') 4 | , lodash = require('lodash') 5 | , conf = require('../conf.js') 6 | , winstonConf = require('../winstonConf.js')() 7 | , sequelize = new Sequelize(conf.db.database, conf.db.user, conf.db.password, {dialect:'postgres', logging:winstonConf.winston.verbose}) 8 | , db = {} 9 | 10 | fs 11 | .readdirSync(__dirname) 12 | .filter(function(file) { 13 | return (file.indexOf('.') !== 0) && (file !== 'index.js') 14 | }) 15 | .forEach(function(file) { 16 | var model = sequelize.import(path.join(__dirname, file)) 17 | db[model.name] = model 18 | }) 19 | 20 | Object.keys(db).forEach(function(modelName) { 21 | if ('associate' in db[modelName]) { 22 | db[modelName].associate(db) 23 | } 24 | }) 25 | 26 | module.exports = lodash.extend({ 27 | sequelize: sequelize, 28 | Sequelize: Sequelize 29 | }, db) -------------------------------------------------------------------------------- /node/models/persons.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sequelize, DataTypes) { 2 | var Person = sequelize.define('Person', { 3 | username: { type: DataTypes.STRING, unique: true}, 4 | password: DataTypes.STRING, 5 | email: DataTypes.STRING, 6 | plan: {type: DataTypes.ENUM('plan1', 'plan2', 'plan3'), allowNull:true}, 7 | rank: {type: DataTypes.ENUM('admin', 'user', 'viewer'), allowNull:true}, 8 | lastlogin: DataTypes.DATE 9 | }, { 10 | classMethods: { 11 | associate: function(models) { 12 | Person.hasMany(models.Stream) 13 | } 14 | } 15 | }) 16 | 17 | return Person 18 | 19 | } 20 | -------------------------------------------------------------------------------- /node/models/servers.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sequelize, DataTypes) { 2 | var Server = sequelize.define('Server', { 3 | ip: { type: DataTypes.STRING, unique: true}, 4 | bandwidth: DataTypes.INTEGER, 5 | status: {type: DataTypes.ENUM('new', 'active', 'inactive', 'archived'), allowNull: true}, 6 | provider: DataTypes.STRING, 7 | geo: DataTypes.ARRAY(DataTypes.FLOAT), 8 | }, { 9 | classMethods: { 10 | associate: function(models) { 11 | Server.hasMany(models.Stream) 12 | } 13 | } 14 | }) 15 | 16 | return Server 17 | 18 | } 19 | -------------------------------------------------------------------------------- /node/models/streams.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sequelize, DataTypes) { 2 | var Stream = sequelize.define('Stream', { 3 | name: { type: DataTypes.STRING, allowNull: false}, 4 | status: {type: DataTypes.ENUM('idle', 'active'), allowNull: true}, 5 | vcodec: {type: DataTypes.ENUM('vp6', 'h264'), allowNull: true}, 6 | acodec: {type: DataTypes.ENUM('mp3', 'aac'), allowNull: true}, 7 | vbit: DataTypes.INTEGER, 8 | sizew: DataTypes.INTEGER, 9 | sizeh: DataTypes.INTEGER, 10 | fps: DataTypes.INTEGER, 11 | abit: DataTypes.INTEGER, 12 | freq: DataTypes.INTEGER, 13 | chan: DataTypes.FLOAT, 14 | datain: DataTypes.BIGINT, 15 | dataout: DataTypes.BIGINT, 16 | bandin: DataTypes.INTEGER, 17 | bandout: DataTypes.INTEGER, 18 | duration: DataTypes.INTEGER 19 | }, { 20 | classMethods: { 21 | associate: function(models) { 22 | Stream.belongsTo(models.Person) 23 | Stream.belongsTo(models.Server) 24 | Stream.hasMany(models.Client) 25 | } 26 | } 27 | }) 28 | 29 | return Stream 30 | 31 | } 32 | -------------------------------------------------------------------------------- /node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node app" 7 | }, 8 | "dependencies": { 9 | "lodash": "~2.4.1", 10 | "connect-roles": "~3.0.3", 11 | "passport": "~0.2.0", 12 | "express": "~4.2.0", 13 | "pg": "~3.1.0", 14 | "sequelize": "~1.7.5", 15 | "body-parser": "~1.2.0", 16 | "method-override": "~1.0.0", 17 | "winston": "~0.7.3", 18 | "express-winston": "~0.2.6" 19 | }, 20 | "devDependencies": { 21 | "grunt": "~0.4.1", 22 | "grunt-develop": "~0.2.2", 23 | "grunt-contrib-watch": "~0.5.3", 24 | "request": "~2.27.0", 25 | "time-grunt": "~0.1.1", 26 | "load-grunt-tasks": "~0.2.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /node/routes/client.js: -------------------------------------------------------------------------------- 1 | var db = require('../models') 2 | var conf = require('../conf') 3 | var stream = require('./stream') 4 | 5 | var Sequelize = require('sequelize') 6 | , chainer = new Sequelize.Utils.QueryChainer 7 | 8 | 9 | //stats for a single stream 10 | exports.streamStats = function(req,res,next){ 11 | //check if client owns the stream 12 | var streamname = req.streams[0].name 13 | db.Stream.findAll({ 14 | where:{ name: streamname }, 15 | include: [db.Client] 16 | }) 17 | .success(function(streamservers_item){ 18 | if (!streamservers_item) next(new Error('Stream or Client not found')) 19 | else{ 20 | stream.global_stats(streamservers_item,function(err,global_stream){ 21 | if (!!err) next(new Error('StreamStats: '+err)) 22 | else res.send([global_stream]) 23 | }) 24 | } 25 | }) 26 | .error(function(err){next(new Error(JSON.stringify(err)))}) 27 | } 28 | 29 | 30 | //stats for all streams 31 | exports.streamsStats = function(req,res,next){ 32 | 33 | var streams_item = req.streams 34 | 35 | var nstreams = streams_item.length 36 | var completed_tasks = 0 37 | var client_stats = { 38 | PersonId: 0, 39 | duration: 0, 40 | avarageDuration: 0, 41 | data: 0, 42 | nclients: 0, 43 | streams:[] 44 | } 45 | 46 | if (nstreams==0) res.send([]) 47 | for (var i = 0; i < streams_item.length; i++) { 48 | var name = streams_item[i].name 49 | if (!!name){ 50 | db.Stream.findAll({ 51 | where:{ name: name }, 52 | include: [db.Client] 53 | }) 54 | .success(function(streamservers_item){ 55 | 56 | 57 | stream.global_stats(streamservers_item,function(err,global_stream){ 58 | if (!!err) next(new Error(err)) 59 | else{ 60 | //build global stats of all streams 61 | var add_client = false 62 | if (!req.params.interval) add_client = true 63 | else if ((req.params.interval=='Day') && ((new Date() - new Date(global_stream.updatedAt)) < 86400000)) add_client = true 64 | else if ((req.params.interval=='Week') && ((new Date() - new Date(global_stream.updatedAt)) < 604800000)) add_client = true 65 | else if ((req.params.interval=='Month') && ((new Date() - new Date(global_stream.updatedAt)) < 2678400000)) add_client = true 66 | 67 | if (add_client){ 68 | client_stats.nclients += global_stream.nclients 69 | client_stats.data += global_stream.datain + global_stream.dataout 70 | client_stats.PersonId = global_stream.PersonId 71 | client_stats.duration += parseInt(global_stream.duration) 72 | } 73 | 74 | client_stats.streams.push(global_stream) 75 | completed_tasks++ 76 | if (completed_tasks==nstreams) onTasksEnd() 77 | } 78 | }) 79 | 80 | 81 | }) 82 | .error(function(err){next(new Error(JSON.stringify(err)))}) 83 | 84 | } 85 | }; 86 | 87 | 88 | 89 | var onTasksEnd = function(){ 90 | //build additional stats 91 | client_stats.avarageDuration = client_stats.duration / nstreams 92 | 93 | res.send([client_stats]) 94 | } 95 | 96 | 97 | } 98 | 99 | 100 | 101 | 102 | 103 | 104 | var getClientIDFromNginx = function(nginxid,serverid){ 105 | if ((!nginxid) || (!serverid)) return false 106 | 107 | //build clientid 108 | var sid_str = serverid.toString() 109 | var sid2 = sid_str[sid_str.length-2]+sid_str[sid_str.length-1] 110 | var cid = parseInt(sid2 + nginxid.toString()) 111 | 112 | return cid 113 | } 114 | 115 | var getStreamnameFromPageName = function(page,name){ 116 | return name 117 | } -------------------------------------------------------------------------------- /node/routes/stream.js: -------------------------------------------------------------------------------- 1 | var db = require('../models') 2 | var conf = require('../conf') 3 | var fs = require('fs'); 4 | var chainer = new db.Sequelize.Utils.QueryChainer 5 | var winstonConf = require('../winstonConf.js')() 6 | 7 | //Create new stream 8 | exports.createNew = function(req,res,next){ 9 | 10 | db.Stream.create({ 11 | name: req.params.streamname 12 | }) 13 | .success(function(new_stream_item){ 14 | //STREAM SUCCESFULLY ADDED: BIND THE OWNER 15 | new_stream_item.setPerson(req.person) 16 | .success(function(){res.send()}) 17 | .error(function(err){next(new Error(JSON.stringify(err)))}) 18 | }) 19 | .error(function(err){next(new Error(JSON.stringify(err)))}) 20 | 21 | } 22 | 23 | //publish or play a stream 24 | exports.onStart = function(req,res,next){ 25 | 26 | //UPDATE STREAM 27 | chainer.add(req.streamserver.updateAttributes({status: 'active'}) 28 | .error(function(err){next(new Error(JSON.stringify(err)))})) 29 | 30 | chainer.add(req.server.updateAttributes({status: 'active'}) 31 | .error(function(err){next(new Error(JSON.stringify(err)))})) 32 | 33 | 34 | //CHECK IF THE CLIENT PLAYING/PUBLISHING IS A SERVER (->PULL/PUSH) 35 | chainer.add(db.Server.find({where:{ip: req.query.addr}}) 36 | .success(function(serv_push_pull){ 37 | 38 | var client_status = '' 39 | if ((!serv_push_pull) || (conf.disable_pushpull)){ 40 | if (req.query.call=='play') client_status = 'playing' 41 | else client_status = 'publishing' 42 | } 43 | else{ 44 | if (req.query.call=='play') client_status = 'pulling' 45 | else client_status = 'pushing' 46 | } 47 | 48 | 49 | //BUILD CLIENTID AND ADD CLIENT TO THE DB 50 | chainer.add(db.Client.find({where:{ClientId: req.query.clientid,StreamId:req.streamserver.id}}) 51 | .success(function(client_item){ 52 | //compute flash version and OS 53 | var flash='',os='' 54 | if (client_status=='playing'){ 55 | os = req.query.flashver.split(' ')[0] 56 | console.log(os) 57 | if (os=='MAC') os='Mac OS' 58 | else if (os=='LNX') os='Linux' 59 | else if (os=='WIN') os='Windows' 60 | else os=null 61 | flash = req.query.flashver.split(' ')[1] 62 | } 63 | else if (client_status=='publishing'){ 64 | os = null 65 | flash = req.query.flashver 66 | } 67 | else{ 68 | os = flash = null 69 | } 70 | 71 | var client_data = { 72 | flash: flash, 73 | os: os, 74 | tcurl: req.query.pageurl, 75 | swfurl: req.query.swfurl, 76 | status: client_status 77 | } 78 | 79 | if (!client_item){ 80 | client_data.ClientId = req.query.clientid 81 | client_data.StreamId = req.streamserver.id 82 | chainer.add(db.Client.create(client_data) 83 | .error(function(err){next(new Error(JSON.stringify(err)))})) 84 | } 85 | else{ 86 | chainer.add(client_item.updateAttributes(client_data) 87 | .error(function(err){next(new Error(JSON.stringify(err)))})) 88 | } 89 | 90 | }) 91 | .error(function(err){next(new Error(JSON.stringify(err)))})) 92 | 93 | 94 | //WAIT FOR THE QUERY CHAIN TO END 95 | chainer.run().success(function(){ 96 | res.send() 97 | }) 98 | .error(function(err){next(new Error(JSON.stringify(err)))}) 99 | 100 | 101 | }) 102 | .error(function(err){next(new Error(JSON.stringify(err)))})) 103 | 104 | 105 | } 106 | 107 | //stop publishing/playing a stream 108 | exports.onDone = function(req,res,next){ 109 | 110 | //UPDATE SERVER/STREAM STATUS IF THE PUBLISH IS DONE 111 | if (req.query.call=='publish_done'){ 112 | chainer.add(req.streamserver.updateAttributes({status: 'idle'}) 113 | .error(function(err){next(new Error(JSON.stringify(err)))})) 114 | chainer.add(req.server.updateAttributes({status: 'inactive'}) 115 | .error(function(err){next(new Error(JSON.stringify(err)))})) 116 | } 117 | 118 | //UPDATE CLIENT DATA 119 | chainer.add(db.Client.find({where: {ClientId: req.query.clientid,StreamId:req.streamserver.id}}) 120 | .success(function(client_item){ 121 | if (!!client_item){ 122 | chainer.add(client_item.updateAttributes({status:'idle'}) 123 | .error(function(err){next(new Error(JSON.stringify(err)))})) 124 | } 125 | }) 126 | .error(function(err){next(new Error(JSON.stringify(err)))})) 127 | 128 | //WAIT FOR THE QUERY CHAIN TO END 129 | chainer.run().success(function(){ 130 | res.send() 131 | }) 132 | .error(function(err){next(new Error(JSON.stringify(err)))}) 133 | 134 | 135 | } 136 | 137 | //update stream data (hls from nginx logs/rtmp directives) 138 | // and clients data (~hls only) 139 | exports.onUpdate = function(req,res,next){ 140 | 141 | //UPDATE THE MAIN STREAM ITEM 142 | if (!!req.query.acodec) req.query.acodec = req.query.acodec.toLowerCase() 143 | if (!!req.query.vcodec) req.query.vcodec = req.query.vcodec.toLowerCase() 144 | 145 | chainer.add(req.stream.updateAttributes({ 146 | vcodec : req.query.vcodec, 147 | vbit : req.query.vbit, 148 | sizew : req.query.sizew, 149 | sizeh : req.query.sizeh, 150 | fps : req.query.fps, 151 | acodec : req.query.acodec, 152 | abit : req.query.abit, 153 | freq : req.query.freq, 154 | chan : req.query.chan, 155 | }) 156 | .error(function(err){next(new Error(JSON.stringify(err)))})) 157 | 158 | if (req.query.idle==true) { 159 | //SET THE STREAM/SERVER TO IDLE 160 | chainer.add(req.streamserver.updateAttributes({status:'idle'}) 161 | .error(function(err){next(new Error(JSON.stringify(err)))})) 162 | } 163 | else{ 164 | //UPDATE THE STREAM/SERVER DATAS 165 | chainer.add(req.streamserver.updateAttributes({ 166 | datain : req.query.datain, 167 | dataout : req.query.dataout, 168 | bandin : req.query.bandin, 169 | bandout : req.query.bandout, 170 | duration: req.query.duration, 171 | status: 'active' 172 | }) 173 | .error(function(err){next(new Error(JSON.stringify(err)))})) 174 | } 175 | 176 | 177 | //CHECK IF THE CLIENTS DATA ARE EMBEDDED IN THE REQUEST 178 | if (!!req.body.clients){ 179 | 180 | req.body.clients.forEach(function(client_data){ 181 | 182 | chainer.add(db.Client.find({where: {ClientId: client_data.id,StreamId:req.streamserver.id}}) 183 | .success(function(client_item){ 184 | 185 | var timestamp = client_data.timestamp 186 | delete client_data.timestamp 187 | 188 | if (!client_item){ 189 | if ((new Date()-timestamp)client_item.updatedAt)){ 204 | chainer.add(client_item.updateAttributes(client_data) 205 | .error(function(err){next(new Error(JSON.stringify(err)))})) 206 | } 207 | else if (conf.debug) console.log('Client log is too old') 208 | 209 | } 210 | }) 211 | .error(function(err){next(new Error(JSON.stringify(err)))})) 212 | }) 213 | 214 | } 215 | 216 | 217 | // CHECK FOR OLD CLIENTS TO DELETE (MAINLY FOR HLS SUPPORT) 218 | if (!!req.streamserver.clients) req.streamserver.clients.forEach(function(client_item){ 219 | if ((new Date() - new Date(client_item.updatedAt))>conf.client_timeout){ 220 | chainer.add(client_item.updateAttributes({status:'idle'}) 221 | .error(function(err){next(new Error(JSON.stringify(err)))})) 222 | } 223 | }) 224 | 225 | //WAIT FOR THE QUERY CHAIN TO END 226 | chainer.run().success(function(){ 227 | res.send() 228 | }) 229 | .error(function(err){next(new Error(JSON.stringify(err)))}) 230 | 231 | } 232 | 233 | 234 | exports.global_stats = function(streams_obj,next){ 235 | //build global stats for a given stream 236 | 237 | var global_stream = { 238 | name: '', 239 | PersonId: 0, 240 | status: 'idle', 241 | vcodec: '', 242 | acodec: '', 243 | vbit: 0, 244 | sizew: 0, 245 | sizeh: 0, 246 | fps: 0, 247 | abit: 0, 248 | freq: 0, 249 | chan: 0, 250 | datain: 0, 251 | dataout: 0, 252 | bandin: 0, 253 | bandout: 0, 254 | duration: 0, 255 | nclients: 0, 256 | createdAt: null, 257 | updatedAt: null, 258 | servers: [] 259 | } 260 | 261 | 262 | for (var i = 0; i < streams_obj.length; i++) { 263 | var stream = streams_obj[i] 264 | if (!!stream.ServerId){ 265 | delete stream.dataValues.PersonId 266 | global_stream.name = stream.name 267 | global_stream.datain += parseInt(stream.datain) 268 | global_stream.dataout += parseInt(stream.dataout) 269 | global_stream.bandin += parseInt(stream.bandin) 270 | global_stream.bandout += parseInt(stream.bandout) 271 | global_stream.status = stream.status 272 | 273 | //delete unnecessary cols 274 | delete stream.dataValues.vcodec 275 | delete stream.dataValues.acodec 276 | delete stream.dataValues.vbit 277 | delete stream.dataValues.sizew 278 | delete stream.dataValues.sizeh 279 | delete stream.dataValues.fps 280 | delete stream.dataValues.abit 281 | delete stream.dataValues.freq 282 | delete stream.dataValues.chan 283 | stream.dataValues.bandin = parseInt(stream.dataValues.bandin) 284 | stream.dataValues.bandout = parseInt(stream.dataValues.bandout) 285 | stream.dataValues.datain = parseInt(stream.dataValues.datain) 286 | stream.dataValues.dataout = parseInt(stream.dataValues.dataout) 287 | 288 | if (new Date(stream.updatedAt)>new Date(global_stream.updatedAt)) 289 | global_stream.updatedAt = stream.updatedAt 290 | 291 | dur = parseInt(stream.duration) 292 | if (dur>global_stream.duration) global_stream.duration = dur 293 | 294 | //count the number of clients 295 | var nclients = stream.clients.length 296 | stream.dataValues.nclients = nclients 297 | global_stream.nclients += nclients 298 | 299 | if (!conf.stats.include_clients) delete stream.dataValues.clients 300 | global_stream.servers.push(stream.dataValues) 301 | } 302 | else{ 303 | global_stream.PersonId = stream.PersonId 304 | global_stream.createdAt = stream.createdAt 305 | 306 | global_stream.vcodec = stream.vcodec 307 | global_stream.acodec = stream.acodec 308 | global_stream.vbit = stream.vbit 309 | global_stream.sizew = stream.sizew 310 | global_stream.sizeh = stream.sizeh 311 | global_stream.fps = stream.fps 312 | global_stream.abit = stream.abit 313 | global_stream.freq = stream.freq 314 | global_stream.chan = stream.chan 315 | 316 | if (new Date(stream.updatedAt)>new Date(global_stream.updatedAt)) 317 | global_stream.updatedAt = stream.updatedAt 318 | 319 | } 320 | 321 | } 322 | if (!!next) next(null,global_stream) 323 | 324 | } 325 | 326 | 327 | 328 | var getClientIDFromNginx = function(nginxid,serverid){ 329 | if ((!nginxid) || (!serverid)) return false 330 | 331 | //build clientid 332 | var sid_str = serverid.toString() 333 | var sid2 = sid_str[sid_str.length-2]+sid_str[sid_str.length-1] 334 | var cid = parseInt(sid2 + nginxid.toString()) 335 | 336 | return cid 337 | } 338 | 339 | var getStreamnameFromPageName = function(page,name){ 340 | return name 341 | } -------------------------------------------------------------------------------- /node/winstonConf.js: -------------------------------------------------------------------------------- 1 | var winston = require('winston') 2 | , expressWinston = require('express-winston') 3 | , conf = require('./conf.js') 4 | 5 | var exp = { 6 | transports: [], 7 | winston: winston, 8 | winston_noConsole: null, 9 | expressWinston: expressWinston, 10 | errorMiddleware: function(err,req,res,next){ 11 | if (!err.stack){ 12 | res.send(500,err) 13 | exp.winston.error(err) 14 | } 15 | else{ 16 | res.send(500,err.stack) 17 | exp.winston.error(err.stack) 18 | } 19 | 20 | //PRINT INPUT DATA 21 | if (!!req.server) winston_noConsole.error(JSON.stringify(req.server),{item: 'req.server'}) 22 | if (!!req.person) winston_noConsole.error(JSON.stringify(req.person),{item: 'req.person'}) 23 | if (!!req.stream) winston_noConsole.error(JSON.stringify(req.stream),{item: 'req.stream'}) 24 | if (!!req.streamserver) winston_noConsole.error(JSON.stringify(req.streamserver),{item: 'req.streamserver'}) 25 | 26 | } 27 | } 28 | 29 | module.exports = function(app){ 30 | 31 | if (!!app){ 32 | 33 | winston_noConsole = new (winston.Logger)() 34 | winston.remove(winston.transports.Console) 35 | 36 | if (conf.debug.console.enabled) { 37 | winston.add(winston.transports.Console,conf.debug.console.options) 38 | exp.transports.push(new winston.transports.Console(conf.debug.console.options)) 39 | } 40 | if (conf.debug.file.enabled) { 41 | winston.add(winston.transports.File, conf.debug.file.options) 42 | winston_noConsole.add(winston.transports.File, conf.debug.file.options) 43 | exp.transports.push(new winston.transports.File(conf.debug.file.options)) 44 | } 45 | } 46 | 47 | return exp 48 | } --------------------------------------------------------------------------------