Compare commits

..

No commits in common. "master" and "parcel-bundler" have entirely different histories.

11 changed files with 130 additions and 425 deletions

3
.gitignore vendored
View File

@ -6,6 +6,3 @@
!/public_htm/.htaccess
/package-lock.json
.cache
/releases
/packages
/electron-build

View File

@ -1,98 +0,0 @@
# Douscord
Created for a study project in <u>Advanced web development</u>, we had to replicate [Discord](https://discordapp.com/) the famous text/vocal/video streaming platform. We had to use *WebSockets*, the *Java Persistence API* and the option we chose is to support *audio streaming* support. We worked on this project for 3-5 weeks.
> This work is under the MIT Licence.
**LEGAL ISSUES**
Discord is an open-source project, but we decided to "*copy*" its behavior without looking at its source. We dumped assets from the original website, any legal issue is protected by non-profit university work.
### Manifest
##### Client
- author: `xdrm-brackets`
- repository: https://www.git.xdrm.io/MTI/discord-client
##### Server
- author: `SeekDaSky`
- repository: https://www.git.xdrm.io/MTI/discord-server
### Technologies
- Java Persistence API ([JPA](http://www.oracle.com/technetwork/java/javaee/tech/persistence-jsp-140049.html))
- WebSocket (*SeekDaSky'*s library [kWebSocket](https://git.seekdasky.ovh/SeekDaSKy/kWebSocket) written in [kotlin](https://kotlinlang.org/))
- [VueJS](https://vuejs.org/) a javascript template renderer framework to manage highly related components in real-time.
- [Electron](https://electronjs.org/) to build native cross-platform desktop applications from the client source.
- [Electron Packager](https://github.com/electron-userland/electron-packager) to build bundled out of the box working applications.
- [Parcel](https://github.com/parcel-bundler/parcel) bundler to build javascript dependencies for either the browser or electron.
## Getting the app
### Linux
#### Debian / Ubuntu
###### With CURL
```bash
curl https://cloud.xdrm.io/index.php/s/bzR594w2MKyc83m/download -o /tmp/douscord.deb;
dpkg -i /tmp/douscord.deb && rm /tmp/douscord.deb;
```
###### With wget
```bash
wget https://cloud.xdrm.io/index.php/s/bzR594w2MKyc83m/download -o /tmp/douscord.deb;
dpkg -i /tmp/douscord.deb && rm /tmp/douscord.deb;
```
###### Or manually from [this link](https://cloud.xdrm.io/index.php/s/bzR594w2MKyc83m/download) then run the following command in the download folder:
```bash
sudo dpkg -i douscord.deb
```
#### From source
```bash
curl https://cloud.xdrm.io/index.php/s/bzR594w2MKyc83m/download -o /douscord-src;
sh ./douscord-src/install.sh;
```

View File

@ -1,43 +0,0 @@
const electron = require('electron');
const app = electron.app; // Module to control application life.
const BrowserWindow = electron.BrowserWindow; // Module to create native browser window.
const path = require('path');
const url = require('url');
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
var mainWindow = null;
// Quit when all windows are closed.
app.on('window-all-closed', function() {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform != 'darwin') {
app.quit();
}
});
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
app.on('ready', function() {
// Create the browser window.
mainWindow = new BrowserWindow({width: 800, height: 600});
// and load the index.html of the app.
// mainWindow.loadURL('file://' + __dirname + '/index.html');
mainWindow.loadURL( url.format({
pathname: path.join(__dirname, 'index.html'),
protocol: 'file:',
slashes: true
}) );
// mainWindow.loadURL('http://douscord/');
// Emitted when the window is closed.
mainWindow.on('closed', function(){
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null;
});
});

View File

@ -1,19 +0,0 @@
{
"name": "douscord",
"description": "Douscord non-copyright respectful copy",
"version": "1.0.0",
"author": "xdrm-brackets <xdrm.brackets.dev@gmail.com> SeekDaSky <mascaro.lucas@yahoo.fr G. Fauvet <gfauvet@gmail.com>",
"license": "MIT",
"private": true,
"dependencies": {
"vue": "^2.5.9",
"vue-router": "^2.5.3"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
],
"devDependencies": {
}
}

View File

@ -1,25 +0,0 @@
#!/bin/bash
# 1. Get + Go to current directory
ROOT="`realpath $(dirname $0)`";
cd $ROOT;
# 2. Install npm dependencies
echo -e "[1/3] installing dependencies";
npm --prefix=$ROOT install;
# 3. Build electron-app
echo -e "[2/3] building electron app";
npm --prefix=$ROOT run build:electron;
# 4. Create launching application
echo -e "[3/3] creating application shorcut";
DESKTOP_APP="[Desktop Entry]\n";
DESKTOP_APP+="Name=Douscord\n";
DESKTOP_APP+="GenericName=Douscord\n"
DESKTOP_APP+="Exec=/bin/bash -c 'cd /home/xdrm-brackets/ubuntu/git.xdrm.io/discord/client/; npm run electron;'\n";
DESKTOP_APP+="Terminal=false\n";
DESKTOP_APP+="Type=Application\n";
DESKTOP_APP+="Categories=Chat;Audio;Messages;Communication\n";
echo -e "$DESKTOP_APP" | sudo tee /usr/share/applications/douscord.desktop > /dev/null && echo ">>> INSTALLATION SUCCESSFUL <<<" || echo ">>> CANNOT CREATE SHORTUT <<<";

View File

@ -1,30 +1,19 @@
{
"name": "douscord",
"name": "ptut-vhost",
"description": "PTUT",
"version": "1.0.0",
"author": "xdrm-brackets <xdrm.brackets.dev@gmail.com> SeekDaSky <mascaro.lucas@yahoo.fr G. Fauvet <gfauvet@gmail.com>",
"license": "MIT",
"private": true,
"scripts": {
"clean": "rm ./public_html/*.html; rm ./public_html/*.js; rm ./public_html/*.css; rm ./public_html/*svg; rm ./public_html/*.map; exit 0",
"clean:all": "npm run clean; npm run clean:electron; npm run clean:release; npm run clean:package; exit 0",
"clean:electron": "rm -r ./electron-build; exit 0",
"clean:release": "rm -r ./releases; exit 0",
"clean:package": "rm -r ./package; exit 0",
"clean": "rm ./public_html/*.{js,css,html,svg,map}",
"dev": "parcel watch ./parcel/index.html --out-dir ./public_html --no-hmr",
"build:alternative": "cross-env NODE_ENV=production parcel watch ./parcel/index.html --out-dir ./public_html --no-hmr",
"build": "parcel build ./parcel/index.html --public-url ./ --out-dir ./public_html --no-source-maps --no-minify",
"build:electron": "parcel build ./parcel/index.html --public-url ./ --out-dir ./electron-build --no-source-maps --no-minify --target=electron; npm run build:electron:setup",
"build:electron:setup": "npm run build:electron:setup-config; npm run build:electron:setup-index;",
"build:electron:setup-config": "cp ./electron.json ./electron-build/package.json; npm --prefix ./electron-build install",
"build:electron:setup-index": "cp ./electron.js ./electron-build/index.js",
"electron": "electron ./electron-build",
"package": "npm run build:electron; electron-packager ./electron-build douscord --asar --platform linux --arch x64 --out ./releases --overwrite;",
"package:deb": "electron-installer-debian --src ./releases/douscord-linux-x64 --dest ./packages --arch x64"
"build": "parcel build ./parcel/index.html --out-dir ./public_html --no-source-maps --no-minify",
"build:electron": "parcel build ./parcel/index.html --out-dir ./public_html --no-source-maps --no-minify --target electron"
},
"dependencies": {
"vue": "^2.5.9",
"vue-hot-reload-api": "^2.3.0",
"vue-router": "^2.5.3"
},
"browserslist": [
@ -39,9 +28,6 @@
"babel-preset-env": "^1.6.0",
"babel-preset-stage-3": "^6.24.1",
"cross-env": "^5.0.5",
"electron": "^1.8.4",
"electron-installer-debian": "^0.8.1",
"electron-packager": "^12.0.0",
"node-sass": "^4.8.3",
"parcel-bundler": "^1.7.0",
"vue-template-compiler": "^2.5.16"

View File

@ -12,9 +12,6 @@ export default class AudioManager{
/* (2) Create the MASTER gain */
this.master = this.ctx.createGain();
this.volume = this.ctx.createGain();
this.peaks = { low: 0, high: 0 };
this.volume_value = 1;
/* (3) Initialise input (typically bound from recorder) */
this.input = null;
@ -59,12 +56,6 @@ export default class AudioManager{
/* (6) Set up our filters' parameters */
this.setUpFilters();
/* (7) Initialise coordinator to manage received */
this.stack = [];
this.stack_size = 2;
this.fade_in = 0.1;
this.fade_out = 0.1;
@ -100,6 +91,7 @@ export default class AudioManager{
}.bind(this.dbg), this.dbg.interval*1000);
}
@ -112,28 +104,27 @@ export default class AudioManager{
---------------------------------------------------------*/
/* (1) Setup EQ#1 -> voice clarity */
this.filters.voice_clarity.type = 'peaking';
this.filters.voice_clarity.frequency.setValueAtTime(3000, this.ctx.currentTime);
this.filters.voice_clarity.Q.setValueAtTime(.8, this.ctx.currentTime);
this.filters.voice_clarity.gain.setValueAtTime(2, this.ctx.currentTime);
this.filters.voice_clarity.frequency.value = 3000;
this.filters.voice_clarity.Q.value = .8;
this.filters.voice_clarity.gain.value = 2;
/* (2) Setup EQ#2 -> voice fullness */
this.filters.voice_fullness.type = 'peaking';
this.filters.voice_fullness.frequency.setValueAtTime(200, this.ctx.currentTime);
this.filters.voice_fullness.Q.setValueAtTime(.8, this.ctx.currentTime);
this.filters.voice_fullness.gain.setValueAtTime(2, this.ctx.currentTime);
this.filters.voice_fullness.frequency.value = 200;
this.filters.voice_fullness.Q.value = .8;
this.filters.voice_fullness.gain.value = 2;
/* (3) Setup EQ#3 -> reduce voice presence */
this.filters.voice_presence.type = 'peaking';
this.filters.voice_presence.frequency.setValueAtTime(5000, this.ctx.currentTime);
this.filters.voice_presence.Q.setValueAtTime(.8, this.ctx.currentTime);
this.filters.voice_presence.gain.setValueAtTime(-2, this.ctx.currentTime);
this.filters.voice_presence.frequency.value = 5000;
this.filters.voice_presence.Q.value = .8;
this.filters.voice_presence.gain.value = -2;
/* (4) Setup EQ#3 -> reduce 'sss' metallic sound */
this.filters.voice_sss.type = 'peaking';
this.filters.voice_sss.frequency.setValueAtTime(7000, this.ctx.currentTime);
this.filters.voice_sss.Q.setValueAtTime(.8, this.ctx.currentTime);
this.filters.voice_sss.gain.setValueAtTime(-8, this.ctx.currentTime);
this.filters.voice_sss.frequency.value = 7000;
this.filters.voice_sss.Q.value = .8;
this.filters.voice_sss.gain.value = -8;
/* (2) Connect filters
@ -160,25 +151,18 @@ export default class AudioManager{
/* (1) Disconnect all by default */
this.input.disconnect();
/* (2) Also link to analyser */
this.input.connect(this.analyser);
/*Chrome fix*/this.network.out.connect(this.output);
/* (3) Get first filter */
/* (2) Get first filter */
let first_filter = this.filters.voice_clarity;
let last_filter = this.filters.voice_sss;
/* (4) If unlink -> connect directly to NETWORK output */
/* (3) If unlink -> connect directly to NETWORK output */
if( unlink === true )
return this.input.connect(this.network.out);
/* (5) If linking -> connect input to volume */
this.input.connect(this.volume);
/* (4) If linking -> connect input to filter stack */
this.input.connect(first_filter);
/* (6) If linking -> connect volume to filter stack */
this.volume.connect(first_filter);
/* (7) If linking -> connect stack end to network.out */
/* (5) If linking -> connect stack end to network.out */
last_filter.connect(this.network.out);
@ -201,6 +185,9 @@ export default class AudioManager{
/* (1) Link through filters */
this.linkFilters();
/* (2) Also link to analyser */
this.input.connect(this.analyser);
gs.get.audio_conn = 2; // voice connected
@ -212,62 +199,7 @@ export default class AudioManager{
---------------------------------------------------------*/
send(_audioprocess){
/* Exit here if not connected */
if( this.ws === null || this.ws.readyState !== 1 )
return;
/* (1) WebSocket send packet
---------------------------------------------------------*/
/* (1) Initialize buffer (Float32Array) */
let buf32 = new Float32Array(AudioManager.BUFFER_SIZE);
/* (2) Extract stream into buffer */
_audioprocess.inputBuffer.copyFromChannel(buf32, 0);
/* (4) Convert for WS connection (Int16Array) */
this.peaks.low = 0;
this.peaks.high = 0;
let buf16 = this.f32toi16(buf32);
/* (5) Send buffer through websocket */
this.ws.send(buf16);
/* (5) Adapt microphone volume if had peaks */
if( this.peaks.high > .01 ) // 30% saturation -> decrease
this.volume_value *= .8;
else if( this.peaks.low > .99 && this.volume_value*1.01 < 1 ) // 90% too low volume + less than 30% saturation -> increase
this.volume_value *= 1.01;
// apply new volume
this.volume.gain.setValueAtTime(this.volume_value, this.ctx.currentTime);
/* (2) WebSocket buffer stack read
---------------------------------------------------------*/
setTimeout(function(){
/* (1) Pop too large stack */
this.stack.length > this.stack_size && this.stack.pop();
/* (2) Read input buffer stack */
if( this.stack.length > 0 ){
// 1. extract our source
let source_node = this.stack.shift();
// 2. Play source node
source_node.start();
}
}.bind(this), 0);
/* (3) Manage analyser
/* (1) Manage analyser
---------------------------------------------------------*/
/* (1) Process only if 'freq_drawer' is set */
if( this.freq_drawer instanceof Function ){
@ -279,7 +211,7 @@ export default class AudioManager{
this.analyser.getByteFrequencyData(freqArray);
// 3. Send to callback
setTimeout(this.freq_drawer.bind(this,freqArray), 0);
this.freq_drawer(freqArray);
}
@ -293,17 +225,34 @@ export default class AudioManager{
this.analyser.getByteTimeDomainData(waveArray);
// 3. Send to callback
setTimeout(this.wave_drawer.bind(this,waveArray), 0);
this.wave_drawer(waveArray);
}
/* (2) WebSocket send packet
---------------------------------------------------------*/
/* (1) Exit here if not connected */
if( this.ws === null || this.ws.readyState !== 1 )
return;
/* (2) Initialize buffer (Float32Array) */
let buf32 = new Float32Array(AudioManager.BUFFER_SIZE);
/* (3) Extract stream into buffer */
_audioprocess.inputBuffer.copyFromChannel(buf32, 0);
/* (4) Convert for WS connection (Int16Array) */
let buf16 = this.f32toi16(buf32);
/* (5) Send buffer through websocket */
this.ws.send(buf16);
// DEBUG
this.dbg.data.packets_sent++;
this.dbg.data.kB_sent += buf16.length * 16. / 8 / 1024;
}
/* (5) Play received chunks (Int16Array)
@ -331,8 +280,11 @@ export default class AudioManager{
source.connect(gain);
gain.connect(this.master);
/* (7) Push in buffer stack */
this.stack.push(source);
/* (7) Start playing */
source.start(this.ctx.currentTime);
this.dbg.data.packets_received++;
this.dbg.data.kB_received += _buffer.length * 16. / 8 / 1024;
}
@ -353,15 +305,8 @@ export default class AudioManager{
let i = 0, l = buf32.length;
/* (3) Convert each value */
for( ; i < l ; i++ ){
for( ; i < l ; i++ )
buf16[i] = (buf32[i] < 0) ? 0x8000 * buf32[i] : 0x7FFF * buf32[i];
( buf32[i] > 0.9 ) && ( this.peaks.high++ );
( buf32[i] < 0.1 ) && ( this.peaks.low++ );
}
/* (4) Report peaks in percentage */
this.peaks.high /= l;
this.peaks.low /= l;
return buf16;
}
@ -422,7 +367,7 @@ export default class AudioManager{
}.bind(this);
/* (3) Debug */
this.ws.onopen = () => (gs.get.audio_conn !== 2 && (gs.get.audio_conn = 1)); // listening
this.ws.onopen = () => ( gs.get.audio_conn = 1 ); // listening
this.ws.onclose = () => ( gs.get.audio_conn = null ); // disconnected
@ -433,60 +378,34 @@ export default class AudioManager{
/* (9) Access microphone + launch all
*
---------------------------------------------------------*/
launch(room_id=0){
launch(wsAddress='wss://ws.douscord.xdrm.io/audio/2'){
/* (1) Start websocket */
this.wsconnect(`wss://ws.douscord.xdrm.io/audio/${room_id}`);
this.wsconnect(wsAddress);
/* (2) Set our streaming binding function */
let streaming_binding = function(stream){
this.recorder = new MediaRecorder(stream);
this.recorder.onstart = function(){
this.bindRecorderStream(stream);
console.warn('[audio] recording');
}.bind(this);
this.recorder.onstop = () => {
this.recorder.stream.getTracks().map( t => t.stop() );
this.recorder = null;
console.warn('[audio] stopped recording');
};
// start recording
this.recorder.start();
}.bind(this);
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
/* (3) If navigator.mediaDevices.getUserMedia */
if( navigator.mediaDevices && navigator.mediaDevices.getUserMedia ){
console.log('[audio] using "navigator.mediaDevices.getUserMedia"')
navigator.mediaDevices.getUserMedia({ audio: true })
.then( stream => {
return navigator.mediaDevices.getUserMedia({ audio: true })
.then(streaming_binding)
.catch((e) => console.warn('[audio] microphone recorder issue', e));
this.recorder = new MediaRecorder(stream);
this.bindRecorderStream(stream);
}
this.recorder.onstart = () => console.warn('[audio] recording');
this.recorder.onstop = () => {
this.recorder.stream.getTracks().map( t => t.stop() );
this.recorder = null;
console.warn('[audio] stopped recording');
};
/* (4) If old version */
if( navigator.getUserMedia ){
// start recording
this.recorder.start();
console.log('[audio] using "navigator.getUserMedia"')
})
.catch( e => console.warn('[audio] microphone permission issue', e) );
return navigator.getUserMedia({ audio: true },
streaming_binding,
(e) => console.warn('[audio] microphone recorder issue', e));
}
console.warn('[audio] recorder not supported');
}else
console.warn('[audio] microphone not supported');
}
@ -497,10 +416,10 @@ export default class AudioManager{
kill(){
/* (1) Close websocket */
this.ws && this.ws.close();
this.ws.close();
/* (2) Stop recording */
this.recorder && this.recorder.stop();
this.recorder.stop();
}

View File

@ -46,20 +46,20 @@ export default class RoomController{
if( type === 'text' )
this[type].current = room.id;
/* (5) If 'voice' room -> toggle audio */
/* (5) Tell websocket: new text room */
if( type === 'text' && window.csock instanceof wscd )
csock.send({ buffer: { rid: room.id } });
/* (6) If 'voice' room -> launch audio */
if( type === 'voice' ){
AudioManager.kill();
csock.send({ buffer: { audio: false } });
if( typeof this[type].current === 'number' )
AudioManager.launch(this[type].current);
AudioManager.launch();
else
AudioManager.kill();
}
/* (6) Tell websocket we connect to room */
if( typeof this[type].current === 'number' && window.csock instanceof wscd )
csock.send({ buffer: { rid: this[type].current } });
/* (6) Update buffer */
this._buffer[type] = {};

View File

@ -182,7 +182,7 @@
display: none;
position: absolute;
top: calc( .5em );
top: calc( 50% - 1em/2 );
left: calc( 100% - .5em - 1em );
width: 1em;
height: 1em;
@ -196,45 +196,6 @@
// only show 'remove' icon on hover
&:hover > span.rem{ display: block; }
// Member List
& > div.member-list{
display: block;
position: relative;
width: calc( 100% - 1em );
overflow: hidden;
& > span{
display: inline-block;
position: relative;
& > span{
display: inline-block;
position: relative;
margin-top: -1.9em;
}
div.icon{
display: inline-block;
position: relative;
width: 1.2em;
height: 1.2em;
margin: .2em .5em;
margin-left: 0;
border-radius: 50% / 50%;
background-color: url() center center no-repeat;
background-size: contain;
}
}
}
}
}

View File

@ -35,12 +35,6 @@
:data-type='r.type'
@click='gs.room.nav(r.type, r.id)'>{{ r.name }}
<span class='rem' @click="gs.popup.show('room.remove'); gs.popup.get('room.remove').data=r"></span>
<div v-if='r.type===`voice`' v-show='r.members.length>0' class='member-list'>
<span v-for='uid in r.members'>
<div class='icon' :style='`background-image: url("https://picsum.photos/150/?random&nonce=${uid}");`'></div>
<span>{{ ( uid == gs.auth.user.uid ) ? 'You' : gs.content.user(uid).username }} </span>
</span>
</div>
</li>
</ul>
</div>
@ -144,25 +138,23 @@ export default {
/* (2) Adjust dimensions */
can.width = buffer.length;
can.height = 256;
let precision = 1; // bigger -> less precise
let precision = 5; // bigger -> less precise
/* (3) Erase previous drawing */
ctx.clearRect(0, 0, can.width, can.height);
ctx.lineWidth = 1;
/* (4) Trace through each value */
for( let i = precision ; i < buffer.length ; i+=precision ){
/* (4) Begin tracing */
ctx.beginPath();
ctx.moveTo(0, can.height-128-buffer[0]/2);
ctx.beginPath();
ctx.strokeStyle = '#44484f';
ctx.moveTo(i, can.height);
ctx.lineTo(i, can.height-buffer[i]);
ctx.stroke();
}
/* (5) Trace through each value */
for( let i = precision ; i < buffer.length ; i+=precision )
ctx.lineTo(i, can.height-128-buffer[i]/2);
/* (6) End tracing */
ctx.lineWidth = 7;
ctx.strokeStyle = '#44484f';
ctx.stroke();
}

35
public_html/index.html Normal file
View File

@ -0,0 +1,35 @@
<html>
<!-- HEADER -->
<head>
<title>Douscord</title>
<!-- META -->
<meta charset="utf-8">
<meta name="author" content="xdrm-brackets (Adrien Marquès)">
<meta name="description" content="[Home] Home page">
<!-- STYLESHEET -->
<link type="text/css" rel="stylesheet" href="/layout.0156ff23.css">
<link type="text/css" rel="stylesheet" href="/global.60d0b554.css">
<link type="text/css" rel="stylesheet" href="/menu.2dbbff61.css">
<link type="text/css" rel="stylesheet" href="/dialog.b6510e56.css">
<link type="text/css" rel="stylesheet" href="/side-menu.d44973a4.css">
<link type="text/css" rel="stylesheet" href="/container.5709ee19.css">
<link type="text/css" rel="stylesheet" href="/pop-up.65b1ed2f.css">
<!-- FONT -->
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro" rel="stylesheet">
<!-- BODY -->
</head>
<body>
<div id="vue"></div>
<script type="text/javascript" src="/main.2909f75e.js"></script>
</body>
</html>