使用Node.js和React开发用于流式传输的应用程序

该材料的作者(我们今天将发布其翻译)说,他正在开发一个应用程序,该应用程序可以让您组织用户桌面上正在发生的事情的流广播(流)。

图片

该应用程序从磁带机接收RTMP格式的流,并将其转换为HLS流,可以在查看器的浏览器中播放该流。 本文将讨论如何使用Node.js和React创建自己的流应用程序。 如果您习惯于看到自己感兴趣的想法,然后立即将自己沉浸在代码中,现在可以查看存储库。

使用基本认证系统进行Web服务器开发


让我们创建一个基于Node.js的简单Web服务器,在该服务器中,使用Passport.js库实现了本地用户身份验证策略。 我们将使用MongoDB作为信息的永久存储。 我们将使用Mongoose ODM库处理数据库。

初始化一个新项目:

$ npm init 

安装依赖项:

 $ 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 

在项目目录中,创建两个文件夹clientserver 。 基于React的前端代码将最终存储在client文件夹中,而后端代码将存储在server文件夹中。 现在我们正在server文件夹中。 即,我们将使用passport.js创建一个身份验证系统。 我们已经安装了护照和护照本地模块。 在描述本地用户身份验证策略之前,请创建一个app.js文件并向其中添加启动简单服务器所需的代码。 如果您将自己运行此代码,请确保已安装MongoDB DBMS并将其作为服务运行。

