Développement d'une application de streaming à l'aide de Node.js et React

L'auteur du matériel, dont nous publions la traduction aujourd'hui, dit qu'il travaille sur une application qui vous permet d'organiser la diffusion en continu (streaming) de ce qui se passe sur le bureau de l'utilisateur.

image

L'application reçoit un flux au format RTMP du streamer et le convertit en un flux HLS, qui peut ĂȘtre lu dans les navigateurs des tĂ©lĂ©spectateurs. Cet article explique comment crĂ©er votre propre application de streaming Ă  l'aide de Node.js et React. Si vous avez l'habitude de voir l'idĂ©e qui vous intĂ©resse, plongez-vous immĂ©diatement dans le code, vous pouvez maintenant regarder dans ce rĂ©fĂ©rentiel.

Développement de serveur Web avec systÚme d'authentification de base


CrĂ©ons un serveur Web simple basĂ© sur Node.js, dans lequel, Ă  l'aide de la bibliothĂšque passport.js, une stratĂ©gie d'authentification des utilisateurs locaux est mise en Ɠuvre. Nous utiliserons MongoDB comme stockage permanent d'informations. Nous travaillerons avec la base de donnĂ©es en utilisant la bibliothĂšque Mongoose ODM.

Initialisez un nouveau projet:

$ npm init 

Installez les dépendances:

 $ 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 

Dans le rĂ©pertoire du projet, crĂ©ez deux dossiers - client et server . Le code frontal basĂ© sur React se retrouvera dans le dossier client et le code principal sera stockĂ© dans le dossier server . Maintenant, nous travaillons dans le dossier du server . À savoir, nous utiliserons passport.js pour crĂ©er un systĂšme d'authentification. Nous avons dĂ©jĂ  installĂ© les modules passeport et passeport local. Avant de dĂ©crire la stratĂ©gie d'authentification des utilisateurs locaux, crĂ©ez un fichier app.js et ajoutez-y le code nĂ©cessaire pour dĂ©marrer un serveur simple. Si vous exĂ©cutez ce code par vous-mĂȘme, assurez-vous que le SGBD MongoDB est installĂ© et qu'il s'exĂ©cute en tant que service.

