Developing an application for streaming using Node.js and React

Original author: Waleed Ahmad
  • Transfer
The author of the material, the translation of which we publish today, says that he is working on an application that allows you to organize streaming broadcasting (streaming) of what is happening on the user's desktop. The application receives a stream in RTMP format from the streamer and converts it into an HLS stream, which can be played in viewers' browsers. This article will talk about how you can create your own streaming application using Node.js and React. If you are used to seeing the idea that interests you, immediately immerse yourself in the code, you can now look into this repository.

image



Web server development with basic authentication system


Let's create a simple web server based on Node.js, in which, using the passport.js library, a local user authentication strategy is implemented. We will use MongoDB as a permanent storage of information. We will work with the database using the Mongoose ODM library.

Initialize a new project:

$ npm init

Install the 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

In the project directory, create two folders - clientand server. The frontend code based on React will get into the folder client, and the backend code will be stored in the folder server. Now we are working in a folder server. Namely, we will use passport.js to create an authentication system. We have already installed the passport and passport-local modules. Before we describe the local user authentication strategy, we create a file app.jsand add the code to it that is needed to start a simple server. If you will run this code on your own, make sure that you have the MongoDB DBMS installed and that it runs as a service.

Here is the code for the file that is in the project at 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}!`));

We downloaded all the middleware necessary for the application, connected to MongoDB, configured the express session to use file storage. Storing sessions will allow them to be restored after a server reboot.

Now we describe passport.js strategies for organizing user registration and authentication. Create a folder in the serverfolder authand place the file in it passport.js. Here is what should be in the file 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, описывающая регистрацию пользователя
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, описывающая аутентификацию пользователя
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;

In addition, we need to describe the scheme for the user model (it will be called UserSchema). Create a folder in the serverfolder databaseand a file in it UserSchema.js.

Here is the file 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;

There UserSchemaare three methods. The method is generateHashdesigned to convert the password, presented in plain text, to a bcrypt hash. We use this method in the passport strategy to convert passwords entered by users into bcrypt hashes. The received password hashes are then stored in the database. The method validPasswordaccepts the password entered by the user and checks it by comparing its hash with the hash stored in the database. The method generateStreamKeygenerates unique strings that we will transfer to users as their streaming keys (stream keys) for RTMP clients.

Here is the file code server/database/Schema.js:

let mongoose = require('mongoose');
exports.User = mongoose.model('User', require('./UserSchema'));

Now that we have defined passport strategies, described the scheme, UserSchemaand created a model based on it, let's initialize passport to app.js.

Here is the code to add to the file server/app.js:

// Это нужно добавить в верхнюю часть файла, рядом с командами импорта
const passport = require('./auth/passport');
app.use(passport.initialize());
app.use(passport.session());

In addition, app.jsyou must register new routes. To do this, add the server/app.jsfollowing code:

// Регистрация маршрутов приложения
app.use('/login', require('./routes/login'));
app.use('/register', require('./routes/register'));

Create the file login.jsand register.jsfolder routes, which is located in the folder server. In these files we define a couple of the above routes and use the passport middleware to organize registration and user authentication.

Here is the file code 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;

Here is the file code 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;

We use the ejs templating engine. Add template files login.ejsand register.ejsa folder viewsthat is located in the folder server.

Here is the contents of the file server/views/login.ejs:



<% include header.ejs %>

<% include navbar.ejs %>
    

Login

    
    
        
            
                                                  <% if (errors.email.length) { %>                     <%= errors.email %>                 <% } %>             
            
                                                  <% if (errors.password.length) { %>                     <%= errors.password %>                 <% } %>             
            
                
                    Don't have an account? Register here.                 
            
                     
    
<% include footer.ejs %>

Here is what should be in the file server/views/register.ejs:



<% include header.ejs %>

<% include navbar.ejs %>
    

Register

    
    
        
            
                                                  <% if (errors.username.length) { %>                     <%= errors.username %>                 <% } %>             
            
                                                  <% if (errors.email.length) { %>                     <%= errors.email %>                 <% } %>             
            
                                              
            
                
                    Have an account? Login here.                 
            
                     
    
<% include footer.ejs %>

We can say we have finished work on the authentication system. Now we will start the creation of the next part of the project and configure the RTMP server.

Configure RTMP server


RTMP (Real-Time Messaging Protocol) is a protocol that was developed for high-performance transmission of video, audio and various data between the tape drive and the server. Twitch, Facebook, YouTube, and many other streaming sites accept RTMP streams and transcode them into HTTP streams (HLS format) before transferring these streams to their CDNs to ensure their high availability.

We use the node-media-server module - Node.js-implementation of the RTMP media server. This media server accepts RTMP streams and converts them to HLS / DASH using the ffmpeg multimedia framework. For the project to work successfully, ffmpeg must be installed on your system. If you are working on Linux and you already have ffmpeg installed, you can find out the path to it by running the following command from the terminal:

$ which ffmpeg
# /usr/bin/ffmpeg

To work with the node-media-server package, ffmpeg version 4.x is recommended. You can check the installed version of ffmpeg like this:

$ 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 do not have ffmpeg installed and you are running Ubuntu, you can install this framework by running the following command:

# Добавьте в систему PPA-репозиторий. Если провести установку без PPA, то установлен будет
# ffmpeg версии 3.x. 
$ sudo add-apt-repository ppa:jonathonf/ffmpeg-4
$ sudo apt install ffmpeg

If you work on Windows, you can download ffmpeg builds for Windows.

Add the configuration file to the 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;

Replace the property value ffmpegwith the path where ffmpeg is installed on your system. If you are working on Windows and downloaded the ffmpeg Windows assembly using the link above, be sure to add the extension to the file name .exe. Then the corresponding fragment of the above code will look like this:

const config = {
        ....
        trans: {
            ffmpeg: 'D:/ffmpeg/bin/ffmpeg.exe',
            ...
};

Now install node-media-server by running the following command:

$ npm install node-media-server --save

Create a serverfile in the folder media_server.js.

Here is the code to put in server/media_server.js:

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;

Using the object is NodeMediaServicequite simple. It provides the RTMP server and allows you to wait for connections. If the streaming key is invalid, the incoming connection can be rejected. We will handle the event of this object prePublish. In the next section, we will add prePublishadditional code to the event listener closure . It will allow you to reject incoming connections with invalid streaming keys. In the meantime, we will accept all incoming connections coming to the default RTMP port (1935). We only need to import the app.jsobject in the file node_media_serverand call its method run.

Add the following code to server/app.js:

// Добавьте это в верхней части app.js,
// туда же, где находятся остальные команды импорта
const node_media_server = require('./media_server');
// Вызовите метод run() в конце файла,
// там, где мы запускаем веб-сервер
node_media_server.run();

Download and install OBS (Open Broadcaster Software). Open the program settings window and go to the section Stream. Select Customin the field Serviceand enter rtmp://127.0.0.1:1935/livein the field Server. The field Stream Keycan be left blank. If the program does not allow you to save the settings without filling in this field, you can enter an arbitrary set of characters in it. Press the button Applyand the button OK. Click the button Start Streamingto start transferring your RTMP stream to your own local server.


Configuring OBS

Go to the terminal and look at what the media server displays there. You will see there information about the incoming stream and the logs of several event listeners.


Data output to the terminal by a media server based on Node.js A

media server provides access to an API that allows you to get a list of connected clients. In order to see this list, you can go to the address in the browser http://127.0.0.1:8888/api/streams. Later, we will use this API in a React application to display a list of broadcast users. Here's what you can see by accessing this API:

{
  "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"
}

Now the backend is almost ready. It is a working streaming server that supports HTTP, RTMP and HLS technologies. However, we have not yet created a system for checking incoming RTMP connections. It should allow us to ensure that the server accepts streams only from authenticated users. Add the following code to the event handler prePublishin the file server/media_server.js:

// Добавьте команду импорта в начало файла
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 {
                // что-то делаем
    });
});
const getStreamKeyFromStreamPath = (path) => {
    let parts = path.split('/');
    return parts[parts.length - 1];
};

In the closure, we query the database to find the user with the streaming key. If the key belongs to the user, we simply allow the user to connect to the server and publish their broadcast. Otherwise, we reject the incoming RTMP connection.

In the next section, we will create a simple client-side application based on React. It is needed in order to allow viewers to watch streaming broadcasts, as well as to allow streamers to generate and view their streaming keys.

Live streaming


Now go to the folder clients. Since we are going to create a React application, we will need a webpack. We also need loaders, which are used to translate JSX code into JavaScript code that browsers understand. Install the following 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

Add to the project, in its root directory, the configuration file for webpack ( webpack.config.js):

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 the file to the project client/index.js:

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(
        
            
        ,
        document.getElementById('root')
    );
}

Here is the contents of the file client/index.scss:

@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;
}

React router is used for routing. In the frontend, we also use bootstrap, and, for broadcasts, video.js. Now add the folder to the clientfolder components, and the file to it Root.js. Here is the contents of the file 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 (
            
                
                                          (                                              )}/>                      (                                              )}/>                      (                                              )}/>                 
            
}

Component React render containing three subcomponents . The component displays a list of translations. The component is responsible for displaying the video.js player. The component is responsible for creating an interface for working with streaming keys.

Create a 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 (
                
                    LIVE                                              
                                                     
                                                                                               {stream.username}                                                               
            );         });         return (             
                

Live Streams

                
                
                    {streams}                 
            
}

This is what the application page looks like.


Frontend of a streaming service

After the component is mounted , a call is made to the NMS API to get a list of clients connected to the system. The NMS API does not provide much information about users. In particular, we can obtain information about streaming keys from it, through which users are connected to an RTMP server. We will use these keys when generating queries to the database to obtain information about user accounts.

In the method, getStreamsInfowe execute the XHR request to /streams/info, but we have not yet created what is capable of responding to this request. Create a file server/routes/streams.jswith the following contents:

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 pass the flow information returned by the NMS API to the backend, in order to obtain information about connected clients.

We perform a database query to obtain a list of users whose streaming keys match those we received from the NMS API. We return the list in JSON format. Register the route in the file server/app.js:

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

As a result, we display a list of active broadcasts. This list contains the username and thumbnail. We will talk about how to create thumbnails for broadcasts at the end of the article. Thumbnails are tied to specific pages where HLS streams are played using video.js.

Create a 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 (
            
                
                    {this.state.stream ? (                         
                            
                    ) : ' Loading ... '}                 
            
}

When mounting the component, we get a user streaming key for initializing the HLS stream in the video.js player.


Turntable

Issue of streaming keys to those who are going to do streaming


Create a component file 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 (
            
                
                    

Streaming Key

                    
                    
                        
                            
{this.state.stream_key}
                        
                        
                                                     
                    
                
                
                    

How to Stream

                    
                    
                        
                            

                                You can use OBS or                                 XSplit to Live stream. If you're                                 using OBS, go to Settings > Stream and select Custom from service dropdown. Enter                                 rtmp://127.0.0.1:1935/live in server input field. Also, add your stream key.                                 Click apply to save.                             

                        
                    
                
            
}

In accordance with the local strategy of passport.js, we, if the user has successfully registered, create a new account for him with a unique streaming key. If the user visits the route /settings, he will be able to see his key. When mounting the component, we execute an XHR request to the backend to find out the existing user streaming key and display it in the component .

The user can generate a new key. To do this, click on the button Generate a new key. This action causes an XHR request to be made to the server to create a new key. The key is created, stored and returned. This allows you to show the new key to the user. In order for this mechanism to work - we need to define the routes GETand POSTfor /settings/stream_key. Create a fileserver/routes/settings.js with the following code:

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;

To generate unique strings, we use the shortid module.

Register new routes in server/app.js:

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


A page that allows streamers to work with their keys

Thumbnail generation for video streams


In component ( client/components/LiveStreams.js), we display thumbnails for streams streamed by streamers:

render() {
    let streams = this.state.live_streams.map((stream, index) => {
        return (
            
                LIVE                                      
                                             
                                                                               {stream.username}                                                   
        );     });     return (         
            

Live Streams

            
            
                {streams}             
        
}

We will generate thumbnails when connecting the stream to the server. We’ll take advantage of the cron task, which, every 5 seconds, creates new thumbnails for broadcast streams.

Add the following helper method to 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
};

We pass the streaming key to the method generateStreamThumbnail.

It starts a separate ffmpeg process, which creates an image based on the HLS stream. We will call this auxiliary method in the closure prePublishafter checking the streaming key ( server/media_server.js):

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);
    });
});

In order to generate fresh thumbnails, we run the cron task and call the helper method ( server/cron/thumbnails.js) described above :

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 task will be performed every 5 seconds. It will receive a list of active streams from the NMS API and generate thumbnails for each stream using a streaming key. The task needs to be imported into server/app.jsand called:

// Добавьте это в верхней части app.js,
const thumbnail_generator = require('./cron/thumbnails');
// Вызовите метод start() в конце файла
thumbnail_generator.start();

Summary


We have just completed the story of developing an application that allows you to organize streaming broadcasts. Perhaps, during the analysis of the code, some fragments of the system were undeservedly forgotten. If you are faced with something incomprehensible - take a look at this repository. If you find any error in the code, the author of the material asks you to inform him about it.

Here is a demonstration of the application.

Dear readers! How would you approach the development of a project similar to the one discussed in this article?


Also popular now: