Mengembangkan aplikasi untuk streaming menggunakan Node.js dan Bereaksi

Penulis materi, terjemahan yang kami terbitkan hari ini, mengatakan bahwa ia sedang mengerjakan aplikasi yang memungkinkan Anda untuk mengatur siaran streaming (streaming) dari apa yang terjadi di desktop pengguna.

gambar

Aplikasi menerima aliran dalam format RTMP dari streamer dan mengubahnya menjadi aliran HLS, yang dapat diputar di browser pemirsa. Artikel ini akan berbicara tentang bagaimana Anda dapat membuat aplikasi streaming Anda sendiri menggunakan Node.js dan Bereaksi. Jika Anda terbiasa melihat ide yang menarik bagi Anda, segera libatkan diri Anda dalam kode, Anda sekarang dapat melihat ke repositori ini .

Pengembangan server web dengan sistem otentikasi dasar


Mari kita membuat server web sederhana berdasarkan Node.js, di mana, menggunakan perpustakaan passport.js, strategi otentikasi pengguna lokal diimplementasikan. Kami akan menggunakan MongoDB sebagai penyimpanan informasi permanen. Kami akan bekerja dengan database menggunakan pustaka Mongoose ODM.

Inisialisasi proyek baru:

$ npm init 

Instal dependensi:

 $ 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 

Di direktori proyek, buat dua folder - client dan server . Kode frontend berdasarkan Bereaksi akan berakhir di folder client , dan kode backend akan disimpan di folder server . Sekarang kami bekerja di folder server . Yaitu, kami akan menggunakan passport.js untuk membuat sistem otentikasi. Kami telah memasang paspor dan modul paspor-lokal. Sebelum kami menjelaskan strategi otentikasi pengguna lokal, buat file app.js dan tambahkan kode yang diperlukan untuk memulai server sederhana. Jika Anda akan menjalankan kode ini sendiri, pastikan Anda telah menginstal DBMS MongoDB dan menjalankannya sebagai layanan.

