├── .gitignore ├── client ├── css │ └── app.css ├── index.html └── js │ └── app.js ├── gulpfile.js ├── package.json ├── server └── app.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /client/css/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | background: #111; 3 | color: #eee; 4 | font-family: 'Ubuntu', sans-serif; 5 | font-weight: 300; 6 | } 7 | 8 | .container { 9 | max-width: 1200px; 10 | margin: 0 auto; 11 | } 12 | 13 | h1 { 14 | font-weight: normal; 15 | } 16 | 17 | pre { 18 | font-family: 'Ubuntu Mono'; 19 | white-space: pre-line; 20 | } -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var server = require('gulp-develop-server'); 3 | var gutil = require('gulp-util'); 4 | 5 | gulp.task('default', [ 'server:start', 'server:restart' ], function() { 6 | 7 | }); 8 | 9 | // run server 10 | gulp.task( 'server:start', function() { 11 | gutil.log('Starting sever'); 12 | server.listen( { path: './server/app.js' } ); 13 | }); 14 | 15 | gulp.task( 'server:restart', function(event) { 16 | gulp.watch('./server/**/*.js', server.restart ); 17 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-tcp-streaming-server", 3 | "version": "1.0.0", 4 | "description": "Streaming video over TCP and WebSockets", 5 | "main": "server/app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Kasper Moskwiak", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "debug": "^2.3.3", 13 | "gulp": "^3.9.1", 14 | "gulp-develop-server": "^0.5.2", 15 | "gulp-util": "^3.0.7" 16 | }, 17 | "dependencies": { 18 | "express": "^4.14.0", 19 | "websocket": "^1.0.23" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Stream over tcp and websockets.

