Desenvolvendo um aplicativo para streaming usando Node.js e React

O autor do material, cuja tradução publicamos hoje, diz que está trabalhando em um aplicativo que permite organizar a transmissão em fluxo contínuo do que está acontecendo na área de trabalho do usuário.

imagem

O aplicativo recebe um fluxo no formato RTMP da serpentina e o converte em um fluxo HLS, que pode ser reproduzido nos navegadores dos espectadores. Este artigo abordará como você pode criar seu próprio aplicativo de streaming usando o Node.js. e o React. Se você está acostumado a ver a ideia que lhe interessa, mergulhe imediatamente no código, agora poderá procurar neste repositório.

Desenvolvimento de servidor Web com sistema básico de autenticação


Vamos criar um servidor Web simples baseado no Node.js, no qual, usando a biblioteca passport.js, uma estratégia de autenticação de usuário local é implementada. Usaremos o MongoDB como um armazenamento permanente de informações. Trabalharemos com o banco de dados usando a biblioteca Mongoose ODM.

Inicialize um novo projeto:

$ npm init 

Instale as dependências:

 $ 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 

No diretório do projeto, crie duas pastas - client e server . O código de front-end baseado em React será finalizado na pasta do client e o código de back-end será armazenado na pasta do server . Agora estamos trabalhando na pasta do server . Ou seja, usaremos o passport.js para criar um sistema de autenticação. Já instalamos os módulos passaporte e passaporte-local. Antes de descrever a estratégia de autenticação do usuário local, crie um arquivo app.js e adicione o código necessário para iniciar um servidor simples. Se você executar esse código por conta própria, verifique se possui o DBMS do MongoDB instalado e se ele é executado como um serviço.

