├── index.js ├── test ├── client.js ├── server.js └── index.js ├── src ├── configs.js ├── mpeg1muxer.js └── videoStream.js ├── .npmignore ├── package.json └── README.md /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/videoStream') -------------------------------------------------------------------------------- /test/client.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws') 2 | const ws = new WebSocket('ws://localhost:5000') 3 | 4 | ws.on('open', () => { 5 | console.log('Connected to stream') 6 | }) 7 | 8 | ws.on('message', (data, flags) => { 9 | console.log(data) 10 | }) -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | const Stream = require('../src/videoStream') 2 | 3 | const options = { 4 | name: 'streamName', 5 | url: 'rtsp://184.72.239.149/vod/mp4:BigBuckBunny_115k.mov', 6 | wsPort: 5000 7 | } 8 | 9 | stream = new Stream(options) 10 | 11 | stream.start() -------------------------------------------------------------------------------- /src/configs.js: -------------------------------------------------------------------------------- 1 | const genFfmpegFormatConfigs = (url, port, width, height) => 2 | ["-rtsp_transport", "tcp", "-i", url, '-f', 'mpegts', '-codec:v', 'mpeg1video', '-s', `${width}x${height}`, '-b:v', '800k', '-r', '30', '-muxdelay', '0.4', `http://localhost:${port}/s1`]; 3 | 4 | module.exports = {genFfmpegFormatConfigs} -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const app = require('express')() 2 | const http = require('http').Server(app) 3 | const ioc = require('socket.io-client') 4 | const port = 5000 5 | const socket = ioc('http://localhost:' + port) 6 | 7 | app.get('/', (req, res) => { 8 | res.send('

Hello world

') 9 | }) 10 | 11 | http.listen(80, () => { 12 | console.log('Listening on *:80') 13 | }) 14 | 15 | socket.on('connect', () => { console.log('Connected !') }) 16 | 17 | socket.on('stream', (data) => { console.log(data) }) -------------------------------------------------------------------------------- /src/mpeg1muxer.js: -------------------------------------------------------------------------------- 1 | const {genFfmpegFormatConfigs} = require('./configs'); 2 | const child_process = require('child_process'); 3 | const EventEmitter = require('events'); 4 | 5 | class Mpeg1Muxer extends EventEmitter { 6 | 7 | constructor(options) { 8 | super(options) 9 | 10 | this.url = options.url; 11 | this.port = options.port; 12 | this.ffmpegPath = options.ffmpegPath; 13 | this.width = options.width; 14 | this.height = options.height; 15 | 16 | this.stream = child_process.spawn(this.ffmpegPath, genFfmpegFormatConfigs(this.url, this.port, this.width, this.height), { 17 | detached: false 18 | }); 19 | 20 | this.inputStreamStarted = true 21 | this.stream.stdout.on('data', (data) => { return this.emit('mpeg1data', data) }) 22 | this.stream.stderr.on('data', (data) => { return this.emit('ffmpegError', data) }) 23 | } 24 | } 25 | 26 | module.exports = Mpeg1Muxer 27 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | node_modules/ 21 | dist/ 22 | 23 | # ========================= 24 | # Operating System Files 25 | # ========================= 26 | 27 | # OSX 28 | # ========================= 29 | 30 | .DS_Store 31 | .AppleDouble 32 | .LSOverride 33 | 34 | # Thumbnails 35 | ._* 36 | 37 | # Files that might appear in the root of a volume 38 | .DocumentRevisions-V100 39 | .fseventsd 40 | .Spotlight-V100 41 | .TemporaryItems 42 | .Trashes 43 | .VolumeIcon.icns 44 | 45 | # Directories potentially created on remote AFP share 46 | .AppleDB 47 | .AppleDesktop 48 | Network Trash Folder 49 | Temporary Items 50 | .apdisk 51 | database.sqlite 52 | src/back/db/database.sqlite 53 | npm-debug.log.1732676291 54 | *.sqlite 55 | src/back/db/database.sqlite-journal 56 | *.sqlite 57 | *.sqlite 58 | *.sqlite 59 | *.sqlite 60 | src/back/db/database.sqlite 61 | src/back/db/database.sqlite 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_id": "node-rtsp-stream-jsmpeg@0.0.2", 3 | "_inBundle": false, 4 | "_location": "/node-rtsp-stream-jsmpeg", 5 | "_requested": { 6 | "type": "version", 7 | "registry": true, 8 | "raw": "node-rtsp-stream-jsmpeg@0.0.2", 9 | "name": "node-rtsp-stream-jsmpeg", 10 | "escapedName": "node-rtsp-stream-jsmpeg", 11 | "rawSpec": "1.0", 12 | "saveSpec": null, 13 | "fetchSpec": "0.0.2" 14 | }, 15 | "author": { 16 | "name": "Zveroslav" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/Zveroslav/node-rtsp-stream-jsmpeg/issues" 20 | }, 21 | "dependencies": { 22 | "jsmpeg": "^1.0.0", 23 | "ws": "^1.1.1", 24 | "http-shutdown": "^1.2" 25 | }, 26 | "description": "RTSP stream through WebSocket for jsmpeg decoder", 27 | "homepage": "", 28 | "licenses": [ 29 | { 30 | "type": "MIT", 31 | "url": "https://github.com/brentertz/scapegoat/blob/master/LICENSE-MIT" 32 | } 33 | ], 34 | "main": "index.js", 35 | "name": "node-rtsp-stream-jsmpeg", 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/Zveroslav/node-rtsp-stream-jsmpeg.git" 39 | }, 40 | "version": "0.0.2" 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-rtsp-stream-jsmpeg 2 | 3 | First of all, it's a based on [**node-rtsp-stream-es6**](https://github.com/Wifsimster/node-rtsp-stream-es6) and [**node-rtsp-stream**] 4 | 5 | ## Differences with the original modules 6 | 7 | - Code based on official documentation of https://github.com/phoboslab/jsmpeg for server side decoding video 8 | 9 | ## Description 10 | 11 | Stream any RTSP stream and output to [WebSocket](https://github.com/websockets/ws) for consumption by [jsmpeg](https://github.com/phoboslab/jsmpeg). 12 | HTML5 streaming video! 13 | 14 | ## Requirements 15 | 16 | You need to download and install [FFMPEG](https://ffmpeg.org/download.html) in server-side. 17 | 18 | ##Installation 19 | 20 | ``` 21 | npm i node-rtsp-stream-jsmpeg 22 | ``` 23 | 24 | ## Server 25 | 26 | ``` 27 | const Stream = require('node-rtsp-stream-jsmpeg') 28 | 29 | const options = { 30 | name: 'streamName', 31 | url: 'rtsp://184.72.239.149/vod/mp4:BigBuckBunny_115k.mov', 32 | wsPort: 3333 33 | } 34 | 35 | stream = new Stream(options) 36 | stream.start() 37 | ``` 38 | 39 | 40 | ## Client 41 | 42 | ``` 43 | 44 | 45 | 46 | 47 | DEMO node-rtsp-stream-jsmpeg 48 | 49 | 50 | 51 |
52 | 53 | 54 |
55 | 56 | 61 | 62 | ``` 63 | 64 | You can find a live stream JSMPEG example here : https://github.com/phoboslab/jsmpeg/blob/master/stream-example.html 65 | -------------------------------------------------------------------------------- /src/videoStream.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws'); 2 | const EventEmitter = require('events'); 3 | const http = require('http'); 4 | const STREAM_MAGIC_BYTES = "jsmp"; 5 | const Mpeg1Muxer = require('./mpeg1muxer'); 6 | 7 | class VideoStream extends EventEmitter { 8 | 9 | constructor(options) { 10 | super(options) 11 | this.name = options.name 12 | this.url = options.url 13 | this.width = options.width 14 | this.height = options.height 15 | this.wsPort = options.wsPort 16 | this.port = options.port || this.getPortForMiddlewareServer(options.wsPort) 17 | this.stream = 0 18 | this.httpServer = null 19 | this.server = null 20 | this.ffmpegPath = options.ffmpegPath || "ffmpeg" 21 | 22 | this.stream2Socket() 23 | } 24 | 25 | stream2Socket() { 26 | var socketServer = new WebSocket.Server({port: this.wsPort, perMessageDeflate: false}); 27 | 28 | socketServer.connectionCount = 0; 29 | socketServer.on('connection', function(socket, upgradeReq) { 30 | socketServer.connectionCount++; 31 | console.log( 32 | 'New WebSocket Connection: ', 33 | (upgradeReq || socket.upgradeReq).socket.remoteAddress, 34 | (upgradeReq || socket.upgradeReq).headers['user-agent'], 35 | '('+socketServer.connectionCount+' total)' 36 | ); 37 | socket.on('close', function(code, message){ 38 | socketServer.connectionCount--; 39 | console.log( 40 | 'Disconnected WebSocket ('+socketServer.connectionCount+' total)' 41 | ); 42 | }); 43 | }); 44 | socketServer.broadcast = function(data) { 45 | socketServer.clients.forEach(function each(client) { 46 | if (client.readyState === WebSocket.OPEN) { 47 | client.send(data); 48 | } 49 | }); 50 | }; 51 | 52 | 53 | // HTTP Server to accept incomming MPEG-TS Stream from ffmpeg 54 | var streamServer = http.createServer( function(request, response) { 55 | var params = request.url.substring(1).split('/'); 56 | 57 | if (params[0] !== 's1') { 58 | console.log( 59 | 'Failed Stream Connection: '+ request.socket.remoteAddress + ':' + 60 | request.socket.remotePort + ' - wrong secret.' 61 | ); 62 | response.end(); 63 | } 64 | response.socket.setTimeout(0); 65 | console.log( 66 | 'Stream Connected: ' + 67 | request.socket.remoteAddress + ':' + 68 | request.socket.remotePort 69 | ); 70 | 71 | request.on('data', function(data){ 72 | socketServer.broadcast(data); 73 | if (request.socket.recording) { 74 | request.socket.recording.write(data); 75 | } 76 | }); 77 | request.on('end',function(){ 78 | console.log('close'); 79 | if (request.socket.recording) { 80 | request.socket.recording.close(); 81 | } 82 | }); 83 | 84 | }); 85 | 86 | streamServer = require('http-shutdown')(streamServer); 87 | streamServer.listen(this.port); 88 | 89 | this.server = socketServer; 90 | this.httpServer = streamServer; 91 | } 92 | 93 | onSocketConnect(socket) { 94 | let streamHeader = Buffer.alloc(8) 95 | streamHeader.write(STREAM_MAGIC_BYTES) 96 | streamHeader.writeUInt16BE(this.width, 4) 97 | streamHeader.writeUInt16BE(this.height, 6) 98 | socket.send(streamHeader, { binary: true }) 99 | console.log(`New connection: ${this.name} - ${this.wsServer.clients.length} total`) 100 | return socket.on("close", function(code, message) { 101 | return console.log(`${this.name} disconnected - ${this.wsServer.clients.length} total`) 102 | }) 103 | } 104 | 105 | start() { 106 | this.mpeg1Muxer = new Mpeg1Muxer({ url: this.url, port: this.port, ffmpegPath: this.ffmpegPath, width: this.width, height: this.height }) 107 | this.mpeg1Muxer.on('mpeg1data', (data) => { return this.emit('camdata', data) }) 108 | 109 | let gettingInputData = false 110 | let gettingOutputData = false 111 | let inputData = [] 112 | let outputData = [] 113 | 114 | this.mpeg1Muxer.on('ffmpegError', (data) => { 115 | data = data.toString() 116 | if (data.indexOf('Input #') !== -1) { gettingInputData = true } 117 | if (data.indexOf('Output #') !== -1) { 118 | gettingInputData = false 119 | gettingOutputData = true 120 | } 121 | if (data.indexOf('frame') === 0) { gettingOutputData = false } 122 | if (gettingInputData) { 123 | inputData.push(data.toString()) 124 | let size = data.match(/\d+x\d+/) 125 | if (size != null) { 126 | size = size[0].split('x') 127 | if (this.width == null) { this.width = parseInt(size[0], 10) } 128 | if (this.height == null) { return this.height = parseInt(size[1], 10) } 129 | } 130 | } 131 | }) 132 | this.mpeg1Muxer.on('ffmpegError', (data) => { return global.process.stderr.write(data) }) 133 | return this 134 | } 135 | 136 | 137 | 138 | stop() { 139 | this.server.close(); 140 | this.httpServer.shutdown(function() {}) 141 | } 142 | 143 | getPortForMiddlewareServer(wsPort){ 144 | let lustNumberPort = wsPort.toString().slice(-1), 145 | middlewareServerPort = wsPort.toString().slice(0, -1) 146 | lustNumberPort = Number(lustNumberPort) + 1 < 10 ? Number(lustNumberPort) + 1 : Number(lustNumberPort) - 1 147 | return middlewareServerPort + lustNumberPort 148 | } 149 | } 150 | 151 | module.exports = VideoStream 152 | --------------------------------------------------------------------------------