├── server ├── conf │ ├── ld.so.conf.d │ │ └── libsrtp2.conf │ ├── turnserver │ │ └── turnserver.conf.template │ ├── webserver │ │ └── .env.template │ ├── systemd │ │ ├── janus.service │ │ ├── websocket-proxy.service.template │ │ └── turnserver.service │ ├── janus │ │ ├── janus.plugin.streaming.jcfg.template │ │ ├── janus.transport.http.jcfg.template │ │ └── janus.jcfg │ └── nginx │ │ └── nginx.conf.template ├── webroot-react │ ├── src │ │ ├── App.css │ │ ├── components │ │ │ ├── Menu │ │ │ │ ├── Microphone │ │ │ │ │ ├── img │ │ │ │ │ │ ├── record-icon.png │ │ │ │ │ │ └── record-stop-icon.png │ │ │ │ │ ├── Microphone.module.css │ │ │ │ │ └── Microphone.js │ │ │ │ ├── JanusStreamSelector │ │ │ │ │ ├── img │ │ │ │ │ │ └── cctv-icon.png │ │ │ │ │ ├── JanusStreamSelector.module.css │ │ │ │ │ └── JanusStreamSelector.js │ │ │ │ ├── MusicSelector │ │ │ │ │ ├── img │ │ │ │ │ │ ├── song-play-icon.png │ │ │ │ │ │ └── song-stop-icon.png │ │ │ │ │ ├── MusicSelector.module.css │ │ │ │ │ └── MusicSelector.js │ │ │ │ ├── Menu.module.css │ │ │ │ └── Menu.js │ │ │ ├── VideoStream │ │ │ │ ├── VideoStream.module.css │ │ │ │ ├── VideoControls │ │ │ │ │ ├── VideoControls.module.css │ │ │ │ │ └── VideoControls.js │ │ │ │ ├── JanusVideo │ │ │ │ │ ├── JanusVideo.module.css │ │ │ │ │ └── JanusVideo.js │ │ │ │ └── VideoStream.js │ │ │ ├── UI │ │ │ │ ├── Button │ │ │ │ │ ├── img │ │ │ │ │ │ ├── mute-on-icon.svg │ │ │ │ │ │ ├── mute-off-icon.svg │ │ │ │ │ │ ├── menu-icon.svg │ │ │ │ │ │ ├── video-play-icon.svg │ │ │ │ │ │ ├── zoom-out-icon.svg │ │ │ │ │ │ ├── zoom-in-icon.svg │ │ │ │ │ │ └── video-pause-icon.svg │ │ │ │ │ ├── Button.module.css │ │ │ │ │ └── Button.js │ │ │ │ └── Animator │ │ │ │ │ ├── Animator.js │ │ │ │ │ └── Animator.css │ │ │ └── LoginForm │ │ │ │ ├── LoginForm.module.css │ │ │ │ └── LoginForm.js │ │ ├── setupTests.js │ │ ├── App.test.js │ │ ├── index.css │ │ ├── index.js │ │ ├── hoc │ │ │ └── withMouseEvents.js │ │ ├── context │ │ │ ├── JanusContext.js │ │ │ └── AppContext.js │ │ ├── App.js │ │ ├── logo.svg │ │ ├── hooks │ │ │ ├── musicPlayer │ │ │ │ └── useMusicPlayer.js │ │ │ ├── microphone │ │ │ │ └── useMicrophone.js │ │ │ └── janus │ │ │ │ └── useJanus.js │ │ └── serviceWorker.js │ ├── public │ │ ├── robots.txt │ │ ├── favicon.ico │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── index.html │ ├── .gitignore │ ├── README.md │ └── package.json ├── webroot │ ├── img │ │ ├── cctv-icon.png │ │ ├── record-icon.png │ │ ├── song-play-icon.png │ │ ├── song-stop-icon.png │ │ ├── record-stop-icon.png │ │ ├── mute-on-icon.svg │ │ ├── mute-off-icon.svg │ │ ├── menu-icon.svg │ │ ├── video-play-icon.svg │ │ ├── zoom-out.svg │ │ ├── zoom-in.svg │ │ └── video-pause-icon.svg │ ├── js │ │ ├── music_player.js │ │ ├── broadcast_mic.js │ │ ├── janus_player.js │ │ └── ui.js │ ├── index.html │ └── style.css ├── websocket-proxy │ ├── app.js │ ├── audio-proxy.js │ ├── package.json │ ├── package-lock.json │ └── websocket-proxy.js ├── readme.MD ├── HOWTO_add_git_hooks.MD └── install.sh ├── .gitignore ├── flowchart.jpg ├── flowchart.odg ├── youtube.png ├── rpi ├── speaker-client │ ├── config.json.template │ ├── speaker-client.service │ ├── package.json │ ├── install.sh │ ├── readme.MD │ ├── speaker-client.js │ └── package-lock.json ├── music-client │ ├── config.json.template │ ├── music-client.service │ ├── package.json │ ├── package-lock.json │ ├── install.sh │ ├── readme.MD │ └── music-client.js └── janus-stream │ ├── janus-stream.service │ ├── janus-stream.sh.template │ ├── install.sh │ └── readme.MD ├── commit.sh ├── README.md └── LICENSE /server/conf/ld.so.conf.d/libsrtp2.conf: -------------------------------------------------------------------------------- 1 | /usr/lib64 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *# 3 | node_modules/ 4 | .env 5 | .project 6 | *.mp3 7 | -------------------------------------------------------------------------------- /server/webroot-react/src/App.css: -------------------------------------------------------------------------------- 1 | .App *:focus { 2 | outline: none; 3 | } -------------------------------------------------------------------------------- /flowchart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leerikss/baby-monitor/HEAD/flowchart.jpg -------------------------------------------------------------------------------- /flowchart.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leerikss/baby-monitor/HEAD/flowchart.odg -------------------------------------------------------------------------------- /youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leerikss/baby-monitor/HEAD/youtube.png -------------------------------------------------------------------------------- /server/conf/turnserver/turnserver.conf.template: -------------------------------------------------------------------------------- 1 | user=babymonitor:$password 2 | realm=$domain 3 | -------------------------------------------------------------------------------- /rpi/speaker-client/config.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "ws_audio_url": "$ws_url", 3 | "token": "$token" 4 | } 5 | -------------------------------------------------------------------------------- /server/webroot-react/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /server/webroot/img/cctv-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leerikss/baby-monitor/HEAD/server/webroot/img/cctv-icon.png -------------------------------------------------------------------------------- /commit.sh: -------------------------------------------------------------------------------- 1 | git add -A && \ 2 | git commit && \ 3 | git push production master && \ 4 | git push origin master 5 | -------------------------------------------------------------------------------- /server/webroot/img/record-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leerikss/baby-monitor/HEAD/server/webroot/img/record-icon.png -------------------------------------------------------------------------------- /server/webroot/img/song-play-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leerikss/baby-monitor/HEAD/server/webroot/img/song-play-icon.png -------------------------------------------------------------------------------- /server/webroot/img/song-stop-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leerikss/baby-monitor/HEAD/server/webroot/img/song-stop-icon.png -------------------------------------------------------------------------------- /server/webroot-react/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leerikss/baby-monitor/HEAD/server/webroot-react/public/favicon.ico -------------------------------------------------------------------------------- /server/webroot-react/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leerikss/baby-monitor/HEAD/server/webroot-react/public/logo192.png -------------------------------------------------------------------------------- /server/webroot-react/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leerikss/baby-monitor/HEAD/server/webroot-react/public/logo512.png -------------------------------------------------------------------------------- /server/webroot/img/record-stop-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leerikss/baby-monitor/HEAD/server/webroot/img/record-stop-icon.png -------------------------------------------------------------------------------- /rpi/music-client/config.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "ws_music_url": "$ws_url", 3 | "token": "$token", 4 | "music_path": "$songs_path" 5 | } 6 | -------------------------------------------------------------------------------- /server/webroot-react/src/components/Menu/Microphone/img/record-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leerikss/baby-monitor/HEAD/server/webroot-react/src/components/Menu/Microphone/img/record-icon.png -------------------------------------------------------------------------------- /server/webroot-react/src/components/Menu/JanusStreamSelector/img/cctv-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leerikss/baby-monitor/HEAD/server/webroot-react/src/components/Menu/JanusStreamSelector/img/cctv-icon.png -------------------------------------------------------------------------------- /server/webroot-react/src/components/Menu/Microphone/img/record-stop-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leerikss/baby-monitor/HEAD/server/webroot-react/src/components/Menu/Microphone/img/record-stop-icon.png -------------------------------------------------------------------------------- /server/webroot-react/src/components/Menu/MusicSelector/img/song-play-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leerikss/baby-monitor/HEAD/server/webroot-react/src/components/Menu/MusicSelector/img/song-play-icon.png -------------------------------------------------------------------------------- /server/webroot-react/src/components/Menu/MusicSelector/img/song-stop-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leerikss/baby-monitor/HEAD/server/webroot-react/src/components/Menu/MusicSelector/img/song-stop-icon.png -------------------------------------------------------------------------------- /server/webroot-react/src/components/Menu/Microphone/Microphone.module.css: -------------------------------------------------------------------------------- 1 | .Microphone { 2 | margin-top: 10px; 3 | cursor: pointer; 4 | display: block; 5 | margin-left: auto; 6 | margin-right: auto; 7 | } 8 | -------------------------------------------------------------------------------- /server/conf/webserver/.env.template: -------------------------------------------------------------------------------- 1 | REACT_APP_JANUS_SERVER_URL='https://$domain:8089/janus' 2 | REACT_APP_MUSIC_PLAYER_URL='wss://$domain/music' 3 | REACT_APP_SPEAKER_URL='wss://$domain/speaker' 4 | REACT_APP_TURN_SERVER_URL='turn:$domain:3478?transport=udp' -------------------------------------------------------------------------------- /server/webroot-react/src/components/Menu/Menu.module.css: -------------------------------------------------------------------------------- 1 | .Menu { 2 | position: fixed; 3 | 4 | width: 95vw; 5 | 6 | max-width: 800px; 7 | 8 | top: 60px; 9 | left: 0; 10 | right: 0; 11 | margin: auto; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /server/webroot-react/src/components/VideoStream/VideoStream.module.css: -------------------------------------------------------------------------------- 1 | .VideoStream { 2 | position: fixed; 3 | 4 | /* 5 | position: fixed; 6 | width: 100vw; 7 | height: 100vh; 8 | overflow: scroll; 9 | text-align: center; 10 | */ 11 | } -------------------------------------------------------------------------------- /rpi/janus-stream/janus-stream.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Janus stream service 3 | After=network.target 4 | 5 | [Service] 6 | User=pi 7 | ExecStart=/opt/janus-stream/janus-stream.sh 8 | Restart=always 9 | RestartSec=5s 10 | 11 | [Install] 12 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /server/conf/systemd/janus.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Janus WebRTC Server 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | ExecStart=/opt/janus/bin/janus -o 8 | Restart=on-abnormal 9 | LimitNOFILE=65536 10 | 11 | [Install] 12 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /server/webroot-react/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /rpi/music-client/music-client.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Music client service 3 | After=network.target 4 | 5 | [Service] 6 | User=pi 7 | WorkingDirectory=/opt/music-client 8 | ExecStart=/usr/bin/node music-client.js 9 | Restart=always 10 | RestartSec=5s 11 | 12 | [Install] 13 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /server/conf/systemd/websocket-proxy.service.template: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=WebSocket Proxy service 3 | After=network.target 4 | 5 | [Service] 6 | WorkingDirectory=$repo/server/websocket-proxy 7 | ExecStart=/usr/bin/node app.js 8 | Restart=always 9 | RestartSec=5s 10 | 11 | [Install] 12 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /server/webroot-react/src/components/VideoStream/VideoControls/VideoControls.module.css: -------------------------------------------------------------------------------- 1 | .VideoControls { 2 | z-index: 10; 3 | 4 | position: fixed; 5 | 6 | width: max-content; 7 | 8 | bottom: 70px; 9 | left: 0; 10 | right: 0; 11 | margin: auto; 12 | 13 | bottom: 2vh; 14 | 15 | } -------------------------------------------------------------------------------- /rpi/speaker-client/speaker-client.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Speaker client service 3 | After=network.target 4 | 5 | [Service] 6 | User=pi 7 | WorkingDirectory=/opt/speaker-client 8 | ExecStart=/usr/bin/node speaker-client.js 9 | Restart=always 10 | RestartSec=5s 11 | 12 | [Install] 13 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /server/conf/systemd/turnserver.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=turnserver - coturn TURN server 3 | After=network.target 4 | 5 | [Service] 6 | ExecStart=/usr/bin/turnserver -c /etc/turnserver.conf -o --no-stdout-log 7 | Type=forking 8 | PIDFile=/var/run/turnserver.pid 9 | Restart=always 10 | 11 | [Install] 12 | WantedBy=default.target 13 | -------------------------------------------------------------------------------- /server/webroot-react/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /server/webroot/img/mute-on-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /server/webroot-react/src/components/UI/Button/img/mute-on-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /server/webroot-react/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .env 26 | -------------------------------------------------------------------------------- /server/conf/janus/janus.plugin.streaming.jcfg.template: -------------------------------------------------------------------------------- 1 | $name: { 2 | type = "rtp" 3 | id = $id 4 | description = "$description" 5 | video = true 6 | videoport = $videoport 7 | videopt = 96 8 | videortpmap = "H264/90000" 9 | videofmtp = "profile-level-id=42e01f;packetization-mode=1" 10 | audio = true 11 | audioport = $audioport 12 | audiopt = 111 13 | audiortpmap = "opus/48000/2" 14 | pin = "$password" 15 | } 16 | -------------------------------------------------------------------------------- /server/webroot/img/mute-off-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /server/webroot-react/src/index.css: -------------------------------------------------------------------------------- 1 | /* Global CSS */ 2 | 3 | *:focus { 4 | outline: none; 5 | } 6 | 7 | body { 8 | margin: 0px; 9 | height: 100vh; 10 | 11 | background: linear-gradient(0deg, rgba(60, 60, 60, 1) 0%, rgba(20, 20, 20, 1) 100%); 12 | 13 | touch-action: pan-x pan-y; 14 | -webkit-tap-highlight-color: transparent; 15 | } 16 | 17 | @media screen and (min-width: 960px) { 18 | html { 19 | overflow-y: scroll; 20 | overflow-x: scroll; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server/webroot-react/src/components/UI/Button/img/mute-off-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /server/webroot-react/src/components/VideoStream/JanusVideo/JanusVideo.module.css: -------------------------------------------------------------------------------- 1 | .Video { 2 | border: 1px solid black; 3 | box-shadow: 5px 5px 10px 0px rgba(0, 0, 0, 1); 4 | transition: height .5s, width .5s; 5 | margin: auto; 6 | /* 7 | position: absolute; 8 | top: 50%; 9 | left: 50%; 10 | transform: translate(-50%, -50%); 11 | */ 12 | } 13 | 14 | .VideoWrapper { 15 | display: grid !important; 16 | height: 100vh; 17 | width: 100vw; 18 | overflow: scroll; 19 | } 20 | -------------------------------------------------------------------------------- /server/websocket-proxy/app.js: -------------------------------------------------------------------------------- 1 | const WebSocketProxy = require("./websocket-proxy"); 2 | const AudioProxy = require("./audio-proxy"); 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | // Music player WS proxy 7 | new WebSocketProxy({ 8 | host: "localhost", 9 | port: 8100, 10 | token: process.env.PASSWORD, 11 | origin: process.env.ORIGIN 12 | }).start(); 13 | 14 | // Mic binary stream WS proxy 15 | new AudioProxy({ 16 | host: "localhost", 17 | port: 8200, 18 | token: process.env.PASSWORD, 19 | origin: process.env.ORIGIN 20 | }).start(); -------------------------------------------------------------------------------- /server/websocket-proxy/audio-proxy.js: -------------------------------------------------------------------------------- 1 | const WebSocketProxy = require("./websocket-proxy"); 2 | const WebSocket = require('ws') 3 | 4 | module.exports = class AudioProxy extends WebSocketProxy { 5 | 6 | constructor(config) { 7 | super(config); 8 | } 9 | 10 | onClientMessage(msgClient, message) { 11 | this.server.clients.forEach(client => { 12 | if (msgClient !== client && 13 | client.role === "receiver" && 14 | super.isAlive(client)) 15 | client.send(message); 16 | }); 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /server/webroot-react/src/components/UI/Animator/Animator.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CSSTransition from 'react-transition-group/CSSTransition'; 3 | import './Animator.css'; 4 | 5 | const DURATION = 500; 6 | 7 | const Animator = (props) => { 8 | return ( 9 | 16 | {props.children} 17 | 18 | ); 19 | } 20 | 21 | export default Animator; -------------------------------------------------------------------------------- /rpi/music-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "music-client", 3 | "version": "1.0.0", 4 | "description": "WebSocket client handling playing music files on a Raspberry PI", 5 | "main": "music-client.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "nodejs", 11 | "omxplayer", 12 | "mp3", 13 | "websocket" 14 | ], 15 | "author": "Leif Eriksson", 16 | "repository": { 17 | "type": "git", 18 | "url": "git@github.com:leerikss/baby-monitor.git" 19 | }, 20 | "license": "ISC" 21 | } -------------------------------------------------------------------------------- /server/webroot-react/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 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /rpi/speaker-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "speaker-client", 3 | "version": "1.0.0", 4 | "description": "WebSocket client handling streaming all incoming binary messages to a Raspberry PI speaker", 5 | "main": "speaker-client.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "nodejs", 11 | "websocket", 12 | "stream", 13 | "pipe", 14 | "speaker" 15 | ], 16 | "author": "Leif Eriksson", 17 | "repository": { 18 | "type": "git", 19 | "url": "git@github.com:leerikss/baby-monitor.git" 20 | }, 21 | "license": "ISC" 22 | } -------------------------------------------------------------------------------- /server/webroot-react/README.md: -------------------------------------------------------------------------------- 1 | A Web based UI for the baby monitoring system built in React JS. 2 | 3 | ## Installation manually 4 | 5 | 1) Create a new file ".env" under [github repo]/server/webroot-react: 6 | ~~~terminal 7 | REACT_APP_JANUS_SERVER_URL='https://[your.domain]:8089/janus' 8 | REACT_APP_MUSIC_PLAYER_URL='wss://[your.domain]/music' 9 | REACT_APP_SPEAKER_URL='wss://[your.domain]/speaker' 10 | REACT_APP_TURN_SERVER_URL='turn:[your.domain]:3478?transport=udp' 11 | ~~~ 12 | 13 | 2) Build 14 | ~~~terminal 15 | cd [github repo]/server/webroot-react] 16 | npm i 17 | npm run build 18 | ~~~ 19 | 20 | 3) Configure your WebServer root to point to: 21 | ~~~[git repo root]/server/webroot-react/build~~~ 22 | 23 | -------------------------------------------------------------------------------- /server/webroot-react/src/components/UI/Animator/Animator.css: -------------------------------------------------------------------------------- 1 | .fade-slide-enter { 2 | display: block; 3 | opacity: 0; 4 | transform: scaleX(0) scaleY(0); 5 | } 6 | 7 | .fade-slide-enter-active { 8 | opacity: 1; 9 | transform: scaleX(1) scaleY(1); 10 | transition: all 500ms ease-in; 11 | } 12 | 13 | .fade-slide-enter-done { 14 | opacity: 1; 15 | display: block; 16 | } 17 | 18 | .fade-slide-exit { 19 | opacity: 1; 20 | transform: scaleX(1) scaleY(1); 21 | } 22 | 23 | .fade-slide-exit-active { 24 | opacity: 0; 25 | transform: scaleX(0) scaleY(0); 26 | transition: all 500ms ease-out; 27 | } 28 | 29 | .fade-slide-exit-done { 30 | opacity: 0; 31 | display: none; 32 | } -------------------------------------------------------------------------------- /server/websocket-proxy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "websocket-proxy", 3 | "version": "1.0.0", 4 | "description": "Proxies websocket messages to all connected clients", 5 | "main": "app.js", 6 | "dependencies": { 7 | "dotenv": "^8.2.0", 8 | "ws": "^7.2.0" 9 | }, 10 | "devDependencies": {}, 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "keywords": [ 15 | "nodejs", 16 | "proxy", 17 | "websocket" 18 | ], 19 | "author": "Leif Eriksson", 20 | "repository": { 21 | "type": "git", 22 | "url": "git@github.com:leerikss/baby-monitor.git" 23 | }, 24 | "license": "ISC" 25 | } -------------------------------------------------------------------------------- /rpi/janus-stream/janus-stream.sh.template: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | gst-launch-1.0 -v rpicamsrc vflip=$vflip hflip=$hflip \ 3 | name=src preview=0 fullscreen=0 bitrate=10000000 \ 4 | annotation-mode=time annotation-text-size=20 \ 5 | ! video/x-h264,width=$width,height=$height,framerate=$framerate/1 \ 6 | ! h264parse \ 7 | ! rtph264pay config-interval=1 pt=96 \ 8 | ! queue max-size-bytes=0 max-size-buffers=0 \ 9 | ! udpsink host=$jhost port=$vport \ 10 | alsasrc device=hw:1 \ 11 | ! audioconvert \ 12 | ! audioresample \ 13 | ! opusenc \ 14 | ! rtpopuspay \ 15 | ! queue max-size-bytes=0 max-size-buffers=0 \ 16 | ! udpsink host=$jhost port=$aport 17 | 18 | -------------------------------------------------------------------------------- /server/webroot-react/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | import { AppContextProvider } from './context/AppContext'; 7 | import { JanusContextProvider } from './context/JanusContext'; 8 | 9 | ReactDOM.render(, 10 | document.getElementById('root')); 11 | 12 | // If you want your app to work offline and load faster, you can change 13 | // unregister() to register() below. Note this comes with some pitfalls. 14 | // Learn more about service workers: https://bit.ly/CRA-PWA 15 | serviceWorker.unregister(); 16 | -------------------------------------------------------------------------------- /server/webroot-react/src/components/Menu/Menu.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import classes from './Menu.module.css'; 4 | import JanusStreamSelector from './JanusStreamSelector/JanusStreamSelector'; 5 | import MusicSelector from './MusicSelector/MusicSelector'; 6 | import Microphone from './Microphone/Microphone'; 7 | 8 | const Menu = (props) => { 9 | 10 | return ( 11 |
12 | 13 | 16 | 19 |
20 | ); 21 | } 22 | 23 | export default Menu; -------------------------------------------------------------------------------- /server/webroot-react/src/components/UI/Button/Button.module.css: -------------------------------------------------------------------------------- 1 | .Button { 2 | padding: 5px; 3 | 4 | margin-left:3px; 5 | margin-right:3px; 6 | 7 | width: 20px; 8 | height: 20px; 9 | 10 | border-radius: 5px; 11 | border: 1px solid white; 12 | box-shadow: 1px 1px rgba(0, 0, 0, .5); 13 | 14 | cursor: pointer; 15 | 16 | background: rgba(0, 0, 0, .7); 17 | background-position: center; 18 | 19 | transition: background 0.5s; 20 | } 21 | 22 | .Button:hover { 23 | background: #444444 radial-gradient(circle, transparent 1%, #444444 1%) center/15000%; 24 | } 25 | 26 | .Button:active { 27 | background-color: #BBBBBB; 28 | background-size: 100%; 29 | transition: background 0s; 30 | } 31 | 32 | .menu { 33 | margin-left: 15px; 34 | margin-right: 15px; 35 | } -------------------------------------------------------------------------------- /server/webroot-react/src/hoc/withMouseEvents.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AppContextActions, AppContext } from '../context/AppContext'; 3 | import { useContext } from 'react'; 4 | 5 | const withMouseEvents = Component => props => { 6 | 7 | const [, uiDispatch] = useContext(AppContext); 8 | 9 | const mouseDownHandler = () => { 10 | uiDispatch({ type: AppContextActions.MOUSE_DOWN }); 11 | } 12 | 13 | const mouseUpHandler = () => { 14 | uiDispatch({ type: AppContextActions.MOUSE_UP }); 15 | } 16 | 17 | return ( 18 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default withMouseEvents 29 | -------------------------------------------------------------------------------- /server/webroot/img/menu-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /server/webroot-react/src/components/LoginForm/LoginForm.module.css: -------------------------------------------------------------------------------- 1 | .LoginForm { 2 | padding: 20px; 3 | max-width: 800px; 4 | margin: auto; 5 | text-align: center; 6 | } 7 | 8 | .LoginForm input { 9 | font-weight: bold; 10 | font-size: 1em; 11 | height: 40px; 12 | width: 100%; 13 | } 14 | 15 | .LoginForm input[type=password] { 16 | border: 3px solid #bbb; 17 | text-align: center; 18 | } 19 | 20 | .LoginForm input[type=checkbox] { 21 | width: 20px; 22 | display: initial; 23 | vertical-align: inherit; 24 | } 25 | 26 | .LoginForm input[type=submit] { 27 | margin: auto; 28 | width: 80%; 29 | margin-top: 20px; 30 | color: #333; 31 | background: #fdfdfd; 32 | background: linear-gradient(to bottom, #fdfdfd 0%, #bebebe 100%); 33 | border: 3px solid #bbb; 34 | border-radius: 10px; 35 | } 36 | 37 | .Label { 38 | display: block; 39 | vertical-align: middle; 40 | font-family: sans-serif; 41 | color: #fff; 42 | } 43 | -------------------------------------------------------------------------------- /server/webroot-react/src/components/UI/Button/img/menu-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /server/webroot/img/video-play-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /server/webroot-react/src/components/UI/Button/img/video-play-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /server/webroot-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "baby-monitor-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.4.0", 8 | "@testing-library/user-event": "^7.2.1", 9 | "jquery": "^3.4.1", 10 | "prop-types": "^15.7.2", 11 | "react": "^16.12.0", 12 | "react-dom": "^16.12.0", 13 | "react-scripts": "3.4.0", 14 | "react-transition-group": "^4.3.0", 15 | "webrtc-adapter": "^7.5.0" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "eslintConfig": { 24 | "extends": "react-app" 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /server/conf/nginx/nginx.conf.template: -------------------------------------------------------------------------------- 1 | 2 | server { 3 | server_name $domain; 4 | 5 | # Music player websocket proxy 6 | location /music { 7 | proxy_pass http://localhost:8100; 8 | proxy_http_version 1.1; 9 | proxy_set_header Upgrade websocket; 10 | proxy_set_header Connection upgrade; 11 | proxy_set_header X-Real-IP $remote_addr; 12 | # timeout default=30 sec, increase to one day 13 | proxy_read_timeout 86400s; 14 | } 15 | 16 | # Mic to speaker websocket proxy 17 | location /speaker { 18 | proxy_pass http://localhost:8200; 19 | proxy_http_version 1.1; 20 | proxy_set_header Upgrade websocket; 21 | proxy_set_header Connection upgrade; 22 | proxy_set_header X-Real-IP $remote_addr; 23 | # timeout default=30 sec, increase to one day 24 | proxy_read_timeout 86400s; 25 | } 26 | 27 | # Send everything else to a local webroot. 28 | root $webroot; 29 | index index.html index.htm; 30 | 31 | location / { 32 | try_files $uri $uri/ index.html; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/readme.MD: -------------------------------------------------------------------------------- 1 | Installation on server 2 | === 3 | 4 | I made a quick installation script to simplify the installation process of the server software on my VPS. 5 | 6 | # Prerequisites 7 | - A Debian based Distro 8 | - NOTE: Tested only on Ubuntu 19.10 9 | - git configured to clone this repository from github 10 | 11 | # Installation 12 | ```console 13 | git clone git@github.com:leerikss/baby-monitor.git 14 | sudo mv baby-monitor /opt 15 | cd /opt/baby-monitor/server 16 | ./install.sh 17 | ``` 18 | - Type the host name of your server 19 | - Change if repository is to be somwhere else than in /opt/baby-monitor 20 | - Type a (hard enough) password 21 | - Will be the access token for viewing the WebRTC Video/Audo feed, as well as obtaining the WebSocket connections 22 | - The script will: 23 | - Install nodejs, npm 24 | - Install the websocket-proxy app and adds a websocket-proxy systemd service 25 | - Installs and configures letsencrypt and nginx 26 | - Installs and configures the Janus Server 27 | - Installs a TURN server (coturn) 28 | - Installs and builds the React JS Web UI 29 | - 30 | -------------------------------------------------------------------------------- /server/webroot-react/src/components/LoginForm/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react' 2 | 3 | import classes from './LoginForm.module.css'; 4 | 5 | export const LoginForm = (props) => { 6 | 7 | const passwordInput = useRef(null); 8 | const turnCheckbox = useRef(false); 9 | 10 | const submitHandler = event => { 11 | event.preventDefault(); 12 | props.submit(passwordInput.current.value, 13 | turnCheckbox.current.checked); 14 | }; 15 | 16 | return ( 17 |
18 | 24 | 29 | 30 |
31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /server/websocket-proxy/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "websocket-proxy", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "async-limiter": { 8 | "version": "1.0.1", 9 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", 10 | "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" 11 | }, 12 | "dotenv": { 13 | "version": "8.2.0", 14 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", 15 | "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" 16 | }, 17 | "ws": { 18 | "version": "7.2.0", 19 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.0.tgz", 20 | "integrity": "sha512-+SqNqFbwTm/0DC18KYzIsMTnEWpLwJsiasW/O17la4iDRRIO9uaHbvKiAS3AHgTiuuWerK/brj4O6MYZkei9xg==", 21 | "requires": { 22 | "async-limiter": "^1.0.0" 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /rpi/music-client/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "music-client", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "async-limiter": { 8 | "version": "1.0.1", 9 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", 10 | "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" 11 | }, 12 | "node-omxplayer": { 13 | "version": "0.6.1", 14 | "resolved": "https://registry.npmjs.org/node-omxplayer/-/node-omxplayer-0.6.1.tgz", 15 | "integrity": "sha512-yRhzO8iDwN6fzMs862xrCDu4vnjpRmus8yzu2S9WP14OqgeB0epdKipJro+C9iqIQ6OD5Ao25ZuftZ1Lw/kisA==" 16 | }, 17 | "ws": { 18 | "version": "7.2.0", 19 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.0.tgz", 20 | "integrity": "sha512-+SqNqFbwTm/0DC18KYzIsMTnEWpLwJsiasW/O17la4iDRRIO9uaHbvKiAS3AHgTiuuWerK/brj4O6MYZkei9xg==", 21 | "requires": { 22 | "async-limiter": "^1.0.0" 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/webroot-react/src/context/JanusContext.js: -------------------------------------------------------------------------------- 1 | import React, { useReducer, createContext } from "react"; 2 | 3 | const JanusContext = createContext(); 4 | 5 | const JanusContextActions = { 6 | SET_STREAMS: 'SET_STREAMS', 7 | SET_CURRENT_STREAM_ID: 'SET_CURRENT_STREAM_ID' 8 | } 9 | 10 | const initialState = { 11 | streams: [], 12 | currentStreamId: null 13 | }; 14 | 15 | const reducer = (state, action) => { 16 | switch (action.type) { 17 | case JanusContextActions.SET_STREAMS: 18 | return { 19 | ...state, 20 | streams: [...action.streams] 21 | }; 22 | case JanusContextActions.SET_CURRENT_STREAM_ID: 23 | return { ...state, currentStreamId: action.streamId } 24 | 25 | default: 26 | return state; 27 | } 28 | } 29 | 30 | const JanusContextProvider = (props) => { 31 | 32 | const [state, dispatch] = useReducer(reducer, initialState); 33 | const value = [ state, dispatch ] 34 | 35 | return ( 36 | 37 | {props.children} 38 | 39 | ); 40 | } 41 | 42 | export { JanusContext, JanusContextProvider, JanusContextActions } -------------------------------------------------------------------------------- /server/webroot-react/src/components/Menu/JanusStreamSelector/JanusStreamSelector.module.css: -------------------------------------------------------------------------------- 1 | .JanusStreamSelector { 2 | display: block; 3 | 4 | font-size: 16px; 5 | font-family: sans-serif; 6 | font-weight: 700; 7 | 8 | color: #fff; 9 | 10 | padding-left: 90px; 11 | line-height: 3em; 12 | 13 | width: 100%; 14 | 15 | border: 1px solid #fff; 16 | box-shadow: 3px 3px 10px rgba(0, 0, 0, .5); 17 | border-radius: .5em; 18 | 19 | appearance: none; 20 | 21 | background-color: rgba(0, 0, 0, 0.7); 22 | background-image: url('./img/cctv-icon.png'), url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23FFFFFF%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'); 23 | background-repeat: no-repeat, no-repeat; 24 | background-position: left 1em top 50%, right 1em top 50%; 25 | background-size: 3em auto, 2em auto; 26 | } 27 | 28 | .Hide { 29 | display: none; 30 | } -------------------------------------------------------------------------------- /rpi/speaker-client/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -a 4 | 5 | read -p "Install nodejs and npm (y/N)?" node 6 | read -p "Server domain? " domain 7 | read -e -p "WebSocket Proxy URL? " -i "wss://$domain/speaker" ws_url 8 | read -p "Authentication token? " token 9 | 10 | # Install requirements 11 | function install_node() { 12 | curl -sL https://deb.nodesource.com/setup_10.x | sudo bash - 13 | sudo apt-get install -y nodejs 14 | sudo npm install npm --global 15 | } 16 | 17 | # Copy scripts 18 | function create_bin() { 19 | sudo apt-get install libasound2-dev 20 | sudo rm -Rf /opt/speaker-client 21 | sudo mkdir /opt/speaker-client 22 | sudo cp *.js* /opt/speaker-client 23 | sudo npm install --unsafe-perm --prefix /opt/speaker-client ws speaker 24 | } 25 | 26 | function create_config() { 27 | sudo -E sh -c 'envsubst < config.json.template > /opt/speaker-client/config.json' 28 | } 29 | 30 | # Init systemd 31 | function init_systemd() { 32 | sudo cp speaker-client.service /etc/systemd/system 33 | sudo systemctl enable speaker-client 34 | sudo systemctl start speaker-client 35 | sudo systemctl reenable speaker-client 36 | } 37 | 38 | case $node in 39 | [Yy]*) install_node ;; 40 | esac 41 | create_bin 42 | create_config 43 | init_systemd 44 | 45 | set +a 46 | -------------------------------------------------------------------------------- /server/webroot-react/src/components/Menu/JanusStreamSelector/JanusStreamSelector.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useContext } from 'react' 2 | import classes from './JanusStreamSelector.module.css' 3 | import { JanusContext, JanusContextActions } from '../../../context/JanusContext'; 4 | 5 | const JanusStreamSelector = () => { 6 | 7 | const [state, dispatch] = useContext(JanusContext); 8 | const [options, setOptions] = useState(null); 9 | 10 | useEffect(() => { 11 | 12 | if (state.streams.length === 0) 13 | return; 14 | 15 | setOptions(state.streams.map((stream) => { 16 | return (); 19 | })); 20 | 21 | }, [state.streams]) 22 | 23 | const selectChangeHandler = (event) => { 24 | 25 | dispatch({ 26 | type: JanusContextActions.SET_CURRENT_STREAM_ID, 27 | streamId: event.target.value 28 | }) 29 | } 30 | 31 | const content = options && options.length > 0 && ( 32 | 37 | ); 38 | 39 | return content; 40 | } 41 | 42 | export default JanusStreamSelector 43 | -------------------------------------------------------------------------------- /rpi/music-client/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -a 4 | 5 | read -p "Install nodejs and npm (y/N)?" node 6 | read -p "Server domain? " domain 7 | read -e -p "WebSocket Proxy URL? " -i "wss://$domain/music" ws_url 8 | read -e -p "Song files path? " -i "/opt/music-client/songs" songs_path 9 | read -p "Authentication token? " token 10 | 11 | function install_node() { 12 | curl -sL https://deb.nodesource.com/setup_10.x | sudo bash - 13 | sudo apt-get install -y nodejs 14 | sudo npm install npm --global 15 | } 16 | 17 | # Copy scripts 18 | function create_app() { 19 | sudo apt-get install omxplayer 20 | sudo rm -Rf /opt/music-client 21 | sudo mkdir -p /opt/music-client 22 | sudo cp *.js* /opt/music-client 23 | sudo npm install --prefix /opt/music-client ws node-omxplayer 24 | } 25 | 26 | function create_config() { 27 | sudo -E sh -c 'envsubst < config.json.template > /opt/music-client/config.json' 28 | } 29 | 30 | function copy_songs() { 31 | sudo mkdir $songs_path 32 | sudo cp -rf songs/* $songs_path/ 33 | } 34 | 35 | # Init systemd 36 | function init_systemd() { 37 | sudo cp music-client.service /etc/systemd/system 38 | sudo systemctl enable music-client 39 | sudo systemctl start music-client 40 | sudo systemctl reenable music-client 41 | } 42 | 43 | case $node in 44 | [Yy]*) install_node ;; 45 | esac 46 | create_app 47 | create_config 48 | copy_songs 49 | init_systemd 50 | 51 | set +a 52 | -------------------------------------------------------------------------------- /server/webroot-react/src/components/Menu/MusicSelector/MusicSelector.module.css: -------------------------------------------------------------------------------- 1 | .MusicSelector { 2 | /* display: none; */ 3 | position: relative; 4 | margin-top: 20px; 5 | } 6 | 7 | .MusicSelector select { 8 | display: block; 9 | 10 | font-size: 16px; 11 | font-family: sans-serif; 12 | font-weight: 700; 13 | 14 | color: #fff; 15 | 16 | padding-left: 90px; 17 | line-height: 3em; 18 | width: 100%; 19 | 20 | border: 1px solid #fff; 21 | box-shadow: 3px 3px 10px rgba(0, 0, 0, .5); 22 | border-radius: .5em; 23 | 24 | appearance: none; 25 | 26 | background-color: rgba(0, 0, 0, 0.7); 27 | background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23FFFFFF%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'); 28 | background-repeat: no-repeat; 29 | background-position: right 1em top 50%; 30 | background-size: 2em auto; 31 | } 32 | 33 | .MusicSelector img { 34 | cursor: pointer; 35 | 36 | position: absolute; 37 | 38 | width: 5em; 39 | 40 | top: -0.9em; 41 | height: auto; 42 | } 43 | 44 | -------------------------------------------------------------------------------- /server/webroot/img/zoom-out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /server/webroot-react/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from 'react'; 2 | import './App.css'; 3 | import { LoginForm } from './components/LoginForm/LoginForm'; 4 | import VideoStream from './components/VideoStream/VideoStream'; 5 | import { AppContext } from './context/AppContext'; 6 | import Animator from './components/UI/Animator/Animator'; 7 | import Menu from './components/Menu/Menu'; 8 | 9 | function App() { 10 | 11 | const [password, setPassword] = useState(null); 12 | const [useTurn, setUseTurn] = useState(null); 13 | const [state] = useContext(AppContext); 14 | 15 | const submitHandler = (pass, useTurn) => { 16 | setPassword(pass); 17 | setUseTurn(useTurn); 18 | } 19 | 20 | const showLoginForm = (password === null); 21 | 22 | return ( 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | ); 43 | } 44 | 45 | export default App; 46 | -------------------------------------------------------------------------------- /server/webroot-react/src/components/UI/Button/img/zoom-out-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /server/webroot/img/zoom-in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 11 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /server/webroot/img/video-pause-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /server/webroot-react/src/components/UI/Button/img/zoom-in-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 11 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /server/webroot-react/src/components/UI/Button/img/video-pause-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /server/webroot-react/src/components/Menu/Microphone/Microphone.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react' 2 | import classes from './Microphone.module.css'; 3 | 4 | import Record from './img/record-icon.png'; 5 | import Stop from './img/record-stop-icon.png'; 6 | import useMicrophone, { MicrophoneStatus } from '../../../hooks/microphone/useMicrophone'; 7 | 8 | const Microphone = (props) => { 9 | 10 | const { init, cleanUp, record, stop, status } = useMicrophone(props.serverUrl, props.password); 11 | const btnRef = useRef(null); 12 | 13 | // Component rendered 14 | useEffect(() => { 15 | init(); 16 | return () => { 17 | cleanUp(); 18 | } 19 | }, [init, cleanUp]); 20 | 21 | // React on status changes 22 | useEffect(() => { 23 | switch (status) { 24 | case MicrophoneStatus.UNAVAILABLE: 25 | break; 26 | case MicrophoneStatus.RECORDING: 27 | btnRef.current.src = Stop; 28 | break; 29 | default: 30 | btnRef.current.src = Record; 31 | break; 32 | } 33 | 34 | }, [status]); 35 | 36 | // Handlers 37 | const toggleRecordHandler = () => { 38 | if (status === MicrophoneStatus.RECORDING) 39 | stop(); 40 | else 41 | record(); 42 | } 43 | 44 | const content = status && status !== MicrophoneStatus.UNAVAILABLE && ( 45 | Record/Stop 51 | ); 52 | 53 | return content; 54 | } 55 | 56 | export default Microphone; 57 | -------------------------------------------------------------------------------- /rpi/speaker-client/readme.MD: -------------------------------------------------------------------------------- 1 | Installation on RPI 2 | === 3 | 4 | The speaker-client will install a nodejs app, that streams incoming WebSocket audio data to the speaker. 5 | In addition, a systemd service will be created for starting/stopping the speaker-client nodejs app. 6 | 7 | # Prerequisites 8 | - A RPI running Raspbian 9 | - Tested on RPI3 running Raspbian Buster Lite 10 | - RPI configured with a working Internet connection 11 | - A speaker connected to the RPI headphone jack 12 | - RPI configured to output the audio to the headphone jack (usually works by default) 13 | - Refer to: https://www.raspberrypi.org/documentation/configuration/audio-config.md 14 | - git configured to clone this repository from github 15 | 16 | ## Increase speaker volume 17 | ```console 18 | alsamixer 19 | ``` 20 | - Refer to: https://wiki.ubuntu.com/Audio/Alsamixer 21 | 22 | # Installation 23 | ```console 24 | git clone git@github.com:leerikss/baby-monitor.git 25 | cd baby-monitor/rpi/speaker-client 26 | ./install.sh 27 | ``` 28 | - First time you run the script, type y to install required nodejs/npm dependencies 29 | - Type the domain name of the server where the websocket-proxy is installed 30 | - The WebSocket Proxy URL is ok by default, if your server NGINX is configured to proxy pass the wss://[domain]/speaker location to the corresponding server nodejs websocket proxy app 31 | - Authentication token needs to match your server configured websocket proxy token 32 | 33 | # Starting/stopping 34 | ```console 35 | sudo systemctl start speaker-client 36 | sudo systemctl stop speaker-client 37 | ``` 38 | - The installation will create a nodejs app into: 39 | /opt/speaker-client 40 | - A systemd service is created to automatically start the speaker-client upon RPI startup 41 | - Upon script failures, the speaker-client will automatically restart 42 | -------------------------------------------------------------------------------- /server/webroot-react/src/components/VideoStream/VideoStream.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext, useRef } from 'react' 2 | 3 | import classes from './VideoStream.module.css'; 4 | import VideoControls from './VideoControls/VideoControls'; 5 | 6 | import JanusVideo from './JanusVideo/JanusVideo'; 7 | import Animator from '../UI/Animator/Animator'; 8 | import { AppContext, AppContextActions } from '../../context/AppContext'; 9 | 10 | const HIDE_CONTROLS_MS = 3000; 11 | 12 | const VideoStream = (props) => { 13 | 14 | const [showControls, setShowControls] = useState(false); 15 | const [state, dispatch] = useContext(AppContext); 16 | 17 | let timeout = useRef(null); 18 | 19 | // Handle showing & hiding the Video Controls 20 | useEffect(() => { 21 | if (state.controlsOpen) { 22 | 23 | if (timeout.current) 24 | clearTimeout(timeout.current); 25 | 26 | timeout.current = setTimeout(() => { 27 | timeout.current = null; 28 | dispatch({ type: AppContextActions.MOUSE_DOWN }); 29 | }, HIDE_CONTROLS_MS); 30 | 31 | setShowControls(true); 32 | } 33 | else { 34 | if (!timeout.current && !state.menuOpen) 35 | setShowControls(false); 36 | } 37 | }, [state.controlsOpen, state.menuOpen, dispatch]); 38 | 39 | return ( 40 |
41 | 46 | 47 | 48 | 49 |
50 | ) 51 | 52 | } 53 | 54 | export default VideoStream; -------------------------------------------------------------------------------- /server/webroot-react/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /rpi/music-client/readme.MD: -------------------------------------------------------------------------------- 1 | Installation on RPI 2 | === 3 | 4 | The music-client will install a nodejs app, that listens for WebSocket commands to play mp3 files to the speaker. In addition, a systemd service will be created for starting/stopping the music-client nodejs app. 5 | NOTE: You will have to add your own mp3 files locally into folder /opt/music-client/songs. 6 | 7 | # Prerequisites 8 | - A RPI running Raspbian 9 | - Tested on RPI3 running Raspbian Buster Lite 10 | - RPI configured with a working Internet connection 11 | - A speaker connected to the RPI headphone jack 12 | - RPI configured to output the audio to the headphone jack (usually works by default) 13 | - Refer to: https://www.raspberrypi.org/documentation/configuration/audio-config.md 14 | - git configured to clone this repository from github 15 | 16 | ## Increase speaker volume 17 | ```console 18 | alsamixer 19 | ``` 20 | - Refer to: https://wiki.ubuntu.com/Audio/Alsamixer 21 | 22 | # Installation 23 | ```console 24 | git clone git@github.com:leerikss/baby-monitor.git 25 | cd baby-monitor/rpi/music-client 26 | ./install.sh 27 | ``` 28 | - First time you run the script, type y to install required nodejs/npm dependencies 29 | - Type the domain name of the server where the websocket-proxy is installed 30 | - The WebSocket Proxy URL is ok by default, if your server NGINX is configured to proxy pass the wss://[domain]/music location to the corresponding server nodejs websocket proxy app 31 | - Authentication token needs to match your server configured websocket proxy token 32 | 33 | # Starting/stopping 34 | ```console 35 | sudo systemctl start music-client 36 | sudo systemctl stop music-client 37 | ``` 38 | - The installation will create a nodejs app into: 39 | /opt/music-client 40 | - A systemd service is created to automatically start the music-client upon RPI startup 41 | - Upon script failures, the music-client will automatically restart 42 | - Add mp3 files of your liking into /opt/music-client/songs 43 | -------------------------------------------------------------------------------- /rpi/speaker-client/speaker-client.js: -------------------------------------------------------------------------------- 1 | /* 2 | Requirements: 3 | 4 | npm install ws 5 | 6 | sudo apt-get install libasound2-dev 7 | npm install speaker 8 | 9 | */ 10 | 11 | const SpeakerClient = function() { 12 | 13 | // Imported modules 14 | const WebSocket = require('ws') 15 | const fs = require('fs') 16 | const Speaker = require('speaker') 17 | 18 | // Private variables 19 | let pingTimeout = null; 20 | let config = null; 21 | 22 | function run() { 23 | 24 | // Read config 25 | const rawdata = fs.readFileSync('config.json'); 26 | config = JSON.parse(rawdata); 27 | 28 | // Construct WebSocket 29 | configureWebSocket( 30 | new WebSocket(config.ws_audio_url + "?role=receiver", config.token)); 31 | } 32 | 33 | function configureWebSocket(client) { 34 | 35 | client.onopen = event => onClientOpen(client, event); 36 | 37 | client.onerror = event => console.log("Audio WS Client error: " + event.message); 38 | 39 | client.onclose = event => console.log("Audio WS Client connection was closed"); 40 | 41 | // Heartbeat 42 | client.on("open", heartbeat); 43 | client.on("ping", heartbeat); 44 | client.on("close", () => clearTimeout(pingTimeout)); 45 | } 46 | 47 | function onClientOpen(client, event) { 48 | // Pipe binary data to speaker 49 | const speaker = new Speaker({ 50 | channels: 1, // 1 channel 51 | bitDepth: 16, // 16-bit samples 52 | sampleRate: 48000 // 44,800 Hz sample rate 53 | }); 54 | const duplex = WebSocket.createWebSocketStream(client); 55 | duplex.pipe(speaker); 56 | } 57 | 58 | function heartbeat() { 59 | console.log("Audio WS Client heartbeat..."); 60 | clearTimeout(pingTimeout); 61 | pingTimeout = setTimeout(() => this.terminate(), 3000 * 2); 62 | } 63 | 64 | return { 65 | run: run 66 | } 67 | }(); 68 | 69 | // Start 70 | SpeakerClient.run(); -------------------------------------------------------------------------------- /server/webroot-react/src/components/UI/Button/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import classes from './Button.module.css'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import Menu from './img/menu-icon.svg'; 7 | import Mute from './img/mute-on-icon.svg'; 8 | import Unmute from './img/mute-off-icon.svg'; 9 | import ZoomIn from './img/zoom-in-icon.svg'; 10 | import ZoomOut from './img/zoom-out-icon.svg'; 11 | import Play from './img/video-play-icon.svg'; 12 | import Pause from './img/video-pause-icon.svg'; 13 | import withMouseEvents from '../../../hoc/withMouseEvents'; 14 | 15 | export const ButtonType = { 16 | MUTE: 'mute', 17 | UNMUTE: 'unmute', 18 | ZOOM_IN: 'zoomIn', 19 | ZOOM_OUT: 'zoomOut', 20 | PLAY: 'play', 21 | PAUSE: 'pause', 22 | MENU: 'menu' 23 | } 24 | 25 | const Button = (props) => { 26 | 27 | let src = null; 28 | switch (props.type) { 29 | case ButtonType.MENU: { 30 | src = Menu; 31 | break; 32 | } 33 | case ButtonType.MUTE: { 34 | src = Mute; 35 | break; 36 | } 37 | case ButtonType.UNMUTE: { 38 | src = Unmute; 39 | break; 40 | } 41 | case ButtonType.ZOOM_IN: { 42 | src = ZoomIn; 43 | break; 44 | } 45 | case ButtonType.ZOOM_OUT: { 46 | src = ZoomOut; 47 | break; 48 | } 49 | case ButtonType.PLAY: { 50 | src = Play; 51 | break; 52 | } 53 | case ButtonType.PAUSE: { 54 | src = Pause; 55 | break; 56 | } 57 | default: { 58 | src = Menu; 59 | } 60 | } 61 | 62 | return ( 63 | {props.type} 67 | ) 68 | } 69 | 70 | Button.propTypes = { 71 | type: PropTypes.oneOf(Object.values(ButtonType)) 72 | } 73 | 74 | export default withMouseEvents(Button); -------------------------------------------------------------------------------- /rpi/janus-stream/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -a 4 | 5 | read -p "Install gstreamer libraries (y/N)?" libs 6 | read -p "Install headless WiFi configuration using RaspiWiFi (y/N)?" rwifi 7 | read -p "Domain name where your Janus WebRTC is installed? " jhost 8 | read -e -p "Janus configured video port? " -i "5001" vport 9 | read -e -p "Janus configured audio port? " -i "5002" aport 10 | read -e -p "Flip video vertically (false)? " -i "false" vflip 11 | read -e -p "Flip video horizontally (false)? " -i "false" hflip 12 | read -e -p "Video Width in px (960)? " -i "960" width 13 | read -e -p "Video height in px (540)? " -i "540" height 14 | read -e -p "Video framerate (24)? " -i "24" framerate 15 | 16 | # Install gstreamer + rpicamsrc 17 | function install_libs() { 18 | sudo apt-get install -y gstreamer1.0-tools gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-plugins-bad libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-alsa 19 | git clone https://github.com/thaytan/gst-rpicamsrc.git /tmp/gst-rpicamsrc 20 | cd /tmp/gst-rpicamsrc 21 | ./autogen.sh --prefix=/usr --libdir=/usr/lib/arm-linux-gnueabihf/ 22 | make && sudo make install 23 | rm -Rf /tmp/gst-rpicamsrc 24 | } 25 | 26 | # Copy scripts 27 | function create_script() { 28 | sudo mkdir -p /opt/janus-stream 29 | sudo -E sh -c 'envsubst < janus-stream.sh.template > /opt/janus-stream/janus-stream.sh' 30 | sudo chmod +x /opt/janus-stream/janus-stream.sh 31 | } 32 | 33 | # Init systemd 34 | function init_systemd() { 35 | sudo cp janus-stream.service /etc/systemd/system 36 | sudo systemctl enable janus-stream 37 | sudo systemctl start janus-stream 38 | sudo systemctl reenable janus-stream 39 | } 40 | 41 | # Install RaspiWiFi 42 | function install_RaspiWiFi() { 43 | sudo systemctl stop janus-stream 44 | git clone https://github.com/jasbur/RaspiWiFi.git /tmp/RaspiWiFi 45 | cd /tmp/RaspiWiFi 46 | sudo python3 initial_setup.py 47 | sudo rm -Rf /tmp/RaspiWiFi 48 | } 49 | 50 | case $libs in 51 | [Yy]*) install_libs ;; 52 | esac 53 | create_script 54 | init_systemd 55 | case $rwifi in 56 | [Yy]*) install_RaspiWiFi ;; 57 | esac 58 | 59 | set +a 60 | -------------------------------------------------------------------------------- /server/webroot-react/src/components/Menu/MusicSelector/MusicSelector.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useRef } from 'react' 2 | import classes from './MusicSelector.module.css'; 3 | 4 | import PlaySong from './img/song-play-icon.png'; 5 | import StopSong from './img/song-stop-icon.png'; 6 | import useMusicPlayer, { MusicPlayerStatus } from '../../../hooks/musicPlayer/useMusicPlayer'; 7 | 8 | const MusicSelector = (props) => { 9 | 10 | const { init, cleanUp, play, stop, status, songs } = useMusicPlayer(props.serverUrl, props.password); 11 | const [options, setOptions] = useState(null); 12 | const [selectedSongId, setSelectedSongId] = useState(null); 13 | const btnRef = useRef(null); 14 | 15 | // Component rendered 16 | useEffect(() => { 17 | init(); 18 | return () => { 19 | cleanUp(); 20 | } 21 | }, [init, cleanUp]); 22 | 23 | // Build song options 24 | useEffect(() => { 25 | 26 | if (songs.length === 0) 27 | return; 28 | 29 | setSelectedSongId(songs[0].id); 30 | 31 | setOptions(songs.map((song) => { 32 | return (); 35 | })); 36 | 37 | }, [songs]) 38 | 39 | // React on status changes 40 | useEffect(() => { 41 | switch (status) { 42 | case MusicPlayerStatus.UNAVAILABLE: 43 | return; 44 | case MusicPlayerStatus.PLAYING: 45 | btnRef.current.src = StopSong; 46 | break; 47 | default: 48 | btnRef.current.src = PlaySong; 49 | break; 50 | } 51 | 52 | }, [status]); 53 | 54 | // Handlers 55 | const selectChangeHandler = (event) => { 56 | setSelectedSongId(event.target.value); 57 | } 58 | 59 | const togglePlayHandler = () => { 60 | if (status === MusicPlayerStatus.PLAYING) 61 | stop(); 62 | else 63 | play(selectedSongId); 64 | } 65 | 66 | const content = status && status !== MusicPlayerStatus.UNAVAILABLE && ( 67 |
68 | 72 | Play/Stop Song 77 |
78 | ); 79 | 80 | 81 | return content; 82 | } 83 | 84 | export default MusicSelector 85 | -------------------------------------------------------------------------------- /server/webroot-react/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /server/webroot-react/src/context/AppContext.js: -------------------------------------------------------------------------------- 1 | import React, { useReducer, createContext } from "react"; 2 | 3 | const AppContext = createContext(); 4 | 5 | const AppContextActions = { 6 | OPEN_MENU: 'OPEN_MENU', 7 | CLOSE_MENU: 'CLOSE_MENU', 8 | MOUSE_DOWN: 'MOUSE_DOWN', 9 | MOUSE_UP: 'MOUSE_UP', 10 | ZOOM_VIDEO: 'ZOOM_VIDEO', 11 | MUTE_VIDEO: 'MUTE_VIDEO', 12 | UNMUTE_VIDEO: 'UNMUTE_VIDEO', 13 | PAUSE_VIDEO: 'PAUSE_VIDEO', 14 | VIDEO_PAUSED: 'VIDEO_PAUSED', 15 | PLAY_VIDEO: 'PLAY_VIDEO', 16 | VIDEO_PLAYING: 'VIDEO_PLAYING', 17 | } 18 | 19 | const VideoStates = { 20 | PAUSE: 'PAUSE', 21 | PAUSED: 'PAUSED', 22 | PLAY: 'PLAY', 23 | PLAYING: 'PLAYING' 24 | } 25 | 26 | const initialState = { 27 | videoState: VideoStates.PAUSED, 28 | videoMuted: false, 29 | videoHeight: 100, 30 | menuOpen: false, 31 | controlsOpen: true 32 | }; 33 | 34 | const reducer = (state, action) => { 35 | switch (action.type) { 36 | 37 | case AppContextActions.OPEN_MENU: 38 | return { ...state, menuOpen: true } 39 | case AppContextActions.CLOSE_MENU: 40 | return { ...state, menuOpen: false } 41 | 42 | case AppContextActions.MOUSE_DOWN: 43 | return { ...state, controlsOpen: false } 44 | case AppContextActions.MOUSE_UP: 45 | return { ...state, controlsOpen: true } 46 | 47 | case AppContextActions.ZOOM_VIDEO: 48 | return { ...state, videoHeight: state.videoHeight + action.zoom } 49 | 50 | case AppContextActions.MUTE_VIDEO: 51 | return { ...state, videoMuted: true }; 52 | case AppContextActions.UNMUTE_VIDEO: 53 | return { ...state, videoMuted: false }; 54 | 55 | case AppContextActions.PAUSE_VIDEO: 56 | return { ...state, videoState: VideoStates.PAUSE }; 57 | case AppContextActions.VIDEO_PAUSED: 58 | return { ...state, videoState: VideoStates.PAUSED }; 59 | case AppContextActions.PLAY_VIDEO: 60 | return { ...state, videoState: VideoStates.PLAY }; 61 | case AppContextActions.VIDEO_PLAYING: 62 | return { ...state, videoState: VideoStates.PLAYING }; 63 | 64 | default: 65 | return state; 66 | } 67 | } 68 | 69 | const AppContextProvider = (props) => { 70 | 71 | const [state, dispatch] = useReducer(reducer, initialState); 72 | const value = [state, dispatch] 73 | 74 | return ( 75 | 76 | {props.children} 77 | 78 | ); 79 | } 80 | 81 | export { AppContext, AppContextProvider, AppContextActions, VideoStates } -------------------------------------------------------------------------------- /rpi/janus-stream/readme.MD: -------------------------------------------------------------------------------- 1 | Installation on RPI 2 | === 3 | 4 | The janus-stream will generate a shell script that will stream video/audio to your Janus WebRTC server using gstreamer. A janus-stream systemd service will also be created. 5 | 6 | # Prerequisites 7 | - A RPI running Raspbian 8 | - Tested on RPI3 and RPI Zero Wireless, running Raspbian Buster Lite 9 | - RPI configured with a working Internet connection 10 | - A Raspicam and a microphone attached to your RPI 11 | - Tested with an infrared raspicam and a USB mini microphone 12 | - git configured to clone this repository from github 13 | 14 | ## raspi-config 15 | sudo raspi-config 16 | 1) Interfacing options - P1 Camera - Enable 17 | 2) Advanced options > Memory Split > 18 | - RPI3: GPU mem set to 256 Gb 19 | - RPI Zero W: GPU mem set to 128 Gb 20 | 21 | ## Update RPI firmware 22 | ```console 23 | sudo apt-get install rpi-update 24 | sudo rpi-update 25 | sudo reboot 26 | ``` 27 | 28 | ## Turn off wlan power save 29 | ```console 30 | sudo iw dev wlan0 set power_save off 31 | sudo pico /etc/network/interfaces 32 | # Add following line: 33 | wireless-power off 34 | ``` 35 | 36 | ## Increase microphone sensitivity 37 | ```console 38 | alsamixer 39 | ``` 40 | - F6 - choose USB 41 | - F4 - set to max 42 | 43 | # Installation 44 | ```console 45 | git clone git@github.com:leerikss/baby-monitor.git 46 | cd baby-monitor/rpi/janus-stream 47 | ./install.sh 48 | ``` 49 | - The installaction script will prompt for various parameters you need to know 50 | - First time you run this script, make sure you will install *gstreamer* and *RaspiWiFi* 51 | - Type the domain name of the server where you have installed your Janus WebRTC server 52 | - Type the video and audio port numbers that your Janus server is configured to listen to 53 | - These ports must match the corresponding values found on your server in: 54 | /opt/janus/etc/janus/janus.plugin.streaming.jcfg 55 | - Each RPI device should have their own configuration block in this file with their own video/audio ports 56 | - The rest of the installation prompts are dealing with the video. Type enter for default values 57 | - Installation of RaspiWiFi starts another installation script 58 | - Check for further information here: https://github.com/jasbur/RaspiWiFi 59 | 60 | # Running/stopping 61 | ```console 62 | sudo systemctl start janus-stream 63 | sudo systemctl stop janus-stream 64 | ``` 65 | - The installation will create a shell script 66 | /opt/janus-stream/janus-stream.sh 67 | - A systemd service is created to automatically start the janus-stream.sh upon RPI startup 68 | - Upon script failures, the janus-stream.sh will automatically restart 69 | - If the RPI cannot access the internet at all, it will restart in Host mode, and you can configure the WiFi network/password headlessly. Refer to https://github.com/jasbur/RaspiWiFi 70 | -------------------------------------------------------------------------------- /server/webroot-react/src/components/VideoStream/VideoControls/VideoControls.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext } from 'react' 2 | import classes from './VideoControls.module.css'; 3 | import Button, { ButtonType } from '../../UI/Button/Button'; 4 | import { AppContext, AppContextActions, VideoStates } from '../../../context/AppContext'; 5 | 6 | // Constants 7 | export const ZOOM = 20; 8 | 9 | const VideoControls = (props) => { 10 | 11 | const [togglePlay, setTogglePlay] = useState(ButtonType.PLAY); 12 | const [toggleMute, setToggleMute] = useState(ButtonType.MUTE); 13 | const [state, dispatch] = useContext(AppContext); 14 | 15 | // Manage play/pause button toggle via context changes 16 | useEffect(() => { 17 | if (state.videoState === VideoStates.PLAYING) 18 | setTogglePlay(ButtonType.PAUSE); 19 | else if(state.videoState === VideoStates.PAUSED) 20 | setTogglePlay(ButtonType.PLAY); 21 | }, [state.videoState]); 22 | 23 | // Manage mute/unmute button toggle via context changes 24 | useEffect(() => { 25 | if (state.videoMuted) 26 | setToggleMute(ButtonType.UNMUTE); 27 | else 28 | setToggleMute(ButtonType.MUTE); 29 | }, [state.videoMuted]); 30 | 31 | // Button click handlers 32 | const playButtonHandler = () => { 33 | if (state.videoState === VideoStates.PAUSED) { 34 | dispatch({ type: AppContextActions.PLAY_VIDEO }) 35 | } else if (state.videoState === VideoStates.PLAYING) { 36 | dispatch({ type: AppContextActions.PAUSE_VIDEO }) 37 | } 38 | } 39 | 40 | const muteButtonHandler = () => { 41 | if (state.videoMuted) { 42 | dispatch({ type: AppContextActions.UNMUTE_VIDEO }) 43 | } else { 44 | dispatch({ type: AppContextActions.MUTE_VIDEO }) 45 | } 46 | } 47 | 48 | const zoomInHandler = () => { 49 | dispatch({ type: AppContextActions.ZOOM_VIDEO, zoom: ZOOM }); 50 | } 51 | 52 | const zoomOutHandler = () => { 53 | dispatch({ type: AppContextActions.ZOOM_VIDEO, zoom: -ZOOM }); 54 | } 55 | 56 | const menuHandler = () => { 57 | const type = (state.menuOpen) ? AppContextActions.CLOSE_MENU : AppContextActions.OPEN_MENU; 58 | dispatch({ type: type }); 59 | } 60 | 61 | return ( 62 |
63 |
69 | ) 70 | } 71 | 72 | export default VideoControls; -------------------------------------------------------------------------------- /server/HOWTO_add_git_hooks.MD: -------------------------------------------------------------------------------- 1 | git hooks 2 | ========= 3 | Some notes on howto add git hooks in order to 4 | a) push code to your personal production server whenever you do a commit 5 | b) deploy the baby-monitor server app on the production server each time the server receives a push 6 | 7 | Server 8 | ====== 9 | 10 | Add git user + make passwordless sudoer 11 | --------------------------------------- 12 | ``` 13 | sudo adduser git 14 | sudo usermod -aG sudo git 15 | su git 16 | cd ~/ 17 | mkdir .ssh && chmod 700 .ssh 18 | touch .ssh/authorized_keys && chmod 600 .ssh/authorized_keys 19 | ``` 20 | - **Add your loca id_rsa.pub to authorized_keys for passwordless ssh!** 21 | - Make git sudo passwordless 22 | ``` 23 | sudo visudo 24 | git ALL=(ALL) NOPASSWD:ALL 25 | ``` 26 | Make the target working dir 27 | --------------------------- 28 | ``` 29 | sudo mkdir -p /opt/baby-monitor 30 | sudo chown -R git.git /opt/baby-monitor 31 | ``` 32 | 33 | Create the git server repo 34 | -------------------------- 35 | ``` 36 | su git 37 | mkdir /home/git/baby-monitor.git 38 | cd /home/git/baby-monitor.git 39 | git init --bare . 40 | touch hooks/post-receive 41 | chmod +x hooks/post-receive 42 | ``` 43 | 44 | Create the git hook for deploying upon push 45 | ------------------------------------------- 46 | - /home/git/baby-monitor.git/hooks/post-receive: 47 | ``` 48 | #!/bin/bash 49 | TARGET="/opt/baby-monitor" 50 | GIT_DIR="/home/git/baby-monitor.git" 51 | BRANCH="master" 52 | 53 | while read oldrev newrev ref 54 | do 55 | # only checking out the master (or whatever branch you would like to deploy) 56 | if [ "$ref" = "refs/heads/$BRANCH" ]; 57 | 58 | then 59 | echo "Ref $ref received. Deploying ${BRANCH} branch to production..." 60 | git --work-tree=$TARGET --git-dir=$GIT_DIR checkout -f $BRANCH 61 | 62 | # Building the website 63 | echo "Building the website" 64 | cd $TARGET/server/webroot-react 65 | npm i 66 | npm run build 67 | 68 | echo "Restarting websocket-proxy" 69 | sudo systemctl restart websocket-proxy 70 | else 71 | echo "Ref $ref received. Doing nothing: only the ${BRANCH} branch may be deployed on this server." 72 | fi 73 | done 74 | ``` 75 | 76 | Local Linux dev env 77 | =================== 78 | 79 | - Add the new remote production repo 80 | ``` 81 | git remote add production ssh://git@yourserver.com:port/home/git/baby-monitor.git 82 | ``` 83 | 84 | - Create post commit hook 85 | ``` 86 | touch .git/hooks/post-commit 87 | chmod +x .git/hooks/post-commit 88 | ``` 89 | - .git/hooks/post-commit: 90 | ``` 91 | #!/bin/sh 92 | git push production master 93 | git push origin master 94 | ``` 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # baby-monitor 2 | 3 | A WebRTC audio/video streaming system featuring bidirectional streams between Raspberry PI devices and a server instance, accessed via a Web UI. 4 | 5 | [![Demo video](youtube.png?raw=true)](https://www.youtube.com/watch?v=qYF7NukS90A) 6 | 7 | ![flowchart](https://raw.githubusercontent.com/leerikss/baby-monitor/master/flowchart.jpg) 8 | 9 | ## My goal 10 | This is a little audio/video monitoring system I created for my own needs whilst on my paternity leave. 11 | I had a few Raspberrys lying around, and thought I'd do something with them. 12 | 13 | My requirements: 14 | - Stream IR video and Audio from multiple Raspberry devices 15 | - Play a lullaby song through a speaker as well as stream audio back from a phone microphone to the Raspberry 16 | - Access the system with a Web UI (iow not write separate mobile clients) 17 | - Not restricted to WLAN (therefore needed a public Server) 18 | - Multiple clients must be able to view the streams simultaneously (=> Janus) 19 | - Should work well on phones (iPhone + Android), as well as on a desktop browser 20 | 21 | I achieved my goals, and it's good enough for my personal needs, but there's certainly plenty of room for improvement. 22 | 23 | ## Installation 24 | To simplify installation I created some shell scripts. 25 | 26 | ### Server 27 | Refer to [server](https://github.com/leerikss/baby-monitor/tree/master/server) 28 | 29 | ### Raspberry 30 | 31 | Includes three different services: 32 | 33 | 1) Stream WebRTC Audio and Video (gstreamer RTP/UDP => Janus) 34 | - Refer to [janus-stream](https://github.com/leerikss/baby-monitor/tree/master/rpi/janus-stream) 35 | 2) Play selected song on a Speaker 36 | - Refer to [music-client](https://github.com/leerikss/baby-monitor/tree/master/rpi/music-client) 37 | 3) Stream microphone Audio to a Speaker 38 | - Refer to [speaker-client](https://github.com/leerikss/baby-monitor/tree/master/rpi/speaker-client) 39 | 40 | ## Tested on 41 | - Android: Chrome, FireFox (Video did not work on my MI9 presumibly due to lack of H264 support) 42 | - IOS: Safari 43 | - Desktop (Ubuntu): Firefox, Chrome 44 | 45 | ## TODO 46 | - A better solution for streaming the phone microphone audio data to the RPI speaker. Maybe integrate WebRTC two-way (UV4L?) instead of a dedicated WebSocket Audio Proxy 47 | - Improve security. Currently authentication is required to access the WebRTC live audio/video feeds, as well as for establishing WebSocket connections. The password however is currently static, and stored plaintext in Janus configuration, as well as in different NodeJS .env files on both the Server as well as on the Raspberrys. 48 | - Dockerize all the Services 49 | - ~~The UI is written in vanilla HTML, JS and CSS; maybe integrate ReactJS and/or some other fancy framework(s)~~. The UI was rewritten in React JS. The React JS webroot is [here](https://github.com/leerikss/baby-monitor/tree/master/server/webroot-react), but I also left the deprecated vanilla html/js/css version left for comparison [here](https://github.com/leerikss/baby-monitor/tree/master/server/webroot). 50 | -------------------------------------------------------------------------------- /server/webroot/js/music_player.js: -------------------------------------------------------------------------------- 1 | musicPlayer = function() { 2 | 3 | let selectedSong = null; 4 | let socket = null; 5 | let config = null; 6 | let reconnect = null; 7 | let events = {}; 8 | let playing = false; 9 | 10 | function init(_config) { 11 | config = _config; 12 | initWebSocket(); 13 | } 14 | 15 | function addEventListener(event, func) { 16 | events[event] = func; 17 | } 18 | 19 | function play(song) { 20 | send({ "request": "play", "song": song }); 21 | } 22 | 23 | function stop() { 24 | send({ "request": "stop" }); 25 | } 26 | 27 | function initWebSocket() { 28 | 29 | socket = new WebSocket(config.url + "?role=transmitter", config.token); 30 | 31 | socket.onopen = function(event) { 32 | console.log("WebSocket Connected!"); 33 | endReconnectLoop(); 34 | } 35 | 36 | socket.onmessage = function(event) { 37 | 38 | var msg = JSON.parse(event.data); 39 | 40 | if (msg.receivers !== undefined) { 41 | console.log("Receivers list: "); 42 | console.log(msg.receivers); 43 | runEvent("receivers", msg.receivers); 44 | send({ "request": "list_songs" }); 45 | } else if (msg.response !== undefined) { 46 | 47 | switch (msg.response) { 48 | 49 | case "list_songs": 50 | runEvent("listsongs", msg); 51 | 52 | case "play": 53 | if (msg.status === "playing") { 54 | playing = true; 55 | runEvent("playing"); 56 | } 57 | 58 | case "stop": 59 | if (msg.status === "stopped") { 60 | playing = false; 61 | runEvent("stopped"); 62 | } 63 | } 64 | } 65 | } 66 | 67 | socket.onclose = event => { 68 | console.log("WebSocket closed. Attempting to reconnect..."); 69 | runEvent("closed"); 70 | startReconnectLoop(); 71 | } 72 | } 73 | 74 | function startReconnectLoop() { 75 | if (reconnect === null) 76 | reconnect = setInterval(initWebSocket, 3000) 77 | } 78 | 79 | function endReconnectLoop() { 80 | clearInterval(reconnect); 81 | reconnect = null; 82 | } 83 | 84 | function runEvent(name, arg1) { 85 | if (typeof events[name] === "function") 86 | events[name](arg1); 87 | } 88 | 89 | function isPlaying() { 90 | return playing; 91 | } 92 | 93 | function send(msg) { 94 | if (socket.readyState === 1) 95 | socket.send(JSON.stringify(msg)); 96 | } 97 | 98 | return { 99 | init: init, 100 | addEventListener: addEventListener, 101 | play: play, 102 | stop: stop, 103 | isPlaying: isPlaying 104 | } 105 | 106 | }(); -------------------------------------------------------------------------------- /server/webroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Baby Monitor 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 |
32 | 33 |
34 | 35 | 36 | 37 | 38 | 39 |
40 | 41 |
42 |
43 | 44 | 45 |
46 | 47 |
48 | 49 | 50 |   51 | 52 | 53 |
54 |
55 | 56 |
57 | 58 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /server/webroot/js/broadcast_mic.js: -------------------------------------------------------------------------------- 1 | broadcastMic = function() { 2 | 3 | let socket = null; 4 | let config = null; 5 | let reconnect = null; 6 | let events = {}; 7 | let mediaStream = null; 8 | let context = null; 9 | let recording = false; 10 | let AudioContext = window.AudioContext || window.webkitAudioContext; 11 | 12 | function init(_config) { 13 | config = _config; 14 | initWebSocket(); 15 | } 16 | 17 | function addEventListener(event, func) { 18 | events[event] = func; 19 | } 20 | 21 | function start() { 22 | if (mediaStream === null) 23 | navigator 24 | .mediaDevices 25 | .getUserMedia({ audio: true, video: false }) 26 | .then(handleSuccess); 27 | } 28 | 29 | function stop() { 30 | if (context !== null) 31 | context.close(); 32 | if (mediaStream !== null) 33 | mediaStream.getTracks().forEach(t => { t.stop(); }); 34 | context = mediaStream = null; 35 | recording = false; 36 | } 37 | 38 | function isRecording() { 39 | return recording; 40 | } 41 | 42 | function initWebSocket() { 43 | 44 | socket = new WebSocket(config.url + "?role=transmitter", config.token); 45 | 46 | socket.onopen = event => { 47 | console.log("WebSocket Connected!"); 48 | endReconnectLoop(); 49 | } 50 | 51 | socket.onmessage = event => { 52 | 53 | var msg = JSON.parse(event.data); 54 | 55 | if (msg.receivers !== undefined) 56 | runEvent("receivers", msg.receivers); 57 | } 58 | 59 | socket.onclose = event => { 60 | console.log("WebSocket closed. Attempting to reconnect..."); 61 | runEvent("closed"); 62 | startReconnectLoop(); 63 | } 64 | } 65 | 66 | function startReconnectLoop() { 67 | if (reconnect === null) 68 | reconnect = setInterval(initWebSocket, 5000) 69 | } 70 | 71 | function endReconnectLoop() { 72 | clearInterval(reconnect); 73 | reconnect = null; 74 | } 75 | 76 | function runEvent(name, arg1) { 77 | if (typeof events[name] === "function") 78 | events[name](arg1); 79 | } 80 | 81 | function getUserMedia(constraints) { 82 | return userMedia(constraints); 83 | } 84 | 85 | function handleSuccess(stream) { 86 | mediaStream = stream; 87 | context = new AudioContext(); 88 | const source = context.createMediaStreamSource(stream); 89 | const processor = context.createScriptProcessor(config.buffer, 1, 1); 90 | source.connect(processor); 91 | processor.connect(context.destination); 92 | processor.onaudioprocess = recorderProcess; 93 | recording = true; 94 | }; 95 | 96 | 97 | function recorderProcess(e) { 98 | var left = e.inputBuffer.getChannelData(0) 99 | if (socket !== null && socket.readyState === 1) 100 | socket.send(convertFloat32ToInt16(left)) 101 | } 102 | 103 | function convertFloat32ToInt16(buffer) { 104 | l = buffer.length 105 | buf = new Int16Array(l) 106 | while (l--) { 107 | buf[l] = Math.min(1, buffer[l]) * 0x7fff 108 | } 109 | return buf.buffer 110 | } 111 | 112 | return { 113 | init: init, 114 | addEventListener: addEventListener, 115 | start: start, 116 | stop: stop, 117 | isRecording: isRecording 118 | } 119 | 120 | }(); -------------------------------------------------------------------------------- /rpi/music-client/music-client.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Requirements: 4 | 5 | npm install ws 6 | 7 | sudo apt-get install omxplayer 8 | npm install node-omxplayer 9 | 10 | */ 11 | 12 | 13 | const MusicClient = function() { 14 | 15 | // Imported modules 16 | const WebSocket = require('ws') 17 | const fs = require('fs') 18 | let Omx = require('node-omxplayer'); 19 | const crypto = require("crypto"); 20 | 21 | // Private variables 22 | let player = null; 23 | let pingTimeout = null; 24 | let config = null; 25 | let songs = []; 26 | 27 | function run() { 28 | 29 | // Read configs 30 | const rawdata = fs.readFileSync('config.json'); 31 | config = JSON.parse(rawdata); 32 | 33 | // Construct WebSocket 34 | configureWebSocket( 35 | new WebSocket(config.ws_music_url + "?role=receiver", config.token)); 36 | } 37 | 38 | function configureWebSocket(client) { 39 | 40 | client.onopen = buildSongs; 41 | 42 | client.onerror = e => console.log("Music WS Client error: " + e.message); 43 | 44 | client.onmessage = (event) => onClientMessage(client, event); 45 | 46 | client.onclose = (event) => console.log("Music WS Client connection was closed"); 47 | 48 | // Heartbeat 49 | client.on("open", heartbeat); 50 | client.on("ping", heartbeat); 51 | client.on("close", () => clearTimeout(pingTimeout)); 52 | } 53 | 54 | function onClientMessage(client, event) { 55 | 56 | var msg = JSON.parse(event.data); 57 | 58 | switch (msg.request) { 59 | 60 | case "list_songs": 61 | var data = JSON.stringify({ "response": "list_songs", "songs": songs }); 62 | console.log("Music WS Client list_songs() returns: " + data); 63 | client.send(data); 64 | break; 65 | 66 | case "play": 67 | if (player && player.running) 68 | stop(); 69 | play(msg.song, () => client.send(JSON.stringify({ "response": "play", "status": "playing" }))); 70 | break; 71 | 72 | case "stop": 73 | stop(); 74 | client.send(JSON.stringify({ "response": "stop", "status": "stopped" })); 75 | break; 76 | } 77 | } 78 | 79 | function buildSongs() { 80 | fs.readdir(config.music_path, function(err, items) { 81 | songs = []; 82 | items.forEach((item) => { 83 | songs.push({ 84 | id: crypto.randomBytes(16).toString("hex"), 85 | name: item 86 | }); 87 | }); 88 | }); 89 | } 90 | 91 | function play(songId, cb) { 92 | const song = songs.find(item => item.id === songId); 93 | if (song !== undefined && song.name !== undefined) { 94 | console.log("Music WS Client Playing " + song.name + ".."); 95 | player = Omx(config.music_path + "/" + song.name); 96 | player.running = true; 97 | cb(); 98 | } 99 | } 100 | 101 | function stop() { 102 | console.log("Music WS Client Stops playing..."); 103 | if (player !== null) { 104 | player.quit(); 105 | player.running = false; 106 | } 107 | } 108 | 109 | function heartbeat() { 110 | console.log("Music WS Client heartbeat..."); 111 | clearTimeout(pingTimeout); 112 | pingTimeout = setTimeout(() => this.terminate(), 3000 * 2); 113 | } 114 | 115 | return { 116 | run: run 117 | } 118 | 119 | }(); 120 | 121 | // Start 122 | MusicClient.run(); -------------------------------------------------------------------------------- /server/webroot-react/src/components/VideoStream/JanusVideo/JanusVideo.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useContext, useEffect } from 'react'; 2 | import { AppContext, AppContextActions, VideoStates } from '../../../context/AppContext'; 3 | import { JanusContext, JanusContextActions } from '../../../context/JanusContext'; 4 | import classes from './JanusVideo.module.css'; 5 | import useJanus from '../../../hooks/janus/useJanus'; 6 | import withMouseEvents from '../../../hoc/withMouseEvents'; 7 | 8 | export const JanusVideo = (props) => { 9 | 10 | const videoEl = useRef(null); 11 | 12 | const [uiState, uiDispatch] = useContext(AppContext); 13 | const [janusState, janusDispatch] = useContext(JanusContext); 14 | 15 | const { init, cleanUp, availableStreams, watchStream } = useJanus( 16 | props.janusUrl, props.password, videoEl, 17 | props.useTurn, props.turnUrl); 18 | 19 | // Init 20 | useEffect(() => { 21 | 22 | init(); 23 | 24 | return () => { 25 | cleanUp(); 26 | } 27 | }, [init, cleanUp]); 28 | 29 | // Janus available Streams changed => dispacth context 30 | useEffect(() => { 31 | 32 | // Dispatch Empty list 33 | if (!availableStreams || availableStreams.length === 0) { 34 | janusDispatch({ 35 | type: JanusContextActions.SET_STREAMS, 36 | streams: [] 37 | }); 38 | return; 39 | } 40 | 41 | // Dispacth first stream as current stream 42 | const currentStreamId = parseInt(availableStreams[0].id); 43 | janusDispatch({ 44 | type: JanusContextActions.SET_CURRENT_STREAM_ID, 45 | streamId: currentStreamId 46 | }) 47 | // Dispatch available streams 48 | janusDispatch({ 49 | type: JanusContextActions.SET_STREAMS, 50 | streams: availableStreams 51 | }); 52 | }, [availableStreams, janusDispatch]); 53 | 54 | // Janus current stream changed => watch it 55 | useEffect(() => { 56 | if (janusState.currentStreamId === null) 57 | return; 58 | watchStream(parseInt(janusState.currentStreamId)); 59 | }, [janusState.currentStreamId, watchStream]); 60 | 61 | // Play/pause video 62 | useEffect(() => { 63 | if (uiState.videoState === VideoStates.PAUSE) 64 | videoEl.current.pause(); 65 | else if (uiState.videoState === VideoStates.PLAY) 66 | videoEl.current.play(); 67 | }, [uiState.videoState]); 68 | 69 | // mute/unmute video 70 | useEffect(() => { 71 | videoEl.current.muted = uiState.videoMuted; 72 | }, [uiState.videoMuted]); 73 | 74 | // Center scroll X 75 | const videoLoadedHandler = () => { 76 | const videoWidth = videoEl.current.getBoundingClientRect().width; 77 | const scrollLeft = (videoWidth / 2) - (window.outerWidth / 2); 78 | videoEl.current.parentElement.scrollLeft = scrollLeft; 79 | }; 80 | 81 | const style = (window.innerHeight > window.innerWidth) ? { 82 | height: uiState.videoHeight + "vh" 83 | } : { 84 | width: uiState.videoHeight + "vw" 85 | } 86 | return ( 87 |
88 | 97 |
98 | ); 99 | } 100 | 101 | export default withMouseEvents(JanusVideo); -------------------------------------------------------------------------------- /server/webroot-react/src/hooks/musicPlayer/useMusicPlayer.js: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useMemo, useCallback } from "react" 2 | 3 | export const MusicPlayerStatus = { 4 | PLAY: "PLAY", 5 | PLAYING: "PLAYING", 6 | STOP: "STOP", 7 | STOPPED: "STOPPED", 8 | AVAILABLE: "AVAILABLE", 9 | UNAVAILABLE: "UNAVAILABLE" 10 | } 11 | 12 | const useMusicPlayer = (url, password) => { 13 | 14 | const [status, setStatus] = useState(MusicPlayerStatus.UNAVAILABLE); 15 | const [songs, setSongs] = useState([]); 16 | 17 | const socket = useRef(null); 18 | const reconnect = useRef(null); 19 | 20 | const cleanUp = useCallback(() => { 21 | clearInterval(reconnect.current); 22 | reconnect.current = null; 23 | }, []); 24 | 25 | const send = useCallback((msg) => { 26 | if (socket.current.readyState === 1) { 27 | console.log("send():"); 28 | console.log(msg); 29 | socket.current.send(JSON.stringify(msg)); 30 | } 31 | }, []); 32 | 33 | const init = useCallback(() => { 34 | 35 | // Define WebSocket 36 | try { 37 | socket.current = new WebSocket(url + "?role=transmitter", password); 38 | } catch (e) { 39 | console.error(e); 40 | return; 41 | } 42 | 43 | socket.current.onopen = function (event) { 44 | console.log("WebSocket Connected!"); 45 | cleanUp(); 46 | } 47 | 48 | socket.current.onclose = event => { 49 | console.log("WebSocket closed. Attempting to reconnect..."); 50 | setStatus(MusicPlayerStatus.UNAVAILABLE); 51 | if (reconnect.current === null) { 52 | reconnect.current = setInterval(init, 3000); 53 | } 54 | } 55 | 56 | socket.current.onmessage = function (event) { 57 | 58 | var msg = JSON.parse(event.data); 59 | 60 | console.log("onmessage():") 61 | console.log(msg); 62 | 63 | // Handle receivers list 64 | if (msg.receivers !== undefined) { 65 | if (msg.receivers.length > 0) { 66 | setStatus(MusicPlayerStatus.AVAILABLE); 67 | send({ "request": "list_songs" }); 68 | } else 69 | setStatus(MusicPlayerStatus.UNAVAILABLE); 70 | } 71 | 72 | // Handle server messages 73 | else if (msg.response !== undefined) { 74 | 75 | switch (msg.response) { 76 | 77 | case "list_songs": 78 | setSongs(msg.songs); 79 | break; 80 | 81 | case "play": 82 | if (msg.status === "playing") 83 | setStatus(MusicPlayerStatus.PLAYING); 84 | break; 85 | 86 | case "stop": 87 | if (msg.status === "stopped") 88 | setStatus(MusicPlayerStatus.STOPPED); 89 | break; 90 | 91 | default: 92 | console.log("Received unknown msg:"); 93 | console.log(msg); 94 | break; 95 | } 96 | } 97 | } 98 | }, [url, password, setSongs, cleanUp, send]); 99 | 100 | const play = useCallback((song) => { 101 | send({ 102 | "request": "play", 103 | "song": song 104 | }); 105 | setStatus(MusicPlayerStatus.PLAY); 106 | }, [send]); 107 | 108 | const stop = useCallback(() => { 109 | send({ 110 | "request": "stop" 111 | }); 112 | setStatus(MusicPlayerStatus.STOP); 113 | }, [send]); 114 | 115 | return useMemo(() => ({ 116 | init: init, 117 | cleanUp: cleanUp, 118 | play: play, 119 | stop: stop, 120 | status: status, 121 | songs: songs, 122 | }), [init, cleanUp, play, stop, status, songs]); 123 | } 124 | 125 | export default useMusicPlayer; -------------------------------------------------------------------------------- /server/webroot-react/src/hooks/microphone/useMicrophone.js: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useMemo, useCallback } from "react" 2 | 3 | export const MicrophoneStatus = { 4 | RECORDING: "RECORDING", 5 | STOPPED: "STOPPED", 6 | AVAILABLE: "AVAILABLE", 7 | UNAVAILABLE: "UNAVAILABLE" 8 | } 9 | 10 | export const AUDIO_BUFFER = 4096; 11 | 12 | const useMicrophone = (url, password) => { 13 | 14 | const [status, setStatus] = useState(MicrophoneStatus.UNAVAILABLE); 15 | 16 | const socket = useRef(null); 17 | const reconnect = useRef(null); 18 | const mediaStream = useRef(null); 19 | const context = useRef(null); 20 | 21 | const stop = useCallback(() => { 22 | if (context.current !== null) 23 | context.current.close(); 24 | if (mediaStream.current !== null) 25 | mediaStream.current.getTracks().forEach(t => { t.stop(); }); 26 | context.current = mediaStream.current = null; 27 | setStatus(MicrophoneStatus.STOPPED); 28 | }, []); 29 | 30 | // Remove possible interval 31 | const cleanUp = useCallback(() => { 32 | stop(); 33 | clearInterval(reconnect.current); 34 | reconnect.current = null; 35 | }, [stop]); 36 | 37 | // Initialize 38 | const init = useCallback(() => { 39 | 40 | try { 41 | socket.current = new WebSocket(url + "?role=transmitter", password); 42 | } catch (e) { 43 | console.error(e); 44 | return; 45 | } 46 | 47 | socket.current.onopen = function (event) { 48 | console.log("WebSocket Connected!"); 49 | cleanUp(); 50 | } 51 | 52 | socket.current.onclose = event => { 53 | console.log("WebSocket closed. Attempting to reconnect..."); 54 | setStatus(MicrophoneStatus.UNAVAILABLE); 55 | if (reconnect.current === null) { 56 | reconnect.current = setInterval(init, 3000); 57 | } 58 | } 59 | 60 | socket.current.onmessage = function (event) { 61 | var msg = JSON.parse(event.data); 62 | console.log("onmessage():") 63 | console.log(msg); 64 | 65 | // Handle receivers list 66 | if (msg.receivers !== undefined) { 67 | if (msg.receivers.length > 0) 68 | setStatus(MicrophoneStatus.AVAILABLE); 69 | else 70 | setStatus(MicrophoneStatus.UNAVAILABLE); 71 | } 72 | } 73 | }, [url, password, cleanUp]); 74 | 75 | const audioProcessor = useCallback( (audio) => { 76 | var mono = audio.inputBuffer.getChannelData(0) 77 | if (socket.current !== null && socket.current.readyState === 1) 78 | socket.current.send(convertFloat32ToInt16(mono)) 79 | }, []); 80 | 81 | const convertFloat32ToInt16 = (buffer) => { 82 | let len = buffer.length; 83 | const buf = new Int16Array(len); 84 | while (len--) { 85 | buf[len] = Math.min(1, buffer[len]) * 0x7fff 86 | } 87 | return buf.buffer 88 | } 89 | 90 | const record = useCallback( () => { 91 | 92 | if (mediaStream.current !== null) 93 | return; 94 | 95 | navigator 96 | .mediaDevices 97 | .getUserMedia({ audio: true, video: false }) 98 | .then((stream) => { 99 | mediaStream.current = stream; 100 | const AudioContext = window.AudioContext || window.webkitAudioContext; 101 | context.current = new AudioContext(); 102 | const source = context.current.createMediaStreamSource(stream); 103 | const processor = context.current.createScriptProcessor(AUDIO_BUFFER, 1, 1); 104 | source.connect(processor); 105 | processor.connect(context.current.destination); 106 | processor.onaudioprocess = audioProcessor; 107 | 108 | setStatus(MicrophoneStatus.RECORDING); 109 | 110 | }); 111 | }, [audioProcessor]); 112 | 113 | return useMemo(() => ({ 114 | init: init, 115 | cleanUp: cleanUp, 116 | record: record, 117 | stop: stop, 118 | status: status 119 | }), [init, cleanUp, record, stop, status]); 120 | } 121 | 122 | export default useMicrophone; -------------------------------------------------------------------------------- /server/conf/janus/janus.transport.http.jcfg.template: -------------------------------------------------------------------------------- 1 | # Web server stuff: whether any should be enabled, which ports they 2 | # should use, whether security should be handled directly or demanded to 3 | # an external application (e.g., web frontend) and what should be the 4 | # base path for the Janus API protocol. You can also specify the 5 | # threading model to use for the HTTP webserver: by default this is 6 | # 'unlimited' (which means a thread per connection, as specified by the 7 | # libmicrohttpd documentation), using a number will make use of a thread 8 | # pool instead. Since long polls are involved, make sure you choose a 9 | # value that doesn't keep new connections waiting. Notice that by default 10 | # all the web servers will try and bind on both IPv4 and IPv6: if you 11 | # want to only bind to IPv4 addresses (e.g., because your system does not 12 | # support IPv6), you should set the web server 'ip' property to '0.0.0.0'. 13 | general: { 14 | json = "indented" # Whether the JSON messages should be indented (default), 15 | # plain (no indentation) or compact (no indentation and no spaces) 16 | base_path = "/janus" # Base path to bind to in the web server (plain HTTP only) 17 | threads = "unlimited" # unlimited=thread per connection, number=thread pool 18 | http = false # Whether to enable the plain HTTP interface 19 | port = 8088 # Web server HTTP port 20 | #interface = "eth0" # Whether we should bind this server to a specific interface only 21 | #ip = "192.168.0.1" # Whether we should bind this server to a specific IP address (v4 or v6) only 22 | https = true # Whether to enable HTTPS (default=false) 23 | secure_port = 8089 # Web server HTTPS port, if enabled 24 | #secure_interface = "eth0" # Whether we should bind this server to a specific interface only 25 | #secure_ip = "192.168.0.1" # Whether we should bind this server to a specific IP address (v4 or v6) only 26 | #acl = "127.,192.168.0." # Only allow requests coming from this comma separated list of addresses 27 | } 28 | 29 | # Janus can also expose an admin/monitor endpoint, to allow you to check 30 | # which sessions are up, which handles they're managing, their current 31 | # status and so on. This provides a useful aid when debugging potential 32 | # issues in Janus. The configuration is pretty much the same as the one 33 | # already presented above for the webserver stuff, as the API is very 34 | # similar: choose the base bath for the admin/monitor endpoint (/admin 35 | # by default), ports, threading model, etc. Besides, you can specify 36 | # a secret that must be provided in all requests as a crude form of 37 | # authorization mechanism, and partial or full source IPs if you want to 38 | # limit access basing on IP addresses. For security reasons, this 39 | # endpoint is disabled by default, enable it by setting admin_http=true. 40 | admin: { 41 | admin_base_path = "/admin" # Base path to bind to in the admin/monitor web server (plain HTTP only) 42 | admin_threads = "unlimited" # unlimited=thread per connection, number=thread pool 43 | admin_http = false # Whether to enable the plain HTTP interface 44 | admin_port = 7088 # Admin/monitor web server HTTP port 45 | #admin_interface = "eth0" # Whether we should bind this server to a specific interface only 46 | #admin_ip = "192.168.0.1" # Whether we should bind this server to a specific IP address (v4 or v6) only 47 | admin_https = false # Whether to enable HTTPS (default=false) 48 | #admin_secure_port = 7889 # Admin/monitor web server HTTPS port, if enabled 49 | #admin_secure_interface = "eth0" # Whether we should bind this server to a specific interface only 50 | #admin_secure_ip = "192.168.0.1 # Whether we should bind this server to a specific IP address (v4 or v6) only 51 | #admin_acl = "127.,192.168.0." # Only allow requests coming from this comma separated list of addresses 52 | } 53 | 54 | # The HTTP servers created in Janus support CORS out of the box, but by 55 | # default they return a wildcard (*) in the 'Access-Control-Allow-Origin' 56 | # header. This works fine in most situations, except when we have to 57 | # respond to a credential request (withCredentials=true in the XHR). If 58 | # you need that, uncomment and set the 'allow_origin' below to specify 59 | # what must be returned in 'Access-Control-Allow-Origin'. More details: 60 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS 61 | cors: { 62 | allow_origin = "https://$domain" 63 | } 64 | 65 | # Certificate and key to use for HTTPS, if enabled (and passphrase if needed). 66 | # You can also disable insecure protocols and ciphers by configuring the 67 | # 'ciphers' property accordingly (no limitation by default). 68 | certificates: { 69 | cert_pem = "/etc/letsencrypt/live/$domain/cert.pem" 70 | cert_key = "/etc/letsencrypt/live/$domain/privkey.pem" 71 | 72 | #cert_pem = "/path/to/cert.pem" 73 | #cert_key = "/path/to/key.pem" 74 | #cert_pwd = "secretpassphrase" 75 | #ciphers = "PFS:-VERS-TLS1.0:-VERS-TLS1.1:-3DES-CBC:-ARCFOUR-128" 76 | } 77 | -------------------------------------------------------------------------------- /server/webroot/style.css: -------------------------------------------------------------------------------- 1 | /* Always show scrollbars on large displays */ 2 | 3 | @media screen and (min-width: 960px) { 4 | html { 5 | overflow-y: scroll; 6 | overflow-x: scroll; 7 | } 8 | } 9 | 10 | 11 | /* General */ 12 | 13 | * { 14 | -webkit-tap-highlight-color: transparent; 15 | } 16 | 17 | *:focus { 18 | outline: none; 19 | } 20 | 21 | body { 22 | margin: 0px; 23 | width: 100vw; 24 | height: 100vh; 25 | background: linear-gradient(0deg, rgba(60, 60, 60, 1) 0%, rgba(20, 20, 20, 1) 100%); 26 | } 27 | 28 | #content { 29 | display: none; 30 | } 31 | 32 | video { 33 | height: 100vh; 34 | border: 1px solid black; 35 | box-shadow: 5px 5px 10px 0px rgba(0, 0, 0, 1); 36 | position: absolute; 37 | left: 0; 38 | right: 0; 39 | margin: auto; 40 | transition: height .5s, top .5s; 41 | } 42 | 43 | 44 | /* Login */ 45 | 46 | #login { 47 | position: fixed; 48 | left: 20px; 49 | top: 20px; 50 | right: 20px; 51 | max-width: 800px; 52 | margin: auto; 53 | } 54 | 55 | input { 56 | display: block; 57 | box-sizing: border-box; 58 | font-weight: bold; 59 | font-size: 1em; 60 | width: 100%; 61 | height: 40px; 62 | padding: 0px; 63 | margin: 0px; 64 | } 65 | 66 | input[type=password] { 67 | border: 3px solid #bbb; 68 | } 69 | 70 | input[type=checkbox] { 71 | width: auto; 72 | display: inline; 73 | vertical-align: inherit; 74 | } 75 | 76 | label { 77 | vertical-align: middle; 78 | font-family: sans-serif; 79 | color: #fff; 80 | } 81 | 82 | input[type=submit] { 83 | margin-top: 20px; 84 | color: #333; 85 | background: #fdfdfd; 86 | background: linear-gradient(to bottom, #fdfdfd 0%, #bebebe 100%); 87 | border: 3px solid #bbb; 88 | border-radius: 10px; 89 | } 90 | 91 | 92 | /* Controls div */ 93 | 94 | #controls { 95 | display: none; 96 | position: fixed; 97 | width: 95%; 98 | max-width: 800px; 99 | top: 80px; 100 | left: 0; 101 | right: 0; 102 | margin: auto; 103 | } 104 | 105 | .select { 106 | display: block; 107 | font-size: 16px; 108 | font-family: sans-serif; 109 | font-weight: 700; 110 | color: #fff; 111 | padding-left: 90px; 112 | line-height: 3em; 113 | width: 100%; 114 | border: 1px solid #fff; 115 | box-shadow: 3px 3px 10px rgba(0, 0, 0, .5); 116 | border-radius: .5em; 117 | -moz-appearance: none; 118 | -webkit-appearance: none; 119 | appearance: none; 120 | background-color: rgba(0, 0, 0, 0.7); 121 | background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23FFFFFF%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'); 122 | background-repeat: no-repeat; 123 | background-position: right 1em top 50%; 124 | background-size: 2em auto; 125 | } 126 | 127 | .select-streams { 128 | background-image: url('img/cctv-icon.png'), url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23FFFFFF%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'); 129 | background-repeat: no-repeat, no-repeat; 130 | background-position: left 1em top 50%, right 1em top 50%; 131 | background-size: 3em auto, 2em auto; 132 | } 133 | 134 | 135 | /* Toggle buttons */ 136 | 137 | .buttons { 138 | background: rgba(0, 0, 0, .7); 139 | padding: 5px; 140 | width: 20px; 141 | border-radius: 5px; 142 | border: 1px solid white; 143 | box-shadow: 1px 1px rgba(0, 0, 0, .5); 144 | cursor: pointer; 145 | } 146 | 147 | .spacer { 148 | padding-left: 30px; 149 | padding-right: 30px; 150 | } 151 | 152 | #toggleMenuBtn { 153 | z-index: 20; 154 | position: fixed; 155 | bottom: 74px; 156 | left: 50%; 157 | transform: translateX(-50%); 158 | } 159 | 160 | #bottomBtns { 161 | z-index: 10; 162 | position: fixed; 163 | bottom: 70px; 164 | left: 50%; 165 | transform: translateX(-50%); 166 | white-space: nowrap; 167 | } 168 | 169 | 170 | /* Ripple effect */ 171 | 172 | .ripple { 173 | background-position: center; 174 | transition: background 0.5s; 175 | } 176 | 177 | .ripple:hover { 178 | background: #444444 radial-gradient(circle, transparent 1%, #444444 1%) center/15000%; 179 | } 180 | 181 | .ripple:active { 182 | background-color: #BBBBBB; 183 | background-size: 100%; 184 | transition: background 0s; 185 | } 186 | 187 | 188 | /* Select song + play button area */ 189 | 190 | #music { 191 | display: none; 192 | position: relative; 193 | } 194 | 195 | #togglePlaySongBtn { 196 | cursor: pointer; 197 | position: absolute; 198 | width: 5em; 199 | height: auto; 200 | top: -0.9em; 201 | } 202 | 203 | 204 | /* Record button */ 205 | 206 | #toggleRecordBtn { 207 | margin-top: 50px; 208 | cursor: pointer; 209 | display: block; 210 | margin-left: auto; 211 | margin-right: auto; 212 | } 213 | -------------------------------------------------------------------------------- /server/webroot-react/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 https://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.0/8 are 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 https://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 https://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 | headers: { 'Service-Worker': 'script' } 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /server/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -a 4 | 5 | read -p "Hostname? " domain 6 | read -e -p "Repository location? " -i "/opt/baby-monitor" repo 7 | read -e -p "Security password? " -i "changeit" password 8 | 9 | BASEDIR=$(pwd) 10 | 11 | function sed_esc() { 12 | echo $(sed -e 's/[&\\/]/\\&/g; s/$/\\/' -e '$s/\\$//' <<<"$1") 13 | } 14 | 15 | function install_node() { 16 | curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - 17 | sudo apt-get install -y nodejs 18 | sudo npm install npm --global 19 | } 20 | 21 | function install_ws_proxy() { 22 | 23 | sudo npm install --prefix "$repo/server/websocket-proxy" ws dotenv 24 | 25 | sudo -E bash -c 'echo -e "PASSWORD=$password" > "$repo/server/websocket-proxy/.env"' 26 | sudo -E bash -c 'echo -e "ORIGIN=https://$domain" >> "$repo/server/websocket-proxy/.env"' 27 | sudo chmod 700 "$repo/server/websocket-proxy/.env" 28 | 29 | # systemd script 30 | sudo -E sh -c 'envsubst < "$repo/server/conf/systemd/websocket-proxy.service.template" \ 31 | > /etc/systemd/system/websocket-proxy.service' 32 | sudo systemctl reenable websocket-proxy 33 | sudo systemctl start websocket-proxy 34 | 35 | } 36 | 37 | function install_nginx() { 38 | sudo apt install nginx 39 | 40 | sudo rm "/etc/nginx/sites-available/$domain" 41 | sudo rm "/etc/nginx/sites-enabled/$domain" 42 | 43 | sudo cp conf/nginx/nginx.conf.template "/etc/nginx/sites-available/$domain" 44 | domain=$(sed_esc "$domain") 45 | wwwhome="$repo/server/webroot-react" 46 | webroot=$(sed_esc "$wwwhome-react") 47 | sudo sed -i -e "s/\$domain/$domain/" -e "s/\$webroot/$webroot/" "/etc/nginx/sites-available/$domain" 48 | sudo ln -s "/etc/nginx/sites-available/$domain" "/etc/nginx/sites-enabled/$domain" 49 | } 50 | 51 | function install_certbot() { 52 | sudo apt-get install software-properties-common 53 | sudo add-apt-repository ppa:certbot/certbot -y 54 | sudo apt install python-certbot-nginx 55 | sudo certbot --nginx -d "$domain" -d "$domain" 56 | sudo systemctl restart nginx 57 | } 58 | 59 | function install_janus() { 60 | 61 | # Libraries 62 | sudo apt install libmicrohttpd-dev libjansson-dev \ 63 | libssl-dev libsofia-sip-ua-dev libglib2.0-dev gtk-doc-tools \ 64 | libopus-dev libogg-dev libcurl4-openssl-dev liblua5.3-dev \ 65 | libconfig-dev pkg-config gengetopt libtool automake autoconf 66 | 67 | # libnice 68 | sudo rm -Rf /tmp/libnice 69 | git clone https://gitlab.freedesktop.org/libnice/libnice /tmp/libnice 70 | cd /tmp/libnice 71 | ./autogen.sh 72 | ./configure --prefix=/usr 73 | make && sudo make install 74 | 75 | # libsrtp2 76 | cd /tmp 77 | rm -Rf libsrtp-2.2.0 78 | wget https://github.com/cisco/libsrtp/archive/v2.2.0.tar.gz 79 | tar xfv v2.2.0.tar.gz 80 | cd libsrtp-2.2.0 81 | ./configure --prefix=/usr --enable-openssl --libdir=/usr/lib64 82 | make shared_library && sudo make install 83 | # Permanent path /usr/lib64 libsrtp2 libs 84 | sudo cp $BASEDIR/conf/ld.so.conf.d/libsrtp2.conf /etc/ld.so.conf.d/libsrtp2.conf 85 | sudo ldconfig 86 | 87 | # Janus 88 | sudo rm -Rf /opt/janus 89 | sudo rm -Rf /tmp/janus-gateway 90 | git clone https://github.com/meetecho/janus-gateway.git /tmp/janus-gateway 91 | cd /tmp/janus-gateway 92 | sh autogen.sh 93 | ./configure --prefix=/opt/janus --disable-websockets --disable-rabbitmq --disable-mqtt --disable-data-channels 94 | make && sudo make install 95 | sudo make configs 96 | 97 | } 98 | 99 | function config_janus() { 100 | 101 | # Janus configs 102 | sudo cp $BASEDIR/conf/janus/*.jcfg /opt/janus/etc/janus 103 | sudo -E sh -c 'envsubst < conf/janus/janus.transport.http.jcfg.template > /opt/janus/etc/janus/janus.transport.http.jcfg' 104 | 105 | read -e -p "Number of streaming devices? " -i 1 devices 106 | sudo rm -f /opt/janus/etc/janus/janus.plugin.streaming.jcfg 107 | sudo touch /opt/janus/etc/janus/janus.plugin.streaming.jcfg 108 | for ((id = 1; id <= $devices; id++)); do 109 | read -e -p "Streaming device [$id] name? " -i "RPI3" name 110 | read -e -p "Streaming device [$id] description? " -i "$name Stream" description 111 | read -e -p "Streaming device [$id] video port number? " -i 5001 videoport 112 | read -e -p "Streaming device [$id] audio port number? " -i 5002 audioport 113 | sudo -E sh -c 'envsubst < conf/janus/janus.plugin.streaming.jcfg.template >> /opt/janus/etc/janus/janus.plugin.streaming.jcfg' 114 | done 115 | 116 | # systemd scripts 117 | sudo cp $BASEDIR/conf/systemd/janus.service /etc/systemd/system 118 | sudo systemctl reenable janus 119 | sudo systemctl start janus 120 | } 121 | 122 | function install_turnserver() { 123 | sudo apt-get install -y coturn 124 | sudo -E sh -c 'envsubst < conf/turnserver/turnserver.conf.template > /etc/turnserver.conf' 125 | sudo cp $BASEDIR/conf/systemd/turnserver.service /etc/systemd/system 126 | sudo systemctl reenable turnserver 127 | sudo systemctl start turnserver 128 | } 129 | 130 | function install_ui() { 131 | sudo -E sh -c 'envsubst < conf/webroot/.env.template > "$repo/server/webroot-react"' 132 | cd "$repo/server/webroot-react" 133 | npm i 134 | npm run build 135 | } 136 | 137 | install_node 138 | install_ws_proxy 139 | install_nginx 140 | install_certbot 141 | install_janus 142 | install_turnserver 143 | install_ui 144 | config_janus 145 | 146 | set +a 147 | -------------------------------------------------------------------------------- /server/webroot/js/janus_player.js: -------------------------------------------------------------------------------- 1 | const janusPlayer = function() { 2 | 3 | let janus = null; 4 | let plugin = null; 5 | let onCleanup = null; 6 | let reInit = null; 7 | let config = {} 8 | 9 | const DEAD_STREAM_MS = 5000; 10 | 11 | function init(_config) { 12 | 13 | config = _config; 14 | 15 | Janus.init({ 16 | debug: false, // true, 17 | dependencies: Janus.useDefaultDependencies(), 18 | callback: callback 19 | }); 20 | } 21 | 22 | function callback() { 23 | 24 | let iceServers = null; 25 | if(config.turnUrl) 26 | iceServers = [{ 27 | url: config.turnUrl, 28 | username: 'babymonitor', 29 | credential: config.pin 30 | }]; 31 | 32 | janus = new Janus({ 33 | 34 | server: config.url, 35 | 36 | iceServers: iceServers, 37 | 38 | success: function() { 39 | 40 | janus.attach({ 41 | 42 | plugin: "janus.plugin.streaming", 43 | 44 | success: function(pluginHandle) { 45 | plugin = pluginHandle; 46 | requestStreams(); 47 | }, 48 | 49 | onmessage: function(msg, jsep) { 50 | if (msg.result !== undefined && msg.result.status !== undefined) { 51 | var status = msg.result.status; 52 | console.log("onmessage: status = " + status); 53 | if (status === "preparing" && jsep !== undefined) 54 | createAnswer(jsep); 55 | } 56 | }, 57 | 58 | onremotestream: function(stream) { 59 | console.log("Got a remote stream"); 60 | console.log(stream); 61 | Janus.attachMediaStream(config.elVideo, stream); 62 | }, 63 | 64 | oncleanup: function() { 65 | console.log("Cleanup!"); 66 | if (onCleanup !== null) { 67 | onCleanup(); 68 | onCleanup = null; 69 | } 70 | }, 71 | 72 | error: function(cause) { 73 | console.error("janus.attach error", cause); 74 | }, 75 | }); 76 | }, 77 | 78 | error: function(error) { 79 | console.error("Janus error:"); 80 | console.log(error); 81 | // Reinit gracefully 82 | if (reInit === null) { 83 | reInit = setTimeout(() => { 84 | console.log("Rerunning init()..."); 85 | init(config); 86 | reInit = null; 87 | }, 1000); 88 | } 89 | } 90 | }); 91 | } 92 | 93 | function requestStreams() { 94 | plugin.send({ 95 | "message": { "request": "list" }, 96 | "success": (result) => { 97 | if (result !== undefined && result["list"] !== undefined) 98 | streamsArrived(result["list"]); 99 | } 100 | }); 101 | } 102 | 103 | function streamsArrived(streams) { 104 | console.log("Streams arrived"); 105 | console.log(streams); 106 | streams = removeDeadStreams(streams); 107 | if (Array.isArray(streams) && streams.length > 0) { 108 | watchStream(streams[0]); 109 | buildDropdown(streams); 110 | } 111 | } 112 | 113 | function removeDeadStreams(streams) { 114 | let newStreams = []; 115 | streams.forEach(stream => { 116 | if (stream.video_age_ms < DEAD_STREAM_MS && 117 | stream.audio_age_ms < DEAD_STREAM_MS) 118 | newStreams.push(stream); 119 | }); 120 | return newStreams; 121 | } 122 | 123 | function watchStream(stream) { 124 | console.log("Requesting watch..."); 125 | plugin.send({ 126 | "message": { 127 | "request": "watch", 128 | id: stream.id, 129 | pin: config.pin 130 | } 131 | }); 132 | } 133 | 134 | function buildDropdown(streams) { 135 | 136 | // Regenerate dropdown 137 | config.elStreams.innerHTML = ""; 138 | 139 | streams.forEach((stream) => { 140 | let option = document.createElement("option"); 141 | option.value = stream.id; 142 | option.appendChild(document.createTextNode(stream.description)); 143 | config.elStreams.appendChild(option); 144 | }); 145 | 146 | // Dropdown on change action 147 | config.elStreams.onchange = function() { 148 | const id = parseInt(this.value); 149 | onCleanup = () => { 150 | console.log("Requesting watch..."); 151 | plugin.send({ "message": { "request": "watch", id: id, "pin": config.pin } }); 152 | }; 153 | plugin.send({ "message": { "request": "stop", "pin": config.pin } }); 154 | }; 155 | } 156 | 157 | function createAnswer(jsep) { 158 | 159 | console.log(jsep); 160 | 161 | console.log("Create answer"); 162 | plugin.createAnswer({ 163 | "pin": config.pin, 164 | "jsep": jsep, 165 | "media": { "audioSend": false, "videoSend": false }, 166 | "success": (jsep) => requestStart(jsep), 167 | "error": function(error) { 168 | console.error("WebRTC error: ", error); 169 | } 170 | }); 171 | } 172 | 173 | function requestStart(jsep) { 174 | console.log("Got SDP"); 175 | plugin.send({ 176 | "message": { "request": "start" }, 177 | "pin": config.pin, 178 | "jsep": jsep 179 | }); 180 | } 181 | 182 | return Object.freeze({ 183 | init: init 184 | }); 185 | 186 | }(); 187 | -------------------------------------------------------------------------------- /rpi/speaker-client/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "speaker-client", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "async-limiter": { 8 | "version": "1.0.1", 9 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", 10 | "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" 11 | }, 12 | "bindings": { 13 | "version": "1.5.0", 14 | "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", 15 | "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", 16 | "requires": { 17 | "file-uri-to-path": "1.0.0" 18 | } 19 | }, 20 | "buffer-alloc": { 21 | "version": "1.2.0", 22 | "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", 23 | "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", 24 | "requires": { 25 | "buffer-alloc-unsafe": "^1.1.0", 26 | "buffer-fill": "^1.0.0" 27 | } 28 | }, 29 | "buffer-alloc-unsafe": { 30 | "version": "1.1.0", 31 | "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", 32 | "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" 33 | }, 34 | "buffer-fill": { 35 | "version": "1.0.0", 36 | "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", 37 | "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" 38 | }, 39 | "core-util-is": { 40 | "version": "1.0.2", 41 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 42 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 43 | }, 44 | "debug": { 45 | "version": "3.2.6", 46 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", 47 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", 48 | "requires": { 49 | "ms": "^2.1.1" 50 | } 51 | }, 52 | "file-uri-to-path": { 53 | "version": "1.0.0", 54 | "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", 55 | "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" 56 | }, 57 | "inherits": { 58 | "version": "2.0.4", 59 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 60 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 61 | }, 62 | "isarray": { 63 | "version": "1.0.0", 64 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 65 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 66 | }, 67 | "ms": { 68 | "version": "2.1.2", 69 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 70 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 71 | }, 72 | "nan": { 73 | "version": "2.14.0", 74 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", 75 | "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" 76 | }, 77 | "process-nextick-args": { 78 | "version": "2.0.1", 79 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 80 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 81 | }, 82 | "readable-stream": { 83 | "version": "2.3.6", 84 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", 85 | "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", 86 | "requires": { 87 | "core-util-is": "~1.0.0", 88 | "inherits": "~2.0.3", 89 | "isarray": "~1.0.0", 90 | "process-nextick-args": "~2.0.0", 91 | "safe-buffer": "~5.1.1", 92 | "string_decoder": "~1.1.1", 93 | "util-deprecate": "~1.0.1" 94 | } 95 | }, 96 | "safe-buffer": { 97 | "version": "5.1.2", 98 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 99 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 100 | }, 101 | "speaker": { 102 | "version": "0.4.2", 103 | "resolved": "https://registry.npmjs.org/speaker/-/speaker-0.4.2.tgz", 104 | "integrity": "sha512-HnQjSRkUmr2ccLdvGAyUEnp513mQ7k+Gv64qLSkMxVUvVl4zB8Nhw/0wWftqujXXYOE7OU7Vc6TUb64qsO33jg==", 105 | "requires": { 106 | "bindings": "^1.3.0", 107 | "buffer-alloc": "^1.1.0", 108 | "debug": "^3.0.1", 109 | "nan": "^2.6.2", 110 | "readable-stream": "^2.3.3" 111 | } 112 | }, 113 | "string_decoder": { 114 | "version": "1.1.1", 115 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 116 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 117 | "requires": { 118 | "safe-buffer": "~5.1.0" 119 | } 120 | }, 121 | "util-deprecate": { 122 | "version": "1.0.2", 123 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 124 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 125 | }, 126 | "ws": { 127 | "version": "7.2.0", 128 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.0.tgz", 129 | "integrity": "sha512-+SqNqFbwTm/0DC18KYzIsMTnEWpLwJsiasW/O17la4iDRRIO9uaHbvKiAS3AHgTiuuWerK/brj4O6MYZkei9xg==", 130 | "requires": { 131 | "async-limiter": "^1.0.0" 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /server/webroot/js/ui.js: -------------------------------------------------------------------------------- 1 | const UI = function() { 2 | 3 | const zoomY = 200; 4 | const duration = 500; 5 | 6 | let video = null; 7 | let config = null; 8 | 9 | function init(_config) { 10 | 11 | config = _config; 12 | 13 | // Events 14 | addVideoEvents(); 15 | addWebSocketEvents(); 16 | initSubmit(); 17 | addClickEvents(); 18 | 19 | } 20 | 21 | function addWebSocketEvents() { 22 | 23 | const critical = () => { 24 | console.log("Critical error (unauthorized?)"); 25 | // location.reload(); 26 | } 27 | musicPlayer.addEventListener("critical", critical); 28 | broadcastMic.addEventListener("critical", critical); 29 | 30 | musicPlayer.addEventListener("closed", () => { 31 | $("#music").hide(duration); 32 | }); 33 | broadcastMic.addEventListener("closed", () => { 34 | $("#toggleRecordBtn").hide(duration); 35 | }); 36 | 37 | musicPlayer.addEventListener("listsongs", (msg) => { 38 | 39 | console.log(msg.songs); 40 | 41 | msg.songs.forEach(song => { 42 | $("#songs").append(""); 44 | }); 45 | }); 46 | 47 | musicPlayer.addEventListener("receivers", (receivers) => { 48 | if (receivers.length === 0) 49 | $("#music").hide(duration); 50 | else { 51 | $("#music").show(duration); 52 | $("#songs > option").each(function() { 53 | if (receivers.filter(r => r.clientId === $(this).attr("id")).length === 0) { 54 | $(this).remove(); 55 | } 56 | }); 57 | } 58 | }); 59 | 60 | broadcastMic.addEventListener("receivers", (receivers) => { 61 | if (receivers.length === 0) 62 | $("#toggleRecordBtn").hide(duration); 63 | else 64 | $("#toggleRecordBtn").show(duration); 65 | }); 66 | 67 | musicPlayer.addEventListener("playing", () => { 68 | $("#togglePlaySongBtn").attr("src", "img/song-stop-icon.png"); 69 | }); 70 | 71 | musicPlayer.addEventListener("stopped", () => { 72 | $("#togglePlaySongBtn").attr("src", "img/song-play-icon.png"); 73 | }); 74 | 75 | } 76 | 77 | function addVideoEvents() { 78 | 79 | video = document.getElementById("video"); 80 | video.volume = 1; // Max vol 81 | 82 | // Center horizontal scrollbar 83 | video.addEventListener('loadeddata', () => { 84 | 85 | // Center scroll 86 | $("html").scrollLeft($("#video").width() / 2 - $(window).width() / 2); 87 | 88 | // Mobile browsers might disallow autoplay, so show menu instead 89 | if (video.paused) 90 | $("#controls").css("display", "block"); 91 | }); 92 | 93 | // Toggle icons based on video events 94 | video.addEventListener("play", () => { 95 | $("#togglePlayBtn").attr("src", "img/video-pause-icon.svg"); 96 | }); 97 | 98 | video.addEventListener("pause", () => { 99 | $("#togglePlayBtn").attr("src", "img/video-play-icon.svg"); 100 | }); 101 | 102 | video.addEventListener("volumechange", () => { 103 | $("#toggleMuteBtn").attr("src", (video.muted) ? 104 | "img/mute-on-icon.svg" : "img/mute-off-icon.svg"); 105 | }); 106 | } 107 | 108 | function initSubmit() { 109 | // login form 110 | $("#login").submit(event => { 111 | event.preventDefault(); 112 | login(); 113 | }); 114 | $("#token").keypress(function(e) { 115 | if (e.which == 13) 116 | $("#login").submit(); 117 | }); 118 | } 119 | 120 | function addClickEvents() { 121 | $("#toggleMenuBtn").click(toggleMenu); 122 | $("#toggleMuteBtn").click(toggleMute); 123 | $("#togglePlayBtn").click(togglePlay); 124 | $("#togglePlaySongBtn").click(togglePlaySong); 125 | $("#toggleRecordBtn").click(toggleRecord); 126 | $("#zoomInBtn").click(zoomIn); 127 | $("#zoomOutBtn").click(zoomOut); 128 | } 129 | 130 | function login() { 131 | 132 | $("#login").hide(duration); 133 | $("#content").show(duration); 134 | 135 | const token = $("#token").val(); 136 | 137 | // Init JS libs 138 | janusPlayer.init({ 139 | url: config.urls.janus, 140 | turnUrl: ( $("#turn").is(':checked') ) ? config.urls.turn : null, 141 | pin: token, 142 | elVideo: config.dom.video, 143 | elStreams: config.dom.streams 144 | }); 145 | 146 | musicPlayer.init({ 147 | url: config.urls.music, 148 | token: token 149 | }); 150 | 151 | broadcastMic.init({ 152 | url: config.urls.speaker, 153 | token: token, 154 | buffer: 4096 155 | }); 156 | } 157 | 158 | function toggleMenu() { 159 | if ($("#controls").is(":hidden")) { 160 | $("#controls").show(duration); 161 | } else { 162 | $("#controls").hide(duration); 163 | } 164 | } 165 | 166 | function togglePlaySong() { 167 | if (!musicPlayer.isPlaying()) 168 | musicPlayer.play($("#songs").find(":selected").val()); 169 | else 170 | musicPlayer.stop(); 171 | } 172 | 173 | function toggleRecord() { 174 | if (!broadcastMic.isRecording()) { 175 | broadcastMic.start(); 176 | $("#toggleRecordBtn").attr("src", "img/record-stop-icon.png"); 177 | } else { 178 | broadcastMic.stop(); 179 | $("#toggleRecordBtn").attr("src", "img/record-icon.png"); 180 | } 181 | } 182 | 183 | function toggleMute() { 184 | video.muted = (video.muted) ? false : true; 185 | } 186 | 187 | function togglePlay() { 188 | if (video.paused) 189 | video.play(); 190 | else 191 | video.pause(); 192 | } 193 | 194 | function zoomIn() { 195 | zoom(zoomY); 196 | } 197 | 198 | function zoomOut() { 199 | zoom(-zoomY); 200 | } 201 | 202 | function zoom(y) { 203 | let height = $("#video").height() + y; 204 | let top = 0; 205 | if (height < $(window).height()) 206 | top = ($(window).height() / 2) - (height / 2); 207 | $("#video").css({ "top": top, "height": height }); 208 | } 209 | 210 | return { 211 | init: init 212 | } 213 | }(); 214 | -------------------------------------------------------------------------------- /server/websocket-proxy/websocket-proxy.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const WebSocket = require('ws') 3 | const url = require('url'); 4 | const crypto = require("crypto"); 5 | 6 | module.exports = class WebSocketProxy { 7 | 8 | constructor(config) { 9 | this.config = config; 10 | this.config.host = config.host || "localhost"; 11 | this.config.maxPayload = config.maxPayload || 100000; 12 | this.config.maxFailedTokens = config.maxFailedTokens || 20; 13 | this.config.pingInterval = config.pingInterval || 3000; 14 | this.bannedIps = {}; 15 | } 16 | 17 | start() { 18 | console.log("Starting WSProxyServer at " + this.config.host + ":" + this.config.port); 19 | this.server = this.initWebSocketServer(); 20 | this.initHttpServer(this.server); 21 | this.interval = 22 | setInterval(this.pingClient.bind(this), 23 | this.config.pingInterval); 24 | } 25 | 26 | initWebSocketServer() { 27 | const server = new WebSocket.Server({ 28 | noServer: true, 29 | maxPayload: this.config.maxPayload 30 | }); 31 | server.on("connection", (client, request) => this.onClientConnect(client, request)); 32 | server.on("error", e => console.error("WebSocket.Server error: " + e)); 33 | return server; 34 | } 35 | 36 | initHttpServer(server) { 37 | const httpServer = http.createServer(); 38 | httpServer.on("upgrade", (request, socket, head) => { 39 | if (!this.isAuthenticated(request) || 40 | !this.isAllowedOrigin(request) || 41 | !this.isAllowedRole(request)) { 42 | socket.destroy(); 43 | } else 44 | server.handleUpgrade(request, socket, head, client => { 45 | server.emit("connection", client, request); 46 | }); 47 | }); 48 | httpServer.listen(this.config.port); 49 | } 50 | 51 | isAuthenticated(request) { 52 | 53 | const ip = request.headers["x-real-ip"] 54 | const bip = this.bannedIps[ip]; 55 | 56 | if (bip !== undefined && bip >= this.config.maxFailedTokens) { 57 | console.log("IP " + ip + " is banned (too many failed token attempts)!"); 58 | return false; 59 | } 60 | 61 | const token = request.headers["sec-websocket-protocol"] 62 | if (token !== this.config.token) { 63 | console.log("Unauthorized token: " + token + ". IP = " + ip); 64 | this.bannedIps[ip] = (bip === undefined) ? 1 : bip + 1; 65 | return false; 66 | } 67 | 68 | if (bip !== undefined) 69 | delete this.bannedIps[ip]; 70 | 71 | return true; 72 | } 73 | 74 | isAllowedOrigin(request) { 75 | 76 | const orig = request.headers.origin; 77 | const parts = url.parse(request.url, true); 78 | const role = parts.query.role; 79 | 80 | if (role === "transmitter" && orig !== this.config.origin) { 81 | console.error("Bad origin headers: " + orig); 82 | return false; 83 | } 84 | return true; 85 | } 86 | 87 | isAllowedRole(request) { 88 | const parts = url.parse(request.url, true); 89 | const role = parts.query.role; 90 | if (role === undefined || 91 | (role !== "receiver" && role !== "transmitter")) { 92 | console.error("Invalid role '" + role + "'"); 93 | return false; 94 | } 95 | return true; 96 | } 97 | 98 | onClientConnect(client, request) { 99 | client.alive = true; 100 | this.setClientId(client); 101 | this.setClientRole(client, request); 102 | console.log("New connection. id=" + client.id + ", role=" + client.role); 103 | this.connectionNotification(client); 104 | client.on("message", message => this.onClientMessage(client, message)); 105 | client.on("error", e => this.onClientError(e)); 106 | client.on("close", (code, reason) => this.onClientClose(client, code, reason)); 107 | client.on("pong", () => this.onClientPong(client)); 108 | } 109 | 110 | setClientId(client) { 111 | client.id = crypto.randomBytes(16).toString("hex"); 112 | } 113 | 114 | setClientRole(client, request) { 115 | const parts = url.parse(request.url, true); 116 | client.role = parts.query.role; 117 | } 118 | 119 | connectionNotification(client) { 120 | const msg = { 121 | "receivers": this.getReceivers() 122 | } 123 | if (client.role === "transmitter") 124 | client.send(JSON.stringify(msg)); 125 | else if (client.role === "receiver") 126 | this.broadcastTransmitters(JSON.stringify(msg)); 127 | } 128 | 129 | getReceivers() { 130 | let sum = 0; 131 | let receivers = []; 132 | this.server.clients.forEach(client => { 133 | if (client.role === "receiver" && this.isAlive(client)) 134 | receivers.push({ clientId: client.id }); 135 | }); 136 | return receivers; 137 | } 138 | 139 | isAlive(client) { 140 | return (client.alive && client.readyState === WebSocket.OPEN); 141 | } 142 | 143 | broadcastTransmitters(message) { 144 | this.server.clients.forEach(client => { 145 | if (client.role === "transmitter" && this.isAlive(client)) 146 | client.send(message); 147 | }); 148 | } 149 | 150 | onClientMessage(msgClient, message) { 151 | this.server.clients.forEach(client => { 152 | if (msgClient !== client && this.isAlive(client)) { 153 | var msg = JSON.parse(message); 154 | msg.clientId = client.id; 155 | client.send(JSON.stringify(msg)); 156 | } 157 | }); 158 | } 159 | 160 | onClientClose(client, code, reason) { 161 | console.log("Client " + client.id + " was closed: code '" + code + "' reason '" + reason + "'"); 162 | 163 | if (client.role === "receiver") 164 | this.broadcastTransmitters(JSON.stringify({ 165 | "receivers": this.getReceivers() 166 | })); 167 | } 168 | 169 | onClientError(e) { 170 | console.error("Socket " + this.id + " error: " + e.message); 171 | } 172 | 173 | pingClient() { 174 | this.server.clients.forEach(client => { 175 | if (client.alive === false) { 176 | console.log("Client " + client.id + 177 | " has not responded to ping, terminating it."); 178 | client.terminate(); 179 | } 180 | client.alive = false; 181 | client.ping(function() {}); 182 | }); 183 | } 184 | 185 | onClientPong(client) { 186 | // console.log("Client " + client.id + " responded to ping"); 187 | client.alive = true; 188 | } 189 | } -------------------------------------------------------------------------------- /server/webroot-react/src/hooks/janus/useJanus.js: -------------------------------------------------------------------------------- 1 | import Janus from './Janus'; 2 | import { useMemo, useCallback, useState, useRef } from 'react'; 3 | 4 | export const STREAM_TTL_MS = 5000; 5 | 6 | const useJanus = (janusUrl, password, videoEl, useTurn, turnUrl) => { 7 | 8 | const [availableStreams, setAvailableStreams] = useState([]); 9 | 10 | let plugin = useRef(null); 11 | let running = useRef(false); 12 | let restart = useRef(null); 13 | 14 | // Exposed method: init 15 | const init = useCallback(() => { 16 | 17 | // Janus callback method 18 | const janusCallback = () => { 19 | 20 | let janus = null, mediaAttached = false; 21 | 22 | const iceServers = useTurn && turnUrl && turnUrl.length &&[ 23 | { 24 | url: turnUrl, 25 | username: 'babymonitor', 26 | credential: password, 27 | } 28 | ]; 29 | 30 | console.log("iceServers:"); 31 | console.log(iceServers); 32 | 33 | janus = new Janus({ 34 | 35 | server: janusUrl, 36 | 37 | iceServers: iceServers, 38 | 39 | success: () => { 40 | janus.attach({ 41 | plugin: "janus.plugin.streaming", 42 | 43 | success: (pluginHandle) => { 44 | plugin.current = pluginHandle; 45 | 46 | request( 47 | { "request": "list" }, 48 | { 49 | "success": ({ list }) => { 50 | streamsArrived(list); 51 | } 52 | } 53 | ); 54 | }, 55 | 56 | onmessage: (msg, jsep) => { 57 | 58 | console.log("onMessage():"); 59 | console.log(msg); 60 | 61 | const { error_code } = msg; 62 | // Unauthorized 63 | if (error_code === 457) { 64 | console.error(msg.error); 65 | setAvailableStreams([]); 66 | return; 67 | } 68 | 69 | const { result } = msg; 70 | if (result === undefined || result.status === undefined) 71 | return; 72 | 73 | const { status } = result; 74 | if (status === "preparing" && jsep !== undefined) 75 | createAnswer(jsep); 76 | 77 | }, 78 | 79 | onremotestream: (stream) => { 80 | if (mediaAttached) 81 | return; 82 | 83 | console.log("onRemoteStream():"); 84 | console.log(stream); 85 | 86 | Janus.attachMediaStream(videoEl.current, stream); 87 | 88 | mediaAttached = true; 89 | running.current = true; 90 | }, 91 | 92 | error: (cause) => { 93 | console.error("janus.attach error", cause); 94 | } 95 | }); 96 | }, 97 | 98 | error: (error) => { 99 | 100 | console.error("Janus error:"); 101 | console.log(error); 102 | 103 | // Reinit gracefully 104 | if (restart.current === null) { 105 | restart.current = setTimeout(() => { 106 | console.log("Rerunning init()..."); 107 | initJanus(); 108 | restart.current = null; 109 | }, 1000); 110 | } 111 | } 112 | }) 113 | 114 | const request = (message, extra) => { 115 | const msg = { 116 | "message": { 117 | ...message, 118 | pin: password 119 | }, 120 | ...extra 121 | }; 122 | plugin.current.send(msg); 123 | 124 | console.log("request():"); 125 | console.log(msg); 126 | }; 127 | 128 | const streamsArrived = (streams) => { 129 | 130 | console.log("streamsArrived():") 131 | console.log(streams); 132 | 133 | if (!Array.isArray(streams) || streams.length === 0) 134 | return; 135 | 136 | streams = streams 137 | .filter(stream => stream.video_age_ms < STREAM_TTL_MS && 138 | stream.audio_age_ms < STREAM_TTL_MS); 139 | 140 | setAvailableStreams(streams); 141 | } 142 | 143 | const createAnswer = (jsep) => { 144 | const msg = { 145 | "pin": password, 146 | "jsep": jsep, 147 | "media": { "audioSend": false, "videoSend": false }, 148 | "success": (jsep) => request({ "request": "start" }, { jsep: jsep }), 149 | "error": function (error) { 150 | console.error("WebRTC error: ", error); 151 | } 152 | }; 153 | plugin.current.createAnswer(msg); 154 | console.log("createAnswer():"); 155 | console.log(msg); 156 | } 157 | }; 158 | 159 | // Init Janus method 160 | const initJanus = () => { 161 | console.log("initJanus()"); 162 | running.current = false; 163 | Janus.init({ 164 | debug: false, // true, 165 | dependencies: Janus.useDefaultDependencies(), 166 | callback: janusCallback 167 | }); 168 | } 169 | 170 | initJanus(); 171 | 172 | }, [janusUrl, turnUrl, password, videoEl, useTurn]); 173 | 174 | const cleanUp = useCallback(() => { 175 | clearTimeout(restart.current); 176 | restart.current = null; 177 | },[]); 178 | 179 | // Exposed method: Watch stream 180 | const watchStream = useCallback((streamId) => { 181 | 182 | console.log("Running:"); 183 | console.log(running.current); 184 | 185 | if (plugin.current === null) 186 | return; 187 | 188 | const msg = { 189 | "message": { 190 | "request": (!running.current) ? "watch" : "switch", 191 | "id": streamId, 192 | pin: password 193 | } 194 | }; 195 | plugin.current.send(msg); 196 | console.log("setCurrentStream():"); 197 | console.log(msg); 198 | 199 | }, [plugin,password,running] ); 200 | 201 | return useMemo( () => ( { 202 | init: init, 203 | cleanUp: cleanUp, 204 | availableStreams: availableStreams, 205 | watchStream: watchStream 206 | }), [init,availableStreams,watchStream,cleanUp] ); 207 | }; 208 | 209 | export default useJanus; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /server/conf/janus/janus.jcfg: -------------------------------------------------------------------------------- 1 | # General configuration: folders where the configuration and the plugins 2 | # can be found, how output should be logged, whether Janus should run as 3 | # a daemon or in foreground, default interface to use, debug/logging level 4 | # and, if needed, shared apisecret and/or token authentication mechanism 5 | # between application(s) and Janus. 6 | general: { 7 | configs_folder = "/usr/local/etc/janus" # Configuration files folder 8 | plugins_folder = "/usr/local/lib/janus/plugins" # Plugins folder 9 | transports_folder = "/usr/local/lib/janus/transports" # Transports folder 10 | events_folder = "/usr/local/lib/janus/events" # Event handlers folder 11 | 12 | # The next settings configure logging 13 | #log_to_stdout = false # Whether the Janus output should be written 14 | # to stdout or not (default=true) 15 | #log_to_file = "/path/to/janus.log" # Whether to use a log file or not 16 | debug_level = 4 # Debug/logging level, valid values are 0-7 17 | #debug_timestamps = true # Whether to show a timestamp for each log line 18 | #debug_colors = false # Whether colors should be disabled in the log 19 | #debug_locks = true # Whether to enable debugging of locks (very verbose!) 20 | 21 | # This is what you configure if you want to launch Janus as a daemon 22 | #daemonize = true # Whether Janus should run as a daemon 23 | # or not (default=run in foreground) 24 | #pid_file = "/path/to/janus.pid" # PID file to create when Janus has been 25 | # started, and to destroy at shutdown 26 | 27 | # There are different ways you can authenticate the Janus and Admin APIs 28 | #api_secret = "janusrocks" # String that all Janus requests must contain 29 | # to be accepted/authorized by the Janus core. 30 | # Useful if you're wrapping all Janus API requests 31 | # in your servers (that is, not in the browser, 32 | # where you do the things your way) and you 33 | # don't want other application to mess with 34 | # this Janus instance. 35 | #token_auth = false # Enable a token based authentication 36 | # mechanism to force users to always provide 37 | # a valid token in all requests. Useful if 38 | # you want to authenticate requests from web 39 | # users. 40 | #token_auth_secret = "janus" # Use HMAC-SHA1 signed tokens (with token_auth). Note that 41 | # without this, the Admin API MUST 42 | # be enabled, as tokens are added and removed 43 | # through messages sent there. 44 | #admin_secret = "janusoverlord" # String that all Janus requests must contain 45 | # to be accepted/authorized by the admin/monitor. 46 | # only needed if you enabled the admin API 47 | # in any of the available transports. 48 | 49 | # Generic settings 50 | #interface = "1.2.3.4" # Interface to use (will be used in SDP) 51 | #server_name = "MyJanusInstance"# Public name of this Janus instance 52 | # as it will appear in an info request 53 | #session_timeout = 60 # How long (in seconds) we should wait before 54 | # deciding a Janus session has timed out. A 55 | # session times out when no request is received 56 | # for session_timeout seconds (default=60s). 57 | # Setting this to 0 will disable the timeout 58 | # mechanism, which is NOT suggested as it may 59 | # risk having orphaned sessions (sessions not 60 | # controlled by any transport and never freed). 61 | # To avoid timeouts, keep-alives can be used. 62 | #candidates_timeout = 45 # How long (in seconds) we should keep hold of 63 | # pending (trickle) candidates before discarding 64 | # them (default=45s). Notice that setting this 65 | # to 0 will NOT disable the timeout, but will 66 | # be considered an invalid value and ignored. 67 | #reclaim_session_timeout = 0 # How long (in seconds) we should wait for a 68 | # janus session to be reclaimed after the transport 69 | # is gone. After the transport is gone, a session 70 | # times out when no request is received for 71 | # reclaim_session_timeout seconds (default=0s). 72 | # Setting this to 0 will disable the timeout 73 | # mechanism, and sessions will be destroyed immediately 74 | # if the transport is gone. 75 | #recordings_tmp_ext = "tmp" # The extension for recordings, in Janus, is 76 | # .mjr, a custom format we devised ourselves. 77 | # By default, we save to .mjr directly. If you'd 78 | # rather the recording filename have a temporary 79 | # extension while it's being saved, and only 80 | # have the .mjr extension when the recording 81 | # is over (e.g., to automatically trigger some 82 | # external scripts), then uncomment and set the 83 | # recordings_tmp_ext property to the extension 84 | # to add to the base (e.g., tmp --> .mjr.tmp). 85 | #event_loops = 8 # By default, Janus handles each have their own 86 | # event loop and related thread for all the media 87 | # routing and management. If for some reason you'd 88 | # rather limit the number of loop/threads, and 89 | # you want handles to share those, you can do that 90 | # configuring the event_loops property: this will 91 | # spawn the specified amount of threads at startup, 92 | # run a separate event loop on each of them, and 93 | # add new handles to one of them when attaching. 94 | # Notice that, while cutting the number of threads 95 | # and possibly reducing context switching, this 96 | # might have an impact on the media delivery, 97 | # especially if the available loops can't take 98 | # care of all the handles and their media in time. 99 | # As such, if you want to use this you should 100 | # provision the correct value according to the 101 | # available resources (e.g., CPUs available). 102 | #opaqueid_in_api = true # Opaque IDs set by applications are typically 103 | # only passed to event handlers for correlation 104 | # purposes, but not sent back to the user or 105 | # application in the related Janus API responses 106 | # or events; in case you need them to be in the 107 | # Janus API too, set this property to 'true'. 108 | #hide_dependencies = true # By default, a call to the "info" endpoint of 109 | # either the Janus or Admin API now also returns 110 | # the versions of the main dependencies (e.g., 111 | # libnice, libsrtp, which crypto library is in 112 | # use and so on). Should you want that info not 113 | # to be disclose, set 'hide_dependencies' to true. 114 | 115 | # The following is ONLY useful when debugging RTP/RTCP packets, 116 | # e.g., to look at unencrypted live traffic with a browser. By 117 | # default it is obviously disabled, as WebRTC mandates encryption. 118 | #no_webrtc_encryption = true 119 | } 120 | 121 | # Certificate and key to use for DTLS (and passphrase if needed). If missing, 122 | # Janus will autogenerate a self-signed certificate to use. Notice that 123 | # self-signed certificates are fine for the purpose of WebRTC DTLS 124 | # connectivity, for the time being, at least until Identity Providers 125 | # are standardized and implemented in browsers. 126 | certificates: { 127 | #cert_pem = "/path/to/certificate.pem" 128 | #cert_key = "/path/to/key.pem" 129 | #cert_pwd = "secretpassphrase" 130 | } 131 | 132 | # Media-related stuff: you can configure whether if you want 133 | # to enable IPv6 support, if RFC4588 support for retransmissions 134 | # should be negotiated or not (off by default), the maximum size 135 | # of the NACK queue (in milliseconds, defaults to 500ms) for retransmissions, the 136 | # range of ports to use for RTP and RTCP (by default, no range is envisaged), the 137 | # starting MTU for DTLS (1200 by default, it adapts automatically), 138 | # how much time, in seconds, should pass with no media (audio or 139 | # video) being received before Janus notifies you about this (default=1s, 140 | # 0 disables these events entirely), how many lost packets should trigger 141 | # a 'slowlink' event to users (default=4), and how often, in milliseconds, 142 | # to send the Transport Wide Congestion Control feedback information back 143 | # to senders, if negotiated (default=1s). Finally, if you're using BoringSSL 144 | # you can customize the frequency of retransmissions: OpenSSL has a fixed 145 | # value of 1 second (the default), while BoringSSL can override that. Notice 146 | # that lower values (e.g., 100ms) will typically get you faster connection 147 | # times, but may not work in case the RTT of the user is high: as such, 148 | # you should pick a reasonable trade-off (usually 2*max expected RTT). 149 | media: { 150 | #ipv6 = true 151 | #max_nack_queue = 500 152 | #rfc_4588 = true 153 | #rtp_port_range = "20000-40000" 154 | #dtls_mtu = 1200 155 | #no_media_timer = 1 156 | #slowlink_threshold = 4 157 | #twcc_period = 200 158 | #dtls_timeout = 500 159 | } 160 | 161 | # NAT-related stuff: specifically, you can configure the STUN/TURN 162 | # servers to use to gather candidates if the gateway is behind a NAT, 163 | # and srflx/relay candidates are needed. In case STUN is not enough and 164 | # this is needed (it shouldn't), you can also configure Janus to use a 165 | # TURN server# please notice that this does NOT refer to TURN usage in 166 | # browsers, but in the gathering of relay candidates by Janus itself, 167 | # e.g., if you want to limit the ports used by a Janus instance on a 168 | # private machine. Furthermore, you can choose whether Janus should be 169 | # configured to do full-trickle (Janus also trickles its candidates to 170 | # users) rather than the default half-trickle (Janus supports trickle 171 | # candidates from users, but sends its own within the SDP), and whether 172 | # it should work in ICE-Lite mode (by default it doesn't). Finally, 173 | # you can also enable ICE-TCP support (beware that it currently *only* 174 | # works if you enable ICE Lite as well), choose which interfaces should 175 | # be used for gathering candidates, and enable or disable the 176 | # internal libnice debugging, if needed. 177 | nat: { 178 | #stun_server = "stun.voip.eutelia.it" 179 | #stun_port = 3478 180 | nice_debug = false 181 | #full_trickle = true 182 | #ice_lite = true 183 | #ice_tcp = true 184 | 185 | # In case you're deploying Janus on a server which is configured with 186 | # a 1:1 NAT (e.g., Amazon EC2), you might want to also specify the public 187 | # address of the machine using the setting below. This will result in 188 | # all host candidates (which normally have a private IP address) to 189 | # be rewritten with the public address provided in the settings. As 190 | # such, use the option with caution and only if you know what you're doing. 191 | # Make sure you keep ICE Lite disabled, though, as it's not strictly 192 | # speaking a publicly reachable server, and a NAT is still involved. 193 | #nat_1_1_mapping = "1.2.3.4" 194 | 195 | # You can configure a TURN server in two different ways: specifying a 196 | # statically configured TURN server, and thus provide the address of the 197 | # TURN server, the transport (udp/tcp/tls) to use, and a set of valid 198 | # credentials to authenticate... 199 | #turn_server = "myturnserver.com" 200 | #turn_port = 3478 201 | #turn_type = "udp" 202 | #turn_user = "myuser" 203 | #turn_pwd = "mypassword" 204 | 205 | # ... or you can make use of the TURN REST API to get info on one or more 206 | # TURN services dynamically. This makes use of the proposed standard of 207 | # such an API (https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00) 208 | # which is currently available in both rfc5766-turn-server and coturn. 209 | # You enable this by specifying the address of your TURN REST API backend, 210 | # the HTTP method to use (GET or POST) and, if required, the API key Janus 211 | # must provide. 212 | #turn_rest_api = "http://yourbackend.com/path/to/api" 213 | #turn_rest_api_key = "anyapikeyyoumayhaveset" 214 | #turn_rest_api_method = "GET" 215 | 216 | # You can also choose which interfaces should be explicitly used by the 217 | # gateway for the purpose of ICE candidates gathering, thus excluding 218 | # others that may be available. To do so, use the 'ice_enforce_list' 219 | # setting and pass it a comma-separated list of interfaces or IP addresses 220 | # to enforce. This is especially useful if the server hosting the gateway 221 | # has several interfaces, and you only want a subset to be used. Any of 222 | # the following examples are valid: 223 | # ice_enforce_list = "eth0" 224 | # ice_enforce_list = "eth0,eth1" 225 | # ice_enforce_list = "eth0,192.168." 226 | # ice_enforce_list = "eth0,192.168.0.1" 227 | # By default, no interface is enforced, meaning Janus will try to use them all. 228 | #ice_enforce_list = "eth0" 229 | 230 | # In case you don't want to specify specific interfaces to use, but would 231 | # rather tell Janus to use all the available interfaces except some that 232 | # you don't want to involve, you can also choose which interfaces or IP 233 | # addresses should be excluded and ignored by the gateway for the purpose 234 | # of ICE candidates gathering. To do so, use the 'ice_ignore_list' setting 235 | # and pass it a comma-separated list of interfaces or IP addresses to 236 | # ignore. This is especially useful if the server hosting the gateway 237 | # has several interfaces you already know will not be used or will simply 238 | # always slow down ICE (e.g., virtual interfaces created by VMware). 239 | # Partial strings are supported, which means that any of the following 240 | # examples are valid: 241 | # ice_ignore_list = "vmnet8,192.168.0.1,10.0.0.1" 242 | # ice_ignore_list = "vmnet,192.168." 243 | # Just beware that the ICE ignore list is not used if an enforce list 244 | # has been configured. By default, Janus ignores all interfaces whose 245 | # name starts with 'vmnet', to skip VMware interfaces: 246 | ice_ignore_list = "vmnet" 247 | } 248 | 249 | # You can choose which of the available plugins should be 250 | # enabled or not. Use the 'disable' directive to prevent Janus from 251 | # loading one or more plugins: use a comma separated list of plugin file 252 | # names to identify the plugins to disable. By default all available 253 | # plugins are enabled and loaded at startup. 254 | plugins: { 255 | disable = libjanus_audiobridge.so,libjanus_echotest.so,libjanus_recordplay.so,libjanus_sip.so,libjanus_textroom.so,libjanus_videocall.so,libjanus_videoroom.so,libjanus_voicemail.so 256 | #disable = "libjanus_voicemail.so,libjanus_recordplay.so" 257 | } 258 | 259 | # You can choose which of the available transports should be enabled or 260 | # not. Use the 'disable' directive to prevent Janus from loading one 261 | # or more transport: use a comma separated list of transport file names 262 | # to identify the transports to disable. By default all available 263 | # transports are enabled and loaded at startup. 264 | transports: { 265 | disable = libjanus_rabbitmq.so,libjanus_pfunix.so 266 | #disable = "libjanus_rabbitmq.so" 267 | } 268 | 269 | # Event handlers allow you to receive live events from Janus happening 270 | # in core and/or plugins. Since this can require some more resources, 271 | # the feature is disabled by default. Setting broadcast to yes will 272 | # enable them. You can then choose which of the available event handlers 273 | # should be loaded or not. Use the 'disable' directive to prevent Janus 274 | # from loading one or more event handlers: use a comma separated list of 275 | # file names to identify the event handlers to disable. By default, if 276 | # broadcast is set to yes all available event handlers are enabled and 277 | # loaded at startup. Finally, you can choose how often media statistics 278 | # (packets sent/received, losses, etc.) should be sent: by default it's 279 | # once per second (audio and video statistics sent separately), but may 280 | # considered too verbose, or you may want to limit the number of events, 281 | # especially if you have many PeerConnections active. To change this, 282 | # just set 'stats_period' to the number of seconds that should pass in 283 | # between statistics for each handle. Setting it to 0 disables them (but 284 | # not other media-related events). 285 | events: { 286 | #broadcast = true 287 | #disable = "libjanus_sampleevh.so" 288 | #stats_period = 5 289 | } 290 | --------------------------------------------------------------------------------