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 |
--------------------------------------------------------------------------------