├── .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 | 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 | 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 |
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 |
13 |
14 |
15 | 16 | 17 | <% if (errors.email.length) { %> 18 | <%= errors.email %> 19 | <% } %> 20 |
21 |
22 | 23 | 24 | <% if (errors.password.length) { %> 25 | <%= errors.password %> 26 | <% } %> 27 |
28 |
29 |
30 | Don't have an account? Register here. 31 |
32 |
33 | 34 |
35 |
36 |
37 | 38 | <%- include('footer') %> 39 | 40 | -------------------------------------------------------------------------------- /server/views/navbar.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/views/register.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%- include('header') %> 4 | 5 | <%- include('navbar') %> 6 | 7 | 8 |
9 |

Register

10 | 11 |
12 | 13 |
14 |
17 | 18 |
19 | 20 | 21 | <% if (errors.username.length) { %> 22 | <%= errors.username %> 23 | <% } %> 24 |
25 | 26 |
27 | 28 | 29 | <% if (errors.email.length) { %> 30 | <%= errors.email %> 31 | <% } %> 32 |
33 | 34 |
35 | 36 | 37 |
38 | 39 |
40 |
41 | Have an account? Login here. 42 |
43 |
44 | 45 | 46 |
47 |
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 | }; --------------------------------------------------------------------------------