Initial commit.
This commit is contained in:
commit
443bf43dfe
32 changed files with 15952 additions and 0 deletions
2
.env.sample
Normal file
2
.env.sample
Normal file
|
@ -0,0 +1,2 @@
|
|||
AUDIO_FILES_ARRAY=["24Tracks2.ogg", "24TracksShouldBeEnoughForAnybody.ogg", "DesmillahHi-VariousVocalEffects-Guitar-AnyoneCanSee.ogg", "Guitar-VocalEffects-Accents.ogg", "HgInt-Desmillah-Etc-Guitar-Hi.ogg", "HgIntHi-GottaGetOut-VariousVocalEffects-Guitar-NothingMatters.ogg", "HgOpenYourEyesIDontWannaDie-Skaramoush-Drums-NoNoNo.ogg", "HgWhatever.ogg", "HiVocalEffects.ogg", "Mix.ogg", "Mix-2.ogg", "Mix-3.ogg", "Mix-4.ogg", "Mix-5.ogg", "Mix-6.ogg"]
|
||||
ADMIN_PASSWORD=3L6rsLFeTTRDPo1hpCHi0rjq9lBSBTie
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
node_modules/
|
||||
*.log
|
||||
*.s??
|
||||
frontend/public/audio/
|
||||
storage.json
|
||||
.env
|
4
config.js
Normal file
4
config.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
module.exports = {
|
||||
files: JSON.parse(process.env.AUDIO_FILES_ARRAY),
|
||||
password: process.env.ADMIN_PASSWORD,
|
||||
};
|
16
docker-compose.yml
Executable file
16
docker-compose.yml
Executable file
|
@ -0,0 +1,16 @@
|
|||
version: '2'
|
||||
services:
|
||||
app:
|
||||
image: alpine:3.7
|
||||
ports:
|
||||
- 3030:3030
|
||||
- 3031:3031
|
||||
- 3032:3000
|
||||
- 9229:9229
|
||||
volumes:
|
||||
- ./:/app
|
||||
working_dir: /app
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- apk add --no-cache nodejs mediainfo yarn && yarn install && yarn run debug
|
7
frontend/.eslintrc.js
Normal file
7
frontend/.eslintrc.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
extends: "react-app",
|
||||
rules: {
|
||||
"no-restricted-globals": "off",
|
||||
"no-undef": "off",
|
||||
},
|
||||
};
|
21
frontend/.gitignore
vendored
Normal file
21
frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
2444
frontend/README.md
Normal file
2444
frontend/README.md
Normal file
File diff suppressed because it is too large
Load diff
24
frontend/package.json
Normal file
24
frontend/package.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "public",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"material-design-icons": "^3.0.1",
|
||||
"react": "^16.3.2",
|
||||
"react-canvas-knob": "^0.5.0",
|
||||
"react-dom": "^16.3.2",
|
||||
"react-router": "^4.2.0",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"react-scripts": "1.1.4",
|
||||
"react-transform-catch-errors": "^1.0.2",
|
||||
"react-transform-hmr": "^1.0.4",
|
||||
"redbox-react": "^1.6.0",
|
||||
"startaudiocontext": "^1.2.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject"
|
||||
}
|
||||
}
|
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
40
frontend/public/index.html
Normal file
40
frontend/public/index.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is added to the
|
||||
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
15
frontend/public/manifest.json
Normal file
15
frontend/public/manifest.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
45
frontend/src/admin.css
Normal file
45
frontend/src/admin.css
Normal file
|
@ -0,0 +1,45 @@
|
|||
.clients input[type="range"] {
|
||||
display: block;
|
||||
float: left;
|
||||
width: 5em;
|
||||
height: 10em;
|
||||
-webkit-appearance: slider-vertical;
|
||||
}
|
||||
|
||||
.clients {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.clients > li {
|
||||
min-width: 350px;
|
||||
width: 15vw;
|
||||
display: block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.sources > li {
|
||||
background-color: red;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.sources > li.enabled {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
.name-input {
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
.knobs {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.knobs li {
|
||||
list-style-type: none;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.track-controls {
|
||||
display: flex;
|
||||
}
|
235
frontend/src/admin.js
Normal file
235
frontend/src/admin.js
Normal file
|
@ -0,0 +1,235 @@
|
|||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Knob from 'react-canvas-knob';
|
||||
|
||||
import './admin.css';
|
||||
|
||||
const HOST = document.location.host.split(':')[0];
|
||||
|
||||
class Admin extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
passwordSubmitted: false,
|
||||
password: '',
|
||||
syncPacket: {},
|
||||
clients: [],
|
||||
};
|
||||
|
||||
let ws = this.ws = new WebSocket(`ws://${HOST}:3031`);
|
||||
|
||||
ws.addEventListener('close', () => window.location.reload());
|
||||
ws.addEventListener('error', () => window.location.reload());
|
||||
|
||||
ws.addEventListener('message', evt => {
|
||||
let data = JSON.parse(evt.data);
|
||||
let state = this.state;
|
||||
let clients = this.state.clients;
|
||||
|
||||
if(data.type == 'client_add') {
|
||||
this.setState({
|
||||
clients: [...clients, data],
|
||||
});
|
||||
}
|
||||
else if(data.type == 'set_sources') {
|
||||
this.setSources(data.id, data.sources);
|
||||
}
|
||||
else if(data.type == 'timecode') {
|
||||
this.setState({
|
||||
syncPacket: data
|
||||
});
|
||||
}
|
||||
else if(data.type == 'set_name') {
|
||||
this.setState({
|
||||
clients: clients.map(client =>
|
||||
client.id == data.id
|
||||
? Object.assign({}, client, {
|
||||
name: data.name
|
||||
})
|
||||
: client
|
||||
),
|
||||
});
|
||||
}
|
||||
else if(data.type == 'client_remove') {
|
||||
this.setState({
|
||||
clients: this.state.clients.filter(client => client.id != data.id),
|
||||
});
|
||||
}
|
||||
else if(data.type == 'get_clients') {
|
||||
this.setState({
|
||||
clients: data.clients,
|
||||
});
|
||||
}
|
||||
else if(data.type == 'hello_admin') {
|
||||
this.setState({
|
||||
passwordSubmitted: true,
|
||||
});
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'get_clients',
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
doHello() {
|
||||
this.ws.send(JSON.stringify({type: 'hello_admin', password: this.state.password }));
|
||||
}
|
||||
setSources(id, sources) {
|
||||
this.setState({
|
||||
clients: this.state.clients.map(client =>
|
||||
client.id == id
|
||||
? Object.assign({}, client, {
|
||||
sources: sources,
|
||||
})
|
||||
: client
|
||||
),
|
||||
});
|
||||
}
|
||||
clickSource(client, source) {
|
||||
this.ws.send(JSON.stringify({
|
||||
type: 'admin_sources',
|
||||
enabled: !source.enabled,
|
||||
id: client.id,
|
||||
url: source.url,
|
||||
}));
|
||||
}
|
||||
adjustPan(client, source, pan) {
|
||||
this.ws.send(JSON.stringify({
|
||||
type: 'admin_pan',
|
||||
pan: pan,
|
||||
id: client.id,
|
||||
url: source.url,
|
||||
}));
|
||||
|
||||
let sources = client.sources.map(x =>
|
||||
x.id == source.id
|
||||
? Object.assign({}, x, { pan: pan })
|
||||
: x
|
||||
);
|
||||
|
||||
this.setSources(client.id, sources);
|
||||
}
|
||||
adjustGain(client, source, gain) {
|
||||
this.ws.send(JSON.stringify({
|
||||
type: 'admin_gain',
|
||||
gain: gain,
|
||||
id: client.id,
|
||||
url: source.url,
|
||||
}));
|
||||
|
||||
let sources = client.sources.map(x =>
|
||||
x.id == source.id
|
||||
? Object.assign({}, x, { gain: gain })
|
||||
: x
|
||||
);
|
||||
|
||||
this.setSources(client.id, sources);
|
||||
}
|
||||
adjustTimecode(timecode) {
|
||||
this.ws.send(JSON.stringify({
|
||||
type: 'admin_timecode',
|
||||
timecode: timecode,
|
||||
}));
|
||||
|
||||
this.setState({
|
||||
syncPacket: {...this.state.syncPacket, timecode: timecode},
|
||||
});
|
||||
}
|
||||
restart() {
|
||||
this.ws.send(JSON.stringify({
|
||||
type: 'admin_restart',
|
||||
}));
|
||||
|
||||
}
|
||||
muteAll() {
|
||||
this.ws.send(JSON.stringify({
|
||||
type: 'admin_mute',
|
||||
}));
|
||||
|
||||
}
|
||||
unmuteAll() {
|
||||
this.ws.send(JSON.stringify({
|
||||
type: 'admin_unmute',
|
||||
}));
|
||||
|
||||
}
|
||||
changePassword(password) {
|
||||
this.setState({
|
||||
password: password,
|
||||
});
|
||||
}
|
||||
submitPassword() {
|
||||
this.doHello();
|
||||
}
|
||||
render() {
|
||||
const state = this.state
|
||||
return (
|
||||
<div>
|
||||
<h1>
|
||||
Source administrator
|
||||
</h1>
|
||||
{!this.state.passwordSubmitted
|
||||
? (
|
||||
<div className="password-input">
|
||||
Enter password: <input type="text" value={this.state.password} onChange={ (evt) => this.changePassword(evt.target.value) } />
|
||||
<button onClick={ () => this.submitPassword() }>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
Current play position: {this.state.syncPacket.timecode / 1000}<br />
|
||||
<input type="range" min={0} max={370000} step={1} value={this.state.syncPacket.timecode} onChange={ evt => this.adjustTimecode(evt.target.value) } />
|
||||
<div className="track-controls">
|
||||
<button onClick={ () => this.restart() }>
|
||||
Start over
|
||||
</button>
|
||||
<button onClick={ () => this.muteAll() }>
|
||||
Mute All Clients
|
||||
</button>
|
||||
<button onClick={ () => this.unmuteAll() }>
|
||||
Unmute All Clients
|
||||
</button>
|
||||
</div>
|
||||
<ul className="clients">
|
||||
{state.clients.length
|
||||
? state.clients.map((client) =>
|
||||
<li key={client.id}>
|
||||
Client {client.name || client.id} <br />
|
||||
Sources: <br />
|
||||
<ul className="sources">
|
||||
{client.sources.map((source) =>
|
||||
<li key={source.url} className={`${source.enabled && 'enabled' }`}>
|
||||
{source.url.replace(/([A-Z])/g, ' $1')} <br />
|
||||
<ul class="knobs">
|
||||
<li>
|
||||
Pan<br />
|
||||
<Knob bgColor="#000" width={75} height={75} min={-1} value={source.pan} max={1} step={0.01} onChange={ (val) => this.adjustPan(client, source, val) } />
|
||||
</li>
|
||||
<li>
|
||||
Gain<br />
|
||||
<Knob bgColor="#000" width={75} height={75} min={0} value={source.gain} max={5} step={0.01} onChange={ (val) => this.adjustGain(client, source, val) } />
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={ () => this.clickSource(client, source) }>
|
||||
Power
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</li>
|
||||
) : <li><h2><strong>No clients connected! Open up another browser to the homepage and click the Client link.</strong></h2></li>}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Admin;
|
24
frontend/src/app.js
Normal file
24
frontend/src/app.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<ul>
|
||||
<li>
|
||||
<Link to="/admin">Admin</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/client">Client</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/simulator">Simulator</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default App
|
248
frontend/src/buffer-client-backend.js
Normal file
248
frontend/src/buffer-client-backend.js
Normal file
|
@ -0,0 +1,248 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
const HOST = window.location.host.split(':')[0];
|
||||
|
||||
class BufferClientBackend {
|
||||
changeName(name, callback) {
|
||||
this._ws.send(JSON.stringify({
|
||||
type: 'set_name',
|
||||
name: name,
|
||||
}));
|
||||
|
||||
callback && callback();
|
||||
}
|
||||
constructor(options) {
|
||||
this._options = options || {};
|
||||
|
||||
this.changeName = _.debounce(this.changeName.bind(this),3000);
|
||||
|
||||
let AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
this._context = options.context || new AudioContext();
|
||||
}
|
||||
afterContextStarted() {
|
||||
const ALLOWED_OFFSET = 5;
|
||||
|
||||
const loadBuffer = (context, url) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Load buffer asynchronously
|
||||
let request = new XMLHttpRequest();
|
||||
request.open("GET", url, true);
|
||||
request.responseType = "arraybuffer";
|
||||
|
||||
let loader = this;
|
||||
|
||||
request.onload = () => {
|
||||
// Asynchronously decode the audio file data in request.response
|
||||
context.decodeAudioData(
|
||||
request.response,
|
||||
function(buffer) {
|
||||
if (!buffer) {
|
||||
reject('error decoding file data: ' + url);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve({
|
||||
url: url,
|
||||
buffer: buffer,
|
||||
node: null,
|
||||
panner: null,
|
||||
pan: 0,
|
||||
gainer: null,
|
||||
gain: 1,
|
||||
startTime: 0,
|
||||
startPosition: 0,
|
||||
enabled: false,
|
||||
});
|
||||
},
|
||||
err => reject(err)
|
||||
);
|
||||
}
|
||||
|
||||
request.onerror = e => reject(e);
|
||||
|
||||
request.send();
|
||||
});
|
||||
}
|
||||
|
||||
const loadAllBuffers = (context, urls) => {
|
||||
return Promise.all(urls.map(url => loadBuffer(context, 'audio/' + url)));
|
||||
}
|
||||
|
||||
let sources = [];
|
||||
|
||||
let syncPacket = {};
|
||||
let timeOffset = 0;
|
||||
let clientId = 0;
|
||||
|
||||
let mixerNode = this._context.createPanner();
|
||||
mixerNode.connect(this._context.destination);
|
||||
|
||||
let outNode = mixerNode;
|
||||
|
||||
const getRealPosition = () => {
|
||||
return syncPacket.timecode + (Date.now() - (syncPacket.timestamp + timeOffset));
|
||||
}
|
||||
|
||||
this.getRealPosition = getRealPosition;
|
||||
|
||||
const restartSource = (source) => {
|
||||
try {
|
||||
source.node.stop();
|
||||
}
|
||||
catch(e) {}
|
||||
|
||||
let node = source.node = newBufferSource();
|
||||
node.buffer = source.buffer;
|
||||
|
||||
node.connect(source.panner);
|
||||
|
||||
let pos = this.getRealPosition();
|
||||
node.start(0, pos / 1000);
|
||||
|
||||
source.startTime = Date.now();
|
||||
source.startPosition = pos;
|
||||
};
|
||||
|
||||
const getOffset = (source) => {
|
||||
let sourceElapsed = Date.now() - source.startTime + source.startPosition;
|
||||
|
||||
let sourceWrap = Math.abs(sourceElapsed % (source.buffer.duration * 1000));
|
||||
return Math.abs(this.getRealPosition() - sourceWrap);
|
||||
}
|
||||
|
||||
let _unusedBufferSource = null;
|
||||
|
||||
const setUnusedBufferSource = () => {
|
||||
if(_unusedBufferSource) {
|
||||
return;
|
||||
}
|
||||
|
||||
_unusedBufferSource = this._context.createBufferSource();
|
||||
}
|
||||
|
||||
const getUnusedBufferSource = () => {
|
||||
let ubs = _unusedBufferSource;
|
||||
_unusedBufferSource = null;
|
||||
return ubs;
|
||||
};
|
||||
|
||||
const newBufferSource = () => {
|
||||
setUnusedBufferSource();
|
||||
return getUnusedBufferSource();
|
||||
}
|
||||
|
||||
setTimeout(setUnusedBufferSource, 10);
|
||||
|
||||
const updateSources = () => {
|
||||
sources.forEach(source => {
|
||||
source.panner.pan.value = source.pan;
|
||||
source.gainer.gain.value = source.gain;
|
||||
|
||||
if(source.enabled && syncPacket.timecode != -1) {
|
||||
let offset;
|
||||
if (!source.startTime || (offset = getOffset(source)) > .005) {
|
||||
console.log(offset);
|
||||
restartSource(source);
|
||||
}
|
||||
}
|
||||
else {
|
||||
source.startTime = 0;
|
||||
source.startPosition = 0;
|
||||
|
||||
try {
|
||||
source.node.stop();
|
||||
}
|
||||
catch(e) {}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let ws = this._ws = new WebSocket(`ws://${HOST}:3031`);
|
||||
|
||||
ws.addEventListener('open', evt => {
|
||||
ws.send(JSON.stringify({type: 'hello', timestamp: Date.now(), }));
|
||||
});
|
||||
|
||||
ws.addEventListener('close', () => window.location.reload());
|
||||
ws.addEventListener('error', () => window.location.reload());
|
||||
|
||||
ws.addEventListener('message', evt => {
|
||||
let data = JSON.parse(evt.data);
|
||||
|
||||
if(data.type == 'hello') {
|
||||
let finalOffset = Date.now() - data.timestamp;
|
||||
timeOffset = (finalOffset - data.offset) / 2;
|
||||
|
||||
this._options.onTimeOffset && this._options.onTimeOffset(timeOffset);
|
||||
|
||||
console.log(timeOffset);
|
||||
|
||||
clientId = data.id;
|
||||
this._options.onClientId(data.id);
|
||||
|
||||
this._options.onMixerNodeCreated && this._options.onMixerNodeCreated(outNode, data.id);
|
||||
}
|
||||
else if(data.type == 'set_sources' && data.id == clientId) {
|
||||
sources.forEach(src => {
|
||||
Object.assign(src, data.sources.find(x => x.url == src.url));
|
||||
});
|
||||
|
||||
// Duplicated structure because screw it.
|
||||
this._options.onSourcesUpdated && this._options.onSourcesUpdated(data.sources);
|
||||
|
||||
updateSources();
|
||||
}
|
||||
else if(data.type == 'timecode') {
|
||||
let now = Date.now();
|
||||
|
||||
syncPacket = data;
|
||||
this._options.onSyncPacket && this._options.onSyncPacket(data);
|
||||
updateSources();
|
||||
}
|
||||
});
|
||||
|
||||
let main = document.querySelector('main');
|
||||
|
||||
fetch(`http://${HOST}:3030/files.json`)
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
return loadAllBuffers(this._context, res);
|
||||
})
|
||||
.then(srcs => {
|
||||
sources = srcs.map(src => {
|
||||
let node = src.node = newBufferSource();
|
||||
|
||||
node.buffer = src.buffer;
|
||||
|
||||
src.panner = this._context.createStereoPanner();
|
||||
node.connect(src.panner);
|
||||
|
||||
src.gainer = this._context.createGain();
|
||||
|
||||
src.gainer.gain.value = src.gain;
|
||||
|
||||
src.panner.connect(src.gainer);
|
||||
|
||||
src.gainer.connect(outNode);
|
||||
|
||||
return src;
|
||||
});
|
||||
|
||||
ws.send({
|
||||
type: 'get_sources',
|
||||
});
|
||||
|
||||
console.log('sources loaded');
|
||||
})
|
||||
.catch(e => {
|
||||
debugger;
|
||||
console.error(e);
|
||||
});
|
||||
|
||||
}
|
||||
resume() {
|
||||
this._context.resume();
|
||||
}
|
||||
}
|
||||
|
||||
export default BufferClientBackend
|
32
frontend/src/buffer-client.js
Normal file
32
frontend/src/buffer-client.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
|
||||
class BufferClient extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{"Client " + (this.props.clientName || this.props.clientId)}
|
||||
<br />
|
||||
<div>
|
||||
Enter client name: <input value={this.props.clientName} onChange={ (evt) => this.props.onChangeName(evt.target.value) } />
|
||||
<input type="button" className="playit" />
|
||||
<br />
|
||||
</div>
|
||||
Current play position: {this.props.realPosition / 1000}
|
||||
<br />
|
||||
Enabled sources:
|
||||
<ul>
|
||||
{this.props.sources.filter(source => source.enabled).map(source =>
|
||||
<li key={source.url}>
|
||||
{source.url}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default BufferClient;
|
22
frontend/src/client.js
Executable file
22
frontend/src/client.js
Executable file
|
@ -0,0 +1,22 @@
|
|||
'use strict';
|
||||
import BufferClient from './buffer-client.js';
|
||||
import React from 'react';
|
||||
|
||||
let e = React.createElement;
|
||||
let n = null;
|
||||
|
||||
class Client extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
componentDidMount() {
|
||||
this.props.onComponentMount && this.props.onComponentMount();
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<BufferClient {...this.props} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Client;
|
24
frontend/src/index.css
Normal file
24
frontend/src/index.css
Normal file
|
@ -0,0 +1,24 @@
|
|||
* {
|
||||
box-sizing: border-box !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 1em;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
width: 90vw;
|
||||
display: block;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
margin: .5em;
|
||||
border-radius: .5em;
|
||||
}
|
||||
|
265
frontend/src/index.js
Normal file
265
frontend/src/index.js
Normal file
|
@ -0,0 +1,265 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import registerServiceWorker from './registerServiceWorker';
|
||||
import BufferClientBackend from './buffer-client-backend';
|
||||
import StartAudioContext from 'startaudiocontext';
|
||||
import { HashRouter, Route, Switch } from 'react-router-dom';
|
||||
|
||||
import './index.css';
|
||||
import App from './app';
|
||||
import Client from './client';
|
||||
import Admin from './admin';
|
||||
import Simulator from './simulator';
|
||||
|
||||
const HOST = window.location.host.split(':')[0];
|
||||
|
||||
const render = () => ReactDOM.render(router(), document.getElementById('root'));
|
||||
|
||||
const router = () => {
|
||||
return <HashRouter>
|
||||
<Switch>
|
||||
<Route path="/" exact component={App} />
|
||||
<Route path="/client" render={(routeProps) => <Client {...routeProps} {...props.client} />} />
|
||||
<Route path="/admin" render={(routeProps) => <Admin {...routeProps} {...props.admin} />} />
|
||||
<Route path="/simulator" render={(routeProps) => <Simulator {...routeProps} {...props.simulator} />} />
|
||||
</Switch>
|
||||
</HashRouter>
|
||||
};
|
||||
|
||||
const getProps = () => {
|
||||
const props = {
|
||||
admin: {},
|
||||
};
|
||||
|
||||
const createClient = () => {
|
||||
const client = {
|
||||
clientName: '',
|
||||
clientId: '',
|
||||
sources: [],
|
||||
realPosition: 0,
|
||||
};
|
||||
|
||||
client.options = {
|
||||
onTimeOffset: timeOffset => {
|
||||
client.realPosition = backend.getRealPosition();
|
||||
render();
|
||||
},
|
||||
onClientId: clientId => {
|
||||
client.clientId = clientId;
|
||||
render();
|
||||
},
|
||||
onSourcesUpdated: sources => {
|
||||
client.sources = sources
|
||||
render();
|
||||
},
|
||||
onSyncPacket: packet => {
|
||||
client.realPosition = backend.getRealPosition();
|
||||
render();
|
||||
}
|
||||
};
|
||||
|
||||
const backend = new BufferClientBackend(client.options);
|
||||
|
||||
client.onChangeName = (clientName) => {
|
||||
backend.changeName(clientName);
|
||||
client.clientName = clientName;
|
||||
render();
|
||||
};
|
||||
|
||||
client.onComponentMount = () => {
|
||||
StartAudioContext(backend._context, '.playit')
|
||||
.then(() => {
|
||||
backend.afterContextStarted();
|
||||
});
|
||||
};
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
props.client = createClient();
|
||||
|
||||
const createSimulator = () => {
|
||||
let AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
let context = new AudioContext();
|
||||
|
||||
const createClient = () => {
|
||||
let clickedX = 0;
|
||||
let clickedY = 0;
|
||||
let down = false;
|
||||
|
||||
let mixerNode = null;
|
||||
|
||||
let options = {
|
||||
context: context,
|
||||
onTimeOffset: timeOffset => {
|
||||
client.realPosition = backend.getRealPosition();
|
||||
render();
|
||||
},
|
||||
onClientId: clientId => {
|
||||
client.clientId = clientId;
|
||||
render();
|
||||
},
|
||||
onSourcesUpdated: sources => {
|
||||
client.sources = sources;
|
||||
render();
|
||||
},
|
||||
onSyncPacket: packet => {
|
||||
client.realPosition = backend.getRealPosition();
|
||||
render();
|
||||
},
|
||||
onMixerNodeCreated: node => {
|
||||
mixerNode = node;
|
||||
mixerNode.panningModel = 'HRTF';
|
||||
mixerNode.distanceModel = 'inverse';
|
||||
mixerNode.refDistance = 1;
|
||||
mixerNode.maxDistance = 500;
|
||||
mixerNode.rolloffFactor = 0.01;
|
||||
mixerNode.coneInnerAngle = 360;
|
||||
mixerNode.coneOuterAngle = 0;
|
||||
mixerNode.codeOuterGain = 0;
|
||||
|
||||
client.x = 50;
|
||||
client.y = 50;
|
||||
|
||||
render();
|
||||
},
|
||||
};
|
||||
|
||||
let backend = new BufferClientBackend(options);
|
||||
|
||||
const updateX = (newX) => {
|
||||
client.x = newX;
|
||||
|
||||
mixerNode.positionX.value = newX - 250;
|
||||
};
|
||||
|
||||
const updateY = (newY) => {
|
||||
client.y = newY;
|
||||
|
||||
mixerNode.positionZ.value = 250 - newY;
|
||||
}
|
||||
|
||||
let client = {
|
||||
sources: [],
|
||||
realPosition: 0,
|
||||
clientId: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
onMouseDown: (evt) => {
|
||||
down = true;
|
||||
clickedX = evt.clientX;
|
||||
clickedY = evt.clientY;
|
||||
},
|
||||
onMouseUp: () => {
|
||||
down = false;
|
||||
},
|
||||
onMouseMove: (evt) => {
|
||||
if(down) {
|
||||
updateX(client.x + evt.clientX - clickedX);
|
||||
updateY(client.y + evt.clientY - clickedY);
|
||||
|
||||
clickedX = evt.clientX;
|
||||
clickedY = evt.clientY;
|
||||
}
|
||||
|
||||
render();
|
||||
},
|
||||
onChangeName: (name) => {
|
||||
backend.changeName(name);
|
||||
client.name = name;
|
||||
render();
|
||||
},
|
||||
};
|
||||
|
||||
StartAudioContext(backend._context, '.playit')
|
||||
.then(() => {
|
||||
backend.afterContextStarted();
|
||||
});
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
let clients = [];
|
||||
|
||||
const doHello = (password) => {
|
||||
ws.send(JSON.stringify({type: 'hello_admin', password: password }));
|
||||
}
|
||||
|
||||
const updateX = (newX) => {
|
||||
simulator.listenerX = newX;
|
||||
|
||||
context.listener.positionX.value = newX - 250;
|
||||
};
|
||||
|
||||
const updateY = (newY) => {
|
||||
simulator.listenerY = newY;
|
||||
|
||||
context.listener.positionZ.value = 250 - newY;
|
||||
}
|
||||
|
||||
let clickedX = 0;
|
||||
let clickedY = 0;
|
||||
let down = false;
|
||||
|
||||
let simulator = {
|
||||
clients: clients,
|
||||
passwordSubmitted: false,
|
||||
password: '',
|
||||
listenerX: 250,
|
||||
listenerY: 250,
|
||||
onNewMixerNode: () => {
|
||||
clients.push(createClient());
|
||||
render();
|
||||
},
|
||||
onMouseDown: (evt) => {
|
||||
down = true;
|
||||
clickedX = evt.clientX;
|
||||
clickedY = evt.clientY;
|
||||
},
|
||||
onMouseUp: () => {
|
||||
down = false;
|
||||
},
|
||||
onMouseMove: (evt) => {
|
||||
if(down) {
|
||||
updateX(simulator.listenerX + evt.clientX - clickedX);
|
||||
updateY(simulator.listenerY + evt.clientY - clickedY);
|
||||
|
||||
clickedX = evt.clientX;
|
||||
clickedY = evt.clientY;
|
||||
}
|
||||
|
||||
render();
|
||||
},
|
||||
onSubmitPassword: (password) => doHello(password),
|
||||
onChangePassword: (password) => {
|
||||
simulator.password = password;
|
||||
render();
|
||||
},
|
||||
};
|
||||
|
||||
let ws = new WebSocket(`ws://${HOST}:3031`);
|
||||
|
||||
ws.addEventListener('message', evt => {
|
||||
let data = JSON.parse(evt.data);
|
||||
|
||||
if(data.type == 'hello_admin') {
|
||||
simulator.passwordSubmitted = true;
|
||||
render();
|
||||
}
|
||||
});
|
||||
|
||||
return simulator;
|
||||
};
|
||||
|
||||
props.simulator = createSimulator();
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
const props = getProps();
|
||||
|
||||
//<Route path="/admin" component={Admin} />
|
||||
//<Route path="/simulator" component={Simulator} />
|
||||
|
||||
render();
|
||||
registerServiceWorker();
|
117
frontend/src/registerServiceWorker.js
Normal file
117
frontend/src/registerServiceWorker.js
Normal file
|
@ -0,0 +1,117 @@
|
|||
// In production, we register a service worker to serve assets from local cache.
|
||||
|
||||
// This lets the app load faster on subsequent visits in production, and gives
|
||||
// it offline capabilities. However, it also means that developers (and users)
|
||||
// will only see deployed updates on the "N+1" visit to a page, since previously
|
||||
// cached resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
|
||||
// This link also includes instructions on opting out of this behavior.
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
);
|
||||
|
||||
export default function register() {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Lets check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl);
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://goo.gl/SC7cgQ'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Is not local host. Just register service worker
|
||||
registerValidSW(swUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the old content will have been purged and
|
||||
// the fresh content will have been added to the cache.
|
||||
// It's the perfect time to display a "New content is
|
||||
// available; please refresh." message in your web app.
|
||||
console.log('New content is available; please refresh.');
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.');
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl)
|
||||
.then(response => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
if (
|
||||
response.status === 404 ||
|
||||
response.headers.get('content-type').indexOf('javascript') === -1
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
}
|
56
frontend/src/simulator.css
Normal file
56
frontend/src/simulator.css
Normal file
|
@ -0,0 +1,56 @@
|
|||
.grid {
|
||||
position: relative;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background-color: #bbb;
|
||||
border-radius: 1em;
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
.speaker::before {
|
||||
content: "";
|
||||
mask-image: url('../node_modules/material-design-icons/hardware/svg/design/ic_speaker_24px.svg');
|
||||
mask-repeat: none;
|
||||
mask-size: contain;
|
||||
background-color: green;
|
||||
width: 100%;
|
||||
z-index: -1;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.speaker {
|
||||
position: absolute;
|
||||
padding-top: 1em;
|
||||
width: 3em;
|
||||
height: 3em;
|
||||
color: #000;
|
||||
border-radius: 3em;
|
||||
vertical-align: bottom;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.head::before {
|
||||
content: "";
|
||||
mask-image: url('../node_modules/material-design-icons/av/svg/design/ic_hearing_24px.svg');
|
||||
mask-repeat: none;
|
||||
mask-size: contain;
|
||||
background-color: #fb9;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.head {
|
||||
position: absolute;
|
||||
top: 250px;
|
||||
left: 250px;
|
||||
width: 3em;
|
||||
height: 3em;
|
||||
background-color: #f00;
|
||||
border-radius: 1.5em;
|
||||
}
|
46
frontend/src/simulator.js
Normal file
46
frontend/src/simulator.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
import BufferClient from './buffer-client.js';
|
||||
import BufferClientBackend from './buffer-client-backend.js';
|
||||
import React from 'react';
|
||||
|
||||
import './simulator.css';
|
||||
import 'material-design-icons/sprites/svg-sprite/svg-sprite-hardware.css';
|
||||
|
||||
class Simulator extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1>
|
||||
Simulator
|
||||
</h1>
|
||||
<button onClick={ () => this.props.onNewMixerNode() }>
|
||||
Create new client
|
||||
</button>
|
||||
{!this.props.passwordSubmitted
|
||||
? <div className="password-input">
|
||||
Enter password: <input type="text" value={this.props.password} onChange={ (evt) => this.props.onChangePassword(evt.target.value) } />
|
||||
<button onClick={ () => this.props.onSubmitPassword(this.props.password) }>
|
||||
Submit
|
||||
</button>
|
||||
</div> : null}
|
||||
<div className="grid">
|
||||
<div className="head" style={{ top: this.props.listenerY, left: this.props.listenerX }} onMouseMove={this.props.onMouseMove} onMouseUp={this.props.onMouseUp} onMouseDown={this.props.onMouseDown} />
|
||||
{this.props.clients.map(client =>
|
||||
<div key={client.clientId} onMouseOut={client.onMouseMove} onMouseMove={client.onMouseMove} onMouseDown={client.onMouseDown} onMouseUp={client.onMouseUp} className="speaker" style={{ top: client.y , left: client.x }}>
|
||||
{client.name || client.clientId}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="clients">
|
||||
{this.props.clients.map(client =>
|
||||
<BufferClient {...client} key={client.clientId} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Simulator;
|
68
frontend/src/testpage.html
Normal file
68
frontend/src/testpage.html
Normal file
|
@ -0,0 +1,68 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Testy</title>
|
||||
</head>
|
||||
<body>
|
||||
<input type="button" class="starty"></script>
|
||||
<script src="/node_modules/startaudiocontext/StartAudioContext.js"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
(() => {
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
const context = new AudioContext();
|
||||
|
||||
const loadBuffer = (context, url) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Load buffer asynchronously
|
||||
let request = new XMLHttpRequest();
|
||||
request.open("GET", url, true);
|
||||
request.responseType = "arraybuffer";
|
||||
|
||||
let loader = this;
|
||||
|
||||
request.onload = () => {
|
||||
// Asynchronously decode the audio file data in request.response
|
||||
context.decodeAudioData(
|
||||
request.response,
|
||||
function(buffer) {
|
||||
if (!buffer) {
|
||||
reject('error decoding file data: ' + url);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve({
|
||||
url: url,
|
||||
buffer: buffer,
|
||||
node: null,
|
||||
panner: null,
|
||||
pan: 0,
|
||||
gainer: null,
|
||||
gain: 1,
|
||||
startTime: 0,
|
||||
startPosition: 0,
|
||||
enabled: false,
|
||||
});
|
||||
},
|
||||
err => reject(err)
|
||||
);
|
||||
}
|
||||
|
||||
request.onerror = e => reject(e);
|
||||
|
||||
request.send();
|
||||
});
|
||||
}
|
||||
|
||||
StartAudioContext(context, '.starty')
|
||||
.then(() => loadBuffer(context, '/audio/24Tracks2.ogg'))
|
||||
.then(buf => {
|
||||
let source = context.createBufferSource();
|
||||
source.buffer = buf.buffer;
|
||||
source.connect(context.destination);
|
||||
source.start();
|
||||
debugger;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
249
frontend/src/yarn.lock
Normal file
249
frontend/src/yarn.lock
Normal file
|
@ -0,0 +1,249 @@
|
|||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
asap@~2.0.3:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
|
||||
|
||||
core-js@^1.0.0:
|
||||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
|
||||
|
||||
d3-array@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.1.tgz#d1ca33de2f6ac31efadb8e050a021d7e2396d5dc"
|
||||
|
||||
d3-collection@1:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.4.tgz#342dfd12837c90974f33f1cc0a785aea570dcdc2"
|
||||
|
||||
d3-color@1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.2.0.tgz#d1ea19db5859c86854586276ec892cf93148459a"
|
||||
|
||||
d3-dispatch@1:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.3.tgz#46e1491eaa9b58c358fce5be4e8bed626e7871f8"
|
||||
|
||||
d3-drag@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.1.tgz#df8dd4c502fb490fc7462046a8ad98a5c479282d"
|
||||
dependencies:
|
||||
d3-dispatch "1"
|
||||
d3-selection "1"
|
||||
|
||||
d3-ease@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.3.tgz#68bfbc349338a380c44d8acc4fbc3304aa2d8c0e"
|
||||
|
||||
d3-format@1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.3.0.tgz#a3ac44269a2011cdb87c7b5693040c18cddfff11"
|
||||
|
||||
d3-interpolate@1, d3-interpolate@^1.1.6:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.2.0.tgz#40d81bd8e959ff021c5ea7545bc79b8d22331c41"
|
||||
dependencies:
|
||||
d3-color "1"
|
||||
|
||||
d3-scale@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.0.0.tgz#fd8ac78381bc2ed741d8c71770437a5e0549a5a5"
|
||||
dependencies:
|
||||
d3-array "^1.2.0"
|
||||
d3-collection "1"
|
||||
d3-format "1"
|
||||
d3-interpolate "1"
|
||||
d3-time "1"
|
||||
d3-time-format "2"
|
||||
|
||||
d3-selection@1, d3-selection@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.3.0.tgz#d53772382d3dc4f7507bfb28bcd2d6aed2a0ad6d"
|
||||
|
||||
d3-time-format@2:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.1.1.tgz#85b7cdfbc9ffca187f14d3c456ffda268081bb31"
|
||||
dependencies:
|
||||
d3-time "1"
|
||||
|
||||
d3-time@1:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.8.tgz#dbd2d6007bf416fe67a76d17947b784bffea1e84"
|
||||
|
||||
d3-timer@^1.0.4:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.7.tgz#df9650ca587f6c96607ff4e60cc38229e8dd8531"
|
||||
|
||||
encoding@^0.1.11:
|
||||
version "0.1.12"
|
||||
resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
|
||||
dependencies:
|
||||
iconv-lite "~0.4.13"
|
||||
|
||||
fbjs@^0.8.16:
|
||||
version "0.8.16"
|
||||
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db"
|
||||
dependencies:
|
||||
core-js "^1.0.0"
|
||||
isomorphic-fetch "^2.1.1"
|
||||
loose-envify "^1.0.0"
|
||||
object-assign "^4.1.0"
|
||||
promise "^7.1.1"
|
||||
setimmediate "^1.0.5"
|
||||
ua-parser-js "^0.7.9"
|
||||
|
||||
iconv-lite@~0.4.13:
|
||||
version "0.4.23"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3"
|
||||
|
||||
is-stream@^1.0.1:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
||||
|
||||
isomorphic-fetch@^2.1.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
|
||||
dependencies:
|
||||
node-fetch "^1.0.1"
|
||||
whatwg-fetch ">=0.10.0"
|
||||
|
||||
js-tokens@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
|
||||
|
||||
lodash@^4.17.10:
|
||||
version "4.17.10"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
|
||||
|
||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
|
||||
dependencies:
|
||||
js-tokens "^3.0.0"
|
||||
|
||||
node-fetch@^1.0.1:
|
||||
version "1.7.3"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
|
||||
dependencies:
|
||||
encoding "^0.1.11"
|
||||
is-stream "^1.0.1"
|
||||
|
||||
object-assign@^4.1.0, object-assign@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
|
||||
performance-now@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
|
||||
|
||||
performance-now@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
||||
|
||||
promise@^7.1.1:
|
||||
version "7.3.1"
|
||||
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
|
||||
dependencies:
|
||||
asap "~2.0.3"
|
||||
|
||||
prop-types@^15.5.8, prop-types@^15.6.0:
|
||||
version "15.6.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca"
|
||||
dependencies:
|
||||
fbjs "^0.8.16"
|
||||
loose-envify "^1.3.1"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
raf@^3.1.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575"
|
||||
dependencies:
|
||||
performance-now "^2.1.0"
|
||||
|
||||
react-dom@^16.3.2:
|
||||
version "16.3.2"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.3.2.tgz#cb90f107e09536d683d84ed5d4888e9640e0e4df"
|
||||
dependencies:
|
||||
fbjs "^0.8.16"
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
prop-types "^15.6.0"
|
||||
|
||||
react-knob@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-knob/-/react-knob-0.1.0.tgz#1a5eb68b013e528be13b6620167d765fa3830494"
|
||||
|
||||
react-motion@^0.5.2:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316"
|
||||
dependencies:
|
||||
performance-now "^0.2.0"
|
||||
prop-types "^15.5.8"
|
||||
raf "^3.1.0"
|
||||
|
||||
react-move@^2.7.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/react-move/-/react-move-2.7.0.tgz#8f5b3d37cc614b9de90e1e2744c80a1e3ff2fcfd"
|
||||
dependencies:
|
||||
d3-interpolate "^1.1.6"
|
||||
d3-timer "^1.0.4"
|
||||
|
||||
react-rotary-knob@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react-rotary-knob/-/react-rotary-knob-1.1.1.tgz#0646d55583c79ffc7d81204d92ee5d5aa996eef8"
|
||||
dependencies:
|
||||
d3-drag "^1.2.1"
|
||||
d3-scale "^2.0.0"
|
||||
d3-selection "^1.3.0"
|
||||
prop-types "^15.6.0"
|
||||
react-svgmt "^1.1.2"
|
||||
uuid "^3.2.1"
|
||||
|
||||
react-svgmt@^1.1.2:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/react-svgmt/-/react-svgmt-1.1.4.tgz#a9e65d870c8e78b50cfff5d49a51b15c5c2c5703"
|
||||
dependencies:
|
||||
d3-ease "^1.0.3"
|
||||
react-motion "^0.5.2"
|
||||
react-move "^2.7.0"
|
||||
|
||||
react@^16.3.2:
|
||||
version "16.3.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-16.3.2.tgz#fdc8420398533a1e58872f59091b272ce2f91ea9"
|
||||
dependencies:
|
||||
fbjs "^0.8.16"
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
prop-types "^15.6.0"
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
|
||||
setimmediate@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
|
||||
|
||||
startaudiocontext@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/startaudiocontext/-/startaudiocontext-1.2.1.tgz#46d2cab5462c791180acc7223e3bbbc3272c8595"
|
||||
|
||||
systemjs@^0.21.3:
|
||||
version "0.21.3"
|
||||
resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-0.21.3.tgz#76467a34a9a12ead3b11028a27345f7649e46204"
|
||||
|
||||
ua-parser-js@^0.7.9:
|
||||
version "0.7.18"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed"
|
||||
|
||||
uuid@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
|
||||
|
||||
whatwg-fetch@>=0.10.0:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f"
|
7280
frontend/yarn.lock
Normal file
7280
frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
59
index.js
Executable file
59
index.js
Executable file
|
@ -0,0 +1,59 @@
|
|||
'use strict';
|
||||
require('dotenv').config();
|
||||
|
||||
const express = require('express');
|
||||
const q = require('q');
|
||||
const fs = require('fs');
|
||||
const cors = require('cors');
|
||||
const execFile = q.denodeify(require('child_process').execFile);
|
||||
const mediainfo = q.denodeify(require('mediainfo-parser').exec);
|
||||
const path = require('path');
|
||||
const ws = require('./ws');
|
||||
const config = require('./config.js');
|
||||
|
||||
const files = config.files;
|
||||
|
||||
const app = express();
|
||||
|
||||
app.listen(3030, () => console.log('Listening...'));
|
||||
|
||||
const whitelist = ['http://localhost:3032', new RegExp('^http://.*:3032')];
|
||||
|
||||
const corsOptions = {
|
||||
origin: (origin, callback) => {
|
||||
let some = whitelist.find(white =>
|
||||
(typeof white == 'string' && white == origin) ||
|
||||
(white.test && white.test(origin))
|
||||
);
|
||||
|
||||
if(!some) {
|
||||
callback(new Error("CORS FAIL"));
|
||||
}
|
||||
else {
|
||||
callback(null, true);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
app.get('/files.json', cors(corsOptions), (req, res) => {
|
||||
res.json(files);
|
||||
})
|
||||
|
||||
const getDuration = (filename) => {
|
||||
return q.resolve()
|
||||
.then(() => mediainfo(filename))
|
||||
.then(meta => meta.file.track[0].duration );
|
||||
};
|
||||
|
||||
q.resolve()
|
||||
.then(() => getDuration(path.join(__dirname + '/frontend/public/audio', files[0])))
|
||||
.then(audioLength => {
|
||||
let startTime = Date.now();
|
||||
|
||||
ws.init(startTime, audioLength, files.map(x => 'audio/' + x));
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
metaFile.close();
|
||||
process.exit(1);
|
||||
});
|
24
latency.js
Normal file
24
latency.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
const ws = require('ws');
|
||||
const wsServer = new ws.Server({
|
||||
port: 3031,
|
||||
});
|
||||
|
||||
const originAllowed = origin => {
|
||||
console.log(origin);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
wsServer.on('connection', client => {
|
||||
let now = Date.now();
|
||||
|
||||
client.send(now);
|
||||
|
||||
client.on('message', msg => {
|
||||
console.log(msg);
|
||||
|
||||
now = Date.now();
|
||||
|
||||
client.send(now);
|
||||
});
|
||||
});
|
4
nodemon.json
Normal file
4
nodemon.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"verbose": true,
|
||||
"ignore": ["*.json", "frontend/**/*"]
|
||||
}
|
31
package.json
Executable file
31
package.json
Executable file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "web-audio-stream-sync",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"debug": "npm-run-all --parallel frontend debug-server",
|
||||
"debug-server": "nodemon --inspect-brk=0.0.0.0:9229 ./index.js",
|
||||
"frontend": "cd ./frontend && yarn install && yarn run start",
|
||||
|
||||
"start": "npm-run-all --parallel static server",
|
||||
"server": "node ./index.js",
|
||||
"latency": "npm-run-all --parallel static latency-server",
|
||||
"latency-server": "node ./latency.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.4",
|
||||
"dotenv": "^5.0.1",
|
||||
"express": "^4.16.2",
|
||||
"http-server": "^0.10.0",
|
||||
"lowdb": "^1.0.0",
|
||||
"mediainfo-parser": "^1.1.5",
|
||||
"node-storage": "^0.0.7",
|
||||
"npm-run-all": "^4.1.2",
|
||||
"q": "^1.5.1",
|
||||
"ws": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^1.14.7"
|
||||
}
|
||||
}
|
281
ws.js
Normal file
281
ws.js
Normal file
|
@ -0,0 +1,281 @@
|
|||
'use strict';
|
||||
const express = require('express');
|
||||
const ws = require('ws');
|
||||
const q = require('q');
|
||||
const fs = require('fs');
|
||||
const cors = require('cors');
|
||||
const execFile = q.denodeify(require('child_process').execFile);
|
||||
const mediainfo = q.denodeify(require('mediainfo-parser').exec);
|
||||
const path = require('path');
|
||||
const lowdb = require('lowdb');
|
||||
const FileSync = require('lowdb/adapters/FileSync');
|
||||
const readFile = q.denodeify(fs.readFile);
|
||||
|
||||
let PASSWORD = require('./config').password;
|
||||
|
||||
let storage = lowdb(new FileSync(path.join(__dirname, 'storage.json')));
|
||||
|
||||
let _savedSources = storage.get('savedSources').value();
|
||||
|
||||
const savedSources = (set) => {
|
||||
if(set) {
|
||||
_savedSources = set
|
||||
storage.set('savedSources', _savedSources).write();
|
||||
}
|
||||
|
||||
return _savedSources;
|
||||
}
|
||||
|
||||
_savedSources = _savedSources || savedSources({});
|
||||
|
||||
let poller;
|
||||
let startTime;
|
||||
let audioLength;
|
||||
let defaultSources;
|
||||
let muted = false;
|
||||
let words = [];
|
||||
|
||||
readFile('words.txt', 'utf8')
|
||||
.then(data => words = data.split(/[\r\n]+/gi));
|
||||
|
||||
let wsServer;
|
||||
|
||||
const originAllowed = origin => {
|
||||
console.log(origin);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const wsSend = (client, data) => q.denodeify(client.send.bind(client))(JSON.stringify(data));
|
||||
|
||||
const wsBroadcast = data => {
|
||||
if(data.constructor.name == 'WebSocket') {
|
||||
throw new Error('You can\'t pass a socket to this function! Broadcast propagates to all connected clients.');
|
||||
}
|
||||
return Promise.all(
|
||||
Array.from(wsServer.clients).map(client => {
|
||||
if(client.readyState != ws.OPEN) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return client._sendPromise(JSON.stringify(typeof data == 'function' ? data() : data));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const setSources = (client, dontBroadcast) => {
|
||||
let ss = savedSources();
|
||||
if(!client.sources || !client.sources.length) {
|
||||
let clientSources = ss[client.name];
|
||||
if(clientSources && clientSources.length) {
|
||||
client.sources = clientSources;
|
||||
}
|
||||
else {
|
||||
client.sources = defaultSources.map(x => Object.assign({}, x));
|
||||
}
|
||||
}
|
||||
|
||||
let msg = {
|
||||
type: 'set_sources',
|
||||
id: client.id,
|
||||
sources: client.sources,
|
||||
};
|
||||
|
||||
ss[client.name] = client.sources;
|
||||
|
||||
savedSources(ss);
|
||||
|
||||
return dontBroadcast
|
||||
? wsSend(msg)
|
||||
: wsBroadcast(msg)
|
||||
};
|
||||
|
||||
const broadcastTimeCode = () => {
|
||||
return wsBroadcast(() => {
|
||||
let timeCode = muted ? -1 : (Date.now() - startTime) % audioLength;
|
||||
return {type: 'timecode', timecode: timeCode, timestamp: Date.now() }
|
||||
})
|
||||
.then(setPoller, setPoller);
|
||||
};
|
||||
|
||||
const stopPoller = () => poller && clearTimeout(poller);
|
||||
|
||||
const setPoller = () => {
|
||||
stopPoller();
|
||||
return poller = setTimeout(broadcastTimeCode, 100);
|
||||
};
|
||||
|
||||
const initServer = () => {
|
||||
wsServer = new ws.Server({
|
||||
port: 3031,
|
||||
});
|
||||
|
||||
wsServer.on('connection', (client, req) => {
|
||||
client.admin = false;
|
||||
client.sources = [];
|
||||
client.id = Math.floor(Math.random() * 1000000);
|
||||
client.name = client.id;
|
||||
client.hello = false;
|
||||
// Attempt to reduce latency.
|
||||
client._sendPromise = q.denodeify(client.send.bind(client));
|
||||
|
||||
client.on('message', msg => {
|
||||
try {
|
||||
let data = JSON.parse(msg);
|
||||
if(data.type == 'hello') {
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
client.name = data.name || client.name;
|
||||
client.hello = true;
|
||||
|
||||
return wsSend(client, {
|
||||
type: 'hello',
|
||||
id: client.id,
|
||||
name: client.name,
|
||||
offset: Date.now() - data.timestamp,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
})
|
||||
.then(() => setSources(client))
|
||||
.then(() =>
|
||||
wsBroadcast({
|
||||
type: 'client_add',
|
||||
id: client.id,
|
||||
sources: client.sources,
|
||||
name: client.name
|
||||
})
|
||||
)
|
||||
.catch(console.error);
|
||||
}
|
||||
else if(data.type == 'hello_admin') {
|
||||
// Gecting the password wrong shouldn't cause a disconnect.
|
||||
if(data.password == PASSWORD) {
|
||||
wsSend(client, data);
|
||||
|
||||
client.hello = client.admin = true;
|
||||
}
|
||||
}
|
||||
else if(!client.hello) {
|
||||
client.close();
|
||||
return;
|
||||
}
|
||||
else if(data.type == 'get_sources') {
|
||||
setSources(srcClient, true);
|
||||
}
|
||||
else if(data.type == 'set_name') {
|
||||
client.name = data.name;
|
||||
client.sources = [];
|
||||
setSources(client);
|
||||
|
||||
wsBroadcast({
|
||||
type: 'set_name',
|
||||
id: client.id,
|
||||
name: client.name,
|
||||
});
|
||||
}
|
||||
// Admin functions. This isn't secure but whatever.
|
||||
else if(client.admin) {
|
||||
if(data.id && data.url) {
|
||||
let srcClient = Array.from(wsServer.clients).find(x => x.id == data.id);
|
||||
if(!srcClient) {
|
||||
console.log(`Client ${data.id} not found!`);
|
||||
return;
|
||||
}
|
||||
|
||||
let source = srcClient.sources.find(x => x.url == data.url);
|
||||
|
||||
if(!source) {
|
||||
console.log(`Source ${data.url} not found!`);
|
||||
return;
|
||||
}
|
||||
|
||||
if(data.type == 'admin_sources') {
|
||||
source.enabled = data.enabled;
|
||||
|
||||
setSources(srcClient);
|
||||
}
|
||||
else if(data.type == 'admin_gain') {
|
||||
source.gain = data.gain;
|
||||
|
||||
setSources(srcClient);
|
||||
}
|
||||
else if(data.type == 'admin_pan') {
|
||||
source.pan = data.pan;
|
||||
|
||||
setSources(srcClient);
|
||||
}
|
||||
}
|
||||
else if(data.type == 'admin_timecode') {
|
||||
startTime = Date.now() - data.timecode;
|
||||
}
|
||||
else if(data.type == 'admin_mute') {
|
||||
muted = true;
|
||||
}
|
||||
else if(data.type == 'admin_unmute') {
|
||||
muted = false;
|
||||
}
|
||||
else if(data.type == 'admin_restart') {
|
||||
startTime = Date.now();
|
||||
}
|
||||
else if(data.type == 'get_clients') {
|
||||
let json = {
|
||||
type: 'get_clients',
|
||||
clients: Array.from(wsServer.clients).filter(x => !x.admin && x.hello).map(client => ({
|
||||
sources: client.sources,
|
||||
name: client.name,
|
||||
id: client.id,
|
||||
})),
|
||||
};
|
||||
|
||||
wsSend(client, json);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
client.on('close', () => {
|
||||
console.log('Disconnection');
|
||||
|
||||
wsBroadcast({
|
||||
type: 'client_remove',
|
||||
id: client.id,
|
||||
});
|
||||
});
|
||||
|
||||
client.on('error', console.error);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
init: (st, al, s) => {
|
||||
audioLength = al;
|
||||
startTime = st;
|
||||
defaultSources = s.map(x => ({
|
||||
enabled: false,
|
||||
url: x,
|
||||
pan: 0,
|
||||
gain: 1,
|
||||
}));
|
||||
|
||||
for(let k in _savedSources) {
|
||||
let def = defaultSources[0];
|
||||
let sources = _savedSources[k];
|
||||
|
||||
if(sources.every(x => x.pan === def.pan && def.gain === x.gain && def.enabled === x.enabled)) {
|
||||
delete _savedSources[k];
|
||||
}
|
||||
|
||||
savedSources(_savedSources);
|
||||
}
|
||||
|
||||
|
||||
if(!wsServer) {
|
||||
initServer();
|
||||
}
|
||||
|
||||
setPoller();
|
||||
},
|
||||
};
|
Loading…
Add table
Reference in a new issue