يقول مؤلف المادة ، التي نُنشر ترجمتها اليوم ، إنه يعمل على تطبيق يتيح لك تنظيم بث البث (البث) لما يحدث على سطح مكتب المستخدم.

يستقبل التطبيق دفقًا بتنسيق RTMP من المشغل ويحوله إلى دفق HLS ، والذي يمكن تشغيله في متصفحات المشاهدين. ستتحدث هذه المقالة عن كيفية إنشاء تطبيق دفق خاص بك باستخدام Node.js و React. إذا كنت معتادًا على رؤية الفكرة التي تهمك ، يمكنك الانغماس فورًا في الكود ، فيمكنك الآن النظر في
هذا المستودع.
تطوير خادم الويب مع نظام التوثيق الأساسي
لنقم بإنشاء خادم ويب بسيط يستند إلى Node.js ، حيث يتم استخدام استراتيجية مصادقة مستخدم محلي باستخدام مكتبة 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
في دليل المشروع ، قم بإنشاء مجلدين -
client
server
. سينتهي رمز الواجهة الأمامية القائم على 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 لتنظيم تسجيل المستخدم والمصادقة. قم بإنشاء مجلد
auth
في مجلد
server
ووضع ملف
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); });
بالإضافة إلى ذلك ، نحتاج إلى وصف المخطط الخاص بنموذج المستخدم (سوف يطلق عليه
UserSchema
). قم
UserSchema.js
مجلد
database
في مجلد
server
،
UserSchema.js
ملف
UserSchema.js
.
فيما يلي رمز لملف
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
ثلاث طرق. تم تصميم طريقة generHash لتحويل كلمة المرور ، المقدمة في شكل نص عادي ، إلى تجزئة bcrypt. نستخدم هذه الطريقة في استراتيجية جواز السفر لتحويل كلمات المرور التي يدخلها المستخدمون إلى تجزئة bcrypt. ثم يتم تخزين تجزئة كلمة المرور المستلمة في قاعدة البيانات. تقبل طريقة
validPassword
كلمة المرور التي أدخلها المستخدم
validPassword
منها من خلال مقارنة علامة التجزئة الخاصة بها مع التجزئة الذي تم تخزينه في قاعدة البيانات.
generateStreamKey
طريقة
generateStreamKey
سلاسل فريدة سننقلها إلى المستخدمين كمفاتيح التدفق الخاصة بهم (مفاتيح التدفق) لعملاء RTMP.
فيما يلي رمز ملف
server/database/Schema.js
:
let mongoose = require('mongoose'); exports.User = mongoose.model('User', require('./UserSchema'));
الآن
UserSchema
أن
UserSchema
استراتيجيات جواز السفر ،
UserSchema
مخطط
UserSchema
نموذجًا بناءً عليه ، فلنبدأ في تهيئة جواز السفر في
app.js
في ما يلي الرمز
server/app.js
ملف
server/app.js
:
بالإضافة إلى ذلك ، يجب تسجيل مسارات جديدة في
app.js
للقيام بذلك ، أضف الكود التالي إلى
server/app.js
:
قم
login.js
ملفات
login.js
و
register.js
في مجلد
routes
الموجود في مجلد
server
. في هذه الملفات ، نحدد اثنين من الطرق المذكورة أعلاه ونستخدم الوسيطة لجواز السفر لتنظيم التسجيل ومصادقة المستخدم.
فيما يلي رمز ملف
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 templating. أضف ملفات قالب
login.ejs
و
register.ejs
إلى مجلد
views
، الموجود في مجلد
server
.
فيما يلي محتويات ملف
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) قبل نقل هذه التدفقات إلى CDNs الخاصة بهم لضمان توفرها العالي.
نستخدم وحدة خادم الوسائط - العقدة - تطبيق Node.js لخادم وسائط RTMP. يقبل خادم الوسائط هذا تدفقات RTMP ويحولها إلى HLS / DASH باستخدام إطار الوسائط المتعددة ffmpeg. لكي يعمل المشروع بنجاح ، يجب تثبيت ffmpeg على نظامك. إذا كنت تعمل على نظام Linux وكان لديك بالفعل تثبيت ffmpeg ، يمكنك معرفة المسار إليه من خلال تشغيل الأمر التالي من الجهاز الطرفي:
$ which ffmpeg # /usr/bin/ffmpeg
للعمل مع حزمة خادم الوسائط ، نوصي باستخدام الإصدار 4.x من ffmpeg. يمكنك التحقق من الإصدار المثبت من 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 ، يمكنك تنزيل
بنيات ffmpeg لنظام Windows.
أضف ملف
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
قم
media_server.js
ملف
media_server.js
في مجلد
server
.
إليك الكود الذي سيتم وضعه في
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
. في القسم التالي ، نضيف رمزًا
prePublish
لإغلاق مستمع الأحداث
prePublish
. سيسمح لك برفض الاتصالات الواردة باستخدام مفاتيح التدفق غير الصالحة. في غضون ذلك ، سوف نقبل جميع الاتصالات الواردة القادمة إلى منفذ RTMP الافتراضي (1935). نحتاج فقط إلى استيراد كائن
app.js
في
node_media_server
واستدعاء طريقة
run
الخاصة به.
أضف الكود التالي إلى
server/app.js
:
قم بتنزيل وتثبيت
OBS (Open Broadcaster Software). افتح نافذة إعدادات التطبيق وانتقل إلى قسم
Stream
. حدد
Custom
في مجال
Service
وأدخل
rtmp://127.0.0.1:1935/live
في حقل
Server
. يمكن ترك حقل
Stream Key
فارغًا. إذا لم يسمح لك البرنامج بحفظ الإعدادات دون ملء هذا الحقل ، يمكنك إدخال مجموعة عشوائية من الأحرف فيه. انقر على زر
Apply
وعلى زر
OK
. انقر فوق الزر "
Start Streaming
لبدء نقل دفق RTMP إلى الخادم المحلي الخاص بك.
تكوين OBSانتقل إلى المحطة وانظر إلى ما يعرضه خادم الوسائط هناك. سترى هناك معلومات حول البث الوارد وسجلات العديد من مستمعي الأحداث.
إخراج البيانات إلى المحطة الطرفية بواسطة خادم وسائط يعتمد على Node.jsيتيح خادم الوسائط الوصول إلى واجهة برمجة التطبيقات ، والتي تتيح لك الحصول على قائمة بالعملاء المتصلين. لمشاهدة هذه القائمة ، يمكنك الذهاب إلى المتصفح على الموقع
http://127.0.0.1:8888/api/streams
. في وقت لاحق ، سوف نستخدم واجهة برمجة التطبيقات هذه في تطبيق React لعرض قائمة بمستخدمي البث. إليك ما يمكنك رؤيته من خلال الوصول إلى واجهة برمجة التطبيقات هذه:
{ "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 الواردة. يجب أن يسمح لنا بالتأكد من أن الخادم يقبل التدفقات فقط من المستخدمين المصادق عليهم. أضف التعليمات البرمجية التالية إلى
prePublish
الأحداث
prePublish
في
server/media_server.js
:
في الختام ، نحن الاستعلام عن قاعدة البيانات للعثور على المستخدم مع مفتاح الدفق. إذا كان المفتاح ملكًا للمستخدم ، فنحن ببساطة نسمح للمستخدم بالاتصال بالخادم ونشر بثه. خلاف ذلك ، نحن نرفض اتصال RTMP الوارد.
في القسم التالي ، سننشئ تطبيقًا بسيطًا من جانب العميل يستند إلى React. هناك حاجة للسماح للمشاهدين بمشاهدة البث المباشر ، وكذلك للسماح للمشاهدون بإنشاء وعرض مفاتيح البث الخاصة بهم.
بث مباشر
اذهب الآن إلى مجلد
clients
. نظرًا لأننا سننشئ تطبيق React ، فسنحتاج إلى حزمة ويب. نحتاج أيضًا إلى برامج التحميل ، والتي تُستخدم لترجمة رمز 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 router يستخدم للتوجيه. في الواجهة الأمامية ، نستخدم أيضًا bootstrap ، وفي عمليات البث ، video.js. الآن قم بإضافة مجلد
components
إلى مجلد
client
، وملف
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 للحصول على قائمة العملاء المتصلين بالنظام. لا يوفر NMS API الكثير من المعلومات حول المستخدمين. على وجه الخصوص ، يمكننا الحصول على معلومات حول دفق المفاتيح منه ، والتي يتم من خلالها توصيل المستخدمين بخادم RTMP. سنستخدم هذه المفاتيح عند إنشاء استعلامات في قاعدة البيانات للحصول على معلومات حول حسابات المستخدمين.
في طريقة
getStreamsInfo
نقوم بتنفيذ طلب XHR على
/streams/info
، لكننا لم ننشئ شيئًا حتى الآن يمكنه الاستجابة لهذا الطلب. قم بإنشاء ملف
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. نرجع القائمة بتنسيق JSON. نسجل المسار في
server/app.js
:
app.use('/streams', require('./routes/streams'));
نتيجة لذلك ، نعرض قائمة البث النشطة. تحتوي هذه القائمة على اسم المستخدم والصورة المصغرة. سنتحدث عن كيفية إنشاء صور مصغرة للبث في نهاية المقال. ترتبط الصور المصغرة بصفحات محددة يتم فيها تشغيل تدفقات HLS باستخدام video.js.
قم
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
:
النتائج
, . , . - —
. - — .
.
! , , ?