这是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}!`)); 

我们下载了连接到MongoDB的应用程序所需的所有中间件,并配置了快速会话以使用文件存储。 存储会话将允许它们在服务器重新启动后恢复。

现在,我们描述用于组织用户注册和身份验证的passport.js策略。 在server文件夹中创建一个auth文件夹,并将passport.js文件放入其中。 这是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; 

另外,我们需要描述用户模型的架构(它将称为UserSchema )。 在server文件夹及其UserSchema.js文件中创建database文件夹。

这是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具有三种方法。 generateHash方法旨在将以纯文本形式显示的密码转换为bcrypt哈希。 我们在护照策略中使用此方法将用户输入的密码转换为bcrypt哈希。 然后将收到的密码哈希存储在数据库中。 validPassword方法接受用户输入的密码,并通过将其哈希值与存储在数据库中的哈希值进行比较来对其进行验证。 generateStreamKey方法generateStreamKey唯一的字符串,我们将这些字符串作为RTMP客户端的流键(流键)传输给用户。

这是server/database/Schema.js文件的代码:

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

现在,我们已经定义了通行证策略,描述了UserSchema方案UserSchema并基于该方案创建了一个模型,让我们在app.js初始化通行证。

这是用以下代码补充server/app.js文件的代码:

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

此外,新路线必须在app.js注册。 为此,将以下代码添加到server/app.js

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

server文件夹中的routes文件夹中创建login.jsregister.js文件。 在这些文件中,我们定义了以上两条路径,并使用护照中间件来组织注册和用户身份验证。

这是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; 

这是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; 

我们使用ejs模板引擎。 将login.ejsregister.ejs模板文件添加到位于server文件夹中的views文件夹中。

这是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> 

这是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> 

可以说我们已经完成了认证系统的工作。 现在,我们将开始创建项目的下一部分,并配置RTMP服务器。

配置RTMP服务器


RTMP(实时消息协议)是为在磁带机和服务器之间高性能传输视频,音频和各种数据而开发的协议。 Twitch,Facebook,YouTube和许多其他流媒体站点接受RTMP流并将其转码为HTTP流(HLS格式),然后再将其传输到CDN,以确保其高可用性。

我们使用RTMP媒体服务器的nod​​e-media-server模块-Node.js-实现。 该媒体服务器接受RTMP流,并使用ffmpeg多媒体框架将其转换为HLS / DASH。 为了使项目成功运行,必须在系统上安装ffmpeg。 如果您使用的是Linux,并且已经安装了ffmpeg,则可以通过从终端运行以下命令来找到其路径:

 $ which ffmpeg # /usr/bin/ffmpeg 

要使用node-media-server软件包,建议使用ffmpeg版本4.x。 您可以像这样检查ffmpeg的安装版本:

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

如果您尚未安装ffmpeg并且正在运行Ubuntu,则可以通过运行以下命令来安装此框架:

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

如果您在Windows上工作,则可以下载Windows的ffmpeg 版本

server/config/default.js配置文件添加到项目中:

 const config = {    server: {        secret: 'kjVkuti2xAyF3JGCzSZTk0YWM5JhI9mgQW4rytXc'    },    rtmp_server: {        rtmp: {            port: 1935,            chunk_size: 60000,            gop_cache: true,            ping: 60,            ping_timeout: 30        },        http: {            port: 8888,            mediaroot: './server/media',            allow_origin: '*'        },        trans: {            ffmpeg: '/usr/bin/ffmpeg',            tasks: [                                   app: 'live',                    hls: true,                    hlsFlags: '[hls_time=2:hls_list_size=3:hls_flags=delete_segments]',                    dash: true,                    dashFlags: '[f=dash:window_size=3:extra_window_size=5]'   }; module.exports = config; 

ffmpeg属性的值替换为系统上ffmpeg的安装路径。 如果您使用的是Windows,并从上面的链接下载了Windows程序集ffmpeg,请不要忘记在文件名中添加.exe扩展名。 然后,上面代码的相应片段将如下所示:

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

现在,通过运行以下命令来安装node-media-server:

 $ npm install node-media-server --save 

server文件夹中创建一个media_server.js文件。

这是放置在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; 

使用NodeMediaService对象非常简单。 它提供了RTMP服务器,并允许您等待连接。 如果流密钥无效,则可以拒绝传入的连接。 我们将处理此prePublish对象的事件。 在下一节中,我们将其他代码添加到prePublish事件监听器闭包中。 它将允许您拒绝使用无效流密钥的传入连接。 同时,我们将接受所有进入默认RTMP端口(1935)的传入连接。 我们只需要在node_media_server导入app.js对象并调用其run方法。

将以下代码添加到server/app.js

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

下载并安装OBS (Open Broadcaster软件)。 打开应用程序设置窗口,然后转到“ Stream部分。 在Service字段中选择Custom ,然后在Service字段中输入rtmp://127.0.0.1:1935/live 。 “ Stream Key字段可以保留为空白。 如果程序不允许您在不填写此字段的情况下保存设置,则可以在其中输入任何字符集。 单击“ Apply按钮和“ OK按钮。 单击“ Start Streaming按钮以开始将RTMP流传输到您自己的本地服务器。


配置OBS

转到终端,然后查看媒体服务器在那里显示的内容。 您将在此处看到有关传入流和几个事件侦听器的日志的信息。


基于Node.js的媒体服务器向终端输出的数据

媒体服务器提供对API的访问权限,该API允许您获取已连接客户端的列表。 为了查看此列表,您可以访问浏览器, http://127.0.0.1:8888/api/streamshttp://127.0.0.1:8888/api/streams 。 稍后,我们将在React应用程序中使用此API来显示广播用户列表。 通过访问此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"       } 

现在后端几乎可以使用了。 它是一种工作正常的流服务器,支持HTTP,RTMP和HLS技术。 但是,我们尚未创建用于检查传入RTMP连接的系统。 它应该允许我们确保服务器仅接受来自经过身份验证的用户的流。 将以下代码添加到server/media_server.jsprePublish事件prePublish中:

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

在闭包中,我们查询数据库以查找具有流密钥的用户。 如果密钥属于用户,我们只允许用户连接到服务器并发布其广播。 否则,我们将拒绝传入的RTMP连接。

在下一节中,我们将基于React创建一个简单的客户端应用程序。 为了允许观看者观看流媒体广播,以及允许流媒体生成并查看其流媒体密钥,这是必需的。

即时串流


现在转到clients文件夹。 由于我们将创建一个React应用程序,因此我们需要一个webpack。 我们还需要加载器,该加载器用于将JSX代码转换为浏览器可以理解的JavaScript代码。 安装以下模块:

 $ 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 

在项目的根目录中,将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    }, }; 

client/index.js文件添加到项目中:

 import React from "react"; import ReactDOM from 'react-dom'; import {BrowserRouter} from 'react-router-dom'; import 'bootstrap'; require('./index.scss'); import Root from './components/Root.js'; if(document.getElementById('root')){    ReactDOM.render(        <BrowserRouter>            <Root/>        </BrowserRouter>,        document.getElementById('root')    ); } 

这是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路由器用于路由。 在前端,我们还使用引导程序,对于广播,还使用video.js。 现在,将components文件夹添加到client文件夹,并将Root.js文件添加到Root.js 。 这是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>          } 

<Root/>组件呈现一个<Router/> React组件,其中包含三个<Route/>子组件。 <LiveStreams/>组件显示广播列表。 <VideoPlayer/>组件负责显示video.js播放器。 <Settings/>组件负责创建用于处理流键的接口。

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

这是应用程序页面的外观。


前端流媒体服务

装入<LiveStreams/>组件后,将调用NMS API以获取连接到系统的客户端列表。 NMS API没有提供有关用户的太多信息。 特别是,我们可以从中获取有关流密钥的信息,通过该流信息用户可以连接到RTMP服务器。 在生成数据库查询以获取有关用户帐户的信息时,我们将使用这些键。

getStreamsInfo方法中getStreamsInfo我们对/streams/info执行XHR请求,但尚未创建可以响应此请求的内容。 创建具有以下内容的server/routes/streams.js

 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; 

我们将NMS API返回的流信息传递给后端,以获得有关已连接客户端的信息。

我们执行数据库查询,以获取其流密钥与从NMS API接收到的流密钥匹配的用户列表。 我们以JSON格式返回列表。 我们在server/app.js注册路由:

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

结果,我们显示了活动广播的列表。 此列表包含用户名和缩略图。 我们将在文章结尾讨论如何为广播创建缩略图。 缩略图与使用video.js播放HLS流的特定页面相关。

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

在安装组件时,我们获得了一个用户流密钥,以在video.js播放器中初始化HLS流。


转盘

向将要流式传输的人员发布流式密钥


创建一个组件文件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(); 

总结


, . , . - — . - — .

.

! , , ?

Source: https://habr.com/ru/post/zh-CN457860/


All Articles