├── server ├── public │ ├── script.js │ ├── fav.png │ ├── style.css │ ├── admin.js │ ├── user.js │ └── pitchFinder.js ├── .gitignore ├── db │ ├── connect.js │ └── seed.js ├── models │ ├── Books.js │ ├── Session.js │ ├── Room.js │ └── User.js ├── middleware │ ├── auth.js │ └── renderer.js ├── package.json ├── server.js ├── routes │ └── api.js └── package-lock.json ├── favicon.ico ├── public ├── favicon.ico ├── manifest.json └── index.html ├── src ├── api │ └── api.js ├── App.css ├── index.js ├── components │ ├── RoomModal │ │ └── RoomModal.js │ ├── Confirm │ │ └── Confim.js │ ├── Modal │ │ └── Modal.js │ ├── AdminView │ │ └── AdminView.js │ ├── ModalNav │ │ └── ModalNav.js │ ├── App │ │ └── App.js │ ├── UserView │ │ └── UserView.js │ ├── LoginForm │ │ └── LoginForm.js │ ├── UserCreateForm │ │ └── UserCreateForm.js │ ├── NavBar │ │ └── NavBar.js │ ├── UsersList │ │ └── UsersList.js │ ├── BookCreateForm │ │ └── BookCreateForm.js │ ├── BooksList │ │ └── BooksList.js │ ├── RoomList │ │ └── RoomList.js │ ├── RoomEdit │ │ └── RoomEdit.js │ └── RoomControls │ │ └── RoomControls.js ├── contexts │ ├── appContext.js │ ├── adminContext.js │ └── userContext.js ├── logo.svg └── serviceWorker.js ├── .gitignore ├── index.html ├── semantic.json ├── package.json ├── main.js └── README.md /server/public/script.js: -------------------------------------------------------------------------------- 1 | // alert("Connected") 2 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CannonFodderr/audiosystem/HEAD/favicon.ico -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CannonFodderr/audiosystem/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | assets/ 3 | server.cert 4 | server.key 5 | .env 6 | .rnd 7 | .mp3 -------------------------------------------------------------------------------- /server/public/fav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CannonFodderr/audiosystem/HEAD/server/public/fav.png -------------------------------------------------------------------------------- /src/api/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const serverAPI = axios.create({ 4 | baseURL: '/api' 5 | }); -------------------------------------------------------------------------------- /server/public/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | padding: 0; 3 | margin: 0; 4 | box-sizing: border-box; 5 | } 6 | body { 7 | background: #3e3e3e; 8 | color: #fff; 9 | } 10 | 11 | audio{ 12 | width: 100%; 13 | } -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /server/db/connect.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const env = require('dotenv').config(); 3 | 4 | module.exports = mongoose.connect(process.env.DEV_DB_URL, { useNewUrlParser: true }) 5 | .then(() => { 6 | console.log(`Connected to db`); 7 | return true; 8 | }) 9 | .catch((err) => { 10 | console.log(err); 11 | return false; 12 | }); 13 | mongoose.set('useCreateIndex', true); -------------------------------------------------------------------------------- /server/models/Books.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const bookSchema = new mongoose.Schema({ 4 | name: { 5 | type: String, 6 | unique: true 7 | }, 8 | author: { 9 | type: String 10 | }, 11 | parts: { 12 | type: [String] 13 | }, 14 | path: String 15 | }); 16 | 17 | const Book = mongoose.model('Book', bookSchema); 18 | module.exports = Book; -------------------------------------------------------------------------------- /server/middleware/auth.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | isLoggedIn(req, res, next) { 3 | if (req.user && req.user != 'undefined') { 4 | return next(); 5 | } 6 | return res.redirect('/login'); 7 | }, 8 | isAdmin(req, res, next) { 9 | if(req.user && req.user.isAdmin){ 10 | return next(); 11 | } 12 | return res.redirect('/logout'); 13 | } 14 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /server/assets/ 6 | /.pnp 7 | .pnp.js 8 | server.cert 9 | server.key 10 | .env 11 | .mp3 12 | # testing 13 | /coverage 14 | auditorio-win32-x64 15 | release-builds 16 | # production 17 | /build 18 | release-builds/ 19 | # misc 20 | .DS_Store 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | Audio System 14 | 15 | 16 |

Audio System Server

17 |

Serving on local IP address, PORT 8080

18 |

Connected to DB