Aqui está o código para o arquivo que está no projeto em 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}!`)); 

Fizemos o download de todo o middleware necessário para o aplicativo, conectado ao MongoDB, configuramos a sessão expressa para usar o armazenamento de arquivos. As sessões de armazenamento permitirão que elas sejam restauradas após a reinicialização do servidor.

Agora, descrevemos as estratégias passport.js para organizar o registro e a autenticação do usuário. Crie uma pasta de auth na pasta do server e coloque o arquivo passport.js nela. Aqui está o que deve estar no arquivo 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; 

Além disso, precisamos descrever o esquema para o modelo do usuário (será chamado de UserSchema ). Crie a pasta do database na pasta do server e nela o arquivo UserSchema.js .

Aqui está o código para o arquivo 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 tem três métodos. O método generateHash foi projetado para converter uma senha, apresentada na forma de texto sem formatação, em um hash bcrypt. Usamos esse método na estratégia de passaporte para converter senhas inseridas pelos usuários em hashes bcrypt. Os hashes de senha recebidos são armazenados no banco de dados. O método validPassword aceita a senha inserida pelo usuário e a verifica comparando seu hash com o hash armazenado no banco de dados. O método generateStreamKey sequências únicas que serão transferidas para os usuários como suas chaves de fluxo contínuo (chaves de fluxo) para clientes RTMP.

Aqui está o código do arquivo server/database/Schema.js :

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

Agora que definimos estratégias de passaporte, descrevemos o esquema UserSchema e criamos um modelo baseado nele, vamos inicializar o passaporte em app.js

Aqui está o código para complementar o arquivo server/app.js :

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

Além disso, novas rotas devem ser registradas em app.js Para fazer isso, adicione o seguinte código ao server/app.js :

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

Crie os arquivos login.js e register.js na pasta routes localizada na pasta do server . Nesses arquivos, definimos algumas das rotas acima e usamos o middleware do passaporte para organizar o registro e a autenticação do usuário.

Aqui está o código para o 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; 

Aqui está o código para o arquivo 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 o mecanismo de modelo ejs. Inclua os arquivos de modelo login.ejs e register.ejs na pasta views , localizada na pasta do server .

Aqui está o conteúdo do 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> 

Aqui está o que deve estar no arquivo 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 dizer que terminamos o trabalho no sistema de autenticação. Agora iniciaremos a criação da próxima parte do projeto e configuraremos o servidor RTMP.

Configurar servidor RTMP


O RTMP (Real-Time Messaging Protocol) é um protocolo desenvolvido para transmissão de alto desempenho de vídeo, áudio e vários dados entre a unidade de fita e o servidor. O Twitch, o Facebook, o YouTube e muitos outros sites de streaming aceitam fluxos RTMP e os codificam para fluxos HTTP (formato HLS) antes de transferir esses fluxos para suas CDNs para garantir sua alta disponibilidade.

Usamos o módulo node-media-server - Node.js-deployment do servidor de mídia RTMP. Esse servidor de mídia aceita fluxos RTMP e os converte em HLS / DASH usando a estrutura multimídia ffmpeg. Para que o projeto funcione com sucesso, o ffmpeg deve estar instalado no seu sistema. Se você estiver trabalhando no Linux e já tiver o ffmpeg instalado, poderá descobrir o caminho para ele executando o seguinte comando no terminal:

 $ which ffmpeg # /usr/bin/ffmpeg 

Para trabalhar com o pacote node-media-server, é recomendável o ffmpeg versão 4.x. Você pode verificar a versão instalada do ffmpeg assim:

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

Se você não possui o ffmpeg instalado e está executando o Ubuntu, pode instalar esta estrutura executando o seguinte comando:

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

Se você trabalha no Windows, pode fazer o download do ffmpeg builds para Windows.

Inclua o arquivo de configuração server/config/default.js no projeto:

 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; 

Substitua o valor da propriedade ffmpeg pelo caminho em que o ffmpeg está instalado no seu sistema. Se você estiver trabalhando no Windows e baixou o assembly ffmpeg do Windows no link acima - não esqueça de adicionar a extensão .exe ao nome do arquivo. Em seguida, o fragmento correspondente do código acima ficará assim:

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

Agora instale o node-media-server executando o seguinte comando:

 $ npm install node-media-server --save 

Crie um arquivo media_server.js na pasta do server .

Aqui está o código para colocar em 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 o objeto NodeMediaService bastante simples. Ele fornece o servidor RTMP e permite aguardar conexões. Se a chave de streaming for inválida, a conexão recebida poderá ser rejeitada. Manipularemos o evento desse objeto prePublish - prePublish . Na próxima seção, adicionamos código adicional ao fechamento do ouvinte de prePublish - prePublish . Isso permitirá que você rejeite as conexões recebidas com chaves de streaming inválidas. Enquanto isso, aceitaremos todas as conexões de entrada que chegarem à porta RTMP padrão (1935). Precisamos apenas importar o objeto app.js no node_media_server e chamar seu método run .

Adicione o seguinte código ao server/app.js :

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

Baixe e instale o OBS (Open Broadcaster Software). Abra a janela de configurações do aplicativo e vá para a seção Stream . Selecione Custom no campo Service e digite rtmp://127.0.0.1:1935/live no campo Server . O campo Stream Key pode ser deixado em branco. Se o programa não permitir salvar as configurações sem preencher esse campo, você poderá inserir qualquer conjunto de caracteres. Clique no botão Apply e no botão OK . Clique no botão Start Streaming para começar a transferir o fluxo RTMP para o seu próprio servidor local.


Configurar OBS

Vá para o terminal e veja o que o servidor de mídia exibe lá. Você verá informações sobre o fluxo recebido e os logs de vários ouvintes de eventos.


Saída de dados para o terminal por um servidor de mídia baseado em Node.js

O servidor de mídia fornece acesso à API, que permite obter uma lista de clientes conectados. Para visualizar esta lista, você pode acessar o navegador em http://127.0.0.1:8888/api/streams . Posteriormente, usaremos essa API em um aplicativo React para exibir uma lista de usuários de transmissão. Aqui está o que você pode ver acessando 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"       } 

Agora, o back-end está quase pronto. É um servidor de fluxo de trabalho compatível com as tecnologias HTTP, RTMP e HLS. No entanto, ainda não criamos um sistema para verificar as conexões RTMP recebidas. Isso deve permitir garantir que o servidor aceite fluxos somente de usuários autenticados. Adicione o seguinte código ao prePublish eventos prePublish no 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]; }; 

No fechamento, consultamos o banco de dados para encontrar o usuário com a chave de streaming. Se a chave pertencer ao usuário, simplesmente permitiremos que ele se conecte ao servidor e publique sua transmissão. Caso contrário, rejeitamos a conexão RTMP recebida.

Na próxima seção, criaremos um aplicativo simples do lado do cliente baseado no React. É necessário para permitir que os espectadores assistam a transmissões de streaming, bem como para permitir que os streamers gerem e visualizem suas chaves de streaming.

Transmissão ao vivo


Agora vá para a pasta clients . Como vamos criar um aplicativo React, precisaremos de um webpack. Também precisamos de carregadores, que são usados ​​para converter o código JSX em código JavaScript que os navegadores entendem. Instale os seguintes 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 

Adicione ao projeto, em seu diretório raiz, o arquivo de configuração do 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    }, }; 

Adicione o arquivo client/index.js ao projeto:

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

Aqui está o conteúdo do 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; } 

O roteador de reação é usado para roteamento. No frontend, também usamos o bootstrap e, para transmissões, video.js. Agora adicione a pasta de components à pasta do client e o arquivo Root.js Aqui está o conteúdo do 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>          } 

O componente <Root/> renderiza um componente <Router/> React que contém três subcomponentes <Route/> . O componente <LiveStreams/> exibe uma lista de transmissões. O componente <VideoPlayer/> é responsável por exibir o player video.js. O componente <Settings/> é responsável por criar uma interface para trabalhar com chaves de streaming.

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

É assim que a página do aplicativo se parece.


Serviço de streaming de front-end

Depois de montar o componente <LiveStreams/> , a API do NMS é chamada para obter uma lista de clientes conectados ao sistema. A API do NMS não fornece muitas informações sobre os usuários. Em particular, podemos obter informações sobre o streaming de chaves, através do qual os usuários estão conectados a um servidor RTMP. Usaremos essas chaves ao gerar consultas no banco de dados para obter informações sobre contas de usuário.

No método getStreamsInfo executamos uma solicitação XHR para /streams/info , mas ainda não criamos algo que possa responder a essa solicitação. Crie um server/routes/streams.js com o seguinte conteúdo:

 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; 

Passamos as informações de fluxo retornadas pela API do NMS para o back-end, a fim de obter informações sobre clientes conectados.

Realizamos uma consulta ao banco de dados para obter uma lista de usuários cujas chaves de streaming correspondem aos que recebemos da API do NMS. Retornamos a lista no formato JSON. Registramos a rota no server/app.js :

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

Como resultado, exibimos uma lista de transmissões ativas. Esta lista contém o nome de usuário e a miniatura. Falaremos sobre como criar miniaturas para transmissões no final do artigo. As miniaturas são vinculadas a páginas específicas em que os fluxos HLS são reproduzidos usando o video.js.

Crie o 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(); 

Sumário


, . , . - — . - — .

.

! , , ?

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


All Articles