I’ve been working on an app which lets you live stream your desktop. It takes in an RTMP stream from the broadcaster and transcodes it into HLS stream that viewers can watch in their web browsers. In this tutorial, I’ll show how you can build your own video streaming app with Nodejs. If you are ‘I want to directly dive into code’ kind of person, I’ve created this repository for you with setup instructions. I am going to break down this tutorial into five parts.

Web Server with Basic authentication

Setting up an RTMP server in Nodejs

Displaying live streams

Issuing streaming keys to broadcasters

Generating live stream thumbnails

Web Server with Basic Authentication

Let’s set up a basic node server with passport local strategy authentication. We will use MongoDB with Mongoose ODM for persistent storage. Initialize a new project by running

$ npm init 1 $ npm init

and install these dependencies.

$ npm install axios bcrypt-nodejs body-parser bootstrap config connect-ensure-login connect-flash cookie-parser ejs express express-session mongoose passport passport-local request session-file-store --save-dev 1 2 3 4 $ npm install axios bcrypt - nodejs body - parser bootstrap config connect - ensure - login connect - flash cookie - parser ejs express express - session mongoose passport passport - local request session - file - store -- save - dev

In your project directory, create two folders client and server. We will place our react components inside the client directory and backend code in the server directory. For this part, we will be working inside the server directory. We are using passport.js for authentication. We have already installed passport and passport-local modules. Before we define our local strategy for authenticating users, let’s create an app.js file and add the necessary code to run a basic web server. Make sure you have MongoDB installed on your system and running as a service.

server/app.js const express = require('express'), Session = require('express-session'), bodyParse = require('body-parser'), mongoose = require('mongoose'), middleware = require('connect-ensure-login'), FileStore = require('session-file-store')(Session), config = require('./config/default'), flash = require('connect-flash'), port = 3333, app = express(); mongoose.connect('mongodb://127.0.0.1/nodeStream' , { useNewUrlParser: true }); app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, './views')); app.use(express.static('public')); app.use(flash()); app.use(require('cookie-parser')()); app.use(bodyParse.urlencoded({extended: true})); app.use(bodyParse.json({extended: true})); app.use(Session({ store: new FileStore({ path : './server/sessions' }), secret: config.server.secret, maxAge : Date().now + (60 * 1000 * 30) })); app.get('*', middleware.ensureLoggedIn(), (req, res) => { res.render('index'); }); app.listen(port, () => console.log(`App listening on ${port}!`)); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 const express = require ( 'express' ) , Session = require ( 'express-session' ) , bodyParse = require ( 'body-parser' ) , mongoose = require ( 'mongoose' ) , middleware = require ( 'connect-ensure-login' ) , FileStore = require ( 'session-file-store' ) ( Session ) , config = require ( './config/default' ) , flash = require ( 'connect-flash' ) , port = 3333 , app = express ( ) ; mongoose . connect ( 'mongodb://127.0.0.1/nodeStream' , { useNewUrlParser : true } ) ; app . set ( 'view engine' , 'ejs' ) ; app . set ( 'views' , path . join ( __dirname , './views' ) ) ; app . use ( express . static ( 'public' ) ) ; app . use ( flash ( ) ) ; app . use ( require ( 'cookie-parser' ) ( ) ) ; app . use ( bodyParse . urlencoded ( { extended : true } ) ) ; app . use ( bodyParse . json ( { extended : true } ) ) ; app . use ( Session ( { store : new FileStore ( { path : './server/sessions' } ) , secret : config . server . secret , maxAge : Date ( ) . now + ( 60 * 1000 * 30 ) } ) ) ; app . get ( '*' , middleware . ensureLoggedIn ( ) , ( req , res ) = > { res . render ( 'index' ) ; } ) ; app . listen ( port , ( ) = > console . log ( ` App listening on $ { port } ! ` ) ) ;

We have bootstrapped all the necessary middlewares for our application, connected to MongoDB and configured express session to use the file storage for session persistence in case of the web server restart. Now we will define our passport strategies for registering and authenticating users. Create a folder named auth with a passport.js file in it and add the following code.

server/auth/passport.js const passport = require('passport'), LocalStrategy = require('passport-local').Strategy, User = require('../database/Schema').User, shortid = require('shortid'); passport.serializeUser( (user, cb) => { cb(null, user); }); passport.deserializeUser( (obj, cb) => { cb(null, obj); }); // Passport strategy for handling user registration passport.use('localRegister', new LocalStrategy({ usernameField: 'email', passwordField: 'password', passReqToCallback: true }, (req, email, password, done) => { User.findOne({$or: [{email: email}, {username: req.body.username}]}, (err, user) => { if (err) return done(err); if (user) { if (user.email === email) { req.flash('email', 'Email is already taken'); } if (user.username === req.body.username) { req.flash('username', 'Username is already taken'); } return done(null, false); } else { let user = new User(); user.email = email; user.password = user.generateHash(password); user.username = req.body.username; user.stream_key = shortid.generate(); user.save( (err) => { if (err) throw err; return done(null, user); }); } }); })); // Passport strategy for authenticating users passport.use('localLogin', new LocalStrategy({ usernameField: 'email', passwordField: 'password', passReqToCallback: true }, (req, email, password, done) => { User.findOne({'email': email}, (err, user) => { if (err) return done(err); if (!user) return done(null, false, req.flash('email', 'Email doesn\'t exist.')); if (!user.validPassword(password)) return done(null, false, req.flash('password', 'Oops! Wrong password.')); return done(null, user); }); })); module.exports = passport; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 const passport = require ( 'passport' ) , LocalStrategy = require ( 'passport-local' ) . Strategy , User = require ( '../database/Schema' ) . User , shortid = require ( 'shortid' ) ; passport . serializeUser ( ( user , cb ) = > { cb ( null , user ) ; } ) ; passport . deserializeUser ( ( obj , cb ) = > { cb ( null , obj ) ; } ) ; // Passport strategy for handling user registration passport . use ( 'localRegister' , new LocalStrategy ( { usernameField : 'email' , passwordField : 'password' , passReqToCallback : true } , ( req , email , password , done ) = > { User . findOne ( { $ or : [ { email : email } , { username : req . body . username } ] } , ( err , user ) = > { if ( err ) return done ( err ) ; if ( user ) { if ( user . email === email ) { req . flash ( 'email' , 'Email is already taken' ) ; } if ( user . username === req . body . username ) { req . flash ( 'username' , 'Username is already taken' ) ; } return done ( null , false ) ; } else { let user = new User ( ) ; user . email = email ; user . password = user . generateHash ( password ) ; user . username = req . body . username ; user . stream_key = shortid . generate ( ) ; user . save ( ( err ) = > { if ( err ) throw err ; return done ( null , user ) ; } ) ; } } ) ; } ) ) ; // Passport strategy for authenticating users passport . use ( 'localLogin' , new LocalStrategy ( { usernameField : 'email' , passwordField : 'password' , passReqToCallback : true } , ( req , email , password , done ) = > { User . findOne ( { 'email' : email } , ( err , user ) = > { if ( err ) return done ( err ) ; if ( ! user ) return done ( null , false , req . flash ( 'email' , 'Email doesn\'t exist.' ) ) ; if ( ! user . validPassword ( password ) ) return done ( null , false , req . flash ( 'password' , 'Oops! Wrong password.' ) ) ; return done ( null , user ) ; } ) ; } ) ) ; module . exports = passport ;

