Make streaming work & much more elegant
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | node_modules/ | ||||||
| @@ -10,7 +10,6 @@ router.get('/', getById); | |||||||
| router.put('/', update); | router.put('/', update); | ||||||
| router.delete('/', _delete); | router.delete('/', _delete); | ||||||
| router.post('/osd', _osd); | router.post('/osd', _osd); | ||||||
| router.post('/stream', _stream); |  | ||||||
|  |  | ||||||
| module.exports = router; | module.exports = router; | ||||||
|  |  | ||||||
| @@ -49,10 +48,3 @@ function _osd(req, res, next) { | |||||||
|         .then((socket) => res.json(socket)) |         .then((socket) => res.json(socket)) | ||||||
|         .catch(err => next(err)); |         .catch(err => next(err)); | ||||||
| } | } | ||||||
|  |  | ||||||
| function _stream(req, res, next) { |  | ||||||
|     console.log("Stream Requester ip: " + req.ip); |  | ||||||
|     cameraService.stream(req.body) |  | ||||||
|         .then((streamPort) => res.json(streamPort)) |  | ||||||
|         .catch(err => next(err)); |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| /*jshint esversion: 6 */ | /*jshint esversion: 6 */ | ||||||
| const path = require('path'); | const path = require('path'); | ||||||
| const Stream = require('../stream/videoStream'); |  | ||||||
| const db = require(path.resolve(__dirname, '../_helpers/db.js')); | const db = require(path.resolve(__dirname, '../_helpers/db.js')); | ||||||
| const mongoose = require('mongoose'); | const mongoose = require('mongoose'); | ||||||
| const cameraHelper = require(path.resolve(__dirname, '../_helpers/cameraHelper.js')); | const cameraHelper = require(path.resolve(__dirname, '../_helpers/cameraHelper.js')); | ||||||
| @@ -13,8 +12,7 @@ module.exports = { | |||||||
|     getById: _getById, |     getById: _getById, | ||||||
|     update: _update, |     update: _update, | ||||||
|     delete: _delete, |     delete: _delete, | ||||||
|     osd: _osd, |     osd: _osd | ||||||
|     stream: _stream |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| async function _getAll() { | async function _getAll() { | ||||||
| @@ -101,27 +99,3 @@ async function _osd({id, option}) { | |||||||
|     } |     } | ||||||
|     return await socket.sendCmd(id, osdHex); |     return await socket.sendCmd(id, osdHex); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function _stream({id, width = 1920, height = 1080}) { |  | ||||||
|     const camera = await Camera.findById(id).lean(); |  | ||||||
|  |  | ||||||
|     // In case a seperate user wants to reach same camera stream |  | ||||||
|     let inUse; |  | ||||||
|     try { |  | ||||||
|         inUse = await cameraHelper.checkPortUse(camera.streamPort); |  | ||||||
|     } catch(err) { |  | ||||||
|         throw err; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!inUse) { |  | ||||||
|         const stream = new Stream({ |  | ||||||
|             name: camera.name || "stream: " + camera.rtsp, |  | ||||||
|             url: 'rtsp://' + camera.rtsp, |  | ||||||
|             port: camera.streamPort, |  | ||||||
|             width: width, |  | ||||||
|             height: height |  | ||||||
|         }); |  | ||||||
|         stream.start(); |  | ||||||
|     } |  | ||||||
|     return camera.streamPort; |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| { | { | ||||||
|     "connectionString": "mongodb://your-mongo-address/db-name", |     "connectionString": "mongodb://localhost:27017/ptzoptics", | ||||||
|     "secret": "your db-secret" |     "secret": "your db-secret" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,38 +0,0 @@ | |||||||
| /*jshint esversion: 6 */ |  | ||||||
| const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path; |  | ||||||
| const child_process = require('child_process'); |  | ||||||
| const EventEmitter = require('events'); |  | ||||||
| const spawn = require('cross-spawn'); |  | ||||||
|  |  | ||||||
| class Mpeg1Muxer extends EventEmitter { |  | ||||||
|  |  | ||||||
|     constructor(options) { |  | ||||||
|         super(options); |  | ||||||
|  |  | ||||||
|         this.url = options.url; |  | ||||||
|         this.width = options.width; |  | ||||||
|         // this.stream = child_process.spawn(ffmpegPath, ['-y', '-loglevel', 'quiet', "-rtsp_transport", "tcp", "-i", this.url, '-vf', 'yadif', '-f', 'mpegts', '-r', '30',  '-codec:v', 'mpeg1video', '-codec:a', 'mp2', '-b:a', '128k', '-b:v', '4096k', '-muxdelay', '0', '-', './app/stream/stream.ts'], { |  | ||||||
|         //     detached: false |  | ||||||
|         // }); |  | ||||||
|         this.stream = child_process.spawn(ffmpegPath, ['-y', '-loglevel', 'quiet', "-rtsp_transport", "tcp", "-i", this.url, '-filter:v', 'scale=1280:-1', '-f', 'mpegts', '-r', '30',  '-codec:v', 'mpeg1video', '-codec:a', 'mp2', '-b:a', '128k', '-b:v', '1500k', '-', './app/stream/stream.ts'], { |  | ||||||
|             detached: false |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         this.inputStreamStarted = true; |  | ||||||
|         this.stream.stdout.on('data', (data) => { return this.emit('mpeg1data', data); }); |  | ||||||
|         this.stream.stderr.on('data', (data) => { return this.emit('ffmpegError', data); }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     stop() { |  | ||||||
|         try { |  | ||||||
|             this.stream.stdout.removeAllListeners(); |  | ||||||
|         } catch(err) { |  | ||||||
|             console.log("Muxer: " + err); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         this.stream.kill(); |  | ||||||
|         this.stream = undefined; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| module.exports = Mpeg1Muxer; |  | ||||||
							
								
								
									
										21
									
								
								app/stream/stream.controller.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/stream/stream.controller.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | const path = require('path'); | ||||||
|  | const express = require('express'); | ||||||
|  | const router = express.Router(); | ||||||
|  |  | ||||||
|  | const db = require(path.resolve(__dirname, '../_helpers/db.js')); | ||||||
|  | const Camera = db.Camera; | ||||||
|  |  | ||||||
|  | const { proxy, scriptUrl } = require('rtsp-relay')(router); | ||||||
|  |  | ||||||
|  | console.log(`Connecting to camera: rtsp://${Camera.rtsp}`); | ||||||
|  |  | ||||||
|  | const handler = proxy({ | ||||||
|  |     url: `rtsp://10.0.1.3:554/1`, | ||||||
|  |     // if your RTSP stream need credentials, include them in the URL as above | ||||||
|  |     verbose: false, | ||||||
|  |     additionalFlags: ['-q', '1'] | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  | router.ws('/', handler); | ||||||
|  |  | ||||||
|  | module.exports = router; | ||||||
| @@ -1,124 +0,0 @@ | |||||||
| /*jshint esversion: 6 */ |  | ||||||
|  |  | ||||||
| // Thanks to:  https://github.com/Wifsimster/node-rtsp-stream-es6 |  | ||||||
| const WebSocket = require('ws'); |  | ||||||
| const EventEmitter = require('events'); |  | ||||||
| const STREAM_MAGIC_BYTES = "jsmp"; |  | ||||||
| const Mpeg1Muxer = require('./mpeg1muxer'); |  | ||||||
|  |  | ||||||
| class VideoStream extends EventEmitter { |  | ||||||
|  |  | ||||||
|     constructor(options) { |  | ||||||
|         super(options); |  | ||||||
|         this.name = options.name; |  | ||||||
|         this.url = options.url; |  | ||||||
|         this.width = options.width; |  | ||||||
|         this.height = options.height; |  | ||||||
|         this.port = options.port; |  | ||||||
|         this.stream = void 0; |  | ||||||
|         this.stream2Socket(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     stream2Socket() { |  | ||||||
|         this.server = new WebSocket.Server({ |  | ||||||
|             port: this.port |  | ||||||
|         }); |  | ||||||
|         this.server.on('connection', (socket) => { |  | ||||||
|             console.log(`New connection: ${this.name}`); |  | ||||||
|             let streamHeader = new Buffer(8); |  | ||||||
|             streamHeader.write(STREAM_MAGIC_BYTES); |  | ||||||
|             streamHeader.writeUInt16BE(this.width, 4); |  | ||||||
|             streamHeader.writeUInt16BE(this.height, 6); |  | ||||||
|             socket.send(streamHeader); |  | ||||||
|  |  | ||||||
|             socket.on('close', () => { |  | ||||||
|                 console.log(`${this.name} disconnected !`); |  | ||||||
|                 if (this.server.clients.length <= 0) { |  | ||||||
|                     this.mpeg1Muxer.stop(); |  | ||||||
|                     this.server.close(); |  | ||||||
|                 } |  | ||||||
|             }); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         this.on('camdata', (data) => { |  | ||||||
|             for (let i in this.server.clients) { |  | ||||||
|                 let client = this.server.clients[i]; |  | ||||||
|                 if (client.readyState === WebSocket.OPEN) { |  | ||||||
|                     client.send(data); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     onSocketConnect(socket) { |  | ||||||
|         let streamHeader = new Buffer(8); |  | ||||||
|         streamHeader.write(STREAM_MAGIC_BYTES); |  | ||||||
|         streamHeader.writeUInt16BE(this.width, 4); |  | ||||||
|         streamHeader.writeUInt16BE(this.height, 6); |  | ||||||
|         socket.send(streamHeader, { |  | ||||||
|             binary: true |  | ||||||
|         }); |  | ||||||
|         console.log(`New connection: ${this.name} - ${this.wsServer.clients.length} total`); |  | ||||||
|         return socket.on("close", function(code, message) { |  | ||||||
|             return console.log(`${this.name} disconnected - ${this.wsServer.clients.length} total`); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     start() { |  | ||||||
|         this.mpeg1Muxer = new Mpeg1Muxer({ |  | ||||||
|             url: this.url, |  | ||||||
|             width: this.width |  | ||||||
|         }); |  | ||||||
|         this.mpeg1Muxer.on('mpeg1data', (data) => { |  | ||||||
|             return this.emit('camdata', data); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         let gettingInputData = false; |  | ||||||
|         let gettingOutputData = false; |  | ||||||
|         let inputData = []; |  | ||||||
|         let outputData = []; |  | ||||||
|  |  | ||||||
|         this.mpeg1Muxer.on('ffmpegError', (data) => { |  | ||||||
|             data = data.toString(); |  | ||||||
|             if (data.indexOf('Input #') !== -1) { |  | ||||||
|                 gettingInputData = true; |  | ||||||
|             } |  | ||||||
|             if (data.indexOf('Output #') !== -1) { |  | ||||||
|                 gettingInputData = false; |  | ||||||
|                 gettingOutputData = true; |  | ||||||
|             } |  | ||||||
|             if (data.indexOf('frame') === 0) { |  | ||||||
|                 gettingOutputData = false; |  | ||||||
|             } |  | ||||||
|             if (gettingInputData) { |  | ||||||
|                 inputData.push(data.toString()); |  | ||||||
|                 let size = data.match(/\d+x\d+/); |  | ||||||
|                 if (size != null) { |  | ||||||
|                     size = size[0].split('x'); |  | ||||||
|                     if (this.width == null) { |  | ||||||
|                         this.width = parseInt(size[0], 10); |  | ||||||
|                     } |  | ||||||
|                     if (this.height == null) { |  | ||||||
|                         return this.height = parseInt(size[1], 10); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|         this.mpeg1Muxer.on('ffmpegError', (data) => { |  | ||||||
|             return global.process.stderr.write(data); |  | ||||||
|         }); |  | ||||||
|         return this; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     stop(serverCloseCallback) { |  | ||||||
|         this.server.close(serverCloseCallback); |  | ||||||
|         this.server.removeAllListeners(); |  | ||||||
|         this.server = undefined; |  | ||||||
|  |  | ||||||
|         this.mpeg1Muxer.stop(); |  | ||||||
|         this.mpeg1Muxer.removeAllListeners(); |  | ||||||
|         this.mpeg1Muxer = undefined; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| module.exports = VideoStream; |  | ||||||
| @@ -21,6 +21,7 @@ | |||||||
|     "mongoose": "^5.2.1", |     "mongoose": "^5.2.1", | ||||||
|     "nodemon": "^1.17.5", |     "nodemon": "^1.17.5", | ||||||
|     "path": "^0.12.7", |     "path": "^0.12.7", | ||||||
|  |     "rtsp-relay": "^1.6.1", | ||||||
|     "tcp-port-used": "^1.0.0", |     "tcp-port-used": "^1.0.0", | ||||||
|     "ws": "1.1.1" |     "ws": "1.1.1" | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -120,7 +120,7 @@ ul.nav { | |||||||
|     margin-left: auto; |     margin-left: auto; | ||||||
| } | } | ||||||
| #streamStage { | #streamStage { | ||||||
|     max-height: 100%; |     /* max-height: 100%; */ | ||||||
|     right: 0; |     right: 0; | ||||||
| } | } | ||||||
| #msgStage { | #msgStage { | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  |  | ||||||
| <html lang="en"> | <html lang="en"> | ||||||
|  |  | ||||||
| <head> | <head> | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								pub/js/jsmpeg.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								pub/js/jsmpeg.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -8,13 +8,5 @@ function changeStream() { | |||||||
|  |  | ||||||
| function startStream() { | function startStream() { | ||||||
|     const canvas = document.getElementById('streamStage'); |     const canvas = document.getElementById('streamStage'); | ||||||
|     const payload = { |     player = new JSMpeg.Player('ws://' + document.location.host + '/stream', {canvas:canvas}); | ||||||
|         width: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth, |  | ||||||
|         height: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     sendCmd('/camera/stream', 'POST', payload).then((camera) => { |  | ||||||
|         const streamPort = JSON.parse(camera); |  | ||||||
|         player = new JSMpeg.Player('ws://' + document.location.hostname + ':' + streamPort, {canvas:canvas}); |  | ||||||
|     }); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ | |||||||
| const path = require('path'); | const path = require('path'); | ||||||
| const express = require('express'); | const express = require('express'); | ||||||
| const app = express(); | const app = express(); | ||||||
|  | const expressWs = require('express-ws')(app); | ||||||
| const cors = require('cors'); | const cors = require('cors'); | ||||||
| const bodyParser = require('body-parser'); | const bodyParser = require('body-parser'); | ||||||
| const errorhandler = require(path.resolve( __dirname, 'app/_helpers/error-handler.js')); | const errorhandler = require(path.resolve( __dirname, 'app/_helpers/error-handler.js')); | ||||||
| @@ -14,19 +15,20 @@ app.use(cors()); | |||||||
|  |  | ||||||
| app.use(express.static(__dirname + '/pub')); | app.use(express.static(__dirname + '/pub')); | ||||||
| app.get('/', (req, res) => { | app.get('/', (req, res) => { | ||||||
|     res.sendFile(path.resolve(__dirname + '/pub/index.html')); |   res.sendFile(path.resolve(__dirname + '/pub/index.html')); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| app.use('/camera', require(path.resolve( __dirname, 'app/camera/camera.controller.js'))); | app.use('/camera', require(path.resolve( __dirname, 'app/camera/camera.controller.js'))); | ||||||
| app.use('/ptz', require(path.resolve( __dirname, 'app/ptz/ptz.controller.js'))); | app.use('/ptz', require(path.resolve( __dirname, 'app/ptz/ptz.controller.js'))); | ||||||
| app.use('/image', require(path.resolve( __dirname, 'app/image/image.controller.js'))); | app.use('/image', require(path.resolve( __dirname, 'app/image/image.controller.js'))); | ||||||
|  | app.use('/stream', require(path.resolve( __dirname, 'app/stream/stream.controller.js'))); | ||||||
| app.get('*', function(req, res){ | app.get('*', function(req, res){ | ||||||
|   res.send('what???', 404); |   res.status(404).send('404 not found'); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| app.use(errorhandler); | app.use(errorhandler); | ||||||
|  |  | ||||||
| const port = process.env.NODE_ENV === 'production' ? 80 : 4000; | const port = process.env.NODE_ENV === 'production' ? 80 : 4000; | ||||||
| const server = app.listen(port, function() { | const server = app.listen(port, function() { | ||||||
|     console.log('Server listening on port ' + port); |   console.log('Server listening on port ' + port); | ||||||
| }); | }); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user