├── .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 |
--------------------------------------------------------------------------------