Voici le code du fichier qui se trouve dans le projet sur 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}!`)); 

Nous avons tĂ©lĂ©chargĂ© tout le middleware nĂ©cessaire Ă  l'application, connectĂ© Ă  MongoDB, configurĂ© la session express pour utiliser le stockage de fichiers. Les sessions de stockage leur permettront d'ĂȘtre restaurĂ©es aprĂšs un redĂ©marrage du serveur.

Nous décrivons maintenant les stratégies passport.js pour organiser l'enregistrement et l'authentification des utilisateurs. Créez un dossier d' auth dans le dossier du server et placez-y le fichier passport.js . Voici ce qui devrait se trouver dans le fichier 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; 

De plus, nous devons décrire le schéma du modÚle utilisateur (il sera appelé UserSchema ). Créez le dossier de database dans le dossier du server et dans celui-ci le fichier UserSchema.js .

Voici le code du fichier 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; 

UserSchema a trois méthodes. La méthode generateHash est conçue pour convertir un mot de passe, présenté sous forme de texte brut, en un hachage bcrypt. Nous utilisons cette méthode dans la stratégie de passeport pour convertir les mots de passe entrés par les utilisateurs en hachages bcrypt. Les hachages de mot de passe reçus sont ensuite stockés dans la base de données. La méthode validPassword accepte le mot de passe entré par l'utilisateur et le vérifie en comparant son hachage avec le hachage stocké dans la base de données. La méthode generateStreamKey des chaßnes uniques que nous transférerons aux utilisateurs comme clés de streaming (clés de flux) pour les clients RTMP.

Voici le code du fichier server/database/Schema.js :

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

Maintenant que nous avons défini des stratégies de passeport, décrit le schéma UserSchema et créé un modÚle basé sur celui-ci, initialisons le passeport dans app.js

Voici le code pour compléter le fichier server/app.js :

 //       ,     const passport = require('./auth/passport'); app.use(passport.initialize()); app.use(passport.session()); 

De plus, les nouveaux itinĂ©raires doivent ĂȘtre enregistrĂ©s dans app.js Pour ce faire, ajoutez le code suivant Ă  server/app.js :

 //    app.use('/login', require('./routes/login')); app.use('/register', require('./routes/register')); 

Créez les fichiers login.js et register.js dans le dossier routes situé dans le dossier du server . Dans ces fichiers, nous définissons quelques-unes des routes ci-dessus et utilisons le middleware de passeport pour organiser l'enregistrement et l'authentification des utilisateurs.

Voici le code du 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; 

Voici le code du fichier 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; 

Nous utilisons le moteur de modélisation ejs. Ajoutez les fichiers de modÚle login.ejs et register.ejs au dossier views , qui se trouve dans le dossier du server .

Voici le contenu du 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> 

Voici ce qui devrait se trouver dans le fichier 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> 

Nous pouvons dire que nous avons terminé le travail sur le systÚme d'authentification. Nous allons maintenant commencer la création de la prochaine partie du projet et configurer le serveur RTMP.

Configurer le serveur RTMP


RTMP (Real-Time Messaging Protocol) est un protocole qui a été développé pour la transmission haute performance de données vidéo, audio et diverses entre le lecteur de bande et le serveur. Twitch, Facebook, YouTube et de nombreux autres sites de streaming acceptent les flux RTMP et les transcodent en flux HTTP (format HLS) avant de transférer ces flux vers leurs CDN pour garantir leur haute disponibilité.

Nous utilisons le module node-media-server - Node.js-implementation du serveur multimĂ©dia RTMP. Ce serveur multimĂ©dia accepte les flux RTMP et les convertit en HLS / DASH Ă  l'aide de l'infrastructure multimĂ©dia ffmpeg. Pour que le projet fonctionne correctement, ffmpeg doit ĂȘtre installĂ© sur votre systĂšme. Si vous travaillez sous Linux et que vous avez dĂ©jĂ  installĂ© ffmpeg, vous pouvez trouver le chemin d'accĂšs en exĂ©cutant la commande suivante Ă  partir du terminal:

 $ which ffmpeg # /usr/bin/ffmpeg 

Pour travailler avec le package node-media-server, ffmpeg version 4.x est recommandé. Vous pouvez vérifier la version installée de ffmpeg comme ceci:

 $ 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) 

Si vous n'avez pas installé ffmpeg et que vous exécutez Ubuntu, vous pouvez installer ce cadre en exécutant la commande suivante:

 #    PPA-.     PPA,    # ffmpeg  3.x. $ sudo add-apt-repository ppa:jonathonf/ffmpeg-4 $ sudo apt install ffmpeg 

Si vous travaillez sous Windows, vous pouvez télécharger les versions ffmpeg pour Windows.

Ajoutez le fichier de configuration server/config/default.js au projet:

 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; 

Remplacez la valeur de la propriĂ©tĂ© ffmpeg par le chemin oĂč ffmpeg est installĂ© sur votre systĂšme. Si vous travaillez sur Windows et avez tĂ©lĂ©chargĂ© l'assembly ffmpeg de Windows Ă  partir du lien ci-dessus - n'oubliez pas d'ajouter l'extension .exe au nom de fichier. Ensuite, le fragment correspondant du code ci-dessus ressemblera Ă  ceci:

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

Installez maintenant node-media-server en exécutant la commande suivante:

 $ npm install node-media-server --save 

Créez un fichier media_server.js dans le dossier du server .

Voici le code Ă  mettre dans 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; 

L'utilisation de l'objet NodeMediaService assez simple. Il fournit le serveur RTMP et vous permet d'attendre les connexions. Si la clĂ© de streaming n'est pas valide, la connexion entrante peut ĂȘtre rejetĂ©e. Nous traiterons l'Ă©vĂ©nement de cet objet de prePublish . Dans la section suivante, nous ajoutons du code supplĂ©mentaire Ă  la fermeture de l'Ă©couteur d'Ă©vĂ©nement de prePublish . Il vous permettra de rejeter les connexions entrantes avec des clĂ©s de streaming non valides. En attendant, nous accepterons toutes les connexions entrantes arrivant sur le port RTMP par dĂ©faut (1935). Il suffit d'importer l'objet app.js dans le node_media_server et d'appeler sa mĂ©thode d' run .

Ajoutez le code suivant Ă  server/app.js :

 //      app.js, //  ,      const node_media_server = require('./media_server'); //   run()   , // ,    - node_media_server.run(); 

TĂ©lĂ©chargez et installez OBS (Open Broadcaster Software). Ouvrez la fenĂȘtre des paramĂštres de l'application et accĂ©dez Ă  la section Stream . SĂ©lectionnez Custom dans le champ Service et entrez rtmp://127.0.0.1:1935/live dans le champ Server . Le champ Stream Key peut ĂȘtre laissĂ© vide. Si le programme ne vous permet pas d'enregistrer les paramĂštres sans remplir ce champ, vous pouvez entrer n'importe quel jeu de caractĂšres. Cliquez sur le bouton Apply et sur le bouton OK . Cliquez sur le bouton Start Streaming pour commencer Ă  transfĂ©rer votre flux RTMP vers votre propre serveur local.


Configurer OBS

Accédez au terminal et regardez ce que le serveur multimédia y affiche. Vous y verrez des informations sur le flux entrant et les journaux de plusieurs écouteurs d'événements.


Sortie de données vers le terminal par un serveur multimédia basé sur Node.js

Le serveur multimédia donne accÚs à l'API, qui vous permet d'obtenir une liste des clients connectés. Pour voir cette liste, vous pouvez aller sur le navigateur à http://127.0.0.1:8888/api/streams . Plus tard, nous utiliserons cette API dans une application React pour afficher une liste d'utilisateurs de diffusion. Voici ce que vous pouvez voir en accédant à cette 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"       } 

Maintenant, le backend est presque prĂȘt. Il s'agit d'un serveur de streaming opĂ©rationnel qui prend en charge les technologies HTTP, RTMP et HLS. Cependant, nous n'avons pas encore créé de systĂšme pour vĂ©rifier les connexions RTMP entrantes. Cela devrait nous permettre de nous assurer que le serveur accepte uniquement les flux provenant d'utilisateurs authentifiĂ©s. Ajoutez le code suivant au prePublish Ă©vĂ©nements prePublish dans le 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]; }; 

Dans la fermeture, nous interrogeons la base de données pour trouver l'utilisateur avec la clé de streaming. Si la clé appartient à l'utilisateur, nous autorisons simplement l'utilisateur à se connecter au serveur et à publier sa diffusion. Sinon, nous rejetons la connexion RTMP entrante.

Dans la section suivante, nous allons créer une application cÎté client simple basée sur React. Il est nécessaire pour permettre aux téléspectateurs de regarder des émissions en streaming, ainsi que pour permettre aux streamers de générer et d'afficher leurs clés de streaming.

Streaming en direct


Allez maintenant dans le dossier clients . Puisque nous allons créer une application React, nous aurons besoin d'un webpack. Nous avons également besoin de chargeurs, qui sont utilisés pour traduire le code JSX en code JavaScript que les navigateurs comprennent. Installez les modules suivants:

 $ 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 

Ajoutez au projet, dans son répertoire racine, le fichier de configuration de 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    }, }; 

Ajoutez le fichier client/index.js au projet:

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

Voici le contenu du 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; } 

Le routeur React est utilisé pour le routage. Dans le frontend, nous utilisons également le bootstrap et, pour les diffusions, video.js. Ajoutez maintenant le dossier des components dossier client et le fichier Root.js à Root.js . Voici le contenu du 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>          } 

Le composant <Root/> affiche un composant <Router/> React qui contient trois sous-composants <Route/> . Le composant <LiveStreams/> affiche une liste des diffusions. Le <VideoPlayer/> est responsable de l'affichage du lecteur video.js. Le composant <Settings/> est chargé de créer une interface pour travailler avec les clés de streaming.

Créez le 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 align="center" 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>       } 

Voici Ă  quoi ressemble la page d'application.


Service de streaming frontal

AprĂšs avoir montĂ© le composant <LiveStreams/> , l'API NMS est appelĂ©e pour obtenir une liste des clients connectĂ©s au systĂšme. L'API NMS ne fournit pas beaucoup d'informations sur les utilisateurs. En particulier, nous pouvons en obtenir des informations sur les clĂ©s de streaming, via lesquelles les utilisateurs sont connectĂ©s Ă  un serveur RTMP. Nous utiliserons ces clĂ©s lors de la gĂ©nĂ©ration de requĂȘtes dans la base de donnĂ©es pour obtenir des informations sur les comptes d'utilisateurs.

Dans la méthode getStreamsInfo nous exécutons une demande XHR vers /streams/info , mais nous n'avons pas encore créé quelque chose qui puisse répondre à cette demande. Créez un server/routes/streams.js avec le contenu suivant:

 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; 

Nous transmettons les informations de flux retournées par l'API NMS au backend, afin d'obtenir des informations sur les clients connectés.

Nous effectuons une requĂȘte de base de donnĂ©es pour obtenir une liste d'utilisateurs dont les clĂ©s de streaming correspondent Ă  celles que nous avons reçues de l'API NMS. Nous renvoyons la liste au format JSON. Nous enregistrons l'itinĂ©raire dans le server/app.js :

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

En consĂ©quence, nous affichons une liste des Ă©missions actives. Cette liste contient le nom d'utilisateur et la miniature. Nous parlerons de la façon de crĂ©er des vignettes pour les Ă©missions Ă  la fin de l'article. Les miniatures sont liĂ©es Ă  des pages spĂ©cifiques oĂč les flux HLS sont lus Ă  l'aide de video.js.

Créez le 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>       } 

HLS- video.js.




,


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

passport.js, , , . /settings — . XHR- <Settings/> .

. Generate a new key . XHR- . , . . — GET POST /settings/stream_key . 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; 

shortid.

server/app.js :

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


,


<LiveStreams/> ( client/components/LiveStreams.js ) :

 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 align="center" 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>   } 

. cron, , 5 , .

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

generateStreamThumbnail .

ffmpeg-, HLS-. prePublish ( 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);                      }); }); 

, cron ( server/cron/thumbnails.js ):

 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; 

5 . API NMS . server/app.js :

 //      app.js, const thumbnail_generator = require('./cron/thumbnails'); //   start()    thumbnail_generator.start(); 

Résumé


, . , . - — . - — .

.

! , , ?

Source: https://habr.com/ru/post/fr457860/


All Articles