We also need to define the schema for our User model. Create a database directory with UserSchema.js file in it and add the following code.

server/database/UserSchema.js let mongoose = require('mongoose'), bcrypt = require('bcrypt-nodejs'), shortid = require('shortid'), Schema = mongoose.Schema; let UserSchema = new Schema({ username: String, email : String, password: String, stream_key : String, }); UserSchema.methods.generateHash = (password) => { return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null); }; UserSchema.methods.validPassword = function(password){ return bcrypt.compareSync(password, this.password); }; UserSchema.methods.generateStreamKey = () => { return shortid.generate(); }; module.exports = UserSchema; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 let mongoose = require ( 'mongoose' ) , bcrypt = require ( 'bcrypt-nodejs' ) , shortid = require ( 'shortid' ) , Schema = mongoose . Schema ; let UserSchema = new Schema ( { username : String , email : String , password : String , stream_key : String , } ) ; UserSchema . methods . generateHash = ( password ) = > { return bcrypt . hashSync ( password , bcrypt . genSaltSync ( 8 ) , null ) ; } ; UserSchema . methods . validPassword = function ( password ) { return bcrypt . compareSync ( password , this . password ) ; } ; UserSchema . methods . generateStreamKey = ( ) = > { return shortid . generate ( ) ; } ; module . exports = UserSchema ;

We have three methods on our User schema. generateHash method will convert plain text password to bcrypt hash. We are using it in our passport strategy for converting plain password strings to bcrypt hash before storing them in the database. validPassword method will take in a plain text password and validate it by comparing it to bcrypt hash stored in our database. generateStreamKey method will generate a unique string that we will issue to users as their streaming key for RTMP clients.

server/database/Schema.js let mongoose = require('mongoose'); exports.User = mongoose.model('User', require('./UserSchema')); 1 2 3 let mongoose = require ( 'mongoose' ) ; exports . User = mongoose . model ( 'User' , require ( './UserSchema' ) ) ;

Now that we have defined our passport strategies, added user schema and created a model from it, let’s initialize passport in app.js.

server/app.js // Add on the top next to imports const passport = require('./auth/passport'); app.use(passport.initialize()); app.use(passport.session()); 1 2 3 4 5 // Add on the top next to imports const passport = require ( './auth/passport' ) ; app . use ( passport . initialize ( ) ) ; app . use ( passport . session ( ) ) ;

Also, register these routes in the app.js file.

server/app.js // Register app routes app.use('/login', require('./routes/login')); app.use('/register', require('./routes/register')); 1 2 3 4 // Register app routes app . use ( '/login' , require ( './routes/login' ) ) ; app . use ( '/register' , require ( './routes/register' ) ) ;

Create a login.js and register.js file under routes directory where we will define these routes and use passport middleware for registration and authentication.

server/routes/login.js const express = require('express'), router = express.Router(), passport = require('passport'); router.get('/', require('connect-ensure-login').ensureLoggedOut(), (req, res) => { res.render('login', { user : null, errors : { email : req.flash('email'), password : req.flash('password') } }); }); router.post('/', passport.authenticate('localLogin', { successRedirect : '/', failureRedirect : '/login', failureFlash : true })); module.exports = router; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const express = require ( 'express' ) , router = express . Router ( ) , passport = require ( 'passport' ) ; router . get ( '/' , require ( 'connect-ensure-login' ) . ensureLoggedOut ( ) , ( req , res ) = > { res . render ( 'login' , { user : null , errors : { email : req . flash ( 'email' ) , password : req . flash ( 'password' ) } } ) ; } ) ; router . post ( '/' , passport . authenticate ( 'localLogin' , { successRedirect : '/' , failureRedirect : '/login' , failureFlash : true } ) ) ; module . exports = router ;

server/routes/register.js const express = require('express'), router = express.Router(), passport = require('passport'); router.get('/', require('connect-ensure-login').ensureLoggedOut(), (req, res) => { res.render('register', { user : null, errors : { username : req.flash('username'), email : req.flash('email') } }); }); router.post('/', require('connect-ensure-login').ensureLoggedOut(), passport.authenticate('localRegister', { successRedirect : '/', failureRedirect : '/register', failureFlash : true }) ); module.exports = router; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const express = require ( 'express' ) , router = express . Router ( ) , passport = require ( 'passport' ) ; router . get ( '/' , require ( 'connect-ensure-login' ) . ensureLoggedOut ( ) , ( req , res ) = > { res . render ( 'register' , { user : null , errors : { username : req . flash ( 'username' ) , email : req . flash ( 'email' ) } } ) ; } ) ; router . post ( '/' , require ( 'connect-ensure-login' ) . ensureLoggedOut ( ) , passport . authenticate ( 'localRegister' , { successRedirect : '/' , failureRedirect : '/register' , failureFlash : true } ) ) ; module . exports = router ;

We are using ejs templating engine. Add login.ejs and register.ejs template to views directory and add the following code.

