├── .babelrc
├── .gitignore
├── LICENSE
├── client
├── components
│ ├── LiveStreams.js
│ ├── LiveStreams.scss
│ ├── Navbar.js
│ ├── Root.js
│ ├── Settings.js
│ └── VideoPlayer.js
├── index.js
└── index.scss
├── package-lock.json
├── package.json
├── readme.md
├── server
├── app.js
├── auth
│ └── passport.js
├── config
│ └── default.js
├── cron
│ └── thumbnails.js
├── database
│ ├── Schema.js
│ └── UserSchema.js
├── helpers
│ └── helpers.js
├── media
│ └── .gitkeep
├── media_server.js
├── routes
│ ├── login.js
│ ├── register.js
│ ├── settings.js
│ ├── streams.js
│ └── user.js
├── sessions
│ └── .gitkeep
├── thumbnails
│ └── .gitkeep
└── views
│ ├── footer.ejs
│ ├── header.ejs
│ ├── index.ejs
│ ├── login.ejs
│ ├── navbar.ejs
│ └── register.ejs
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react"]
3 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | public
3 | node_modules
4 | npm-debug.log
5 |
6 | server/thumbnails/*
7 | !server/thumbnails/.gitkeep
8 |
9 | #ignore sessions but keep folder
10 | server/sessions/*
11 | !server/sessions/.gitkeep
12 |
13 | # ignore media but keep folder
14 | server/media/*
15 | !server/media/.gitkeep
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Waleed Ahmad
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/client/components/LiveStreams.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import axios from 'axios';
3 | import {Link} from 'react-router-dom';
4 | import './LiveStreams.scss';
5 | import config from '../../server/config/default';
6 |
7 |
8 | export default class Navbar extends React.Component {
9 |
10 | constructor(props) {
11 | super(props);
12 | this.state = {
13 | live_streams: []
14 | }
15 | }
16 |
17 | componentDidMount() {
18 | this.getLiveStreams();
19 | }
20 |
21 | getLiveStreams() {
22 | axios.get('http://127.0.0.1:' + config.rtmp_server.http.port + '/api/streams')
23 | .then(res => {
24 | let streams = res.data;
25 | if (typeof (streams['live'] !== 'undefined')) {
26 | this.getStreamsInfo(streams['live']);
27 | }
28 | });
29 | }
30 |
31 | getStreamsInfo(live_streams) {
32 | axios.get('/streams/info', {
33 | params: {
34 | streams: live_streams
35 | }
36 | }).then(res => {
37 | this.setState({
38 | live_streams: res.data
39 | }, () => {
40 | console.log(this.state);
41 | });
42 | });
43 | }
44 |
45 | render() {
46 | let streams = this.state.live_streams.map((stream, index) => {
47 | return (
48 |
49 |
LIVE
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | {stream.username}
59 |
60 |
61 |
62 | );
63 | });
64 |
65 | return (
66 |
67 |
Live Streams
68 |
69 |
70 |
71 | {streams}
72 |
73 |
74 | )
75 | }
76 | }
--------------------------------------------------------------------------------
/client/components/LiveStreams.scss:
--------------------------------------------------------------------------------
1 | .streams {
2 |
3 | .stream {
4 |
5 | .live-label {
6 | position: absolute;
7 | top: 10px;
8 | left: 20px;
9 | background-color: red;
10 | color: white;
11 | padding: 1px 5px;
12 | }
13 |
14 | .stream-thumbnail {
15 | img {
16 | width: 100%;
17 | }
18 | }
19 |
20 | .username {
21 |
22 | position: absolute;
23 | bottom: 10px;
24 | left: 20px;
25 | background-color: black;
26 |
27 | a {
28 | font-size: 18px;
29 | color: white;
30 | text-decoration: none;
31 | padding: 2px 8px;
32 | }
33 | }
34 |
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/client/components/Navbar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Link} from 'react-router-dom';
3 |
4 | export default class Navbar extends React.Component {
5 | render() {
6 | return (
7 |
8 |
9 |
10 | NodeStream
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Go Live
23 |
24 |
25 |
26 | Github
27 |
28 |
29 | Logout
30 |
31 |
32 |
33 |
34 |
35 | )
36 | }
37 | }
--------------------------------------------------------------------------------
/client/components/Root.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {Router, Route} from 'react-router-dom';
3 | import Navbar from './Navbar';
4 | import LiveStreams from './LiveStreams';
5 | import Settings from './Settings';
6 |
7 | import VideoPlayer from './VideoPlayer';
8 | const customHistory = require("history").createBrowserHistory();
9 |
10 | export default class Root extends React.Component {
11 |
12 | constructor(props){
13 | super(props);
14 | }
15 |
16 | render(){
17 | return (
18 |
19 |
20 |
21 | (
22 |
23 | )}/>
24 |
25 | (
26 |
27 | )}/>
28 |
29 | (
30 |
31 | )}/>
32 |
33 |
34 | )
35 | }
36 | }
--------------------------------------------------------------------------------
/client/components/Settings.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import axios from 'axios';
3 |
4 | export default class Navbar extends React.Component {
5 |
6 | constructor(props){
7 | super(props);
8 |
9 | this.state = {
10 | stream_key : ''
11 | };
12 |
13 | this.generateStreamKey = this.generateStreamKey.bind(this);
14 | }
15 |
16 | componentDidMount() {
17 | this.getStreamKey();
18 | }
19 |
20 | generateStreamKey(e){
21 | axios.post('/settings/stream_key')
22 | .then(res => {
23 | this.setState({
24 | stream_key : res.data.stream_key
25 | });
26 | })
27 | }
28 |
29 | getStreamKey(){
30 | axios.get('/settings/stream_key')
31 | .then(res => {
32 | this.setState({
33 | stream_key : res.data.stream_key
34 | });
35 | })
36 | }
37 |
38 | render() {
39 | return (
40 |
41 |
42 |
Streaming Key
43 |
44 |
45 |
46 |
47 |
{this.state.stream_key}
48 |
49 |
50 |
53 | Generate a new key
54 |
55 |
56 |
57 |
58 |
59 |
60 |
How to Stream
61 |
62 |
63 |
64 |
65 |
66 | You can use OBS or
67 | XSplit to Live stream. If you're
68 | using OBS, go to Settings > Stream and select Custom from service dropdown.
69 | Enter rtmp://127.0.0.1:1935/live in server input field. Also, add your stream key.
70 | Click apply to save.
71 |
72 |
73 |
74 |
75 |
76 | )
77 | }
78 | }
--------------------------------------------------------------------------------
/client/components/VideoPlayer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import videojs from 'video.js'
3 | import axios from 'axios';
4 | import config from '../../server/config/default';
5 |
6 |
7 | export default class VideoPlayer extends React.Component {
8 |
9 | constructor(props) {
10 | super(props);
11 |
12 | this.state = {
13 | stream: false,
14 | videoJsOptions: null
15 | }
16 | }
17 |
18 | componentDidMount() {
19 |
20 | axios.get('/user', {
21 | params: {
22 | username: this.props.match.params.username
23 | }
24 | }).then(res => {
25 | this.setState({
26 | stream: true,
27 | videoJsOptions: {
28 | autoplay: false,
29 | controls: true,
30 | sources: [{
31 | src: 'http://127.0.0.1:' + config.rtmp_server.http.port + '/live/' + res.data.stream_key + '/index.m3u8',
32 | type: 'application/x-mpegURL'
33 | }],
34 | fluid: true,
35 | }
36 | }, () => {
37 | this.player = videojs(this.videoNode, this.state.videoJsOptions, function onPlayerReady() {
38 | console.log('onPlayerReady', this)
39 | });
40 | });
41 | })
42 | }
43 |
44 | componentWillUnmount() {
45 | if (this.player) {
46 | this.player.dispose()
47 | }
48 | }
49 |
50 | render() {
51 | return (
52 |
53 |
54 | {this.state.stream ? (
55 |
56 | this.videoNode = node} className="video-js vjs-big-play-centered"/>
57 |
58 | ) : ' Loading ... '}
59 |
60 |
61 | )
62 | }
63 | }
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from 'react-dom';
3 | import {BrowserRouter} from 'react-router-dom';
4 | import 'bootstrap';
5 | require('./index.scss');
6 | import Root from './components/Root.js';
7 |
8 | if(document.getElementById('root')){
9 | ReactDOM.render(
10 |
11 |
12 | ,
13 | document.getElementById('root')
14 | );
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/client/index.scss:
--------------------------------------------------------------------------------
1 | @import '~bootstrap/dist/css/bootstrap.css';
2 | @import '~video.js/dist/video-js.css';
3 |
4 | @import url('https://fonts.googleapis.com/css?family=Dosis');
5 |
6 | html,body{
7 | font-family: 'Dosis', sans-serif;
8 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-livestream",
3 | "version": "1.0.1",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "watch": "./node_modules/.bin/webpack",
9 | "start": "./node_modules/.bin/supervisor -w server ./server/app.js"
10 | },
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "@popperjs/core": "^2.11.0",
15 | "axios": "^0.24.0",
16 | "bcryptjs": "^2.4.3",
17 | "body-parser": "^1.19.0",
18 | "bootstrap": "^4.6.1",
19 | "config": "^3.3.6",
20 | "connect-ensure-login": "^0.1.1",
21 | "connect-flash": "^0.1.1",
22 | "connect-mongo": "^4.6.0",
23 | "cookie-parser": "^1.4.6",
24 | "cron": "^1.8.2",
25 | "ejs": "^3.1.6",
26 | "express": "^4.17.1",
27 | "express-session": "^1.17.2",
28 | "history": "^5.1.0",
29 | "jquery": "^3.6.0",
30 | "mongoose": "^6.0.14",
31 | "node-media-server": "^2.3.8",
32 | "passport": "^0.5.0",
33 | "passport-local": "^1.0.0",
34 | "react": "^16.14.0",
35 | "react-dom": "^16.14.0",
36 | "react-router-dom": "^4.3.1",
37 | "shortid": "^2.2.16",
38 | "video.js": "^7.17.0"
39 | },
40 | "devDependencies": {
41 | "@babel/core": "^7.16.0",
42 | "@babel/preset-env": "^7.16.4",
43 | "@babel/preset-react": "^7.16.0",
44 | "babel-loader": "^8.2.3",
45 | "css-loader": "^6.5.1",
46 | "file-loader": "^6.2.0",
47 | "mini-css-extract-plugin": "^2.4.5",
48 | "node-sass": "^6.0.1",
49 | "sass-loader": "^12.3.0",
50 | "style-loader": "^3.3.1",
51 | "supervisor": "^0.12.0",
52 | "url-loader": "^4.1.1",
53 | "webpack": "^5.64.4",
54 | "webpack-cli": "^4.9.1"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | ### NodeStream
2 |
3 | See complete tutorial [here](https://quantizd.com/building-live-streaming-app-with-node-js-and-react/).
4 |
5 |
6 | #### Install ffmpeg for RTMP to HLS transcoding
7 |
8 | ```
9 | # On Ubuntu 18.04
10 |
11 | $ sudo add-apt-repository ppa:jonathonf/ffmpeg-4
12 | $ sudo apt install ffmpeg
13 |
14 | # check version
15 | $ ffmpeg --version
16 |
17 | # You can download Windows builds from ffmpeg site.
18 | ```
19 |
20 | ### Prerequisites
21 |
22 | #### MongoDB
23 |
24 | Make sure you have **MongoDB** installed on your system. We use Mongoose for accessing database.
25 | Check MongoDB docs on how to install MongoDB on your operating system.
26 |
27 | [Linux](https://docs.mongodb.com/manual/administration/install-on-linux/)
28 |
29 | [Windows](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-windows/)
30 |
31 | [Mac](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-os-x/)
32 |
33 | #### Python 2.7
34 | Make sure you have python 2.x installed and added to path.
35 |
36 | ### Configuration
37 | Change ffmpeg path in node media server configuration to your
38 | own installed path.
39 |
40 | Also change secret string. It will be used for session encryption.
41 |
42 | ```
43 | cd nodeStream && nano /server/config/default.js
44 |
45 | const config = {
46 | server: {
47 | secret: 'kjVkuti2xAyF3JGCzSZTk0YWM5JhI9mgQW4rytXc',
48 | port : 3333
49 | },
50 | rtmp_server: {
51 | rtmp: {
52 | port: 1935,
53 | chunk_size: 60000,
54 | gop_cache: true,
55 | ping: 60,
56 | ping_timeout: 30
57 | },
58 | http: {
59 | port: 8888,
60 | mediaroot: './server/media',
61 | allow_origin: '*'
62 | },
63 | trans: {
64 | ffmpeg: '/usr/bin/ffmpeg',
65 | tasks: [
66 | {
67 | app: 'live',
68 | hls: true,
69 | hlsFlags: '[hls_time=2:hls_list_size=3:hls_flags=delete_segments]',
70 | dash: true,
71 | dashFlags: '[f=dash:window_size=3:extra_window_size=5]'
72 | }
73 | ]
74 | }
75 | }
76 | };
77 | ```
78 |
79 | #### Install dependencies, build code and run server
80 | ```
81 | $ npm install
82 |
83 | # run webpack and watch for changes
84 | $ npm run watch
85 |
86 | # run node server with supervisor and watch for changes
87 | $ npm run start
88 | ```
89 | #### Streaming with OBS
90 |
91 | Go to Settings > Stream. Select Custom service and `rtmp://127.0.0.1:1935/live`
92 | in server input. Enter your streaming key issued by NodeStream and click Apply.
93 | Click start streaming to broadcast your stream.
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | const express = require('express'),
2 | path = require('path'),
3 | session = require('express-session'),
4 | bodyParse = require('body-parser'),
5 | passport = require('./auth/passport'),
6 | mongoose = require('mongoose'),
7 | middleware = require('connect-ensure-login'),
8 | MongoStore = require('connect-mongo');
9 | config = require('./config/default'),
10 | flash = require('connect-flash'),
11 | port = config.server.port,
12 | app = express(),
13 | node_media_server = require('./media_server'),
14 | thumbnail_generator = require('./cron/thumbnails');
15 |
16 | mongoose.connect('mongodb://127.0.0.1/nodeStream' , { useNewUrlParser: true });
17 |
18 | app.set('view engine', 'ejs');
19 | app.set('views', path.join(__dirname, './views'));
20 | app.use(express.static('public'));
21 | app.use('/thumbnails', express.static('server/thumbnails'));
22 | app.use(flash());
23 |
24 | app.use(require('cookie-parser')());
25 | app.use(bodyParse.urlencoded({extended: true}));
26 | app.use(bodyParse.json({extended: true}));
27 |
28 | app.use(session({
29 | store: MongoStore.create({
30 | mongoUrl: 'mongodb://127.0.0.1/nodeStream',
31 | ttl: 14 * 24 * 60 * 60 // = 14 days. Default
32 | }),
33 | secret: config.server.secret,
34 | maxAge : Date().now + (60 * 1000 * 30),
35 | resave : true,
36 | saveUninitialized : false,
37 | }));
38 |
39 | app.use(passport.initialize());
40 | app.use(passport.session());
41 |
42 | // Register app routes
43 | app.use('/login', require('./routes/login'));
44 | app.use('/register', require('./routes/register'));
45 | app.use('/settings', require('./routes/settings'));
46 | app.use('/streams', require('./routes/streams'));
47 | app.use('/user', require('./routes/user'));
48 |
49 | app.get('/logout', (req, res) => {
50 | req.logout();
51 | return res.redirect('/login');
52 | });
53 |
54 | app.get('*', middleware.ensureLoggedIn(), (req, res) => {
55 | res.render('index');
56 | });
57 |
58 | app.listen(port, () => console.log(`App listening on ${port}!`));
59 | node_media_server.run();
60 | thumbnail_generator.start();
61 |
--------------------------------------------------------------------------------
/server/auth/passport.js:
--------------------------------------------------------------------------------
1 | const passport = require('passport'),
2 | LocalStrategy = require('passport-local').Strategy,
3 | User = require('../database/Schema').User,
4 | shortid = require('shortid');
5 |
6 | passport.serializeUser( (user, cb) => {
7 | cb(null, user);
8 | });
9 |
10 | passport.deserializeUser( (obj, cb) => {
11 | cb(null, obj);
12 | });
13 |
14 | passport.use('localRegister', new LocalStrategy({
15 | usernameField: 'email',
16 | passwordField: 'password',
17 | passReqToCallback: true
18 | },
19 | (req, email, password, done) => {
20 | User.findOne({$or: [{email: email}, {username: req.body.username}]}, (err, user) => {
21 | if (err)
22 | return done(err);
23 | if (user) {
24 | if (user.email === email) {
25 | req.flash('email', 'Email is already taken');
26 | }
27 | if (user.username === req.body.username) {
28 | req.flash('username', 'Username is already taken');
29 | }
30 |
31 | return done(null, false);
32 | } else {
33 | let user = new User();
34 | user.email = email;
35 | user.password = user.generateHash(password);
36 | user.username = req.body.username;
37 | user.stream_key = shortid.generate();
38 | user.save( (err) => {
39 | if (err)
40 | throw err;
41 | return done(null, user);
42 | });
43 | }
44 | });
45 | }));
46 |
47 | passport.use('localLogin', new LocalStrategy({
48 | usernameField: 'email',
49 | passwordField: 'password',
50 | passReqToCallback: true
51 | },
52 | (req, email, password, done) => {
53 |
54 | User.findOne({'email': email}, (err, user) => {
55 | if (err)
56 | return done(err);
57 |
58 | if (!user)
59 | return done(null, false, req.flash('email', 'Email doesn\'t exist.'));
60 |
61 | if (!user.validPassword(password))
62 | return done(null, false, req.flash('password', 'Oops! Wrong password.'));
63 |
64 | return done(null, user);
65 | });
66 | }));
67 |
68 |
69 | module.exports = passport;
--------------------------------------------------------------------------------
/server/config/default.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | server: {
3 | secret: 'kjVkuti2xAyF3JGCzSZTk0YWM5JhI9mgQW4rytXc',
4 | port : 3333
5 | },
6 | rtmp_server: {
7 | rtmp: {
8 | port: 1935,
9 | chunk_size: 60000,
10 | gop_cache: true,
11 | ping: 60,
12 | ping_timeout: 30
13 | },
14 | http: {
15 | port: 8888,
16 | mediaroot: './server/media',
17 | allow_origin: '*'
18 | },
19 | trans: {
20 | ffmpeg: '/usr/bin/ffmpeg',
21 | tasks: [
22 | {
23 | app: 'live',
24 | hls: true,
25 | hlsFlags: '[hls_time=2:hls_list_size=3:hls_flags=delete_segments]',
26 | dash: true,
27 | dashFlags: '[f=dash:window_size=3:extra_window_size=5]'
28 | }
29 | ]
30 | }
31 | }
32 | };
33 |
34 | module.exports = config;
--------------------------------------------------------------------------------
/server/cron/thumbnails.js:
--------------------------------------------------------------------------------
1 | const CronJob = require('cron').CronJob,
2 | axios = require('axios'),
3 | helpers = require('../helpers/helpers'),
4 | config = require('../config/default'),
5 | port = config.rtmp_server.http.port;
6 |
7 | const job = new CronJob('*/5 * * * * *', function () {
8 | axios.get('http://127.0.0.1:' + port + '/api/streams')
9 | .then(response => {
10 | let streams = response.data;
11 | if (typeof (streams['live'] !== undefined)) {
12 | let live_streams = streams['live'];
13 | for (let stream in live_streams) {
14 | if (!live_streams.hasOwnProperty(stream)) continue;
15 | helpers.generateStreamThumbnail(stream);
16 | }
17 | }
18 | })
19 | .catch(error => {
20 | console.log(error);
21 | });
22 | }, null, true);
23 |
24 | module.exports = job;
--------------------------------------------------------------------------------
/server/database/Schema.js:
--------------------------------------------------------------------------------
1 | let mongoose = require('mongoose');
2 |
3 | exports.User = mongoose.model('User', require('./UserSchema'));
4 |
--------------------------------------------------------------------------------
/server/database/UserSchema.js:
--------------------------------------------------------------------------------
1 | let mongoose = require('mongoose'),
2 | bcrypt = require('bcryptjs'),
3 | shortid = require('shortid'),
4 | Schema = mongoose.Schema;
5 |
6 | let UserSchema = new Schema({
7 | username: String,
8 | email : String,
9 | password: String,
10 | stream_key : String,
11 | });
12 |
13 | UserSchema.methods.generateHash = (password) => {
14 | return bcrypt.hashSync(password, bcrypt.genSaltSync(8));
15 | };
16 |
17 | UserSchema.methods.validPassword = function(password){
18 | return bcrypt.compareSync(password, this.password);
19 | };
20 |
21 | UserSchema.methods.generateStreamKey = () => {
22 | return shortid.generate();
23 | };
24 |
25 |
26 | module.exports = UserSchema;
--------------------------------------------------------------------------------
/server/helpers/helpers.js:
--------------------------------------------------------------------------------
1 | const spawn = require('child_process').spawn,
2 | config = require('../config/default'),
3 | cmd = config.rtmp_server.trans.ffmpeg;
4 |
5 | const generateStreamThumbnail = (stream_key) => {
6 | const args = [
7 | '-y',
8 | '-i', 'http://127.0.0.1:8888/live/'+stream_key+'/index.m3u8',
9 | '-ss', '00:00:01',
10 | '-vframes', '1',
11 | '-vf', 'scale=-2:300',
12 | 'server/thumbnails/'+stream_key+'.png',
13 | ];
14 |
15 | spawn(cmd, args, {
16 | detached: true,
17 | stdio: 'ignore'
18 | }).unref();
19 | };
20 |
21 | module.exports = {
22 | generateStreamThumbnail : generateStreamThumbnail
23 | };
24 |
25 |
--------------------------------------------------------------------------------
/server/media/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/waleedahmad/node-stream/a7f42e3dad2eeb6da3fea7800a26ae2b730d3197/server/media/.gitkeep
--------------------------------------------------------------------------------
/server/media_server.js:
--------------------------------------------------------------------------------
1 | const NodeMediaServer = require('node-media-server'),
2 | config = require('./config/default').rtmp_server,
3 | User = require('./database/Schema').User,
4 | helpers = require('./helpers/helpers');
5 |
6 | nms = new NodeMediaServer(config);
7 |
8 | nms.on('prePublish', async (id, StreamPath, args) => {
9 | let stream_key = getStreamKeyFromStreamPath(StreamPath);
10 | console.log('[NodeEvent on prePublish]', `id=${id} StreamPath=${StreamPath} args=${JSON.stringify(args)}`);
11 |
12 | User.findOne({stream_key: stream_key}, (err, user) => {
13 | if (!err) {
14 | if (!user) {
15 | let session = nms.getSession(id);
16 | session.reject();
17 | } else {
18 | helpers.generateStreamThumbnail(stream_key);
19 | }
20 | }
21 | });
22 | });
23 |
24 | const getStreamKeyFromStreamPath = (path) => {
25 | let parts = path.split('/');
26 | return parts[parts.length - 1];
27 | };
28 |
29 | module.exports = nms;
30 |
--------------------------------------------------------------------------------
/server/routes/login.js:
--------------------------------------------------------------------------------
1 | const express = require('express'),
2 | router = express.Router(),
3 | passport = require('passport');
4 |
5 | router.get('/',
6 | require('connect-ensure-login').ensureLoggedOut(),
7 | (req, res) => {
8 | res.render('login', {
9 | user : null,
10 | errors : {
11 | email : req.flash('email'),
12 | password : req.flash('password')
13 | }
14 | });
15 | });
16 |
17 | router.post('/', passport.authenticate('localLogin', {
18 | successRedirect : '/',
19 | failureRedirect : '/login',
20 | failureFlash : true
21 | }));
22 |
23 | module.exports = router;
24 |
25 |
--------------------------------------------------------------------------------
/server/routes/register.js:
--------------------------------------------------------------------------------
1 | const express = require('express'),
2 | router = express.Router(),
3 | passport = require('passport');
4 |
5 | router.get('/',
6 | require('connect-ensure-login').ensureLoggedOut(),
7 | (req, res) => {
8 | res.render('register', {
9 | user : null,
10 | errors : {
11 | username : req.flash('username'),
12 | email : req.flash('email')
13 | }
14 | });
15 | });
16 |
17 | router.post('/',
18 | require('connect-ensure-login').ensureLoggedOut(),
19 | passport.authenticate('localRegister', {
20 | successRedirect : '/',
21 | failureRedirect : '/register',
22 | failureFlash : true
23 | })
24 | );
25 |
26 |
27 | module.exports = router;
28 |
29 |
--------------------------------------------------------------------------------
/server/routes/settings.js:
--------------------------------------------------------------------------------
1 | const express = require('express'),
2 | router = express.Router(),
3 | User = require('../database/Schema').User,
4 | shortid = require('shortid');
5 |
6 | router.get('/stream_key',
7 | require('connect-ensure-login').ensureLoggedIn(),
8 | (req, res) => {
9 | User.findOne({email: req.user.email}, (err, user) => {
10 | if (!err) {
11 | res.json({
12 | stream_key: user.stream_key
13 | })
14 | }
15 | });
16 | });
17 |
18 | router.post('/stream_key',
19 | require('connect-ensure-login').ensureLoggedIn(),
20 | (req, res) => {
21 |
22 | User.findOneAndUpdate({
23 | email: req.user.email
24 | }, {
25 | stream_key: shortid.generate()
26 | }, {
27 | upsert: true,
28 | new: true,
29 | }, (err, user) => {
30 | if (!err) {
31 | res.json({
32 | stream_key: user.stream_key
33 | })
34 | }
35 | });
36 | });
37 |
38 |
39 | module.exports = router;
40 |
41 |
--------------------------------------------------------------------------------
/server/routes/streams.js:
--------------------------------------------------------------------------------
1 | const express = require('express'),
2 | router = express.Router(),
3 | User = require('../database/Schema').User;
4 |
5 | router.get('/info',
6 | require('connect-ensure-login').ensureLoggedIn(),
7 | (req, res) => {
8 | if(req.query.streams){
9 | let streams = JSON.parse(req.query.streams);
10 | let query = {$or: []};
11 | for (let stream in streams) {
12 | if (!streams.hasOwnProperty(stream)) continue;
13 | query.$or.push({stream_key : stream});
14 | }
15 |
16 | User.find(query,(err, users) => {
17 | if (err)
18 | return;
19 | if (users) {
20 | res.json(users);
21 | }
22 | });
23 | }
24 | });
25 | module.exports = router;
26 |
27 |
--------------------------------------------------------------------------------
/server/routes/user.js:
--------------------------------------------------------------------------------
1 | const express = require('express'),
2 | router = express.Router(),
3 | User = require('../database/Schema').User;
4 |
5 | router.get('/',
6 | require('connect-ensure-login').ensureLoggedIn(),
7 | (req, res) => {
8 |
9 | if(req.query.username){
10 | User.findOne({
11 | username : req.query.username
12 | },(err, user) => {
13 | if (err)
14 | return;
15 | if (user) {
16 | res.json({
17 | stream_key : user.stream_key
18 | });
19 | }
20 | });
21 | }else{
22 | res.json({});
23 | }
24 | });
25 |
26 | module.exports = router;
27 |
28 |
--------------------------------------------------------------------------------
/server/sessions/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/waleedahmad/node-stream/a7f42e3dad2eeb6da3fea7800a26ae2b730d3197/server/sessions/.gitkeep
--------------------------------------------------------------------------------
/server/thumbnails/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/waleedahmad/node-stream/a7f42e3dad2eeb6da3fea7800a26ae2b730d3197/server/thumbnails/.gitkeep
--------------------------------------------------------------------------------
/server/views/footer.ejs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/views/header.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 | NodeStream
7 |
8 |
--------------------------------------------------------------------------------
/server/views/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 | NodeStream
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/server/views/login.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%- include('header') %>
4 |
5 | <%- include('navbar') %>
6 |
7 |
8 |
9 |
Login
10 |
11 |
12 |
36 |
37 |
38 | <%- include('footer') %>
39 |
40 |
--------------------------------------------------------------------------------
/server/views/navbar.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
NodeStream
4 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Github
14 |
15 | <% if (!user) { %>
16 |
17 | Login
18 |
19 |
20 |
21 | Register
22 |
23 | <% } else { %>
24 |
25 | Logout
26 |
27 | <% } %>
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/server/views/register.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%- include('header') %>
4 |
5 | <%- include('navbar') %>
6 |
7 |
8 |
9 |
Register
10 |
11 |
12 |
13 |
48 |
49 |
50 | <%- include('footer') %>
51 |
52 |
53 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
3 | const devMode = process.env.NODE_ENV !== 'production';
4 | const webpack = require('webpack');
5 |
6 | module.exports = {
7 | entry : './client/index.js',
8 | output : {
9 | filename : 'bundle.js',
10 | path : path.resolve(__dirname, 'public')
11 | },
12 | module : {
13 | rules : [
14 | {
15 | test: /\.s?[ac]ss$/,
16 | use: [
17 | MiniCssExtractPlugin.loader,
18 | { loader: 'css-loader', options: { url: false, sourceMap: true } },
19 | { loader: 'sass-loader', options: { sourceMap: true } }
20 | ],
21 | },
22 | {
23 | test: /\.js$/,
24 | exclude: /node_modules/,
25 | use: "babel-loader"
26 | },
27 | {
28 | test: /\.woff($|\?)|\.woff2($|\?)|\.ttf($|\?)|\.eot($|\?)|\.svg($|\?)/,
29 | loader: 'url-loader'
30 | },
31 | {
32 | test: /\.(png|jpg|gif)$/,
33 | use: [{
34 | loader: 'file-loader',
35 | options: {
36 | outputPath: '/',
37 | },
38 | }],
39 | },
40 | ]
41 | },
42 | devtool: 'source-map',
43 | plugins: [
44 | new MiniCssExtractPlugin({
45 | filename: "style.css"
46 | }),
47 | new webpack.ProvidePlugin({
48 | $: 'jquery',
49 | jQuery: 'jquery'
50 | })
51 |
52 | ],
53 | mode : devMode ? 'development' : 'production',
54 | watch : devMode,
55 | performance: {
56 | hints: process.env.NODE_ENV === 'production' ? "warning" : false
57 | },
58 | };
--------------------------------------------------------------------------------