19 | 20 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | } 9 | 10 | .App-header { 11 | background-color: #282c34; 12 | min-height: 100vh; 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | justify-content: center; 17 | font-size: calc(10px + 2vmin); 18 | color: white; 19 | } 20 | 21 | .App-link { 22 | color: #61dafb; 23 | } 24 | 25 | @keyframes App-logo-spin { 26 | from { 27 | transform: rotate(0deg); 28 | } 29 | to { 30 | transform: rotate(360deg); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import 'semantic-ui-css/semantic.min.css'; 4 | import App from './components/App/App'; 5 | import {AppContextStore} from './contexts/appContext'; 6 | 7 | ReactDOM.hydrate( 8 | 9 | 10 | , 11 | document.getElementById('root')); 12 | 13 | // If you want your app to work offline and load faster, you can change 14 | // unregister() to register() below. Note this comes with some pitfalls. 15 | // Learn more about service workers: http://bit.ly/CRA-PWA 16 | // serviceWorker.unregister(); 17 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "body-parser": "^1.18.3", 14 | "ejs": "^2.6.1", 15 | "express": "^4.16.4", 16 | "express-session": "^1.15.6", 17 | "mongoose": "^5.4.9", 18 | "passport": "^0.4.0", 19 | "passport-local": "^1.0.0", 20 | "passport-local-mongoose": "^5.0.1", 21 | "peer": "^0.2.10" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/RoomModal/RoomModal.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Modal, Header} from 'semantic-ui-react'; 3 | 4 | class RoomModalTemplate extends Component{ 5 | render(){ 6 | return( 7 | this.props.handleModalClose() }> 8 |
9 | 10 | {this.props.content} 11 | 12 | 13 | ) 14 | } 15 | } 16 | 17 | export default RoomModalTemplate; -------------------------------------------------------------------------------- /src/components/Confirm/Confim.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import {Confirm, Button} from 'semantic-ui-react'; 3 | 4 | class ConfirmTemplate extends Component{ 5 | constructor(props){ 6 | super(props); 7 | } 8 | render(){ 9 | return( 10 |
11 | this.props.handleConfirm()} 14 | onCancel={() => this.props.handleCancel()} 15 | onClose={() => this.props.handleCancel()} 16 | header={this.props.header} 17 | content={this.props.content} 18 | /> 19 |
20 | ) 21 | } 22 | } 23 | 24 | 25 | export default ConfirmTemplate; -------------------------------------------------------------------------------- /server/models/Session.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | 4 | const SessionSchema = new mongoose.Schema({ 5 | user: { 6 | type: mongoose.Schema.Types.ObjectId, 7 | ref: 'User', 8 | required: true 9 | }, 10 | date: { 11 | type: mongoose.Schema.Types.Date, 12 | default: Date.now() 13 | }, 14 | room: { 15 | type: mongoose.Schema.Types.ObjectId, 16 | ref: 'Room', 17 | required: true 18 | }, 19 | book: { 20 | type: mongoose.Schema.Types.ObjectId, 21 | ref: 'Book', 22 | required: true 23 | }, 24 | part: { 25 | type: String, 26 | required: true 27 | } 28 | }); 29 | 30 | 31 | 32 | const Session = mongoose.model('Session', SessionSchema); 33 | module.exports = Session; -------------------------------------------------------------------------------- /server/models/Room.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const passportLocalMongoose = require('passport-local-mongoose'); 3 | 4 | const roomSchema = new mongoose.Schema({ 5 | password: { 6 | type: String, 7 | }, 8 | username: { 9 | type: String, 10 | }, 11 | isAdmin: { 12 | type: Boolean, 13 | default: false 14 | }, 15 | currentUser: { 16 | type: mongoose.Schema.Types.ObjectId, 17 | ref: 'User', 18 | default: null 19 | }, 20 | currentBook: { 21 | type: mongoose.Schema.Types.ObjectId, 22 | ref: 'Book', 23 | default: null 24 | }, 25 | currentPart: { 26 | type: String, 27 | default: null 28 | } 29 | }); 30 | 31 | roomSchema.plugin(passportLocalMongoose); 32 | 33 | const Room = mongoose.model('Room', roomSchema); 34 | module.exports = Room; 35 | -------------------------------------------------------------------------------- /src/contexts/appContext.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {serverAPI} from '../api/api'; 3 | const Context = React.createContext(); 4 | 5 | export class AppContextStore extends Component{ 6 | state = { 7 | room: null 8 | } 9 | setRoom = room => { 10 | this.setState({room}); 11 | } 12 | fetchRoomData = loginData => { 13 | serverAPI.post('/login', loginData) 14 | .then((res) => { 15 | if(res.data.room){ 16 | this.setRoom(res.data.room) 17 | } 18 | }); 19 | } 20 | render(){ 21 | return( 22 | 26 | {this.props.children} 27 | 28 | ) 29 | } 30 | } 31 | 32 | 33 | export default Context; -------------------------------------------------------------------------------- /server/middleware/renderer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOMServer from 'react-dom/server' 3 | 4 | // import our main App component 5 | import App from '../src/App'; 6 | 7 | const path = require("path"); 8 | const fs = require("fs"); 9 | 10 | export default (req, res, next) => { 11 | 12 | // point to the html file created by CRA's build tool 13 | const filePath = path.resolve(__dirname, '..', '..', 'build', 'index.html'); 14 | 15 | fs.readFile(filePath, 'utf8', (err, htmlData) => { 16 | if (err) { 17 | console.error('err', err); 18 | return res.status(404).end() 19 | } 20 | 21 | // render the app as a string 22 | const html = ReactDOMServer.renderToString(); 23 | 24 | // inject the rendered app into our html and send it 25 | return res.send( 26 | htmlData.replace( 27 | '
', 28 | `
${html}
` 29 | ) 30 | ); 31 | }); 32 | } -------------------------------------------------------------------------------- /server/models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const UserSchema = new mongoose.Schema({ 4 | firstName: { 5 | type: String, 6 | }, 7 | lastName: { 8 | type: String 9 | }, 10 | email: { 11 | type: String, 12 | unique: true, 13 | }, 14 | isAdmin: { 15 | type: Boolean, 16 | default: false, 17 | }, 18 | currentRoom: { 19 | type: mongoose.Schema.Types.ObjectId, 20 | ref: 'Room', 21 | default: null 22 | }, 23 | currentBook: { 24 | type: mongoose.Schema.Types.ObjectId, 25 | ref: 'Book', 26 | default: null 27 | }, 28 | lastChapter: { 29 | type: String, 30 | }, 31 | bookList: { 32 | type: [mongoose.Schema.Types.ObjectId], 33 | ref: 'BookList' 34 | }, 35 | sessions: { 36 | type: [mongoose.Schema.Types.ObjectId], 37 | ref: 'Sessions' 38 | } 39 | }); 40 | 41 | const User = mongoose.model('User', UserSchema); 42 | module.exports = User; -------------------------------------------------------------------------------- /src/components/Modal/Modal.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Modal, Header, Button, Icon, Segment} from 'semantic-ui-react'; 3 | 4 | class ModalTemplate extends Component{ 5 | renderTriggerButton = () => { 6 | return ( 7 |
8 | 9 | 14 | 15 |
16 | ) 17 | } 18 | render(){ 19 | return( 20 | 21 |
22 | 23 | {this.props.content} 24 | 25 | 26 | ) 27 | } 28 | } 29 | 30 | export default ModalTemplate; -------------------------------------------------------------------------------- /semantic.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "semantic\\", 3 | "paths": { 4 | "source": { 5 | "config": "src/theme.config", 6 | "definitions": "src/definitions/", 7 | "site": "src/site/", 8 | "themes": "src/themes/" 9 | }, 10 | "output": { 11 | "packaged": "dist\\", 12 | "uncompressed": "dist\\components\\", 13 | "compressed": "dist\\components\\", 14 | "themes": "dist\\themes\\" 15 | }, 16 | "clean": "dist/" 17 | }, 18 | "permission": false, 19 | "autoInstall": false, 20 | "rtl": false, 21 | "components": ["reset", "site", "button", "container", "divider", "flag", "header", "icon", "image", "input", "label", "list", "loader", "rail", "reveal", "segment", "step", "breadcrumb", "form", "grid", "menu", "message", "table", "ad", "card", "comment", "feed", "item", "statistic", "accordion", "calendar", "checkbox", "dimmer", "dropdown", "embed", "modal", "nag", "placeholder", "popup", "progress", "slider", "rating", "search", "shape", "sidebar", "sticky", "tab", "text", "toast", "transition", "api", "form", "state", "visibility"], 22 | "version": "2.7.1" 23 | } 24 | -------------------------------------------------------------------------------- /src/components/AdminView/AdminView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import RoomList from '../RoomList/RoomList'; 3 | import UsersList from '../UsersList/UsersList'; 4 | import BooksList from '../BooksList/BooksList'; 5 | import NavBar from '../NavBar/NavBar'; 6 | import adminContext from '../../contexts/adminContext'; 7 | 8 | class AdminView extends React.Component{ 9 | renderDisplayContent = () => { 10 | if(!this.context.activeMenuItem){ 11 | return
Loading...
12 | } 13 | if(this.context.activeMenuItem === "Rooms"){ 14 | return 15 | } 16 | if(this.context.activeMenuItem === "Books"){ 17 | return 18 | } 19 | if(this.context.activeMenuItem === "Users"){ 20 | return 21 | } 22 | } 23 | render(){ 24 | return( 25 |
26 | 27 | {this.renderDisplayContent()} 28 |
29 | ) 30 | } 31 | } 32 | AdminView.contextType = adminContext; 33 | export default AdminView; -------------------------------------------------------------------------------- /src/components/ModalNav/ModalNav.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Modal, Header, Button, Icon, Segment} from 'semantic-ui-react'; 3 | import adminContext from '../../contexts/adminContext'; 4 | 5 | 6 | class ModalNavTemplate extends Component{ 7 | renderTriggerButton = () => { 8 | return ( 9 |
10 | 11 | 16 | 17 |
18 | ) 19 | } 20 | render(){ 21 | return( 22 | this.context.dispalyNavModal(false) }> 23 |
24 | 25 | {this.props.content} 26 | 27 | 28 | ) 29 | } 30 | } 31 | 32 | ModalNavTemplate.contextType = adminContext; 33 | export default ModalNavTemplate; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "korenmern", 3 | "version": "0.1.0", 4 | "private": true, 5 | "main": "main.js", 6 | "productName": "Audio System", 7 | "dependencies": { 8 | "axios": "^0.18.0", 9 | "cors": "^2.8.5", 10 | "dotenv": "^6.2.0", 11 | "formidable": "^1.2.1", 12 | "ip": "^1.1.5", 13 | "peerjs": "^0.3.20", 14 | "pitchfinder": "^2.0.9", 15 | "react": "^16.7.0", 16 | "react-dom": "^16.7.0", 17 | "react-scripts": "^2.1.5", 18 | "react-semantic-ui-range": "^0.6.2", 19 | "semantic-ui-css": "^2.4.1", 20 | "semantic-ui-react": "^0.85.0" 21 | }, 22 | "scripts": { 23 | "start": "electron main.js", 24 | "build": "react-scripts build", 25 | "build-start": "npm run build && electron main.js", 26 | "package-win": "electron-packager . --overwrite --ignore=.mp3 --platform=win32 --arch=x64 --asar --prune=true --out=release-builds --version-string.CompanyName=CE --version-string.FileDescription=CE --version-string.ProductName=\"Audio System\"", 27 | "package-win-with-assets": "electron-packager . --overwrite --platform=win32 --arch=x64 --asar --prune=true --out=release-builds --version-string.CompanyName=CE --version-string.FileDescription=CE --version-string.ProductName=\"Audio System\"" 28 | }, 29 | "eslintConfig": { 30 | "extends": "react-app" 31 | }, 32 | "browserslist": [ 33 | ">0.2%", 34 | "not dead", 35 | "not ie <= 11", 36 | "not op_mini all" 37 | ], 38 | "devDependencies": { 39 | "electron": "^4.0.5", 40 | "webpack-cli": "^3.2.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/App/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import ModalTemplate from '../Modal/Modal'; 3 | import LoginForm from '../LoginForm/LoginForm'; 4 | import appContext from '../../contexts/appContext'; 5 | import UserView from '../UserView/UserView'; 6 | import AdminView from '../AdminView/AdminView'; 7 | import {AdminContextStore} from '../../contexts/adminContext'; 8 | import {UserContextStore} from '../../contexts/userContext'; 9 | 10 | class App extends Component{ 11 | renderApp = () => { 12 | if(!this.context.room){ 13 | return( 14 |
15 | } actions="" /> 16 |
17 | ) 18 | } 19 | if(this.context.room.isAdmin === false){ 20 | return ( 21 |
22 | 23 | 24 | 25 |
26 | ) 27 | } 28 | if(this.context.room.isAdmin === true){ 29 | return ( 30 |
31 | 32 | 33 | 34 |
35 | ) 36 | } 37 | } 38 | render(){ 39 | return( 40 |
41 | {this.renderApp()} 42 |
43 | ) 44 | } 45 | } 46 | 47 | App.contextType = appContext; 48 | export default App; -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/components/UserView/UserView.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Button, Icon} from 'semantic-ui-react'; 3 | import userContext from '../../contexts/userContext'; 4 | 5 | 6 | 7 | class UserView extends Component{ 8 | renderView = () => { 9 | let room = this.props.room; 10 | if(!room || !room.currentUser || !room.currentBook || !room.currentPart ){ 11 | return ( 12 | 48 | 49 | ) 50 | } 51 | }; 52 | 53 | LoginForm.contextType = appContext; 54 | export default LoginForm; 55 | 56 | 57 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const env = require('dotenv').config(); 2 | const express = require('express'); 3 | const https = require('https'); 4 | const bodyParser = require('body-parser'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const ExpressPeerServer = require('peer').ExpressPeerServer; 8 | const passport = require('passport'); 9 | const LocalStrategy = require('passport-local').Strategy; 10 | const app = express(); 11 | const cors = require('cors'); 12 | 13 | const apiRoutes = require('./routes/api'); 14 | 15 | app.set('view engine', 'ejs'); 16 | 17 | app.use(express.static(path.join(__dirname, 'public'))); 18 | app.use(express.static(path.resolve(__dirname, '../', 'build'))) 19 | app.use(express.json()); 20 | app.use(bodyParser.urlencoded({extended: true})); 21 | app.use(cors()); 22 | 23 | app.use(require('express-session')({ 24 | secret: 'kornishon', 25 | resave: false, 26 | saveUninitialized: false 27 | })); 28 | 29 | app.use(passport.initialize()); 30 | app.use(passport.session()); 31 | 32 | let dbConnection = require('./db/connect'); 33 | 34 | // CONFIG PASSPORT 35 | const Room = require('./models/Room'); 36 | passport.use(new LocalStrategy(Room.authenticate())); 37 | passport.serializeUser(Room.serializeUser()); 38 | passport.deserializeUser(Room.deserializeUser()); 39 | 40 | let rootPath = path.join(__dirname, '../'); 41 | let dirPath = path.join(__dirname + '/'); 42 | 43 | const server = https.createServer({ 44 | key: fs.readFileSync(rootPath + 'server.key'), 45 | cert: fs.readFileSync(rootPath + 'server.cert') 46 | }, app); 47 | 48 | app.use('/api', apiRoutes); 49 | 50 | app.get('/', (req, res) => { 51 | res.sendFile('index.html', {root: __dirname}); 52 | }); 53 | app.get('/status', (req, res)=> { 54 | res.sendFile(dirPath + 'index.html'); 55 | }) 56 | // CONFIG PEER SERVER 57 | const options = { 58 | debug: true 59 | } 60 | const peerserver = ExpressPeerServer(server, options); 61 | 62 | peerserver.on('connection', (id) => { 63 | console.log("Connected: ", id); 64 | peerserver.emit({cmd: "connection", user: id}); 65 | }); 66 | 67 | peerserver.on('disconnect', (id) => { 68 | console.log("Disconnected: ", id); 69 | }); 70 | 71 | app.use('/peerjs', peerserver); 72 | 73 | 74 | require('./db/seed'); 75 | 76 | module.exports = server; 77 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, Tray, Menu, dialog } = require('electron') 2 | const server = require('./server/server'); 3 | const env = require('dotenv').config(); 4 | const ip = require('ip') 5 | let localIP = ip.address('public', 'ipv4'); 6 | // Get localhost ip 7 | 8 | app.commandLine.appendSwitch('ignore-certificate-errors'); 9 | let appIcon = null; 10 | function createWindow () { 11 | // Create the browser window. 12 | let win = new BrowserWindow({ 13 | width: 400, 14 | height: 150, 15 | autoHideMenuBar: true, 16 | useContentSize: true, 17 | resizable: false, 18 | webPreferences: { 19 | webSecurity: false, 20 | allowDisplayingInsecureContent: true, 21 | allowRunningInsecureContent: true 22 | } 23 | }); 24 | win.setMenu(null); 25 | // win.webContents.openDevTools() 26 | appIcon = new Tray(__dirname + '/favicon.ico'); 27 | let contextMenu = Menu.buildFromTemplate([ 28 | { label: 'Show App', click: function(){ 29 | app.isQuiting = false; 30 | win.show(); 31 | } }, 32 | { label: 'Quit', click: function(){ 33 | app.isQuiting = true; 34 | app.quit(); 35 | } } 36 | ]); 37 | appIcon.setContextMenu(contextMenu); 38 | win.on('minimize',function(event){ 39 | event.preventDefault(); 40 | win.hide(); 41 | return false; 42 | }); 43 | 44 | win.on('close', function (event) { 45 | if(!app.isQuiting){ 46 | event.preventDefault(); 47 | win.hide(); 48 | } 49 | return false; 50 | }); 51 | require('./server/db/connect').then((isConnected) => { 52 | if(!isConnected){ 53 | dialog.showErrorBox('DB ERROR', "Check if MongoDB service is running"); 54 | app.isQuiting = true; 55 | app.quit() 56 | } else { 57 | win.loadFile(__dirname + '/index.html'); 58 | } 59 | win.focus(); 60 | }); 61 | appIcon.setToolTip('Audio System Server'); 62 | appIcon.on('click', () => { 63 | win.show(); 64 | win.focus() 65 | }) 66 | } 67 | 68 | app.on('ready', createWindow); 69 | 70 | server.listen(8080, localIP, () => { 71 | console.log(`Serving on ${localIP}:8080`); 72 | }); -------------------------------------------------------------------------------- /server/db/seed.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const User = require('../models/User'); 3 | const Room = require('../models/Room'); 4 | const Book = require('../models/Books'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | 8 | const admin = { 9 | firstName: "Admin", 10 | lastName: "istrator", 11 | isAdmin: true, 12 | email: "admin@admin.com" 13 | } 14 | const firstUser = { 15 | firstName: "User", 16 | lastName: "Doe", 17 | email: "user@doe.com" 18 | } 19 | // Create Admin and First User 20 | // *************************** 21 | User.find().then(allUsers => { 22 | if(allUsers.length <= 0){ 23 | User.create(admin).then((createAdmin) =>{ 24 | console.log("Created Admin:", createAdmin); 25 | User.create(firstUser).then((createdUser) => { 26 | console.log("Created Admin:", createdUser); 27 | }); 28 | }) 29 | } 30 | }) 31 | .then(() => { 32 | Room.find().then((allRooms) => { 33 | if(allRooms.length <= 0){ 34 | Room.register(new Room({username: "admin"}), "admin").then((createdLobby) => { 35 | console.log("CreatedLobby:" , createdLobby) 36 | createdLobby.isAdmin = true; 37 | createdLobby.save(); 38 | for(let i = 0; i <= 4; i++){ 39 | let password = `Room${i}` 40 | Room.register(new Room({username: `Room ${i}`}), password) 41 | .then((createdRoom) => { 42 | console.log(createdRoom) 43 | }) 44 | } 45 | }) 46 | } 47 | }) 48 | }) 49 | .then(() => { 50 | Book.find().then((allBooks) => { 51 | if(allBooks.length <= 0){ 52 | const folderPath = path.join(__dirname, '../assets/books'); 53 | const booksFolder = fs.readdirSync(folderPath); 54 | booksFolder.forEach((bookName) => { 55 | const bookPath = `${folderPath}/${bookName}` 56 | const bookFiles = fs.readdirSync(bookPath); 57 | console.log(bookFiles); 58 | Book.create({ 59 | name: bookName, 60 | parts: bookFiles, 61 | }) 62 | .then((createdBook) => console.log("Created Book:", createdBook)) 63 | }); 64 | } 65 | }) 66 | }) 67 | .catch((err) => console.error(err)); 68 | 69 | 70 | // Create Rooms 71 | // ************ 72 | 73 | 74 | 75 | // Create Books 76 | // ************ 77 | 78 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/UserCreateForm/UserCreateForm.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Form, Message ,Button} from 'semantic-ui-react'; 3 | import adminContext from '../../contexts/adminContext'; 4 | 5 | class UserCreateForm extends Component{ 6 | state = { 7 | firstName: null, 8 | lastName: null, 9 | email: null, 10 | err: null 11 | } 12 | validateForm = () => { 13 | if(!this.state.firstName || this.state.firstName.length < 2){ 14 | return this.setState({err: "Invalid firstName"}) 15 | } 16 | if(!this.state.lastName || this.state.lastName.length < 2){ 17 | return this.setState({err: "Invalid lastName"}) 18 | } 19 | this.setState({err: null}); 20 | this.context.createNewUser({firstName: this.state.firstName, lastName: this.state.lastName, email: this.state.email}); 21 | } 22 | setFirstName = (event) => { 23 | this.setState({firstName: event.target.value}) 24 | } 25 | setLastName = (event) => { 26 | this.setState({lastName: event.target.value}) 27 | } 28 | setEmail = (event) => { 29 | this.setState({email: event.target.value}) 30 | } 31 | renderErrorMessage = () => { 32 | if(this.state.err === null){ 33 | return; 34 | } 35 | return ( 36 | 37 | ) 38 | } 39 | render(){ 40 | return( 41 |
42 | {this.renderErrorMessage()} 43 | 44 | 45 | {this.setFirstName(e)}} autoFocus required/> 46 | 47 | 48 | 49 | {this.setLastName(e)}} required/> 50 | 51 | 52 | 53 | {this.setEmail(e)}} required/> 54 | 55 | 56 |
57 | ) 58 | } 59 | }; 60 | 61 | UserCreateForm.contextType = adminContext; 62 | export default UserCreateForm; 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/components/NavBar/NavBar.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Menu, Input} from 'semantic-ui-react'; 3 | import adminContext from '../../contexts/adminContext'; 4 | import ModalNav from '../ModalNav/ModalNav'; 5 | import UserCreateForm from '../UserCreateForm/UserCreateForm'; 6 | import BookCreateForm from '../BookCreateForm/BookCreateForm'; 7 | 8 | class NavBar extends Component{ 9 | renderAddButton = () => { 10 | if(!this.context.activeMenuItem){ 11 | return 12 | } 13 | if(this.context.activeMenuItem === 'Books'){ 14 | return } actions="" icon="book"/> 15 | } 16 | if(this.context.activeMenuItem === 'Users'){ 17 | return } actions="" icon="add user"/> 18 | } 19 | } 20 | renderSearchBox = () => { 21 | if(!this.context.activeMenuItem){ 22 | return
23 | } 24 | if(this.context.activeMenuItem === 'Books'){ 25 | return ( 26 | 32 | ) 33 | } 34 | if(this.context.activeMenuItem === 'Users'){ 35 | return ( 36 | 42 | ) 43 | } 44 | } 45 | renderMenu = () => { 46 | return ( 47 | 48 | 49 | 50 | 51 | 52 | {this.renderAddButton()} 53 | {this.renderSearchBox()} 54 | 55 | 56 | ) 57 | } 58 | render(){ 59 | return( 60 |
61 | {this.renderMenu()} 62 |
63 | ) 64 | } 65 | } 66 | 67 | NavBar.contextType = adminContext; 68 | export default NavBar -------------------------------------------------------------------------------- /src/components/UsersList/UsersList.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Card, Icon, Button} from 'semantic-ui-react'; 3 | import ConfirmTemplate from '../Confirm/Confim'; 4 | import {serverAPI} from '../../api/api'; 5 | import adminContext from '../../contexts/adminContext'; 6 | 7 | const INITIAL_STATE = { showConfirm: false, selectedUser: null} 8 | 9 | class UsersList extends Component{ 10 | state = INITIAL_STATE; 11 | handleConfirm = () => { 12 | this.deleteUser(); 13 | } 14 | handleCancel = () => { 15 | this.setState({showConfirm: false, selectedUser: null}) 16 | } 17 | deleteUser = () => { 18 | serverAPI.delete(`/users/${this.state.selectedUser._id}`) 19 | .then(() => { 20 | this.setState({showConfirm: false, selectedUser: null}); 21 | this.context.fetchUsersList(); 22 | }) 23 | .catch(err => console.log(err)); 24 | } 25 | renderUsersList = () => { 26 | let users = this.context.users; 27 | if(!users || users.length < 1){ 28 | return
Loading users...
29 | } else { 30 | return users.map(user => { 31 | if(user.isAdmin){ 32 | return 33 | } 34 | return ( 35 | 36 | 37 | {user.firstName} {user.lastName} 38 | 39 | 40 | 44 | 45 | 46 | 47 | ) 48 | }) 49 | } 50 | } 51 | renderContent = () => { 52 | if(!this.state.showConfirm && !this.state.selectedUser){ 53 | return( 54 | 55 | {this.renderUsersList()} 56 | 57 | ) 58 | } else { 59 | let content = `Are you sure you want to delete "${this.state.selectedUser.firstName} ${this.state.selectedUser.lastName}" ?`; 60 | return ( 61 |
62 | 69 |
70 | ) 71 | } 72 | } 73 | componentDidMount(){ 74 | this.context.fetchUsersList() 75 | } 76 | render(){ 77 | return ( 78 |
79 | {this.renderContent()} 80 |
81 | ) 82 | } 83 | } 84 | 85 | UsersList.contextType = adminContext; 86 | export default UsersList; -------------------------------------------------------------------------------- /src/components/BookCreateForm/BookCreateForm.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Form, Message ,Button, Dimmer, Loader} from 'semantic-ui-react'; 3 | import adminContext from '../../contexts/adminContext'; 4 | 5 | class BookCreateForm extends Component{ 6 | constructor(props){ 7 | super(props); 8 | this.parts = null; 9 | } 10 | state = { 11 | name: null, 12 | author: null, 13 | err: null, 14 | showLoader: false 15 | } 16 | validateForm = () => { 17 | if(!this.state.name || this.state.name.length < 2){ 18 | return this.setState({err: "Invalid name"}) 19 | } 20 | if(!this.state.author || this.state.author.length < 2){ 21 | return this.setState({err: "Invalid author"}) 22 | } 23 | this.setState({err: null, showLoader: true}); 24 | this.handleUpload() 25 | } 26 | handleUpload = () => { 27 | const data = new FormData(); 28 | data.set("name", this.state.name) 29 | data.set("author", this.state.author) 30 | for(let i = 0; i < this.parts.length; i++){ 31 | data.append(`file${i}`, this.parts[i], this.parts[i].name) 32 | } 33 | this.context.createNewBook(data); 34 | } 35 | setName = (event) => { 36 | this.setState({name: event.target.value}) 37 | } 38 | setAuthor = (event) => { 39 | this.setState({author: event.target.value}) 40 | } 41 | setParts = (event) => { 42 | this.parts = event.target.files 43 | } 44 | renderErrorMessage = () => { 45 | if(this.state.err === null){ 46 | return; 47 | } 48 | return ( 49 | 50 | ) 51 | } 52 | renderContent = () => { 53 | if(this.state.showLoader){ 54 | return ( 55 | 56 | Uploading Files... 57 | 58 | ) 59 | } else { 60 | return ( 61 |
62 | {this.renderErrorMessage()} 63 | 64 | 65 | {this.setName(e)}} autoFocus required/> 66 | 67 | 68 | 69 | {this.setAuthor(e)}} required/> 70 | 71 | 72 | 73 | {this.setParts(e)}} required/> 74 | 75 | 76 |
77 | ) 78 | } 79 | } 80 | render(){ 81 | return( 82 |
83 | {this.renderContent()} 84 |
85 | ) 86 | } 87 | }; 88 | 89 | BookCreateForm.contextType = adminContext; 90 | export default BookCreateForm; 91 | 92 | 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multi-Room Control/Monitoring Audio System 2 | #### MERN Stack, WebRTC, Audio API 3 | 4 | #### TL`DR 5 | Imagine a recording studio control room only with multiple recording rooms. 6 | The control room is able to monitor, talkback, control playback, volume and many other element for each room. This is a digital version of an analog system created to help with reading disabilities treatment. 7 | 8 | **[Full article on Medium](https://medium.com/@izhaki.idan/audio-for-learning-disabilities-713ea040e3c6 "Web Audio for learning disabilities 9 | ")** 10 | 11 | #### Can I try it? 12 | 13 | Yep but, Because This is a local project that supposed to work offline I compiled an **Electron Demo Server** that you can download, It also includes some media assets for you to try out. 14 | 15 | ##### Electron Setup 16 | 17 | * **Windows Only** 18 | * Running a mongoDB service is required. 19 | * [Download and unzip the file](https://drive.google.com/open?id=1CGHlPloR0fQEVhB6VTPLaqXyk3sNtgcA "Electron Demo"). 20 | * Run "Audio System.exe" to run the local server. 21 | * Open 2 chrome/firefox tabs for Admin and User 22 | ##### if you are connecting from a remote computer use your IPv4 address instead of localhost. 23 | ##### **Tab 1 - Admin** 24 | * Enter Admin credentials: 25 | * Username: admin 26 | * Password: admin 27 | * Setup Room 0 and save. 28 | ##### **Tab 2 - User** 29 | * Enter User credetials (Change numbers for diffrent rooms): 30 | * Username: Room 0 31 | * Password: Room0 32 | * **Enjoy!** 33 | 34 | #### What can it do? 35 | The application is in its early stages but already implements: 36 | * One central *control room (ADMIN)* that can connect & control and listen to many *listening rooms (USERS)*. 37 | * Admin can recive many connections but send data commands only for one room at a time (While listening...) 38 | * WebRTC (PeerJS) - handles peer 2 peer connection for data and audio streaming connection. 39 | * Web Audio API is used to handle Stream to Audio Context / Audio Context to Stream & Gain control over audio elements. 40 | * Node serverside API handles authentication and data fetching from mongoDB. 41 | * Pitch Detection - *optional* for triggering audio context oscillator over the User playback. 42 | * Heavy workload (Audio Context, Stream Generation & Pitch Detection )implemented on the user side to prevent overloading the admin. 43 | * Node scans assets/books folder and generates media database. 44 | * Audio is buffered and streamed to client. 45 | * Centrelized state managment with React Context System. 46 | * Visual indicators for the online status of each room: 47 | * *grey* - disconnected 48 | * *green* - connected 49 | * *orange* - listening to room 50 | 51 | *While listening to a room admin can:* 52 | 53 | * Start, Stop, Pause, FF, Rewind audio playback 54 | * Hold talkback button to interact with current room 55 | * Activate / Deactive Pitch detection Oscillator and ajust the OSC gain. 56 | * View current room playback time 57 | 58 | ## REQUIREMENTS 59 | * *UPDATED CHROME OR FIREFOX* - for Audio HTML elements stream capture. 60 | * An active MongoDB **local** service. 61 | * *GET OPEN SSL Certificate* - save as server.cert + server.key - for serving HTTPS (Required for proper audio context streams otherwise mose browsers will silence the output). 62 | * *Create media library* - server/assets/books, each subfolder will be logged in the DB, each file in subfolder will be added as parts array. *NOTE:*(This project refers to audio books but you can change the scan path...) 63 | * *SETUP ENV VARIABLES* - PORT, HOST, DEV_DB_URL 64 | * *Run* : npm install, num run build || num run start-build (with nodemon) 65 | * *This project runs localy* to provide service without internet connection. 66 | -------------------------------------------------------------------------------- /src/components/BooksList/BooksList.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Card, Button, Icon} from 'semantic-ui-react'; 3 | import ConfirmTemplate from '../Confirm/Confim'; 4 | import {serverAPI} from '../../api/api'; 5 | import adminContext from '../../contexts/adminContext'; 6 | 7 | let INITIAL_STATE = {showConfirm: false, selectedBook: null} 8 | 9 | class BooksList extends Component{ 10 | state = INITIAL_STATE 11 | handleConfirm = book => { 12 | this.deleteBook(book) 13 | } 14 | handleCancel = () => { 15 | this.setState({showConfirm: false, selectedBook: null}) 16 | } 17 | deleteBook = book => { 18 | serverAPI.delete(`/books/${this.state.selectedBook._id}`) 19 | .then(() => { 20 | this.setState({showConfirm: false, selectedBook: null}); 21 | this.context.fetchBooksList(); 22 | }) 23 | .catch(err => console.log(err)); 24 | } 25 | renderAuthor = book => { 26 | if(!book.author){ 27 | return 28 | } 29 | return {book.author} 30 | } 31 | renderBooksList = () => { 32 | let books = this.context.books; 33 | if(!books || books.length < 1){ 34 | return
Loading books...
35 | } else { 36 | return books.map(book => { 37 | return ( 38 | 39 | 40 | {book.name} 41 | 42 |

Author: {this.renderAuthor(book)}

43 | {book.parts.length} parts 44 |
45 | 46 |
47 | 54 | 62 |
63 |
64 |
65 |
66 | ) 67 | }) 68 | } 69 | } 70 | renderContent = () => { 71 | if(!this.state.showConfirm && !this.state.selectedBook){ 72 | return ( 73 | 74 | {this.renderBooksList()} 75 | 76 | ) 77 | } else { 78 | let content = `Are you sure you want to delete "${this.state.selectedBook.name}" ?`; 79 | return ( 80 |
81 | 88 |
89 | ) 90 | } 91 | } 92 | componentDidMount(){ 93 | this.context.fetchBooksList() 94 | } 95 | render(){ 96 | return ( 97 |
98 | {this.renderContent()} 99 |
100 | ) 101 | } 102 | } 103 | BooksList.contextType = adminContext; 104 | export default BooksList; -------------------------------------------------------------------------------- /src/components/RoomList/RoomList.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Button, Card, Icon, Container} from 'semantic-ui-react'; 3 | import adminContext from '../../contexts/adminContext'; 4 | import RoomEdit from '../RoomEdit/RoomEdit'; 5 | import RoomControls from '../RoomControls/RoomControls'; 6 | import RoomModalTemplate from '../RoomModal/RoomModal'; 7 | 8 | const INITIAL_STATE = { 9 | showModal: false 10 | } 11 | 12 | class RoomList extends Component{ 13 | state = INITIAL_STATE; 14 | handleModalClose = () => { 15 | this.setState({showModal: false}); 16 | this.context.setSelectedRoom(null); 17 | } 18 | showModal = (room, mode) => { 19 | this.setState({showModal: true}) 20 | if(mode === "edit"){ 21 | this.context.editSelectedRoom(room) 22 | } 23 | if(mode === "view"){ 24 | this.context.setSelectedRoom(room) 25 | } 26 | } 27 | renderRoomControlsOrEdit = () => { 28 | if(!this.context.selectedRoom){ 29 | return
30 | } else if(this.context.selectedRoom && this.context.editRoom && this.state.showModal){ 31 | let header = `Edit ${this.context.selectedRoom.username}`; 32 | return ( 33 | }/> 34 | ) 35 | } else if(this.context.selectedRoom && this.state.showModal){ 36 | let header = `${this.context.selectedRoom.username} Controls`; 37 | return ( 38 | }/> 39 | ) 40 | } else { 41 | return
42 | } 43 | } 44 | renderCardExtraContent = (room) => { 45 | return( 46 | 47 |
48 | 49 | 50 |
51 |
52 | ) 53 | } 54 | renderCardMeta = room => { 55 | if(!room.currentUser){ 56 | return 57 | } else { 58 | return {room.currentUser.firstName} {room.currentUser.lastName} 59 | } 60 | } 61 | renderCardDescription = room => { 62 | if(!room.currentBook){ 63 | return 64 | } else { 65 | return {room.currentBook.name}
{ room.currentPart }
66 | } 67 | 68 | } 69 | renderCardContent = (room, color) => { 70 | return( 71 | 72 | {this.context.setSelectedRoom(room)}}>{room.username} 73 | {this.renderCardMeta(room)} 74 | {this.renderCardDescription(room)} 75 | 76 | ) 77 | } 78 | setStatusIconColor = room => { 79 | if(this.context.currentCall && this.context.currentCall === room.username){ 80 | return 'orange'; 81 | } else { 82 | return this.context.onlineRooms.includes(room.username) ? 'green' : 'grey'; 83 | } 84 | } 85 | renderRoomlist = () => { 86 | if(!this.props.rooms || this.props.rooms.length < 1){ 87 | return
Loading rooms...
88 | } 89 | return ( 90 | 91 | {this.props.rooms.map((room, index) => { 92 | let color = this.setStatusIconColor(room) 93 | if(room.isAdmin === true){ 94 | return 95 | } else if(this.context.selectedRoom && room.username === this.context.selectedRoom.username) { 96 | return ( 97 | 98 | {this.renderCardContent(room, color)} 99 | {this.renderCardExtraContent(room)} 100 | {this.renderRoomControlsOrEdit()} 101 | 102 | ) 103 | } else { 104 | return ( 105 | 106 | {this.renderCardContent(room, color)} 107 | {this.renderCardExtraContent(room)} 108 | 109 | ) 110 | } 111 | })} 112 | 113 | ) 114 | } 115 | render(){ 116 | // console.log(this.context); 117 | return( 118 | 119 |

Room List

120 | {this.renderRoomlist()} 121 |
122 | ) 123 | } 124 | } 125 | 126 | RoomList.contextType = adminContext; 127 | export default RoomList -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/contexts/adminContext.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {serverAPI} from '../api/api' 3 | import PeerClient from 'peerjs'; 4 | const Context = React.createContext(); 5 | const Peer = window.Peer; 6 | 7 | const peerConfig = { 8 | host: window.location.hostname, 9 | port: '8080', 10 | path: '/peerjs', 11 | sdpSemantics: 'unified-plan', 12 | debug: 0, 13 | } 14 | const INITIAL_STATE = { 15 | activeMenuItem: "Rooms", 16 | peer: null, 17 | isPeerInitialized: false, 18 | isNavModalOpen: false, 19 | rooms: [], 20 | users: [], 21 | books: [], 22 | onlineRooms: [], 23 | selectedRoom: null, 24 | editRoom: false, 25 | currentConnection: null, 26 | currentCall: null, 27 | connData: {}, 28 | userMicGainSlider: 50, 29 | userPlayerGainSlider: 70, 30 | } 31 | 32 | export class AdminContextStore extends Component{ 33 | constructor(props){ 34 | super(props) 35 | this.state = INITIAL_STATE; 36 | } 37 | handleMenuItemClick = (e, {name}) => { 38 | this.setState({activeMenuItem: name, isNavModalOpen: false}) 39 | } 40 | dispalyNavModal = isNavModalOpen => { 41 | this.setState({isNavModalOpen }); 42 | } 43 | createNewUser = user => { 44 | serverAPI.post('/users', user) 45 | .then(() => this.fetchUsersList()) 46 | .catch(err => console.log(err)); 47 | } 48 | fetchUsersList = () => { 49 | serverAPI.get('/users') 50 | .then(res => this.setState({ users: res.data, isNavModalOpen: false})) 51 | .catch(err => { console.log(err) }); 52 | } 53 | fetchBooksList = () => { 54 | serverAPI.get('/books') 55 | .then(res => this.setState({books: res.data, isNavModalOpen: false})) 56 | .catch(err => { console.log(err) }); 57 | } 58 | createNewBook = book => { 59 | serverAPI.post('/books', book) 60 | .then(() => { this.fetchBooksList() }) 61 | .catch(err => console.log(err)); 62 | } 63 | getAllRooms = async () => { 64 | serverAPI.get('/rooms') 65 | .then((res) => { 66 | this.setState({rooms: res.data}); 67 | }) 68 | .catch((err) => {console.log(err)}); 69 | } 70 | setOnlineRooms = onlineRooms => { 71 | this.setState({onlineRooms}); 72 | } 73 | setPeerInitialized = isPeerInitialized => { 74 | this.setState({isPeerInitialized}) 75 | } 76 | setSelectedRoom = selectedRoom =>{ 77 | this.setState({selectedRoom, editRoom: false, currentCall: null, currentConnection: null, connData: null}); 78 | } 79 | setCurrentConnection = currentConnection => { 80 | console.log("SET NEW CONNECTION"); 81 | this.setState({currentConnection}); 82 | } 83 | editSelectedRoom = selectedRoom => { 84 | this.setState({selectedRoom, editRoom: true, currentCall: null}) 85 | } 86 | setPeerConnection = async () => { 87 | await this.setState({peer: new Peer('admin', peerConfig ), isPeerInitialized: true }); 88 | } 89 | setCurrentCommand = cmd => { 90 | this.setState({cmd}) 91 | } 92 | setCurrentCall = call => { 93 | if(!this.state.currentCall){ 94 | this.setState({currentCall: call}); 95 | } 96 | } 97 | hangCurrentCall = () => { 98 | if(this.state.currentCall){ 99 | console.log("HANGING UP CURRENT CALL"); 100 | this.setState({currentCall: null}); 101 | } 102 | } 103 | componentDidMount(){ 104 | this.setPeerConnection().then(()=>{ 105 | this.state.peer.on('connection', (conn)=>{ 106 | let newOnlineRoomsList = [...this.state.onlineRooms, conn.peer]; 107 | this.setOnlineRooms(newOnlineRoomsList); 108 | conn.on('open', () => { 109 | conn.send({cmd: "admin connect"}); 110 | }) 111 | conn.on('close', ()=>{ 112 | let newOnlineRoomsList = this.state.onlineRooms.filter(room => room !== conn.peer); 113 | this.setOnlineRooms(newOnlineRoomsList); 114 | }); 115 | conn.on('data', (data) => { 116 | if(data.cmd ==="update"){ 117 | this.setState({connData: data, userMicGainSlider: data.micGain * 100, userPlayerGainSlider: data.playerGain * 100}); 118 | } 119 | if(data.cmd === "user answered"){ 120 | this.setCurrentConnection(conn) 121 | } 122 | }); 123 | }) 124 | this.getAllRooms() 125 | }) 126 | .catch(err => console.log(err)) 127 | } 128 | componentWillUnmount(){ 129 | this.state.peer.destroy(); 130 | } 131 | render(){ 132 | return( 133 | 150 | {this.props.children} 151 | 152 | ) 153 | } 154 | } 155 | 156 | export default Context; -------------------------------------------------------------------------------- /src/components/RoomEdit/RoomEdit.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Button, Segment, List, Icon} from 'semantic-ui-react'; 3 | import {serverAPI} from '../../api/api'; 4 | import adminContext from '../../contexts/adminContext'; 5 | 6 | let INITIAL_STATE = { 7 | allUsers: null, 8 | allBooks: null, 9 | currentUser: null, 10 | currentBook: null, 11 | currentPart: null 12 | } 13 | 14 | class RoomEdit extends Component{ 15 | state = INITIAL_STATE; 16 | renderCurrentRoomData = () => { 17 | let listArr = [] 18 | for(let key in this.context.selectedRoom){ 19 | listArr.push(key); 20 | }; 21 | return listArr.map((key, index) => { 22 | return {key}: {this.context.selectedRoom[key]} 23 | }); 24 | } 25 | renderUserSelection = () => { 26 | if(!this.context.users){ 27 | return 28 | } else { 29 | let currentUserId = ""; 30 | let optionContent = ""; 31 | if(this.context.selectedRoom && this.context.selectedRoom.currentUser){ 32 | let currentUser = this.context.selectedRoom.currentUser; 33 | currentUserId = currentUser._id; 34 | optionContent = `${currentUser.firstName} ${currentUser.lastName}`; 35 | } 36 | return ( 37 | 43 | ) 44 | } 45 | } 46 | renderUserList = () => { 47 | return this.context.users.map((user) => { 48 | return 49 | }) 50 | } 51 | renderBookSelection = () => { 52 | let books = this.context.books 53 | if(!books || books.length < 1 ){ 54 | return 55 | } else { 56 | let currentBookId = ""; 57 | let optionContent = ""; 58 | if(this.context.selectedRoom && this.context.selectedRoom.currentBook){ 59 | let currentBook = this.context.selectedRoom.currentBook; 60 | currentBookId = currentBook._id; 61 | optionContent = `${currentBook.name}`; 62 | } 63 | return ( 64 | 69 | ) 70 | } 71 | } 72 | renderBookList = () => { 73 | return this.context.books.map((book) => { 74 | return 75 | }) 76 | } 77 | renderBookParts = () => { 78 | if(!this.state.currentBook){ 79 | return; 80 | } else { 81 | let currentPart = "" 82 | if(this.context.selectedRoom && this.context.selectedRoom.currentPart){ 83 | currentPart = this.context.selectedRoom.currentPart; 84 | } 85 | return( 86 | 91 | ) 92 | } 93 | } 94 | renderPartsList = () => { 95 | let books = this.context.books; 96 | if(books && books.length > 0){ 97 | let selectedBook = books.filter(book => book._id === this.state.currentBook); 98 | return selectedBook[0].parts.map((part, index) => { 99 | return 100 | }) 101 | } 102 | } 103 | renderButtons = () =>{ 104 | if(this.state.currentUser && this.state.currentBook && this.state.currentPart){ 105 | return( 106 |
107 | 108 | 109 |
110 | ) 111 | } else { 112 | return 113 | } 114 | } 115 | updateCurrentRoomData = () => { 116 | let data = { 117 | currentUser: this.state.currentUser, 118 | currentBook: this.state.currentBook, 119 | currentPart: this.state.currentPart 120 | } 121 | serverAPI.put(`/rooms/${this.context.selectedRoom._id}`, data) 122 | .then(() => { 123 | this.context.getAllRooms(); 124 | this.props.handleModalClose(); 125 | }) 126 | .catch(err => console.log(err)); 127 | } 128 | clearCurrentRoomData = async () => { 129 | await this.setState({ 130 | currentUser: null, 131 | currentBook: null, 132 | currentPart: null 133 | }) 134 | this.updateCurrentRoomData(); 135 | } 136 | setCurrentState = () => { 137 | let selectedRoom = this.context.selectedRoom; 138 | if(!selectedRoom){ 139 | return; 140 | } else{ 141 | let currentUser = selectedRoom.currentUser ? selectedRoom.currentUser._id : null; 142 | let currentBook = selectedRoom.currentBook ? selectedRoom.currentBook._id : null; 143 | let currentPart = selectedRoom.currentPart ? selectedRoom.currentPart : null; 144 | this.setState({currentUser, currentBook, currentPart}); 145 | } 146 | } 147 | componentDidMount(){ 148 | this.context.fetchUsersList(); 149 | this.context.fetchBooksList(); 150 | this.setCurrentState(); 151 | } 152 | render(){ 153 | return( 154 | 155 |

Select User

156 | {this.renderUserSelection()} 157 |

Select Book

158 | {this.renderBookSelection()} 159 |

Select File

160 | {this.renderBookParts()} 161 |
162 | {this.renderButtons()} 163 |
164 | ) 165 | } 166 | } 167 | 168 | RoomEdit.contextType = adminContext; 169 | export default RoomEdit; 170 | -------------------------------------------------------------------------------- /server/public/admin.js: -------------------------------------------------------------------------------- 1 | // PEER CONNECTION 2 | 3 | // let reconnectAttempts = 0; 4 | let ctx; 5 | let adminMicGain; 6 | let outputToUser; 7 | const startCtx = document.getElementById('startctx'); 8 | const disconnectButton = document.getElementById('disconnect'); 9 | const adminPlayer = document.querySelector('audio'); 10 | const talkBackButton = document.getElementById('talkback'); 11 | const userMicGainSlider = document.querySelector('#micGain'); 12 | const userPlayerGainSlider = document.querySelector('#playerGain'); 13 | const startUserPlayerBtn = document.getElementById('startUserPlayer'); 14 | const pauseUserPlayerBtn = document.getElementById('pauseUserPlayer'); 15 | const stopUserPlayerBtn = document.getElementById('stopUserPlayer'); 16 | const timeDisplay = document.getElementById('timeDisplay'); 17 | const rewind = document.getElementById('rewind'); 18 | const fforward = document.getElementById('fforward'); 19 | const oscActive = document.getElementById('oscActive'); 20 | const oscGain = document.getElementById('oscGain'); 21 | const host = window.location.hostname; 22 | 23 | 24 | // const updateRoomsList = () => { 25 | // const roomsList = document.getElementById('roomList'); 26 | // roomsList.innerHTML = ""; 27 | // for(room in peer.connections){ 28 | // roomsList.innerHTML += `${room}` 29 | // } 30 | // } 31 | 32 | peer.on('disconnected', () => { 33 | location.reload(); 34 | // let reconnectInterval = setInterval(() => { 35 | // if(reconnectAttempts <= 3){ 36 | // peer.reconnect(); 37 | // reconnectAttempts += 1; 38 | // } else { 39 | // console.log("Unable to connect..."); 40 | // clearInterval(reconnectInterval); 41 | // } 42 | // }, 3000); 43 | }) 44 | 45 | peer.on('connection', (conn) => { 46 | conn.on('open', () => { 47 | // startUserPlayerBtn.removeAttribute('disabled'); 48 | conn.send({cmd: "admin connect"}); 49 | console.log('got user online') 50 | // startCtx.removeAttribute('disabled'); 51 | updateRoomsList() 52 | conn.on('data', (data) => { 53 | if(data.cmd === "update"){ 54 | // console.log(data) 55 | // Mic & Player Gain Status 56 | userMicGainSlider.value = data.micGain * 100; 57 | userPlayerGainSlider.value = data.playerGain * 100; 58 | // Playin status 59 | // if(data.isPlaying){ 60 | // startUserPlayerBtn.setAttribute('disabled', true); 61 | // stopUserPlayerBtn.removeAttribute('disabled'); 62 | // pauseUserPlayerBtn.removeAttribute('disabled'); 63 | // } else { 64 | // startUserPlayerBtn.removeAttribute('disabled'); 65 | // stopUserPlayerBtn.setAttribute('disabled', true); 66 | // pauseUserPlayerBtn.setAttribute('disabled', true); 67 | // } 68 | } 69 | if(data.cmd === "player status"){ 70 | let min = Math.floor(data.value / 60) 71 | let sec = Math.round(data.value - min * 60); 72 | let timeString = `${min}:${sec}` 73 | timeDisplay.innerHTML = timeString 74 | } 75 | }) 76 | oscActive.addEventListener('change', (e) => { 77 | if(e.target.value === "on"){ 78 | oscActive.setAttribute('value', "off"); 79 | } else { 80 | oscActive.setAttribute('value', "on"); 81 | } 82 | conn.send({cmd: "osc state", value: e.target.value}); 83 | }) 84 | oscGain.addEventListener('change', (e) => { 85 | conn.send({cmd: "osc gain", value: e.target.value / 10000 }); 86 | }) 87 | userMicGainSlider.addEventListener('change', (e) => { 88 | conn.send({cmd: "mic gain", value: e.target.value / 100 }); 89 | }); 90 | userPlayerGainSlider.addEventListener('change', (e) => { 91 | conn.send({cmd: "player gain", value: e.target.value / 100 }); 92 | }); 93 | talkBackButton.addEventListener('mousedown', () => { 94 | adminMicGain.gain.value = 0.7; 95 | console.log(adminMicGain.gain.value) 96 | conn.send("Talkback open") 97 | }); 98 | talkBackButton.addEventListener('mouseup', () => { 99 | adminMicGain.gain.value = 0; 100 | console.log(adminMicGain.gain.value) 101 | conn.send("Talkback closed") 102 | }); 103 | 104 | startUserPlayerBtn.addEventListener('click', () => { 105 | conn.send({cmd: "player start"}) 106 | startUserPlayerBtn.setAttribute('disabled', true); 107 | stopUserPlayerBtn.removeAttribute('disabled'); 108 | pauseUserPlayerBtn.removeAttribute('disabled'); 109 | rewind.removeAttribute('disabled'); 110 | fforward.removeAttribute('disabled'); 111 | }); 112 | pauseUserPlayerBtn.addEventListener('click', () => { 113 | conn.send({cmd: "player pause"}); 114 | pauseUserPlayerBtn.setAttribute('disabled', true); 115 | startUserPlayerBtn.removeAttribute('disabled'); 116 | }); 117 | stopUserPlayerBtn.addEventListener('click', () => { 118 | conn.send({cmd: "player stop"}); 119 | stopUserPlayerBtn.setAttribute('disabled', true); 120 | pauseUserPlayerBtn.setAttribute('disabled', true); 121 | startUserPlayerBtn.removeAttribute('disabled'); 122 | }); 123 | rewind.addEventListener('click', () => { 124 | conn.send({cmd: "rewind"}); 125 | }); 126 | fforward.addEventListener('click', () => { 127 | conn.send({cmd: "fforward"}); 128 | }) 129 | }); 130 | 131 | }) 132 | const disableTransportButtons = () => { 133 | startUserPlayerBtn.setAttribute('disabled', true); 134 | stopUserPlayerBtn.setAttribute('disabled', true); 135 | rewind.setAttribute('diabled', true); 136 | fforward.setAttribute('diabled', true); 137 | } 138 | startCtx.addEventListener('click', () => { 139 | startCtx.setAttribute('disabled', true); 140 | disconnectButton.removeAttribute('disabled'); 141 | setupAdminMic(); 142 | }); 143 | 144 | 145 | 146 | const setupAdminMic = async () => { 147 | ctx = await new AudioContext(); 148 | navigator.mediaDevices.getUserMedia({audio: true}) 149 | .then((micStream) => { 150 | outputToUser = ctx.createMediaStreamDestination(); 151 | let micSource = ctx.createMediaStreamSource(micStream); 152 | adminMicGain = ctx.createGain(); 153 | adminMicGain.gain.value = 0; 154 | micSource.connect(adminMicGain); 155 | adminMicGain.connect(outputToUser); 156 | callToUser() 157 | }) 158 | .catch(err => console.error(err)) 159 | } 160 | 161 | const callToUser = () => { 162 | let call = peer.call('user', outputToUser.stream); 163 | disconnectButton.addEventListener('click', () => { 164 | startCtx.removeAttribute('disabled'); 165 | disconnectButton.setAttribute('disabled', true); 166 | 167 | // ctx.close(); 168 | ctx = null; 169 | disableTransportButtons(); 170 | call.close(); 171 | }); 172 | call.on('stream', stream => { 173 | console.log("Got Stream...") 174 | adminPlayer.srcObject = stream; 175 | adminPlayer.play(); 176 | }) 177 | call.on('error', (err) => { 178 | console.log(err); 179 | }) 180 | } 181 | 182 | -------------------------------------------------------------------------------- /server/routes/api.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const Room = require('../models/Room'); 3 | const User = require('../models/User'); 4 | const Book = require('../models/Books'); 5 | const {isLoggedIn, isAdmin} = require('../middleware/auth'); 6 | const passport = require('passport'); 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | const formidable = require('formidable'); 10 | 11 | // AUTHENTICATION 12 | router.post('/login', passport.authenticate('local'), (req, res) => { 13 | if(!req.user){ 14 | return res.json({err: "Username or Password are incorrect"}) 15 | } 16 | return res.json({room: req.user}); 17 | }); 18 | 19 | router.post('/logout', (req, res) => { 20 | res.send("logout"); 21 | }); 22 | 23 | // ROOMS 24 | // Get all rooms 25 | router.get('/rooms', isAdmin, (req, res) => { 26 | Room.find().populate('currentUser').populate('currentBook').exec() 27 | .then((allRooms) => { 28 | res.json(allRooms); 29 | }); 30 | }); 31 | 32 | // Get Room Data 33 | router.get('/rooms/:roomId', isLoggedIn, (req, res) => { 34 | Room.findById(req.params.roomId) 35 | .then(foundRoom => { 36 | res.json(foundRoom); 37 | }) 38 | .catch(err => { 39 | console.log(err) 40 | }) 41 | }); 42 | router.put(`/rooms/:roomId`, isLoggedIn, (req, res) => { 43 | Room.findOneAndUpdate({_id: req.params.roomId}, req.body) 44 | .then(updatedRoom => { 45 | res.json(updatedRoom); 46 | }) 47 | .catch(err => console.log(err)); 48 | }) 49 | 50 | // USER 51 | router.get('/users', isLoggedIn, (req, res) =>{ 52 | User.find() 53 | .then(allUsers => { 54 | res.json(allUsers) 55 | }) 56 | .catch(err => console.log(err)); 57 | }); 58 | 59 | router.post('/users', isLoggedIn, (req, res) => { 60 | let newUser = req.body 61 | User.findOne({email: newUser.email}) 62 | .then((foundUser) => { 63 | if(!foundUser){ 64 | User.create(newUser) 65 | .then(() => {res.send({msg: "Created User"})}) 66 | } else { 67 | res.json({msg: "E-mail already registered"}) 68 | } 69 | }) 70 | .catch((err) => { 71 | console.log(err); 72 | res.json({msg: "Error", data: err}); 73 | }) 74 | }); 75 | 76 | // Find user by id 77 | router.get('/users/:userId',isLoggedIn, (req, res) => { 78 | User.findById(req.params.userId) 79 | .then(foundUser => { 80 | res.json(foundUser) 81 | }) 82 | .catch(err => console.log(err)); 83 | }); 84 | 85 | router.delete('/users/:userId',isLoggedIn, (req, res) => { 86 | User.findOneAndDelete({_id: req.params.userId}).then((deletedUser) => { 87 | console.log("Deleted:" ,deletedUser); 88 | res.json({msg: "Deleted User", data: deletedUser}) 89 | }) 90 | .catch(() => { 91 | console.log(err); 92 | res.json({msg: "Error", data: err}) 93 | }) 94 | }); 95 | 96 | // BOOKS 97 | router.get('/books', isLoggedIn, (req, res) =>{ 98 | Book.find() 99 | .then(allBooks => { 100 | res.json(allBooks) 101 | }) 102 | .catch(err => console.log(err)); 103 | }); 104 | 105 | router.post('/books', isLoggedIn, (req, res) => { 106 | let bookData = { parts: []} 107 | let newBookFolder; 108 | let progress = 0; 109 | let form = new formidable.IncomingForm() 110 | let assetsFolder = path.join(__dirname, "../assets/") 111 | if(!fs.existsSync(assetsFolder)) fs.mkdirSync(assetsFolder); 112 | let booksFolder = path.join(assetsFolder, '/books'); 113 | if(!fs.existsSync(booksFolder)) fs.mkdirSync(booksFolder); 114 | let tempFolder = path.join(assetsFolder, "/books/temp"); 115 | if(!fs.existsSync(tempFolder)) fs.mkdirSync(tempFolder); 116 | form.uploadDir = tempFolder; 117 | form.maxFileSize = 2000 * 1024 * 1024; 118 | form.multiples = true; 119 | form.parse(req, (err, fields, files) => { 120 | newBookFolder = path.join(booksFolder, fields.name) 121 | }); 122 | form.on('field', function(field, value) { 123 | bookData[field] = value; 124 | }); 125 | form.on('fileBegin', (name, file) => { 126 | console.log("Uploading: ", name) 127 | 128 | }); 129 | form.on('error', (err) => { 130 | console.log(err); 131 | }); 132 | form.on('aborted', () => { 133 | console.log("Aborted"); 134 | }) 135 | form.on('file', function(field, file) { 136 | let fileType = file.name.split('.').pop(); 137 | if(fileType === "mp3" || fileType === "wav"){ 138 | fs.renameSync(file.path, form.uploadDir + "/" + file.name); 139 | bookData.parts.push(file.name); 140 | } else { 141 | fs.unlinkSync(file.path) 142 | console.log("Not an audio file: ", file.name); 143 | } 144 | }) 145 | form.on('end', function() { 146 | console.log("Form ended") 147 | if(bookData.parts.length < 1){ 148 | fs.rmdirSync(tempFolder); 149 | res.json({msg: "No files uploaded"}) 150 | } else { 151 | fs.renameSync(tempFolder, newBookFolder) 152 | Book.create(bookData) 153 | .then(createdBook => { 154 | res.send({msg: "Created book", data: createdBook}); 155 | }) 156 | .catch(err => { 157 | console.log(err); 158 | res.send({msg: "Error", err}); 159 | }) 160 | } 161 | }); 162 | 163 | }) 164 | 165 | 166 | // Find book by id 167 | router.get('/books/:bookId', (req, res) => { 168 | Book.findById(req.params.userId) 169 | .then(foundBook => { 170 | res.json(foundBook) 171 | }) 172 | .catch(err => console.log(err)); 173 | }); 174 | 175 | router.delete('/books/:bookId',isLoggedIn, (req, res) => { 176 | Book.findOneAndDelete({_id: req.params.bookId}).then((deletedBook) => { 177 | let deleteFolerPath = path.join(__dirname, '../assets/books/', deletedBook.name) 178 | if(fs.existsSync(deleteFolerPath)){ 179 | let files = fs.readdirSync(deleteFolerPath) 180 | files.forEach(file => { 181 | fs.unlinkSync(path.join(deleteFolerPath, file)) 182 | }); 183 | fs.rmdirSync(deleteFolerPath); 184 | } 185 | res.json({msg: "Deleted Book", data: deletedBook}) 186 | }) 187 | .catch(err => { 188 | console.log(err); 189 | res.json({msg: "Error", data: err}) 190 | }) 191 | }); 192 | 193 | 194 | router.get('/audio', (req, res) => { 195 | console.log("Get file request from:", req.user); 196 | Room.findById(req.user._id).populate('currentBook').populate('currentUser').exec().then((foundRoom) => { 197 | if(!foundRoom || !foundRoom.currentBook.name || !foundRoom.currentPart){ 198 | console.log("Missing path data") 199 | res.send("missin data") 200 | } else { 201 | // SEND FILE TO ROOM 202 | const bookPath = path.join(__dirname, `../assets/books/${foundRoom.currentBook.name}/${foundRoom.currentPart}`); 203 | console.log(bookPath) 204 | const state = fs.statSync(bookPath); 205 | const fileSize = state.size; 206 | const range = req.headers.range; 207 | if(range){ 208 | const parts = range.replace(/bytes=/, "").split("-"); 209 | const start = parseInt(parts[0], 10); 210 | const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1; 211 | const chunkSize = (end-start)+1; 212 | const file = fs.createReadStream(bookPath, {start, end}); 213 | const head = { 214 | 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 215 | 'Accept-Ranges': 'bytes', 216 | 'Content-Length': chunkSize, 217 | 'Content-Type': 'audio/mp3', 218 | } 219 | console.log("Streaming...") 220 | res.writeHead(206, head); 221 | file.pipe(res); 222 | }else{ 223 | const head = { 224 | 'Content-Length': fileSize, 225 | 'Content-Type': 'audio/mp3', 226 | } 227 | console.log("Streaming first part") 228 | res.writeHead(200, head) 229 | fs.createReadStream(bookPath).pipe(res) 230 | } 231 | } 232 | }) 233 | 234 | }); 235 | 236 | module.exports = router; -------------------------------------------------------------------------------- /server/public/user.js: -------------------------------------------------------------------------------- 1 | // PEER CONNECTION 2 | const roomName = document.getElementById('room-title').innerHTML; 3 | const host = window.location.hostname; 4 | 5 | const peer = new Peer(roomName, { 6 | host: host, 7 | port: '8080', 8 | path: '/peerjs', 9 | sdpSemantics: 'unified-plan', 10 | debug: 0, 11 | }); 12 | 13 | let reconnectAttempts = 0; 14 | let call = null; 15 | let isAdminConnected = false; 16 | let isPlaying = false; 17 | let updateAdminTimer; 18 | // DOM Elements 19 | const startCtxBtn = document.getElementById('startctx'); 20 | const userPlayer = document.querySelector('audio'); 21 | 22 | // AudioContext Setup 23 | let ctx; 24 | let sampleRate; 25 | let micGain; 26 | let playerGain; 27 | let micStream; 28 | 29 | let micToAdminGain; 30 | let playerToAdminGain; 31 | // Streams Variables 32 | let playerStream; 33 | let micStreamToOutput; 34 | let clonedMicStream; 35 | let clonedPlayerStream; 36 | 37 | let micGainValue = 0.5; 38 | let playerGainValue = 0.7; 39 | let analyser; 40 | let dataArray; 41 | let pitchArr = []; 42 | 43 | // Load Pitchfinder 44 | let PitchFinder = window.PitchFinder 45 | let detectPitch = PitchFinder.YIN(); 46 | let isOscActive = true; 47 | let isOscPlaying = false; 48 | let oscGainValue = 0.001; 49 | 50 | const playOsc = pitch => { 51 | let osc = ctx.createOscillator(); 52 | let oscGain = ctx.createGain(); 53 | osc.type = 'sine'; 54 | osc.frequency.setValueAtTime(pitch*100, ctx.currentTime); 55 | oscGain.gain.value = oscGainValue; 56 | osc.connect(oscGain); 57 | oscGain.connect(ctx.destination) 58 | isOscPlaying = true; 59 | setTimeout(() => { isOscPlaying = false}, 1000); 60 | osc.start(); 61 | oscGain.gain.setValueAtTime(0, ctx.currentTime + 0.8) 62 | setTimeout(() => { 63 | osc.stop(); 64 | }, 1000) 65 | } 66 | 67 | const pitchDetector = () => { 68 | if(!isPlaying){ 69 | return; 70 | } 71 | // Populate the dataArray from the analyser method 72 | let dataArray = new Uint8Array(analyser.fftSize) 73 | analyser.getByteTimeDomainData(dataArray); 74 | // Detect pitch and push to array; 75 | let pitch = detectPitch(dataArray, { sampleRate: 48000}); 76 | pitchArr.push(pitch); 77 | if(pitchArr.length > 2){ 78 | pitchArr.shift() 79 | } 80 | // Pervent overloading the oscillator 81 | if(isOscActive && !isOscPlaying && pitchArr[1] && pitchArr[1] !== pitchArr[0]){ 82 | playOsc(pitchArr[1]) 83 | } 84 | requestAnimationFrame(pitchDetector) 85 | } 86 | 87 | peer.on('disconnected', () => { 88 | console.log("Trying to reconnect to server"); 89 | reconnect() 90 | }) 91 | 92 | if(document.readyState === "complete"){ 93 | console.log("Ready") 94 | // Start Audio Context after document is loaded 95 | startCtxBtn.addEventListener('click', () => { 96 | startCtxBtn.setAttribute('disabled', true); 97 | startCtxBtn.style.display = "none"; 98 | initUserPlayback() 99 | }); 100 | } 101 | 102 | 103 | 104 | // Get Stream from audio player 105 | const initUserPlayback = () => { 106 | ctx = new(window.AudioContext || window.webkitAudioContext)(); 107 | // SETUP ANALYSER 108 | sampleRate = ctx.sampleRate; 109 | analyser = ctx.createAnalyser(); 110 | // Capture Stream from audio player 111 | if(isChromeBrowser()){ 112 | playerStream = userPlayer.captureStream(); 113 | console.log("Chrome Capture"); 114 | } else { 115 | playerStream = userPlayer.mozCaptureStream(); 116 | console.log("FireFox Capture"); 117 | } 118 | playerGain = ctx.createGain() 119 | playerGain.gain.value = playerGainValue; 120 | let playerStreamToOutput = ctx.createMediaStreamSource(playerStream); 121 | let filter = ctx.createBiquadFilter(); 122 | filter.type = "highpass"; 123 | filter.frequency.value = 250; 124 | playerStreamToOutput.connect(filter); 125 | filter.connect(analyser); 126 | analyser.connect(playerGain); 127 | playerGain.connect(ctx.destination); 128 | getUserMicStream() 129 | } 130 | 131 | // Get stream from user Microphone 132 | let getUserMicStream = () => { 133 | let constraints = { video: false, audio: true }; 134 | navigator.mediaDevices.getUserMedia(constraints) 135 | .then(stream => { 136 | micStream = stream; 137 | micGain = ctx.createGain() 138 | micGain.gain.value = micGainValue; 139 | micStreamToOutput = ctx.createMediaStreamSource(micStream); 140 | micStreamToOutput.connect(micGain); 141 | micGain.connect(ctx.destination); 142 | setupConnection(); 143 | }) 144 | .catch((err) => console.error(err)); 145 | } 146 | let isChromeBrowser = () => { 147 | if(navigator.userAgent.indexOf("Chrome") >= 0){ 148 | console.log("Chrome") 149 | return true 150 | } else if(navigator.userAgent.indexOf("Firefox") >= 0){ 151 | console.log("FireFox") 152 | return false 153 | } 154 | } 155 | let reconnect = () => { 156 | ctx.close(); 157 | // ctx = null; 158 | setupConnection(); 159 | reconnectAttempts += 1; 160 | console.log(reconnectAttempts); 161 | } 162 | // Setup peer 2 peer connection listen on incoming data; 163 | const setupConnection = () =>{ 164 | console.log(isAdminConnected); 165 | let conn = peer.connect('admin', {serialization: "json"}); 166 | if(!conn){ 167 | console.log(peer.connections); 168 | if(confirm("Unable to connect reload page?")){ 169 | location.reload(); 170 | } 171 | } 172 | conn.on('open', () => { 173 | isAdminConnected = true; 174 | conn.send({cmd: "user online"}); 175 | }); 176 | conn.on('close', () => { 177 | isAdminConnected = false; 178 | console.log("Admin Disconnected") 179 | clearInterval(updateAdminTimer); 180 | if(!isAdminConnected){ 181 | reconnect(); 182 | } 183 | }); 184 | conn.on('data', (data) => { 185 | const updateAdminUi = () => { 186 | conn.send({cmd: "update" ,micGain: micGainValue, playerGain: playerGainValue, isPlaying: isPlaying, playerTime: userPlayer.currentTime, isAdminConnected: isAdminConnected }); 187 | } 188 | if(data.cmd === "admin connect"){ 189 | console.log("Admin Connected"); 190 | updateAdminTimer = setInterval(() => { 191 | updateAdminUi() 192 | }, 1000); 193 | } 194 | if(data.cmd === "mic gain"){ 195 | micGainValue = data.value; 196 | micGain.gain.setValueAtTime(data.value, ctx.currentTime); 197 | micToAdminGain.gain.setValueAtTime(data.value, ctx.currentTime); 198 | } 199 | if(data.cmd === "player gain"){ 200 | playerGainValue = data.value; 201 | playerGain.gain.setValueAtTime(data.value, ctx.currentTime); 202 | playerToAdminGain.gain.setValueAtTime(data.value, ctx.currentTime); 203 | } 204 | if(data.cmd === "osc state"){ 205 | if(data.value === "off"){ 206 | isOscActive = false; 207 | } else { 208 | isOscActive = true; 209 | }; 210 | } 211 | if(data.cmd === "osc gain"){ 212 | oscGainValue = data.value; 213 | } 214 | if(data.cmd === "player start"){ 215 | userPlayer.play(); 216 | isPlaying = true; 217 | pitchDetector(); 218 | } 219 | if(data.cmd === "player pause"){ 220 | isPlaying = false; 221 | userPlayer.pause(); 222 | } 223 | if(data.cmd === "player stop"){ 224 | isPlaying = false; 225 | userPlayer.currentTime = 0; 226 | userPlayer.pause(); 227 | } 228 | if(data.cmd === "rewind"){ 229 | let newTransportPosition = userPlayer.currentTime - 30 > 0 ? userPlayer.currentTime - 30 : 0; 230 | userPlayer.currentTime = newTransportPosition; 231 | } 232 | if(data.cmd === "fforward"){ 233 | let maxDuration = userPlayer.duration; 234 | let newTransportPosition = userPlayer.currentTime + 30 < maxDuration ? userPlayer.currentTime + 30 : maxDuration; 235 | userPlayer.currentTime = newTransportPosition; 236 | } 237 | }); 238 | peer.on('call', (call) => { 239 | console.log("Got call from admin"); 240 | createStreamToAdmin(call) 241 | isAdminConnected = true; 242 | }); 243 | } 244 | 245 | // 1. Clone active AUDIO & MIC Streams 246 | // 2. Create Audio Context sources form cloned streams. 247 | // 3. Create NEW Audio Context destination & connect cloned streams; 248 | // 4 .Peer calls admin with newly created Audio Context destination. 249 | let createStreamToAdmin = async (call) => { 250 | clonedMicStream = micStream.clone(); 251 | clonedPlayerStream = playerStream.clone(); 252 | let streamToAdmin = ctx.createMediaStreamDestination(); 253 | let micStreamToAdmin = ctx.createMediaStreamSource(clonedMicStream); 254 | let playerStreamToAdmin = ctx.createMediaStreamSource(clonedPlayerStream); 255 | // Stream volume mix controls 256 | micToAdminGain = ctx.createGain(); 257 | playerToAdminGain = ctx.createGain(); 258 | // Init mix controls gain 259 | micToAdminGain.gain.setValueAtTime(micGainValue, ctx.currentTime); 260 | playerToAdminGain.gain.setValueAtTime(playerGainValue, ctx.currentTime); 261 | micStreamToAdmin.connect(micToAdminGain); 262 | playerStreamToAdmin.connect(playerToAdminGain) 263 | micToAdminGain.connect(streamToAdmin); 264 | playerToAdminGain.connect(streamToAdmin); 265 | let options = { 266 | 'constraints': { 267 | 'mandatory': { 268 | 'OfferToReceiveAudio': true, 269 | 'OfferToReceiveVideo': false 270 | } 271 | } 272 | } 273 | call.answer(streamToAdmin.stream) 274 | call.on('stream', (adminStream) => { 275 | console.log("Got stream from admin...", adminStream); 276 | let adminMicPlayer = document.getElementById('adminMicPlayer'); 277 | adminMicPlayer.srcObject= adminStream; 278 | adminMicPlayer.play() 279 | console.log(adminMicPlayer); 280 | // let adminTBSource = ctx.createMediaStreamSource(adminStream); 281 | // adminStream.connect(ctx.destination); 282 | }); 283 | } 284 | 285 | 286 | 287 | 288 | 289 | 290 | -------------------------------------------------------------------------------- /src/components/RoomControls/RoomControls.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Button, Segment, Icon, Header, ButtonGroup} from 'semantic-ui-react'; 3 | import adminContext from '../../contexts/adminContext'; 4 | const Peer = window.Peer; 5 | 6 | let ctx; 7 | let adminMicGain; 8 | let outputToUser; 9 | let startCtx; 10 | let disconnectButton; 11 | let adminPlayer; 12 | let talkBackButton; 13 | let userMicGainSlider; 14 | let timeDisplay; 15 | let userPlayerGainSlider; 16 | let startUserPlayerBtn; 17 | let pauseUserPlayerBtn; 18 | let stopUserPlayerBtn; 19 | let rewind 20 | let fforward; 21 | let oscActive; 22 | let oscGain; 23 | 24 | class RoomControls extends React.Component{ 25 | constructor(props){ 26 | super(props) 27 | this.state = { 28 | call: null 29 | } 30 | } 31 | renderRoomMixControls = () => { 32 | return( 33 | 34 |

Mic Volume

35 | 45 |

Sound Volume

46 | 56 |
57 | ) 58 | } 59 | renderOscControls = () => { 60 | return( 61 | 62 |

OSC

63 | 64 | 74 |
75 | ) 76 | } 77 | renderPlayerControls = () => { 78 | let isDisabled = this.context.onlineRooms.includes(this.context.selectedRoom.username) ? false : true; 79 | return ( 80 | 81 |

Connection

82 | 83 |