server/views/login.ejs <!doctype html> <html lang="en"> <% include header.ejs %> <body> <% include navbar.ejs %> <div class="container app mt-5"> <h4>Login</h4> <hr class="my-4"> <div class="row"> <form action="/login" method="post" class="col-xs-12 col-sm-12 col-md-8 col-lg-6"> <div class="form-group"> <label>Email address</label> <input type="email" name="email" class="form-control" placeholder="Enter email" required> <% if (errors.email.length) { %> <small class="form-text text-danger"><%= errors.email %></small> <% } %> </div> <div class="form-group"> <label>Password</label> <input type="password" name="password" class="form-control" placeholder="Password" required> <% if (errors.password.length) { %> <small class="form-text text-danger"><%= errors.password %></small> <% } %> </div> <div class="form-group"> <div class="leader"> Don't have an account? Register <a href="/register">here</a>. </div> </div> <button type="submit" class="btn btn-dark btn-block">Login</button> </form> </div> </div> <% include footer.ejs %> </body> </html> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <!doctype html> <html lang = "en" > <% include header . ejs %> <body> <% include navbar . ejs %> <div class = "container app mt-5" > <h4> Login </h4> <hr class = "my-4" > <div class = "row" > <form action = "/login" method = "post" class = "col-xs-12 col-sm-12 col-md-8 col-lg-6" > <div class = "form-group" > <label> Email address </label> <input type = "email" name = "email" class = "form-control" placeholder = "Enter email" required > <% if ( errors . email . length ) { %> <small class = "form-text text-danger" > <%= errors . email %> </small> <% } %> </div> <div class = "form-group" > <label> Password </label> <input type = "password" name = "password" class = "form-control" placeholder = "Password" required > <% if ( errors . password . length ) { %> <small class = "form-text text-danger" > <%= errors . password %> </small> <% } %> </div> <div class = "form-group" > <div class = "leader" > Don't have an account? Register <a href = "/register" > here </a> . </div> </div> <button type = "submit" class = "btn btn-dark btn-block" > Login </button> </form> </div> </div> <% include footer . ejs %> </body> </html>

server/views/register.ejs <!doctype html> <html lang="en"> <% include header.ejs %> <body> <% include navbar.ejs %> <div class="container app mt-5"> <h4>Register</h4> <hr class="my-4"> <div class="row"> <form action="/register" method="post" class="col-xs-12 col-sm-12 col-md-8 col-lg-6"> <div class="form-group"> <label>Username</label> <input type="text" name="username" class="form-control" placeholder="Enter username" required> <% if (errors.username.length) { %> <small class="form-text text-danger"><%= errors.username %></small> <% } %> </div> <div class="form-group"> <label>Email address</label> <input type="email" name="email" class="form-control" placeholder="Enter email" required> <% if (errors.email.length) { %> <small class="form-text text-danger"><%= errors.email %></small> <% } %> </div> <div class="form-group"> <label>Password</label> <input type="password" name="password" class="form-control" placeholder="Password" required> </div> <div class="form-group"> <div class="leader"> Have an account? Login <a href="/login">here</a>. </div> </div> <button type="submit" class="btn btn-dark btn-block">Register</button> </form> </div> </div> <% include footer.ejs %> </body> </html> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 <!doctype html> <html lang = "en" > <% include header . ejs %> <body> <% include navbar . ejs %> <div class = "container app mt-5" > <h4> Register </h4> <hr class = "my-4" > <div class = "row" > <form action = "/register" method = "post" class = "col-xs-12 col-sm-12 col-md-8 col-lg-6" > <div class = "form-group" > <label> Username </label> <input type = "text" name = "username" class = "form-control" placeholder = "Enter username" required > <% if ( errors . username . length ) { %> <small class = "form-text text-danger" > <%= errors . username %> </small> <% } %> </div> <div class = "form-group" > <label> Email address </label> <input type = "email" name = "email" class = "form-control" placeholder = "Enter email" required > <% if ( errors . email . length ) { %> <small class = "form-text text-danger" > <%= errors . email %> </small> <% } %> </div> <div class = "form-group" > <label> Password </label> <input type = "password" name = "password" class = "form-control" placeholder = "Password" required > </div> <div class = "form-group" > <div class = "leader" > Have an account? Login <a href = "/login" > here </a> . </div> </div> <button type = "submit" class = "btn btn-dark btn-block" > Register </button> </form> </div> </div> <% include footer . ejs %> </body> </html>

We are pretty much done with authentication. Now we will move onto the next part of this tutorial and set up our RTMP server.

Setting up an RTMP server

Real-Time Messaging Protocol (RTMP) was designed for high-performance transmission of video, audio, and data between broadcaster and server. Twitch, Facebook, Youtube, and many other sites who offer live streaming accepts RTMP streams and transcodes then into HTTP streams (HLS format) before distributing them to their CDNs for high availability.

We are using node-media-server, a Node.js implementation of RTMP media server. It accepts RTMP streams and remux them to HLS/DASH using ffmpeg. Make sure you have ffmpeg installed on your system. If you are running Linux and already have ffmpeg installed, you can find your installation path by running this command from the terminal.

$ which ffmpeg # /usr/bin/ffmpeg 1 2 $ which ffmpeg # /usr/bin/ffmpeg

node-media-server recommends ffmpeg 4.x version. You can check your version by running this command.

$ ffmpeg --version # ffmpeg version 4.1.3-0york1~18.04 Copyright (c) 2000-2019 the # FFmpeg developers built with gcc 7 (Ubuntu 7.3.0-27ubuntu1~18.04) 1 2 3 $ ffmpeg -- version # ffmpeg version 4.1.3-0york1~18.04 Copyright (c) 2000-2019 the # FFmpeg developers built with gcc 7 (Ubuntu 7.3.0-27ubuntu1~18.04)

If you don’t have ffmpeg installed and running Ubuntu, you can install it by running these commands from the terminal.

# Add PPA. If you install without PPA, it will install # ffmpeg version 3.x. $ sudo add-apt-repository ppa:jonathonf/ffmpeg-4 $ sudo apt install ffmpeg 1 2 3 4 5 # Add PPA. If you install without PPA, it will install # ffmpeg version 3.x. $ sudo add - apt - repository ppa : jonathonf / ffmpeg - 4 $ sudo apt install ffmpeg

If you are running Windows, you can download ffmpeg windows builds. Add this config file to your project.

server/config/default.js const config = { server: { secret: 'kjVkuti2xAyF3JGCzSZTk0YWM5JhI9mgQW4rytXc' }, rtmp_server: { rtmp: { port: 1935, chunk_size: 60000, gop_cache: true, ping: 60, ping_timeout: 30 }, http: { port: 8888, mediaroot: './server/media', allow_origin: '*' }, trans: { ffmpeg: '/usr/bin/ffmpeg', tasks: [ { app: 'live', hls: true, hlsFlags: '[hls_time=2:hls_list_size=3:hls_flags=delete_segments]', dash: true, dashFlags: '[f=dash:window_size=3:extra_window_size=5]' } ] } } }; module.exports = config; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const config = { server : { secret : 'kjVkuti2xAyF3JGCzSZTk0YWM5JhI9mgQW4rytXc' } , rtmp_server : { rtmp : { port : 1935 , chunk_size : 60000 , gop_cache : true , ping : 60 , ping_timeout : 30 } , http : { port : 8888 , mediaroot : './server/media' , allow_origin : '*' } , trans : { ffmpeg : '/usr/bin/ffmpeg' , tasks : [ { app : 'live' , hls : true , hlsFlags : '[hls_time=2:hls_list_size=3:hls_flags=delete_segments]' , dash : true , dashFlags : '[f=dash:window_size=3:extra_window_size=5]' } ] } } } ; module . exports = config ;

