Our backend is pretty much ready. Its a working HTTP/RTMP/HLS streaming server. However, we still need to validate incoming RTMP connections to ensure that only authenticated user’s streams are accepted. Add this code to your prePublish event listener closure.
Xem phần 1: Xây dựng hệ thống Live Streaming viết từ Node.js và React để dạy học thời dịch bệnh Covid-19
// Add import at the start of file
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 {
// do stuff
}
}
});
});
const getStreamKeyFromStreamPath = (path) => {
let parts = path.split('/');
return parts[parts.length - 1];
};
Inside closure, we are querying the database to find a user with the streaming key. If it belongs to a user, we would simply let them connect and publish their stream. Otherwise, we reject the incoming RTMP connection.
In the next part of this tutorial, we will build a basic React frontend to allow users to view live streams, generate and view their streaming keys.
Displaying Live Streams
For this part, we will be working in the client directory. Since its a react app, we will be using webpack and necessary loaders to transpile JSX into browser ready JavaScript. Install these modules.
npm install @babel/core @babel/preset-env @babel/preset-reactbabel-loader css-loader file-loader mini-css-extract-pluginnode-sass sass-loader style-loader url-loader webpack webpack-clireact react-dom react-router-dom video.js jquery bootstrap historypopper.js
Add this webpack config to your project.
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
},
};
Add an index.js file with the following code.
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')
);
}
@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;
}
We are using react-router for routing and bootstrap on the frontend along with video.js for displaying live streams. Add components directory with Root.js file in it and add the following code.
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/> component renders a react <Router/> to hold three sub <Route/> components. <LiveStreams/> component will render all the live streams. <VideoPlayer/> will render video.js player components. <Settings/> component will provide an interface for generating a new streaming key.
Create LiveStreams.js component.
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 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>
)
}
}
After our component mounts, we are making a call to NMS API to retrieve all the connected clients. NMS API does not have much information about the user other than their streaming key through which they are connected to our RTMP server. We will use the streaming key to query our database to get users records. In getStreamsInfo method, we are making an XHR request to /streams/info which we have not yet defined. Create a server/routes/streams.js file and add the following code to it. We will pass on the streams returned from the NMS API to our backend to retrieve information about connected clients.
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;
We are querying the database to select all the users with matched streaming keys that we retrieved from NMS API and return them as a JSON response. Register this route in the app.js file.
server/app.js
app.use('/streams', require('./routes/streams'));
app.use('/streams', require('./routes/streams'));
In the end, we are rendering live streams with username and thumbnails. We will generate thumbnails for our streams in the last part of this tutorial. These thumbnails are linked to the individual pages where HLS streams are played inside a video.js player component. Create VideoPlayer.js component.
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>
)
}
}
On component mount, we retrieve the user’s streaming key to initiate an HLS stream inside video.js player.
Issuing streaming keys to broadcasters
Create Settings.js component.
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>
)
}
}
Inside our passport’s local strategy, when a user successfully registers, we create a new user record with a unique streaming key. If a user visits /settings route, they will be able to view their existing key. When components mounts, we make an XHR call to the backend to retrieve user’s existing streaming key and render it inside our <Settings/> component.
Users can generate a new key by clicking Generate a new key button which makes an XHR call to the backend to create a new key, save it to user collection and also return it so that it can be rendered inside the component. We need to define both GET and POST /settings/stream_key routes. Create a server/routes/settings.js file and add the following code.
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;
We using shortid module for generating unique strings. Register these routes in the app.js file.
app.use('/settings', require('./routes/settings'));
Generating Live Stream Thumbnails
In <LiveStreams/> components, we are displaying thumbnail images for live streams.
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 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>
)
}
We will be generating these thumbnails whenever a new stream connects to our server. We will run a cron job to generate new thumbnails for live streams every 5 seconds. Add this helper method inside 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
};
We are passing the streaming key to generateStreamThumbnail. It spawns a detached ffmpeg process to generate thumbnail image from HLS stream. We will call this helper method inside prePublish closure after validating the streaming key.
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);
}
}
});
});
To generate fresh thumbnails, we will run a cron job and call this helper method from it.
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;
This cron job will execute every 5 seconds, retrieve active streams from NMS API and generate thumbnails for each stream using the streaming key. Import this job inside the app.js file and run it.
// Add import at the start of file
const thumbnail_generator = require('./cron/thumbnails');
// Call start method at the end of file
thumbnail_generator.start();
Our real-time live streaming app is ready. I might have missed some details in this tutorial, but you can access complete code in this repository. If you run into an issue, please report it by creating a new issue in the repository so that it can be addressed. Setup and usage instructions are available in the repository’s readme file.
Không chỉ giáo dục bị ảnh hưởng, các doanh nghiệp cũng dừng hoạt động vì dịch bệnh Covid-19
Theo Báo cáo của Bộ Lao động - Thương binh & Xã hội về những ảnh hưởng của dịch Covid-19 cho biết, 30 tỉnh, thành có 322 doanh nghiệp dừng hoạt động, 553 đơn vị giảm quy mô hoặc thu hẹp sản xuất kinh doanh, 30 hợp tác xã và gần 300.000 hộ gia đình phải dừng hoạt động, thu hẹp sản xuất.
Chưa kể, hơn 1.000 lao động tại 22 tỉnh, thành bị mất việc, trong đó, hơn một phần ba đến từ ngành dịch vụ lưu trú và ăn uống, 10% là ngành công nghiệp chế biến, chế tạo.
Các ngành khác có lao động bị ảnh hưởng khác là nông, lâm và thủy sản; cung cấp nước; quản lý và xử lý rác thải, nước thải; vận tải, kho bãi; bán lẻ, sửa chữa ôtô, xe máy...
Có hơn 15.000 lao động Trung Quốc đang làm việc tại Việt Nam nhưng một nửa về quê ăn Tết và đa phần chưa quay lại làm việc do dịch Covid-19. "Việc quản lý và điều hành doanh nghiệp của lao động Trung Quốc chủ yếu qua điện thoại và Internet nên các đơn vị cũng gặp khó khăn", Bộ Lao động Thương binh & Xã hội thông tin.
Thị trường lao động quý I cũng như cả năm 2020, đặc biệt trong lĩnh vực công nghiệp và dịch vụ được dự báo thiếu hụt và dịch chuyển mạnh do nhiều doanh nghiệp đóng cửa, thu hẹp chuỗi sản xuất. Theo Bộ Lao động Thương binh & Xã hội, tình hình sẽ càng căng thẳng ở các doanh nghiệp gia công cho những công ty đa quốc gia, doanh nghiệp có đầu vào nguyên liệu phụ thuộc vào Trung Quốc.
Khảo sát từ cộng đồng doanh nghiệp của Ban nghiên cứu phát triển kinh tế tư nhân cho thấy, không chỉ bị sụt giảm doanh thu, thiếu nguyên liệu, nhiều công ty gặp khó khăn vì tâm lý người lao động bị ảnh hưởng. Một mặt, người lao động lo ngại dịch lây lan, một mặt việc học sinh chưa đến trường cũng ảnh hưởng tâm lý các phụ huynh. Trong khi đó, nhiều các cơ sở sản xuất lớn vẫn phải với trách nhiệm chi trả quyền lợi cho cho nhân công giữa bối cảnh đang khó khăn.
Một doanh nghiệp Nhật cũng cho biết tâm lý của người lao động ảnh hưởng mạnh khiến nhiều nhà máy phải đóng cửa lâu, mới hoạt động trở lại từ tuần này. Trong khi đó, một số thương hiệu bán lẻ chuẩn bị khai trương cửa hàng mới cũng đang chờ các thông tin về diễn biến dịch nCoV mới đưa ra quyết định. Đơn vị này cho rằng, sau dịch, các doanh nghiệp có thể bị dồn đơn hàng dẫn đến việc đẩy mạnh việc làm thêm giờ. Do đó, doanh nghiệp đề xuất Chính phủ xem xét để có quy định hỗ trợ việc này.