Desarrollando una aplicación para streaming usando Node.js y React

El autor del material, cuya traducción publicamos hoy, dice que está trabajando en una aplicación que le permite organizar la transmisión (transmisión) de lo que está sucediendo en el escritorio del usuario.

imagen

La aplicación recibe una transmisión en formato RTMP del transmisor y la convierte en una transmisión HLS, que se puede reproducir en los navegadores de los espectadores. Este artículo hablará sobre cómo puede crear su propia aplicación de transmisión usando Node.js y React. Si está acostumbrado a ver la idea que le interesa, sumérjase inmediatamente en el código, ahora puede buscar en este repositorio.

Desarrollo de servidor web con sistema básico de autenticación.


Creemos un servidor web simple basado en Node.js, en el que, utilizando la biblioteca passport.js, se implemente una estrategia de autenticación de usuario local. Utilizaremos MongoDB como almacenamiento permanente de información. Trabajaremos con la base de datos utilizando la biblioteca Mongoose ODM.

Inicializar un nuevo proyecto:

$ npm init 

Instalar las dependencias:

 $ 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 

En el directorio del proyecto, cree dos carpetas: client y server . El código de interfaz basado en React terminará en la carpeta del client , y el código de back-end se almacenará en la carpeta del server . Ahora estamos trabajando en la carpeta del server . A saber, usaremos passport.js para crear un sistema de autenticación. Ya hemos instalado el pasaporte y los módulos de pasaporte local. Antes de describir la estrategia de autenticación de usuario local, cree un archivo app.js y agregue el código necesario para iniciar un servidor simple. Si va a ejecutar este código por su cuenta, asegúrese de tener instalado el DBMS MongoDB y de que se ejecute como un servicio.