Change ffmpeg value to your own ffmpeg installation path. If you’re running windows and downloaded Windows builds from the above link, make sure you add .exe extension add the end of your path.

const config = { .... trans: { ffmpeg: 'D:/ffmpeg/bin/ffmpeg.exe', ... } } }; 1 2 3 4 5 6 7 8 const config = { . . . . trans : { ffmpeg : 'D:/ffmpeg/bin/ffmpeg.exe' , . . . } } } ;

Also, install node-media-server by running

$ npm install node-media-server --save 1 $ npm install node - media - server -- save

Create media_server.js file and add this code.

const NodeMediaServer = require('node-media-server'), config = require('./config/default').rtmp_server; nms = new NodeMediaServer(config); nms.on('prePublish', async (id, StreamPath, args) => { let stream_key = getStreamKeyFromStreamPath(StreamPath); console.log('[NodeEvent on prePublish]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`); }); const getStreamKeyFromStreamPath = (path) => { let parts = path.split('/'); return parts[parts.length - 1]; }; module.exports = nms; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const NodeMediaServer = require ( 'node-media-server' ) , config = require ( './config/default' ) . rtmp_server ; nms = new NodeMediaServer ( config ) ; nms . on ( 'prePublish' , async ( id , StreamPath , args ) = > { let stream_key = getStreamKeyFromStreamPath ( StreamPath ) ; console . log ( '[NodeEvent on prePublish]' , ` id = $ { id } StreamPath = $ { StreamPath } args = $ { JSON . stringify ( args ) } ` ) ; } ) ; const getStreamKeyFromStreamPath = ( path ) = > { let parts = path . split ( '/' ) ; return parts [ parts . length - 1 ] ; } ; module . exports = nms ;

NodeMediaServer usage is pretty straight forward. It runs an RTMP server and lets you listen to connection events. You can reject an incoming connection if a streaming key is invalid. We will be listening to its prePublish event. We will add more code inside prePublish event listener closure to reject incoming connections with invalid streaming keys in the next part of this tutorial. For now, we are accepting all incoming connection on default 1935 RTMP port. Now, all we have to do is import nms object in the app.js file and call its run method.

server/app.js // Add this on the top of app.js file // next to all imports const node_media_server = require('./media_server'); // and call run() method at the end // file where we start our web server node_media_server.run(); 1 2 3 4 5 6 7 8 // Add this on the top of app.js file // next to all imports const node_media_server = require ( './media_server' ) ; // and call run() method at the end // file where we start our web server node_media_server . run ( ) ;

Download Open Broadcaster Software (OBS) and install it on your PC. Go to Settings > Stream. Select Custom service and enter rtmp://127.0.0.1:1935/live in Server input. You can leave Stream Key input empty or add a random string if it doesn’t let you save settings. Click Apply and Ok. Click start streaming button to broadcast your RTMP stream to your local server.

Head over to your terminal to see the output from your node media server. You will see an incoming stream with a few events listener logs.

Node media server exposes an API to list all connected clients. You can access it in your browser at http://127.0.0.1:8888/api/streams. Later on, we will use this API in our frontend React app to show live streaming users. You will see an output like this.

{ "live": { "0wBic-qV4": { "publisher": { "app": "live", "stream": "0wBic-qV4", "clientId": "WMZTQAEY", "connectCreated": "2019-05-12T16:13:05.759Z", "bytes": 33941836, "ip": "::ffff:127.0.0.1", "audio": { "codec": "AAC", "profile": "LC", "samplerate": 44100, "channels": 2 }, "video": { "codec": "H264", "width": 1920, "height": 1080, "profile": "High", "level": 4.2, "fps": 60 } }, "subscribers": [ { "app": "live", "stream": "0wBic-qV4", "clientId": "GNJ9JYJC", "connectCreated": "2019-05-12T16:13:05.985Z", "bytes": 33979083, "ip": "::ffff:127.0.0.1", "protocol": "rtmp" } ] } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 { "live" : { "0wBic-qV4" : { "publisher" : { "app" : "live" , "stream" : "0wBic-qV4" , "clientId" : "WMZTQAEY" , "connectCreated" : "2019-05-12T16:13:05.759Z" , "bytes" : 33941836 , "ip" : "::ffff:127.0.0.1" , "audio" : { "codec" : "AAC" , "profile" : "LC" , "samplerate" : 44100 , "channels" : 2 } , "video" : { "codec" : "H264" , "width" : 1920 , "height" : 1080 , "profile" : "High" , "level" : 4.2 , "fps" : 60 } } , "subscribers" : [ { "app" : "live" , "stream" : "0wBic-qV4" , "clientId" : "GNJ9JYJC" , "connectCreated" : "2019-05-12T16:13:05.985Z" , "bytes" : 33979083 , "ip" : "::ffff:127.0.0.1" , "protocol" : "rtmp" } ] } } }

Our backend is pretty much ready. Its a working HTTP/RTMP/HLS streaming server. However, we still need to validate incoming RTMP connections to ensure that only authenticated user’s streams are accepted. Add this code to your prePublish event listener closure.

server/media_server.js // Add import at the start of file const User = require('./database/Schema').User; nms.on('prePublish', async (id, StreamPath, args) => { let stream_key = getStreamKeyFromStreamPath(StreamPath); console.log('[NodeEvent on prePublish]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`); User.findOne({stream_key: stream_key}, (err, user) => { if (!err) { if (!user) { let session = nms.getSession(id); session.reject(); } else { // do stuff } } }); }); const getStreamKeyFromStreamPath = (path) => { let parts = path.split('/'); return parts[parts.length - 1]; }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // Add import at the start of file const User = require ( './database/Schema' ) . User ; nms . on ( 'prePublish' , async ( id , StreamPath , args ) = > { let stream_key = getStreamKeyFromStreamPath ( StreamPath ) ; console . log ( '[NodeEvent on prePublish]' , ` id = $ { id } StreamPath = $ { StreamPath } args = $ { JSON . stringify ( args ) } ` ) ; User . findOne ( { stream_key : stream_key } , ( err , user ) = > { if ( ! err ) { if ( ! user ) { let session = nms . getSession ( id ) ; session . reject ( ) ; } else { // do stuff } } } ) ; } ) ; const getStreamKeyFromStreamPath = ( path ) = > { let parts = path . split ( '/' ) ; return parts [ parts . length - 1 ] ; } ;

Inside closure, we are querying the database to find a user with the streaming key. If it belongs to a user, we would simply let them connect and publish their stream. Otherwise, we reject the incoming RTMP connection.

In the next part of this tutorial, we will build a basic React frontend to allow users to view live streams, generate and view their streaming keys.

Displaying Live Streams

For this part, we will be working in the client directory. Since its a react app, we will be using webpack and necessary loaders to transpile JSX into browser ready JavaScript. Install these modules.

$ npm install @babel/core @babel/preset-env @babel/preset-react babel-loader css-loader file-loader mini-css-extract-plugin node-sass sass-loader style-loader url-loader webpack webpack-cli react react-dom react-router-dom video.js jquery bootstrap history popper.js 1 2 3 4 5 $ npm install @ babel / core @ babel / preset - env @ babel / preset - react babel - loader css - loader file - loader mini - css - extract - plugin node - sass sass - loader style - loader url - loader webpack webpack - cli react react - dom react - router - dom video .js jquery bootstrap history popper .js

Add this webpack config to your project.

const path = require('path'); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const devMode = process.env.NODE_ENV !== 'production'; const webpack = require('webpack'); module.exports = { entry : './client/index.js', output : { filename : 'bundle.js', path : path.resolve(__dirname, 'public') }, module : { rules : [ { test: /\.s?[ac]ss$/, use: [ MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { url: false, sourceMap: true } }, { loader: 'sass-loader', options: { sourceMap: true } } ], }, { test: /\.js$/, exclude: /node_modules/, use: "babel-loader" }, { test: /\.woff($|\?)|\.woff2($|\?)|\.ttf($|\?)|\.eot($|\?)|\.svg($|\?)/, loader: 'url-loader' }, { test: /\.(png|jpg|gif)$/, use: [{ loader: 'file-loader', options: { outputPath: '/', }, }], }, ] }, devtool: 'source-map', plugins: [ new MiniCssExtractPlugin({ filename: "style.css" }), new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery' }) ], mode : devMode ? 'development' : 'production', watch : devMode, performance: { hints: process.env.NODE_ENV === 'production' ? "warning" : false }, }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 const path = require ( 'path' ) ; const MiniCssExtractPlugin = require ( "mini-css-extract-plugin" ) ; const devMode = process . env . NODE_ENV !== 'production' ; const webpack = require ( 'webpack' ) ; module . exports = { entry : './client/index.js' , output : { filename : 'bundle.js' , path : path . resolve ( __dirname , 'public' ) } , module : { rules : [ { test : / \ . s ? [ ac ] ss $ / , use : [ MiniCssExtractPlugin . loader , { loader : 'css-loader' , options : { url : false , sourceMap : true } } , { loader : 'sass-loader' , options : { sourceMap : true } } ] , } , { test : / \ . js $ / , exclude : / node_modules / , use : "babel-loader" } , { test : / \ . woff ( $ | \ ? ) | \ . woff2 ( $ | \ ? ) | \ . ttf ( $ | \ ? ) | \ . eot ( $ | \ ? ) | \ . svg ( $ | \ ? ) / , loader : 'url-loader' } , { test : / \ . ( png | jpg | gif ) $ / , use : [ { loader : 'file-loader' , options : { outputPath : '/' , } , } ] , } , ] } , devtool : 'source-map' , plugins : [ new MiniCssExtractPlugin ( { filename : "style.css" } ) , new webpack . ProvidePlugin ( { $ : 'jquery' , jQuery : 'jquery' } ) ] , mode : devMode ? 'development' : 'production' , watch : devMode , performance : { hints : process . env . NODE_ENV === 'production' ? "warning" : false } , } ;

Add an index.js file with the following code.

import React from "react"; import ReactDOM from 'react-dom'; import {BrowserRouter} from 'react-router-dom'; import 'bootstrap'; require('./index.scss'); import Root from './components/Root.js'; if(document.getElementById('root')){ ReactDOM.render( <BrowserRouter> <Root/> </BrowserRouter>, document.getElementById('root') ); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import React from "react" ; import ReactDOM from 'react-dom' ; import { BrowserRouter } from 'react-router-dom' ; import 'bootstrap' ; require ( './index.scss' ) ; import Root from './components/Root.js' ; if ( document . getElementById ( 'root' ) ) { ReactDOM . render ( < BrowserRouter > < Root / > < / BrowserRouter > , document . getElementById ( 'root' ) ) ; }

@import '~bootstrap/dist/css/bootstrap.css'; @import '~video.js/dist/video-js.css'; @import url('https://fonts.googleapis.com/css?family=Dosis'); html,body{ font-family: 'Dosis', sans-serif; } 1 2 3 4 5 6 7 8 @import '~bootstrap/dist/css/bootstrap.css' ; @import '~video.js/dist/video-js.css' ; @import url ( 'https://fonts.googleapis.com/css?family=Dosis' ) ; html,body { font-family : 'Dosis' , sans-serif ; }

We are using react-router for routing and bootstrap on the frontend along with video.js for displaying live streams. Add components directory with Root.js file in it and add the following code.

client/components/Root.js import React from "react"; import {Router, Route} from 'react-router-dom'; import Navbar from './Navbar'; import LiveStreams from './LiveStreams'; import Settings from './Settings'; import VideoPlayer from './VideoPlayer'; const customHistory = require("history").createBrowserHistory(); export default class Root extends React.Component { constructor(props){ super(props); } render(){ return ( <Router history={customHistory} > <div> <Navbar/> <Route exact path="/" render={props => ( <LiveStreams {...props} /> )}/> <Route exact path="/stream/:username" render={(props) => ( <VideoPlayer {...props}/> )}/> <Route exact path="/settings" render={props => ( <Settings {...props} /> )}/> </div> </Router> ) } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import React from "react" ; import { Router , Route } from 'react-router-dom' ; import Navbar from './Navbar' ; import LiveStreams from './LiveStreams' ; import Settings from './Settings' ; import VideoPlayer from './VideoPlayer' ; const customHistory = require ( "history" ) . createBrowserHistory ( ) ; export default class Root extends React . Component { constructor ( props ) { super ( props ) ; } render ( ) { return ( < Router history = { customHistory } > < div > < Navbar / > < Route exact path = "/" render = { props = > ( < LiveStreams { . . . props } / > ) } /> <Route exact path="/s tream / : username " render={(props) => ( <VideoPlayer {...props}/> )}/> <Route exact path=" / settings " render = { props = > ( < Settings { . . . props } / > ) } / > < / div > < / Router > ) } }

<Root/> component renders a react <Router/> to hold three sub <Route/> components. <LiveStreams/> component will render all the live streams. <VideoPlayer/> will render video.js player components. <Settings/> component will provide an interface for generating a new streaming key.

Create LiveStreams.js component.

client/components/LiveStreams.js import React from 'react'; import axios from 'axios'; import {Link} from 'react-router-dom'; import './LiveStreams.scss'; import config from '../../server/config/default'; export default class Navbar extends React.Component { constructor(props) { super(props); this.state = { live_streams: [] } } componentDidMount() { this.getLiveStreams(); } getLiveStreams() { axios.get('http://127.0.0.1:' + config.rtmp_server.http.port + '/api/streams') .then(res => { let streams = res.data; if (typeof (streams['live'] !== 'undefined')) { this.getStreamsInfo(streams['live']); } }); } getStreamsInfo(live_streams) { axios.get('/streams/info', { params: { streams: live_streams } }).then(res => { this.setState({ live_streams: res.data }, () => { console.log(this.state); }); }); } render() { let streams = this.state.live_streams.map((stream, index) => { return ( <div className="stream col-xs-12 col-sm-12 col-md-3 col-lg-4" key={index}> <span className="live-label">LIVE</span> <Link to={'/stream/' + stream.username}> <div className="stream-thumbnail"> <img src={'/thumbnails/' + stream.stream_key + '.png'}/> </div> </Link> <span className="username"> <Link to={'/stream/' + stream.username}> {stream.username} </Link> </span> </div> ); }); return ( <div className="container mt-5"> <h4>Live Streams</h4> <hr className="my-4"/> <div className="streams row"> {streams} </div> </div> ) } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 import React from 'react' ; import axios from 'axios' ; import { Link } from 'react-router-dom' ; import './LiveStreams.scss' ; import config from '../../server/config/default' ; export default class Navbar extends React . Component { constructor ( props ) { super ( props ) ; this . state = { live_streams : [ ] } } componentDidMount ( ) { this . getLiveStreams ( ) ; } getLiveStreams ( ) { axios . get ( 'http://127.0.0.1:' + config . rtmp_server . http . port + '/api/streams' ) . then ( res = > { let streams = res . data ; if ( typeof ( streams [ 'live' ] !== 'undefined' ) ) { this . getStreamsInfo ( streams [ 'live' ] ) ; } } ) ; } getStreamsInfo ( live_streams ) { axios . get ( '/streams/info' , { params : { streams : live _ streams } } ) . then ( res = > { this . setState ( { live_streams : res . data } , ( ) = > { console . log ( this . state ) ; } ) ; } ) ; } render ( ) { let streams = this . state . live_streams . map ( ( stream , index ) = > { return ( < div className = "stream col-xs-12 col-sm-12 col-md-3 col-lg-4" key = { index } > < span className = "live-label" > LIVE < /span> <Link to={'/s tream / ' + stream.username}> <div className="stream-thumbnail"> <img src={' / thumbnails / ' + stream.stream_key + ' . png '}/> </div> </Link> <span className="username"> <Link to={' / stream / ' + stream . username } > { stream . username } < /Link> </s pan > < / div > ) ; } ) ; return ( < div className = "container mt-5" > < h4 > Live Streams < / h4 > < hr className = "my-4" / > < div className = "streams row" > { streams } < / div > < / div > ) } }

After our component mounts, we are making a call to NMS API to retrieve all the connected clients. NMS API does not have much information about the user other than their streaming key through which they are connected to our RTMP server. We will use the streaming key to query our database to get users records. In getStreamsInfo method, we are making an XHR request to /streams/info which we have not yet defined. Create a server/routes/streams.js file and add the following code to it. We will pass on the streams returned from the NMS API to our backend to retrieve information about connected clients.

const express = require('express'), router = express.Router(), User = require('../database/Schema').User; router.get('/info', require('connect-ensure-login').ensureLoggedIn(), (req, res) => { if(req.query.streams){ let streams = JSON.parse(req.query.streams); let query = {$or: []}; for (let stream in streams) { if (!streams.hasOwnProperty(stream)) continue; query.$or.push({stream_key : stream}); } User.find(query,(err, users) => { if (err) return; if (users) { res.json(users); } }); } }); module.exports = router; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const express = require ( 'express' ) , router = express . Router ( ) , User = require ( '../database/Schema' ) . User ; router . get ( '/info' , require ( 'connect-ensure-login' ) . ensureLoggedIn ( ) , ( req , res ) = > { if ( req . query . streams ) { let streams = JSON . parse ( req . query . streams ) ; let query = { $ or : [ ] } ; for ( let stream in streams ) { if ( ! streams . hasOwnProperty ( stream ) ) continue ; query . $ or . push ( { stream_key : stream } ) ; } User . find ( query , ( err , users ) = > { if ( err ) return ; if ( users ) { res . json ( users ) ; } } ) ; } } ) ; module . exports = router ;

We are querying the database to select all the users with matched streaming keys that we retrieved from NMS API and return them as a JSON response. Register this route in the app.js file.

server/app.js app.use('/streams', require('./routes/streams')); 1 app . use ( '/streams' , require ( './routes/streams' ) ) ;

In the end, we are rendering live streams with username and thumbnails. We will generate thumbnails for our streams in the last part of this tutorial. These thumbnails are linked to the individual pages where HLS streams are played inside a video.js player component. Create VideoPlayer.js component.

client/components/VideoPlayer.js import React from 'react'; import videojs from 'video.js' import axios from 'axios'; import config from '../../server/config/default'; export default class VideoPlayer extends React.Component { constructor(props) { super(props); this.state = { stream: false, videoJsOptions: null } } componentDidMount() { axios.get('/user', { params: { username: this.props.match.params.username } }).then(res => { this.setState({ stream: true, videoJsOptions: { autoplay: false, controls: true, sources: [{ src: 'http://127.0.0.1:' + config.rtmp_server.http.port + '/live/' + res.data.stream_key + '/index.m3u8', type: 'application/x-mpegURL' }], fluid: true, } }, () => { this.player = videojs(this.videoNode, this.state.videoJsOptions, function onPlayerReady() { console.log('onPlayerReady', this) }); }); }) } componentWillUnmount() { if (this.player) { this.player.dispose() } } render() { return ( <div className="row"> <div className="col-xs-12 col-sm-12 col-md-10 col-lg-8 mx-auto mt-5"> {this.state.stream ? ( <div data-vjs-player> <video ref={node => this.videoNode = node} className="video-js vjs-big-play-centered"/> </div> ) : ' Loading ... '} </div> </div> ) } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 import React from 'react' ; import videojs from 'video.js' import axios from 'axios' ; import config from '../../server/config/default' ; export default class VideoPlayer extends React . Component { constructor ( props ) { super ( props ) ; this . state = { stream : false , videoJsOptions : null } } componentDidMount ( ) { axios . get ( '/user' , { params : { username : this . props . match . params . username } } ) . then ( res = > { this . setState ( { stream : true , videoJsOptions : { autoplay : false , controls : true , sources : [ { src : 'http://127.0.0.1:' + config . rtmp_server . http . port + '/live/' + res . data . stream_key + '/index.m3u8' , type : 'application/x-mpegURL' } ] , fluid : true , } } , ( ) = > { this . player = videojs ( this . videoNode , this . state . videoJsOptions , function onPlayerReady ( ) { console . log ( 'onPlayerReady' , this ) } ) ; } ) ; } ) } componentWillUnmount ( ) { if ( this . player ) { this . player . dispose ( ) } } render ( ) { return ( < div className = "row" > < div className = "col-xs-12 col-sm-12 col-md-10 col-lg-8 mx-auto mt-5" > { this . state . stream ? ( < div data - vjs - player > < video ref = { node = > this . videoNode = node } className = "video-js vjs-big-play-centered" / > < / div > ) : ' Loading ... ' } < / div > < / div > ) } }

On component mount, we retrieve the user’s streaming key to initiate an HLS stream inside video.js player.

Issuing streaming keys to broadcasters

Create Settings.js component.

client/components/Settings.js import React from 'react'; import axios from 'axios'; export default class Navbar extends React.Component { constructor(props){ super(props); this.state = { stream_key : '' }; this.generateStreamKey = this.generateStreamKey.bind(this); } componentDidMount() { this.getStreamKey(); } generateStreamKey(e){ axios.post('/settings/stream_key') .then(res => { this.setState({ stream_key : res.data.stream_key }); }) } getStreamKey(){ axios.get('/settings/stream_key') .then(res => { this.setState({ stream_key : res.data.stream_key }); }) } render() { return ( <React.Fragment> <div className="container mt-5"> <h4>Streaming Key</h4> <hr className="my-4"/> <div className="col-xs-12 col-sm-12 col-md-8 col-lg-6"> <div className="row"> <h5>{this.state.stream_key}</h5> </div> <div className="row"> <button className="btn btn-dark mt-2" onClick={this.generateStreamKey}> Generate a new key </button> </div> </div> </div> <div className="container mt-5"> <h4>How to Stream</h4> <hr className="my-4"/> <div className="col-12"> <div className="row"> <p> You can use <a target="_blank" href="https://obsproject.com/">OBS</a> or <a target="_blank" href="https://www.xsplit.com/">XSplit</a> to Live stream. If you're using OBS, go to Settings > Stream and select Custom from service dropdown. Enter <b>rtmp://127.0.0.1:1935/live</b> in server input field. Also, add your stream key. Click apply to save. </p> </div> </div> </div> </React.Fragment> ) } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 import React from 'react' ; import axios from 'axios' ; export default class Navbar extends React . Component { constructor ( props ) { super ( props ) ; this . state = { stream_key : '' } ; this . generateStreamKey = this . generateStreamKey . bind ( this ) ; } componentDidMount ( ) { this . getStreamKey ( ) ; } generateStreamKey ( e ) { axios . post ( '/settings/stream_key' ) . then ( res = > { this . setState ( { stream_key : res . data . stream _ key } ) ; } ) } getStreamKey ( ) { axios . get ( '/settings/stream_key' ) . then ( res = > { this . setState ( { stream_key : res . data . stream _ key } ) ; } ) } render ( ) { return ( < React . Fragment > < div className = "container mt-5" > < h4 > Streaming Key < / h4 > < hr className = "my-4" / > < div className = "col-xs-12 col-sm-12 col-md-8 col-lg-6" > < div className = "row" > < h5 > { this . state . stream_key } < / h5 > < / div > < div className = "row" > < button className = "btn btn-dark mt-2" onClick = { this . generateStreamKey } > Generate a new key < / button > < / div > < / div > < / div > < div className = "container mt-5" > < h4 > How to Stream < / h4 > < hr className = "my-4" / > < div className = "col-12" > < div className = "row" > < p > You can use < a target = "_blank" href = "https://obsproject.com/" > OBS < / a > or < a target = "_blank" href = "https://www.xsplit.com/" > XSplit < / a > to Live stream . If you ' re using OBS , go to Settings > Stream and select Custom from service dropdown . Enter < b > rtmp : //127.0.0.1:1935/live</b> in server input field. Also, add your stream key. Click apply to save . < / p > < / div > < / div > < / div > < / React . Fragment > ) } }

Inside our passport’s local strategy, when a user successfully registers, we create a new user record with a unique streaming key. If a user visits /settings route, they will be able to view their existing key. When components mounts, we make an XHR call to the backend to retrieve user’s existing streaming key and render it inside our <Settings/> component.

Users can generate a new key by clicking Generate a new key button which makes an XHR call to the backend to create a new key, save it to user collection and also return it so that it can be rendered inside the component. We need to define both GET and POST /settings/stream_key routes. Create a server/routes/settings.js file and add the following code.

server/routes/settings.js const express = require('express'), router = express.Router(), User = require('../database/Schema').User, shortid = require('shortid'); router.get('/stream_key', require('connect-ensure-login').ensureLoggedIn(), (req, res) => { User.findOne({email: req.user.email}, (err, user) => { if (!err) { res.json({ stream_key: user.stream_key }) } }); }); router.post('/stream_key', require('connect-ensure-login').ensureLoggedIn(), (req, res) => { User.findOneAndUpdate({ email: req.user.email }, { stream_key: shortid.generate() }, { upsert: true, new: true, }, (err, user) => { if (!err) { res.json({ stream_key: user.stream_key }) } }); }); module.exports = router; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 const express = require ( 'express' ) , router = express . Router ( ) , User = require ( '../database/Schema' ) . User , shortid = require ( 'shortid' ) ; router . get ( '/stream_key' , require ( 'connect-ensure-login' ) . ensureLoggedIn ( ) , ( req , res ) = > { User . findOne ( { email : req . user . email } , ( err , user ) = > { if ( ! err ) { res . json ( { stream_key : user . stream _ key } ) } } ) ; } ) ; router . post ( '/stream_key' , require ( 'connect-ensure-login' ) . ensureLoggedIn ( ) , ( req , res ) = > { User . findOneAndUpdate ( { email : req . user . email } , { stream_key : shortid . generate ( ) } , { upsert : true , new : true , } , ( err , user ) = > { if ( ! err ) { res . json ( { stream_key : user . stream _ key } ) } } ) ; } ) ; module . exports = router ;

We using shortid module for generating unique strings. Register these routes in the app.js file.

server/app.js app.use('/settings', require('./routes/settings')); 1 app . use ( '/settings' , require ( './routes/settings' ) ) ;

Generating Live Stream Thumbnails

In <LiveStreams/> components, we are displaying thumbnail images for live streams.

render() { let streams = this.state.live_streams.map((stream, index) => { return ( <div className="stream col-xs-12 col-sm-12 col-md-3 col-lg-4" key={index}> <span className="live-label">LIVE</span> <Link to={'/stream/' + stream.username}> <div className="stream-thumbnail"> <img src={'/thumbnails/' + stream.stream_key + '.png'}/> </div> </Link> <span className="username"> <Link to={'/stream/' + stream.username}> {stream.username} </Link> </span> </div> ); }); return ( <div className="container mt-5"> <h4>Live Streams</h4> <hr className="my-4"/> <div className="streams row"> {streams} </div> </div> ) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 render ( ) { let streams = this . state . live_streams . map ( ( stream , index ) = > { return ( < div className = "stream col-xs-12 col-sm-12 col-md-3 col-lg-4" key = { index } > < span className = "live-label" > LIVE < /span> <Link to={'/s tream / ' + stream.username}> <div className="stream-thumbnail"> <img src={' / thumbnails / ' + stream.stream_key + ' . png '}/> </div> </Link> <span className="username"> <Link to={' / stream / ' + stream . username } > { stream . username } < /Link> </s pan > < / div > ) ; } ) ; return ( < div className = "container mt-5" > < h4 > Live Streams < / h4 > < hr className = "my-4" / > < div className = "streams row" > { streams } < / div > < / div > ) }

We will be generating these thumbnails whenever a new stream connects to our server. We will run a cron job to generate new thumbnails for live streams every 5 seconds. Add this helper method inside server/helpers/helpers.js.

server/helpers/helpers.js const spawn = require('child_process').spawn, config = require('../config/default'), cmd = config.rtmp_server.trans.ffmpeg; const generateStreamThumbnail = (stream_key) => { const args = [ '-y', '-i', 'http://127.0.0.1:8888/live/'+stream_key+'/index.m3u8', '-ss', '00:00:01', '-vframes', '1', '-vf', 'scale=-2:300', 'server/thumbnails/'+stream_key+'.png', ]; spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref(); }; module.exports = { generateStreamThumbnail : generateStreamThumbnail }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const spawn = require ( 'child_process' ) . spawn , config = require ( '../config/default' ) , cmd = config . rtmp_server . trans . ffmpeg ; const generateStreamThumbnail = ( stream_key ) = > { const args = [ '-y' , '-i' , 'http://127.0.0.1:8888/live/' + stream_key + '/index.m3u8' , '-ss' , '00:00:01' , '-vframes' , '1' , '-vf' , 'scale=-2:300' , 'server/thumbnails/' + stream_key + '.png' , ] ; spawn ( cmd , args , { detached : true , stdio : 'ignore' } ) . unref ( ) ; } ; module . exports = { generateStreamThumbnail : generateStreamThumbnail } ;

We are passing the streaming key to generateStreamThumbnail. It spawns a detached ffmpeg process to generate thumbnail image from HLS stream. We will call this helper method inside prePublish closure after validating the streaming key.

nms.on('prePublish', async (id, StreamPath, args) => { let stream_key = getStreamKeyFromStreamPath(StreamPath); console.log('[NodeEvent on prePublish]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`); User.findOne({stream_key: stream_key}, (err, user) => { if (!err) { if (!user) { let session = nms.getSession(id); session.reject(); } else { helpers.generateStreamThumbnail(stream_key); } } }); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 nms . on ( 'prePublish' , async ( id , StreamPath , args ) = > { let stream_key = getStreamKeyFromStreamPath ( StreamPath ) ; console . log ( '[NodeEvent on prePublish]' , ` id = $ { id } StreamPath = $ { StreamPath } args = $ { JSON . stringify ( args ) } ` ) ; User . findOne ( { stream_key : stream_key } , ( err , user ) = > { if ( ! err ) { if ( ! user ) { let session = nms . getSession ( id ) ; session . reject ( ) ; } else { helpers . generateStreamThumbnail ( stream_key ) ; } } } ) ; } ) ;

To generate fresh thumbnails, we will run a cron job and call this helper method from it.

const CronJob = require('cron').CronJob, request = require('request'), helpers = require('../helpers/helpers'), config = require('../config/default'), port = config.rtmp_server.http.port; const job = new CronJob('*/5 * * * * *', function () { request .get('http://127.0.0.1:' + port + '/api/streams', function (error, response, body) { let streams = JSON.parse(body); if (typeof (streams['live'] !== undefined)) { let live_streams = streams['live']; for (let stream in live_streams) { if (!live_streams.hasOwnProperty(stream)) continue; helpers.generateStreamThumbnail(stream); } } }); }, null, true); module.exports = job; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const CronJob = require ( 'cron' ) . CronJob , request = require ( 'request' ) , helpers = require ( '../helpers/helpers' ) , config = require ( '../config/default' ) , port = config . rtmp_server . http . port ; const job = new CronJob ( '*/5 * * * * *' , function ( ) { request . get ( 'http://127.0.0.1:' + port + '/api/streams' , function ( error , response , body ) { let streams = JSON . parse ( body ) ; if ( typeof ( streams [ 'live' ] !== undefined ) ) { let live_streams = streams [ 'live' ] ; for ( let stream in live_streams ) { if ( ! live_streams . hasOwnProperty ( stream ) ) continue ; helpers . generateStreamThumbnail ( stream ) ; } } } ) ; } , null , true ) ; module . exports = job ;

This cron job will execute every 5 seconds, retrieve active streams from NMS API and generate thumbnails for each stream using the streaming key. Import this job inside the app.js file and run it.

// Add import at the start of file const thumbnail_generator = require('./cron/thumbnails'); // Call start method at the end of file thumbnail_generator.start(); 1 2 3 4 5 // Add import at the start of file const thumbnail_generator = require ( './cron/thumbnails' ) ; // Call start method at the end of file thumbnail_generator . start ( ) ;

Our real-time live streaming app is ready. I might have missed some details in this tutorial, but you can access complete code in this repository. If you run into an issue, please report it by creating a new issue in the repository so that it can be addressed. Setup and usage instructions are available in the repository’s readme file.

Here’s a demo of our app.