Berikut adalah kode untuk file yang ada dalam proyek di 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}!`)); 

Kami mengunduh semua middleware yang diperlukan untuk aplikasi, yang terhubung ke MongoDB, mengonfigurasi sesi ekspres untuk menggunakan penyimpanan file. Menyimpan sesi akan memungkinkan mereka untuk dipulihkan setelah server reboot.

Sekarang kami menjelaskan strategi passport.js untuk mengatur pendaftaran dan otentikasi pengguna. Buat folder auth di folder server dan tempatkan file passport.js di dalamnya. Inilah yang seharusnya ada di file server/auth/passport.js :

 const passport = require('passport'),    LocalStrategy = require('passport-local').Strategy,    User = require('../database/Schema').User,    shortid = require('shortid'); passport.serializeUser( (user, cb) => {    cb(null, user); }); passport.deserializeUser( (obj, cb) => {    cb(null, obj); }); //  passport,    passport.use('localRegister', new LocalStrategy({        usernameField: 'email',        passwordField: 'password',        passReqToCallback: true    },    (req, email, password, done) => {        User.findOne({$or: [{email: email}, {username: req.body.username}]}, (err, user) => {            if (err)                return done(err);            if (user) {                if (user.email === email) {                    req.flash('email', 'Email is already taken');                               if (user.username === req.body.username) {                    req.flash('username', 'Username is already taken');                               return done(null, false);            } else {                let user = new User();                user.email = email;                user.password = user.generateHash(password);                user.username = req.body.username;                user.stream_key = shortid.generate();                user.save( (err) => {                    if (err)                        throw err;                    return done(null, user);                });                   });    })); //  passport,    passport.use('localLogin', new LocalStrategy({        usernameField: 'email',        passwordField: 'password',        passReqToCallback: true    },    (req, email, password, done) => {        User.findOne({'email': email}, (err, user) => {            if (err)                return done(err);            if (!user)                return done(null, false, req.flash('email', 'Email doesn\'t exist.'));            if (!user.validPassword(password))                return done(null, false, req.flash('password', 'Oops! Wrong password.'));            return done(null, user);        });    })); module.exports = passport; 

Selain itu, kita perlu menggambarkan skema untuk model pengguna (itu akan disebut UserSchema ). Buat folder database di folder server , dan di dalamnya file UserSchema.js .

Berikut adalah kode untuk file 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 memiliki tiga metode. Metode generateHash dirancang untuk mengubah kata sandi, disajikan dalam bentuk teks biasa, menjadi hash bcrypt. Kami menggunakan metode ini dalam strategi paspor untuk mengubah kata sandi yang dimasukkan oleh pengguna menjadi hash bcrypt. Hash kata sandi yang diterima kemudian disimpan dalam database. Metode validPassword menerima kata sandi yang dimasukkan oleh pengguna dan memverifikasinya dengan membandingkan hash-nya dengan hash yang disimpan dalam database. Metode generateStreamKey string unik yang akan kami transfer ke pengguna sebagai kunci streaming (kunci aliran) mereka untuk klien RTMP.

Berikut adalah kode file server/database/Schema.js :

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

Sekarang kita telah menetapkan strategi paspor, menjelaskan skema UserSchema dan membuat model berdasarkan itu, mari kita inisialisasi paspor di app.js

Berikut adalah kode untuk melengkapi file server/app.js :

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

Selain itu, rute baru harus didaftarkan di app.js Untuk melakukan ini, tambahkan kode berikut ke server/app.js :

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

Buat file login.js dan register.js di folder routes terletak di folder server . Dalam file-file ini kami mendefinisikan beberapa rute di atas dan menggunakan middleware paspor untuk mengatur registrasi dan otentikasi pengguna.

Berikut adalah kode untuk file 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; 

Berikut adalah kode untuk file 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; 

Kami menggunakan mesin templat ejs. Tambahkan file login.ejs dan register.ejs ke folder views , yang terletak di folder server .

Berikut ini isi file 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> 

Inilah yang seharusnya ada di file 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> 

Kami dapat mengatakan kami telah selesai bekerja pada sistem otentikasi. Sekarang kita akan mulai membuat bagian selanjutnya dari proyek dan mengkonfigurasi server RTMP.

Konfigurasikan Server RTMP


RTMP (Real-Time Messaging Protocol) adalah protokol yang dikembangkan untuk transmisi video, audio, dan berbagai data antara tape drive dan server yang berkinerja tinggi. Kedutan, Facebook, YouTube, dan banyak situs streaming lainnya menerima aliran RTMP dan mentranskodenya menjadi aliran HTTP (format HLS) sebelum mentransfer aliran ini ke CDN mereka untuk memastikan ketersediaannya yang tinggi.

Kami menggunakan modul node-media-server - Node.js-implementasi server media RTMP. Server media ini menerima stream RTMP dan mengubahnya menjadi HLS / DASH menggunakan kerangka kerja ffmpeg multimedia. Agar proyek berhasil, ffmpeg harus diinstal pada sistem Anda. Jika Anda bekerja di Linux dan Anda sudah menginstal ffmpeg, Anda bisa mengetahui path ke sana dengan menjalankan perintah berikut dari terminal:

 $ which ffmpeg # /usr/bin/ffmpeg 

Untuk bekerja dengan paket node-media-server, ffmpeg versi 4.x direkomendasikan. Anda dapat memeriksa versi ffmpeg yang diinstal seperti ini:

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

Jika Anda belum menginstal ffmpeg dan menjalankan Ubuntu, Anda dapat menginstal kerangka kerja ini dengan menjalankan perintah berikut:

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

Jika Anda bekerja pada Windows, Anda dapat mengunduh ffmpeg build untuk Windows.

Tambahkan file konfigurasi server/config/default.js ke proyek:

 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; 

Ganti nilai properti ffmpeg dengan path tempat ffmpeg diinstal pada sistem Anda. Jika Anda bekerja pada Windows dan mengunduh ffmpeg rakitan Windows dari tautan di atas - jangan lupa untuk menambahkan ekstensi .exe ke nama file. Maka fragmen yang sesuai dari kode di atas akan terlihat seperti ini:

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

Sekarang instal node-media-server dengan menjalankan perintah berikut:

 $ npm install node-media-server --save 

Buat file media_server.js di folder server .

Berikut adalah kode untuk dimasukkan ke 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; 

Menggunakan objek NodeMediaService cukup sederhana. Ini menyediakan server RTMP dan memungkinkan Anda untuk menunggu koneksi. Jika kunci streaming tidak valid, koneksi yang masuk dapat ditolak. Kami akan menangani acara objek prePublish ini. Di bagian berikutnya, kami menambahkan kode tambahan ke penutupan pendengar acara prePublish - prePublish . Ini akan memungkinkan Anda untuk menolak koneksi masuk dengan kunci streaming yang tidak valid. Sementara itu, kami akan menerima semua koneksi masuk yang datang ke port RTMP default (1935). Kita hanya perlu mengimpor objek app.js dalam node_media_server dan memanggil metode run .

Tambahkan kode berikut ke server/app.js :

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

Unduh dan instal OBS (Open Broadcaster Software). Buka jendela pengaturan aplikasi dan buka bagian Stream . Pilih Custom di bidang Service dan masukkan rtmp://127.0.0.1:1935/live di bidang Server . Bidang Stream Key dapat dibiarkan kosong. Jika program tidak memungkinkan Anda untuk menyimpan pengaturan tanpa mengisi bidang ini, Anda dapat memasukkan serangkaian karakter yang sewenang-wenang di dalamnya. Klik pada tombol Apply dan pada tombol OK . Klik tombol Start Streaming untuk mulai mentransfer aliran RTMP Anda ke server lokal Anda sendiri.


Konfigurasikan OBS

Pergi ke terminal dan lihat apa yang ditampilkan server media di sana. Anda akan melihat informasi di sana tentang aliran masuk dan log dari beberapa pendengar acara.


Output data ke terminal oleh server media berdasarkan Node.js

Server media memberikan akses ke API, yang memungkinkan Anda untuk mendapatkan daftar klien yang terhubung. Untuk melihat daftar ini, Anda dapat membuka browser di http://127.0.0.1:8888/api/streams . Nantinya, kami akan menggunakan API ini dalam aplikasi Bereaksi untuk menampilkan daftar pengguna siaran. Inilah yang dapat Anda lihat dengan mengakses API ini:

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

Sekarang backend hampir siap. Ini adalah server streaming yang berfungsi yang mendukung teknologi HTTP, RTMP dan HLS. Namun, kami belum membuat sistem untuk memeriksa koneksi RTMP yang masuk. Itu harus memungkinkan kami untuk memastikan bahwa server menerima aliran hanya dari pengguna yang diautentikasi. Tambahkan kode berikut ke prePublish event prePublish di file server/media_server.js :

 //       const User = require('./database/Schema').User; nms.on('prePublish', async (id, StreamPath, args) => {    let stream_key = getStreamKeyFromStreamPath(StreamPath);    console.log('[NodeEvent on prePublish]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);    User.findOne({stream_key: stream_key}, (err, user) => {        if (!err) {            if (!user) {                let session = nms.getSession(id);                session.reject();            } else {                // -                       }); }); const getStreamKeyFromStreamPath = (path) => {    let parts = path.split('/');    return parts[parts.length - 1]; }; 

Pada penutupan, kami meminta basis data untuk menemukan pengguna dengan kunci streaming. Jika kunci milik pengguna, kami hanya mengizinkan pengguna untuk terhubung ke server dan menerbitkan siaran mereka. Kalau tidak, kami menolak koneksi RTMP yang masuk.

Di bagian selanjutnya, kita akan membuat aplikasi sisi klien sederhana berdasarkan React. Hal ini diperlukan untuk memungkinkan pemirsa menonton siaran streaming, serta untuk memungkinkan streamer menghasilkan dan melihat kunci streaming mereka.

Streaming langsung


Sekarang buka folder clients . Karena kita akan membuat aplikasi Bereaksi, kita akan memerlukan paket web. Kami juga membutuhkan pemuat, yang digunakan untuk menerjemahkan kode JSX ke dalam kode JavaScript yang dimengerti oleh browser. Pasang modul-modul berikut:

 $ 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 

Tambahkan ke proyek, di direktori root, file konfigurasi untuk 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    }, }; 

Tambahkan file client/index.js ke proyek:

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

Berikut ini isi dari file client/index.scss :

 @import '~bootstrap/dist/css/bootstrap.css'; @import '~video.js/dist/video-js.css'; @import url('https://fonts.googleapis.com/css?family=Dosis'); html,body{  font-family: 'Dosis', sans-serif; } 

React router digunakan untuk routing. Di frontend, kami juga menggunakan bootstrap, dan, untuk siaran, video.js. Sekarang tambahkan folder components ke folder client , dan file Root.js ke Root.js . Berikut ini isi dari file client/components/Root.js :

 import React from "react"; import {Router, Route} from 'react-router-dom'; import Navbar from './Navbar'; import LiveStreams from './LiveStreams'; import Settings from './Settings'; import VideoPlayer from './VideoPlayer'; const customHistory = require("history").createBrowserHistory(); export default class Root extends React.Component {    constructor(props){        super(props);       render(){        return (            <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>          } 

Komponen <Root/> merender komponen <Router/> React yang berisi tiga subkomponen <Route/> . Komponen <LiveStreams/> menampilkan daftar siaran. Komponen <VideoPlayer/> bertanggung jawab untuk menampilkan pemutar video.js. Komponen <Settings/> bertanggung jawab untuk membuat antarmuka untuk bekerja dengan kunci streaming.

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

Seperti inilah tampilan halaman aplikasi.


Layanan streaming frontend

Setelah memasang komponen <LiveStreams/> , API NMS dipanggil untuk mendapatkan daftar klien yang terhubung ke sistem. API NMS tidak memberikan banyak informasi tentang pengguna. Secara khusus, kami dapat memperoleh informasi tentang kunci streaming darinya, di mana pengguna terhubung ke server RTMP. Kami akan menggunakan kunci-kunci ini ketika membuat kueri ke basis data untuk mendapatkan informasi tentang akun pengguna.

Dalam metode getStreamsInfo kami menjalankan permintaan XHR ke /streams/info , tetapi kami belum membuat sesuatu yang dapat menanggapi permintaan ini. Buat file server/routes/streams.js dengan konten berikut:

 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; 

Kami meneruskan informasi aliran yang dikembalikan oleh NMS API ke backend, untuk mendapatkan informasi tentang klien yang terhubung.

Kami melakukan kueri basis data untuk mendapatkan daftar pengguna yang kunci streaming-nya cocok dengan yang kami terima dari NMS API. Kami mengembalikan daftar dalam format JSON. Kami mendaftarkan rute dalam file server/app.js :

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

Hasilnya, kami menampilkan daftar siaran aktif. Daftar ini berisi nama pengguna dan thumbnail. Kami akan berbicara tentang cara membuat thumbnail untuk siaran di akhir artikel. Gambar kecil diikat ke halaman tertentu tempat streaming HLS diputar menggunakan video.js.

Buat 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(); 

Ringkasan


, . , . - β€” . - β€” .

.

! , , ?

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


All Articles