Aquí está el código para el archivo que está en el proyecto en 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}!`)); 

Descargamos todo el middleware necesario para la aplicación, conectado a MongoDB, configuramos la sesión rápida para usar el almacenamiento de archivos. Almacenar sesiones les permitirá restaurarlas después de reiniciar el servidor.

Ahora describimos las estrategias de passport.js para organizar el registro y la autenticación de usuarios. Cree una carpeta de auth en la carpeta del server y coloque el archivo passport.js en ella. Esto es lo que debe estar en el archivo 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; 

Además, necesitamos describir el esquema para el modelo de usuario (se llamará UserSchema ). Cree la carpeta de la database en la carpeta del server , y en ella el archivo UserSchema.js .

Aquí está el código para el archivo 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 tiene tres métodos. El método generateHash está diseñado para convertir una contraseña, presentada en forma de texto sin formato, en un hash bcrypt. Utilizamos este método en la estrategia de pasaporte para convertir las contraseñas ingresadas por los usuarios en hash de bcrypt. Los hashes de contraseña recibidos se almacenan en la base de datos. El método validPassword acepta la contraseña ingresada por el usuario y la verifica comparando su hash con el hash almacenado en la base de datos. El método generateStreamKey cadenas únicas que transferiremos a los usuarios como sus claves de transmisión (claves de transmisión) para clientes RTMP.

Aquí está el código del archivo server/database/Schema.js :

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

Ahora que hemos definido estrategias de pasaporte, descrito el esquema UserSchema y creado un modelo basado en él, inicialicemos el pasaporte en app.js

Aquí está el código para complementar el archivo server/app.js :

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

Además, las nuevas rutas deben registrarse en app.js Para hacer esto, agregue el siguiente código a server/app.js :

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

Cree los archivos login.js y register.js en la carpeta de routes ubicada en la carpeta del server . En estos archivos definimos un par de las rutas anteriores y usamos el middleware del pasaporte para organizar el registro y la autenticación del usuario.

Aquí está el código para el 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; 

Aquí está el código para el archivo 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; 

Usamos el motor de plantillas ejs. Agregue los archivos de plantilla login.ejs y register.ejs a la carpeta de views , que se encuentra en la carpeta del server .

Aquí está el contenido del 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> 

Esto es lo que debe estar en el archivo 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> 

Podemos decir que hemos terminado el trabajo en el sistema de autenticación. Ahora comencemos la creación de la siguiente parte del proyecto y configuremos el servidor RTMP.

Configurar el servidor RTMP


RTMP (Protocolo de mensajería en tiempo real) es un protocolo que se desarrolló para la transmisión de video, audio y varios datos de alto rendimiento entre la unidad de cinta y el servidor. Twitch, Facebook, YouTube y muchos otros sitios de transmisión aceptan transmisiones RTMP y las transcodifican en transmisiones HTTP (formato HLS) antes de transferir estas transmisiones a sus CDN para garantizar su alta disponibilidad.

Usamos el módulo de servidor de medios de nodo - Node.js-implementación del servidor de medios RTMP. Este servidor de medios acepta transmisiones RTMP y las convierte a HLS / DASH utilizando el marco multimedia ffmpeg. Para que el proyecto funcione con éxito, ffmpeg debe estar instalado en su sistema. Si está trabajando en Linux y ya tiene instalado ffmpeg, puede encontrar la ruta a él ejecutando el siguiente comando desde el terminal:

 $ which ffmpeg # /usr/bin/ffmpeg 

Para trabajar con el paquete node-media-server, se recomienda ffmpeg versión 4.x. Puede verificar la versión instalada de ffmpeg de esta manera:

 $ 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 no tiene instalado ffmpeg y está ejecutando Ubuntu, puede instalar este marco ejecutando el siguiente comando:

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

Si trabaja en Windows, puede descargar compilaciones de ffmpeg para Windows.

Agregue el archivo de configuración server/config/default.js al proyecto:

 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; 

Reemplace el valor de la propiedad ffmpeg con la ruta donde está instalado ffmpeg en su sistema. Si está trabajando en Windows y descargó el ensamblado de Windows ffmpeg desde el enlace de arriba, no olvide agregar la extensión .exe al nombre del archivo. Entonces el fragmento correspondiente del código anterior se verá así:

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

Ahora instale node-media-server ejecutando el siguiente comando:

 $ npm install node-media-server --save 

Cree un archivo media_server.js en la carpeta del server .

Aquí está el código para poner en 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; 

Usar el objeto NodeMediaService bastante simple. Proporciona el servidor RTMP y le permite esperar las conexiones. Si la clave de transmisión no es válida, la conexión entrante se puede rechazar. prePublish el evento de este objeto prePublish . En la siguiente sección, agregamos código adicional al cierre del detector de eventos prePublish . Le permitirá rechazar conexiones entrantes con claves de transmisión no válidas. Mientras tanto, aceptaremos todas las conexiones entrantes que lleguen al puerto RTMP predeterminado (1935). Solo necesitamos importar el objeto app.js en el node_media_server y llamar a su método de run .

Agregue el siguiente código a server/app.js :

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

Descargue e instale OBS (Open Broadcaster Software). Abra la ventana de configuración de la aplicación y vaya a la sección Stream . Seleccione Custom en el campo Service e ingrese rtmp://127.0.0.1:1935/live en el campo Server . El campo Stream Key transmisión se puede dejar en blanco. Si el programa no le permite guardar la configuración sin completar este campo, puede ingresar cualquier conjunto de caracteres en él. Haga clic en el botón Apply y en el botón OK . Haga clic en el botón Start Streaming para comenzar a transferir su transmisión RTMP a su propio servidor local.


Configurar OBS

Vaya a la terminal y mire lo que muestra el servidor de medios allí. Verá allí información sobre la transmisión entrante y los registros de varios oyentes de eventos.


Salida de datos al terminal por un servidor de medios basado en Node.js

El servidor de medios le da acceso a la API, que le permite obtener una lista de clientes conectados. Para ver esta lista, puede ir al navegador en http://127.0.0.1:8888/api/streams . Más tarde, utilizaremos esta API en una aplicación React para mostrar una lista de usuarios de difusión. Esto es lo que puede ver accediendo a esta 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"       } 

Ahora el backend está casi listo. Es un servidor de transmisión en funcionamiento que admite las tecnologías HTTP, RTMP y HLS. Sin embargo, todavía no hemos creado un sistema para verificar las conexiones RTMP entrantes. Debe permitirnos asegurarnos de que el servidor acepte transmisiones solo de usuarios autenticados. Agregue el siguiente código al prePublish eventos prePublish en el 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]; }; 

En el cierre, consultamos la base de datos para encontrar al usuario con la clave de transmisión. Si la clave pertenece al usuario, simplemente permitimos que el usuario se conecte al servidor y publique su transmisión. De lo contrario, rechazamos la conexión RTMP entrante.

En la siguiente sección, crearemos una aplicación simple del lado del cliente basada en React. Es necesario para permitir a los espectadores ver transmisiones de transmisión, así como para permitir que los transmisores generen y vean sus claves de transmisión.

Transmisión en vivo


Ahora ve a la carpeta de clients . Como vamos a crear una aplicación React, necesitaremos un paquete web. También necesitamos cargadores, que se utilizan para traducir el código JSX en código JavaScript que los navegadores entienden. Instale los siguientes módulos:

 $ 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 

Agregue al proyecto, en su directorio raíz, el archivo de configuración para 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    }, }; 

Agregue el archivo client/index.js al proyecto:

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

Aquí está el contenido del 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 se utiliza para enrutar. En la interfaz, también usamos bootstrap y, para transmisiones, video.js. Ahora agregue la carpeta de components a la carpeta del client y el archivo Root.js Aquí está el contenido del 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>          } 

El componente <Root/> representa un componente React <Router/> que contiene tres subcomponentes <Route/> . El componente <LiveStreams/> muestra una lista de transmisiones. El <VideoPlayer/> es responsable de mostrar el reproductor video.js. El componente <Settings/> es responsable de crear una interfaz para trabajar con claves de transmisión.

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

Así es como se ve la página de la aplicación.


Servicio de transmisión frontend

Después de montar el componente <LiveStreams/> , se llama a la API de NMS para obtener una lista de clientes conectados al sistema. La API de NMS no proporciona mucha información sobre los usuarios. En particular, podemos obtener información sobre la transmisión de claves a través de ella, a través de la cual los usuarios se conectan a un servidor RTMP. Utilizaremos estas claves cuando generemos consultas a la base de datos para obtener información sobre las cuentas de los usuarios.

En el método getStreamsInfo ejecutamos una solicitud XHR a /streams/info , pero aún no hemos creado algo que pueda responder a esta solicitud. Cree un server/routes/streams.js con los siguientes contenidos:

 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; 

Pasamos la información de flujo devuelta por la API de NMS al back-end para obtener información sobre los clientes conectados.

Realizamos una consulta de base de datos para obtener una lista de usuarios cuyas claves de transmisión coinciden con las que recibimos de la API de NMS. Devolvemos la lista en formato JSON. Registramos la ruta en el server/app.js :

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

Como resultado, mostramos una lista de transmisiones activas. Esta lista contiene el nombre de usuario y la miniatura. Hablaremos sobre cómo crear miniaturas para las transmisiones al final del artículo. Las miniaturas están vinculadas a páginas específicas donde se reproducen transmisiones HLS usando video.js.

Cree el 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(); 

Resumen


, . , . - — . - — .

.

! , , ?

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


All Articles