13 | 14 |
15 | 16 |
17 |
18 | # Stream h264
19 | ffmpeg -f v4l2 -framerate 25 -video_size 640x480 -i /dev/video0  -vcodec libx264 -profile:v main -g 25 -r 25 -b:v 500k -keyint_min 250 -strict experimental -pix_fmt yuv420p -movflags empty_moov+default_base_moof -an -preset ultrafast -f mp4 tcp://localhost:9090
20 | 
21 | # Stream VP8
22 | ffmpeg -f v4l2 -framerate 25 -video_size 640x480 -i /dev/video0  -vcodec libvpx -b:v 3500k -r 25 -crf 10 -quality realtime -speed 16 -threads 8 -an -g 25 -f webm tcp://localhost:9090
23 |   
24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /client/js/app.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 'use strict'; 3 | 4 | var codecString = ''; 5 | /** 6 | * Set to whatever codec you are using 7 | */ 8 | 9 | // codecString = 'video/mp4; codecs="avc1.42C028"'; 10 | codecString = 'video/webm; codecs="vp8"'; 11 | // codecString = 'video/webm; codecs="vp9"'; 12 | 13 | 14 | 15 | var video = document.getElementById('video'); 16 | var mediaSource = new MediaSource(); 17 | video.src = window.URL.createObjectURL(mediaSource); 18 | var buffer = null; 19 | var queue = []; 20 | 21 | var bufferArray = []; 22 | 23 | function updateBuffer(){ 24 | if (queue.length > 0 && !buffer.updating) { 25 | buffer.appendBuffer(queue.shift()); 26 | } 27 | } 28 | 29 | /** 30 | * Mediasource 31 | */ 32 | function sourceBufferHandle(){ 33 | buffer = mediaSource.addSourceBuffer(codecString); 34 | buffer.mode = 'sequence'; 35 | 36 | buffer.addEventListener('update', function() { // Note: Have tried 'updateend' 37 | console.log('update'); 38 | updateBuffer(); 39 | }); 40 | 41 | buffer.addEventListener('updateend', function() { 42 | console.log('updateend'); 43 | updateBuffer(); 44 | }); 45 | 46 | initWS(); 47 | } 48 | 49 | mediaSource.addEventListener('sourceopen', sourceBufferHandle) 50 | 51 | function initWS(){ 52 | var ws = new WebSocket('ws://' + window.location.hostname + ':' + window.location.port, 'echo-protocol'); 53 | ws.binaryType = "arraybuffer"; 54 | 55 | ws.onopen = function(){ 56 | console.info('WebSocket connection initialized'); 57 | }; 58 | 59 | ws.onmessage = function (event) { 60 | console.info('Recived WS message.', event); 61 | 62 | if(typeof event.data === 'object'){ 63 | if (buffer.updating || queue.length > 0) { 64 | queue.push(event.data); 65 | } else { 66 | buffer.appendBuffer(event.data); 67 | video.play(); 68 | } 69 | } 70 | }; 71 | 72 | } 73 | 74 | 75 | })(); -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var net = require('net'); 3 | var path = require('path'); 4 | var express = require('express'); 5 | var app = express(); 6 | var WebSocketServer = require('websocket').server; 7 | 8 | var firstPacket = []; 9 | 10 | var options = { 11 | root: path.resolve(__dirname, '../client/'), 12 | httpPort: 8080, 13 | tcpPort: 9090 14 | }; 15 | 16 | // Send static files with express 17 | app.use(express.static(options.root)); 18 | 19 | /** All ws clients */ 20 | var wsClients = []; 21 | 22 | /** 23 | * Send index.html over http 24 | */ 25 | app.get('/', function(req, res){ 26 | res.sendFile('index.html', options); 27 | }); 28 | 29 | /** HTTP server */ 30 | var server = http.createServer(app); 31 | server.listen(options.httpPort); 32 | 33 | /** TCP server */ 34 | var tcpServer = net.createServer(function(socket) { 35 | socket.on('data', function(data){ 36 | 37 | /** 38 | * We are saving first packets of stream. These packets will be send to every new user. 39 | * This is hack. Video won't start whitout them. 40 | */ 41 | if(firstPacket.length < 3){ 42 | console.log('Init first packet', firstPacket.length); 43 | firstPacket.push(data); 44 | } 45 | 46 | /** 47 | * Send stream to all clients 48 | */ 49 | wsClients.map(function(client, index){ 50 | client.sendBytes(data); 51 | }); 52 | }); 53 | }); 54 | 55 | tcpServer.listen(options.tcpPort, 'localhost'); 56 | 57 | 58 | /** Websocet */ 59 | var wsServer = new WebSocketServer({ 60 | httpServer: server, 61 | autoAcceptConnections: false 62 | }); 63 | 64 | wsServer.on('request', function(request) { 65 | var connection = request.accept('echo-protocol', request.origin); 66 | console.log((new Date()) + ' Connection accepted.'); 67 | 68 | if(firstPacket.length){ 69 | /** 70 | * Every user will get beginnig of stream 71 | **/ 72 | firstPacket.map(function(packet, index){ 73 | connection.sendBytes(packet); 74 | }); 75 | 76 | } 77 | 78 | /** 79 | * Add this user to collection 80 | */ 81 | wsClients.push(connection); 82 | 83 | connection.on('close', function(reasonCode, description) { 84 | console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.'); 85 | }); 86 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Streaming Video over TCP and WebSockets with node.js 2 | 3 | This is experimental streaming server in node.js. Ingest stream is sent over TCP to server then it is redistributed to all clients over WebSockets. 4 | 5 | ## Ingest stream 6 | [FFMPEG](https://ffmpeg.org/) can be used to ingest stream. In this example I use [v4l2](https://trac.ffmpeg.org/wiki/Capture/Webcam) to caputre camera on linux. 7 | ``` 8 | -f v4l2 -framerate 25 -video_size 640x480 -i /dev/video0 9 | ``` 10 | ### VP8 (using libvpx) 11 | ``` 12 | ffmpeg -f v4l2 -framerate 25 -video_size 640x480 -i /dev/video0 -vcodec libvpx -b:v 3500k -r 25 -crf 10 -quality realtime -speed 16 -threads 8 -an -g 25 -f webm tcp://localhost:9090 13 | ``` 14 | ### H.264 (using libx264) 15 | To stream MP4 it needs to be [ISO BMFF](https://en.wikipedia.org/wiki/ISO_base_media_file_format) compatible, so `-movflags` is set to `empty_moov+default_base_moof`. 16 | 17 | ``` 18 | ffmpeg -f v4l2 -framerate 25 -video_size 640x480 -i /dev/video0 -vcodec libx264 -profile:v main -g 25 -r 25 -b:v 500k -keyint_min 250 -strict experimental -pix_fmt yuv420p -movflags empty_moov+default_base_moof -an -preset ultrafast -f mp4 tcp://localhost:9090 19 | ``` 20 | ## Stream flow 21 | 22 | ``` 23 | FFMPEG ---TCP---> NODE.JS Server -- WebSockets --> [client 0] MediaSource Video 24 | |-- WebSockets --> [client 1] MediaSource Video 25 | |-- WebSockets --> [client 2] MediaSource Video 26 | ``` 27 | Ingest stream is sent over TCP to node.js server. Every packet of stream is sent to clients using WebSockets. 28 | 29 | [MediaSource](https://developer.mozilla.org/en-US/docs/Web/API/MediaSource) is used to retrive video stream. 30 | 31 | ## How to use 32 | 33 | Install 34 | ``` 35 | npm install 36 | ``` 37 | 38 | Start application 39 | ``` 40 | gulp 41 | ``` 42 | In browser go to `localhost:8080` 43 | 44 | Stream video to `tcp://localhost:9090` 45 | ``` 46 | ffmpeg (...) tcp://localhost:9090 47 | ``` 48 | 49 | According to video codec set `codecString` in `client/js/app.js` line `9` to right value. 50 | 51 | You can customize ports in `server\app.js` lines `12` and `13`. 52 | 53 | ## 'First packet' hack :) 54 | 55 | There is little hack in lines `41-44` and `68-76`. Server stores beginning of stream in array which is sent to every new client. Every client will receive couple of start frames. 56 | 57 | Without that hack video won't start for users who start watching in the middle of stream. Perhaps there is solution in better ffmpeg setting. 58 | --------------------------------------------------------------------------------