extended api, optimized some stuff
This commit is contained in:
parent
0fa7ca78a6
commit
7dc3fb6ece
8 changed files with 95 additions and 51 deletions
|
@ -34,8 +34,28 @@ class Api {
|
||||||
}
|
}
|
||||||
|
|
||||||
#setup() {
|
#setup() {
|
||||||
this.#registerEndpoint(constants.API_PLAY, constants.REQUEST_METHOD_POST, () => {
|
this.#registerEndpoint(constants.API_PLAY, constants.REQUEST_METHOD_POST, async () => {
|
||||||
new AudioServer('/mnt/kingston/public/LEFTOVER.flac');
|
if (global.audioserver !== undefined) {
|
||||||
|
await global.audioserver.destroy();
|
||||||
|
}
|
||||||
|
global.audioserver = new AudioServer('/home/velvettear/mounts/kingston/public/test.pcm');
|
||||||
|
});
|
||||||
|
this.#registerEndpoint(constants.API_PAUSE, constants.REQUEST_METHOD_POST, async () => {
|
||||||
|
if (global.audioserver === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
global.audioserver.pausePlayback();
|
||||||
|
});
|
||||||
|
this.#registerEndpoint(constants.API_RESUME, constants.REQUEST_METHOD_POST, async () => {
|
||||||
|
if (global.audioserver === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.#registerEndpoint(constants.API_STOP, constants.REQUEST_METHOD_POST, async () => {
|
||||||
|
if (global.audioserver === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
global.audioserver.stopPlayback();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,13 +13,12 @@ class AudioServer {
|
||||||
this.port = 0;
|
this.port = 0;
|
||||||
this.buffer = {
|
this.buffer = {
|
||||||
file: file,
|
file: file,
|
||||||
stream: fs.createReadStream(file),
|
stream: fs.createReadStream(file)
|
||||||
limit: (config?.audio.bufferlimit || 256) * 1048576
|
|
||||||
};
|
};
|
||||||
this.clients = [];
|
this.clients = [];
|
||||||
this.sockets = [];
|
this.sockets = [];
|
||||||
this.broadcasts = {};
|
this.broadcasts = {};
|
||||||
this.position = 0;
|
this.progress = 0;
|
||||||
this.server = net.createServer();
|
this.server = net.createServer();
|
||||||
this.#prepare();
|
this.#prepare();
|
||||||
}
|
}
|
||||||
|
@ -41,16 +40,17 @@ class AudioServer {
|
||||||
this.sockets.push(socket);
|
this.sockets.push(socket);
|
||||||
});
|
});
|
||||||
this.server.on('error', (err) => {
|
this.server.on('error', (err) => {
|
||||||
|
logger.error('ERROR IN AUDIOSERVER ' + err);
|
||||||
reject('an error occured preparing the audio server for file \'' + this.file + '\' > ' + err);
|
reject('an error occured preparing the audio server for file \'' + this.file + '\' > ' + err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const stats = await stat(this.buffer.file);
|
const stats = await stat(this.buffer.file);
|
||||||
this.buffer.size = stats.size;
|
this.buffer.size = stats.size;
|
||||||
let divisor = 30;
|
let percentage = 30;
|
||||||
if (!(isNaN(config.audio?.threshold))) {
|
if (!(isNaN(config.audio?.threshold))) {
|
||||||
divisor = config.audio.threshold;
|
percentage = config.audio.threshold;
|
||||||
}
|
}
|
||||||
this.buffer.threshold = (this.buffer.size / 100) / divisor;
|
this.buffer.threshold = (this.buffer.size / 100) * percentage;
|
||||||
this.#announceAudioServer();
|
this.#announceAudioServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,6 +79,7 @@ class AudioServer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
client.audiosocket = socket;
|
client.audiosocket = socket;
|
||||||
|
client.audiosocket.setNoDelay(config?.audio?.nodelay || false);
|
||||||
this.clients.push(client);
|
this.clients.push(client);
|
||||||
this.#setClientState(client, constants.CLIENT_STATE_REGISTERED);
|
this.#setClientState(client, constants.CLIENT_STATE_REGISTERED);
|
||||||
client.audiosocket.on('connect', () => {
|
client.audiosocket.on('connect', () => {
|
||||||
|
@ -98,7 +99,7 @@ class AudioServer {
|
||||||
logger.debug(msg);
|
logger.debug(msg);
|
||||||
});
|
});
|
||||||
client.audiosocket.on('drain', () => {
|
client.audiosocket.on('drain', () => {
|
||||||
if (this.buffer.stream === undefined || !this.buffer.stream.isPaused()) {
|
if (this.buffer.stream === undefined || this.buffer.stream.isPaused() === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// logger.debug(client.getTag() + ' backpressure is relieved, resuming read stream...');
|
// logger.debug(client.getTag() + ' backpressure is relieved, resuming read stream...');
|
||||||
|
@ -179,26 +180,26 @@ class AudioServer {
|
||||||
async #handleStatePlaying(client) {
|
async #handleStatePlaying(client) {
|
||||||
logger.debug(client.getTag() + ' has started playback...');
|
logger.debug(client.getTag() + ' has started playback...');
|
||||||
// TODO: remove - test only
|
// TODO: remove - test only
|
||||||
await sleep(5000);
|
// await sleep(10000);
|
||||||
this.#pausePlayback();
|
// this.#pausePlayback();
|
||||||
}
|
}
|
||||||
|
|
||||||
async #handleStatePaused(client, data) {
|
async #handleStatePaused(client, data) {
|
||||||
if (client === undefined || data === undefined) {
|
if (client === undefined || data === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger.debug(client.getTag() + ' paused playback at position \'' + data.position + '\'...');
|
logger.debug(client.getTag() + ' paused playback, progress: \'' + data.progress + '/' + this.buffer.size + '...');
|
||||||
// TODO: remove - test only
|
// TODO: remove - test only
|
||||||
await sleep(1);
|
// await sleep(100);
|
||||||
this.#startPlayback();
|
// this.#startPlayback();
|
||||||
}
|
}
|
||||||
|
|
||||||
async #handleStateStopped(client, data) {
|
async #handleStateStopped(client, data) {
|
||||||
logger.debug(client.getTag() + ' stopped playback at position \'' + data.position + '\'...');
|
logger.debug(client.getTag() + ' stopped playback, progress: ' + data.progress + '/' + this.buffer.size + '...');
|
||||||
}
|
}
|
||||||
|
|
||||||
async #handleStateError(client, data) {
|
async #handleStateError(client, data) {
|
||||||
logger.error(client.getTag() + ' experienced an error during playback at position \'' + data.position + '\': ' + data.error);
|
logger.error(client.getTag() + ' experienced an error during playback, progress: \'' + data.progress + '/' + this.buffer.size + ': ' + error);
|
||||||
}
|
}
|
||||||
|
|
||||||
#getClientById(clientId) {
|
#getClientById(clientId) {
|
||||||
|
@ -217,38 +218,51 @@ class AudioServer {
|
||||||
async #announceAudioServer() {
|
async #announceAudioServer() {
|
||||||
this.broadcasts[constants.CLIENT_STATE_REGISTERED] = await new Message('audio:initialize', {
|
this.broadcasts[constants.CLIENT_STATE_REGISTERED] = await new Message('audio:initialize', {
|
||||||
port: this.server.address().port,
|
port: this.server.address().port,
|
||||||
|
settings: {
|
||||||
size: this.buffer.size,
|
size: this.buffer.size,
|
||||||
threshold: this.buffer.threshold
|
threshold: this.buffer.threshold,
|
||||||
|
// TODO: GET AUDIO INFO FROM DATABASE
|
||||||
|
audio: {
|
||||||
|
channels: 2,
|
||||||
|
bitDepth: 16,
|
||||||
|
sampleRate: 44100
|
||||||
|
}
|
||||||
|
}
|
||||||
}).broadcast(true);
|
}).broadcast(true);
|
||||||
logger.debug('sent broadcast for audio server to client(s) \'' + this.broadcasts[constants.CLIENT_STATE_REGISTERED] + '\'...');
|
logger.debug('sent broadcast for audio server to client(s) \'' + this.broadcasts[constants.CLIENT_STATE_REGISTERED] + '\'...');
|
||||||
}
|
}
|
||||||
|
|
||||||
async #startPlayback() {
|
async #startPlayback() {
|
||||||
this.broadcasts[constants.CLIENT_STATE_PLAYING] = await new Message('audio:play', { position: this.position }).broadcast();
|
this.broadcasts[constants.CLIENT_STATE_PLAYING] = await new Message('audio:play').broadcast();
|
||||||
logger.debug('sent broadcast to start playback to client(s) \'' + this.broadcasts[constants.CLIENT_STATE_PLAYING] + '\'...');
|
logger.debug('sent broadcast to start playback to client(s) \'' + this.broadcasts[constants.CLIENT_STATE_PLAYING] + '\'...');
|
||||||
}
|
}
|
||||||
|
|
||||||
async #stopPlayback() {
|
async pausePlayback() {
|
||||||
this.broadcasts[constants.CLIENT_STATE_STOPPED] = await new Message('audio:stop').broadcast();
|
|
||||||
logger.debug('sent broadcast to stop playback to client(s) \'' + this.broadcasts[constants.CLIENT_STATE_STOPPED] + '\'...');
|
|
||||||
}
|
|
||||||
|
|
||||||
async #pausePlayback() {
|
|
||||||
this.broadcasts[constants.CLIENT_STATE_PAUSED] = await new Message('audio:pause').broadcast();
|
this.broadcasts[constants.CLIENT_STATE_PAUSED] = await new Message('audio:pause').broadcast();
|
||||||
logger.debug('sent broadcast to pause playback to client(s) \'' + this.broadcasts[constants.CLIENT_STATE_PAUSED] + '\'...');
|
logger.debug('sent broadcast to pause playback to client(s) \'' + this.broadcasts[constants.CLIENT_STATE_PAUSED] + '\'...');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async stopPlayback() {
|
||||||
|
this.broadcasts[constants.CLIENT_STATE_STOPPED] = await new Message('audio:stop').broadcast();
|
||||||
|
logger.debug('sent broadcast to stop playback to client(s) \'' + this.broadcasts[constants.CLIENT_STATE_STOPPED] + '\'...');
|
||||||
|
}
|
||||||
|
|
||||||
async #transmitFile() {
|
async #transmitFile() {
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.buffer.stream.on('data', (data) => {
|
this.buffer.stream.on('data', (data) => {
|
||||||
for (let index = 0; index < this.clients.length; index++) {
|
for (let index = 0; index < this.clients.length; index++) {
|
||||||
const client = this.clients[index];
|
const client = this.clients[index];
|
||||||
|
if (client.audiosocket.destroyed) {
|
||||||
|
this.clients.splice(index, 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (client.audiosocket.write(data) !== true) {
|
if (client.audiosocket.write(data) !== true) {
|
||||||
// logger.debug(client.getTag() + ' detected backpressure, pausing read stream...');
|
// logger.debug(client.getTag() + ' detected backpressure, pausing read stream...');
|
||||||
this.buffer.stream.pause();
|
this.buffer.stream.pause();
|
||||||
}
|
}
|
||||||
if (client.audiosocket.bytesWritten >= this.buffer.size) {
|
if (client.audiosocket.bytesWritten >= this.buffer.size) {
|
||||||
|
logger.warn(client.getTag() + ' transmitted audio file after ' + (Date.now() - timestamp) + 'ms');
|
||||||
client.audiosocket.end();
|
client.audiosocket.end();
|
||||||
client.audiosocket.destroy();
|
client.audiosocket.destroy();
|
||||||
}
|
}
|
||||||
|
@ -260,6 +274,7 @@ class AudioServer {
|
||||||
});
|
});
|
||||||
this.buffer.stream.on('error', (error) => {
|
this.buffer.stream.on('error', (error) => {
|
||||||
// TODO: handle with try / catch
|
// TODO: handle with try / catch
|
||||||
|
logger.debug('STREAM ERROR!');
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -277,6 +292,7 @@ class AudioServer {
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
this.server.close((err) => {
|
this.server.close((err) => {
|
||||||
if (err !== undefined) {
|
if (err !== undefined) {
|
||||||
|
logger.error('ERROR CLOSING AUDIOSERVER ' + err);
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
resolve();
|
resolve();
|
||||||
|
|
|
@ -30,15 +30,15 @@ class Client {
|
||||||
|
|
||||||
#listenForEvents() {
|
#listenForEvents() {
|
||||||
logger.debug(this.getTag() + ' connected to communication server...');
|
logger.debug(this.getTag() + ' connected to communication server...');
|
||||||
this.socket.on('timeout', () => {
|
|
||||||
this.#handleEventTimeout();
|
|
||||||
});
|
|
||||||
this.socket.on('close', () => {
|
this.socket.on('close', () => {
|
||||||
this.#handleEventClose()
|
this.#handleEventClose()
|
||||||
});
|
});
|
||||||
this.socket.on('end', () => {
|
this.socket.on('end', () => {
|
||||||
this.#handleEventEnd()
|
this.#handleEventEnd()
|
||||||
});
|
});
|
||||||
|
this.socket.on('error', (error) => {
|
||||||
|
this.#handleEventError(error);
|
||||||
|
});
|
||||||
this.socket.on('data', (data) => {
|
this.socket.on('data', (data) => {
|
||||||
this.#handleEventData(data, this.socket)
|
this.#handleEventData(data, this.socket)
|
||||||
});
|
});
|
||||||
|
@ -50,20 +50,6 @@ class Client {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async #handleEventData(data) {
|
|
||||||
eventparser.parse(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
#handleEventTimeout() {
|
|
||||||
logger.warn(this.getTag() + ' timed out');
|
|
||||||
this.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
#handleEventHeartbeatTimeout() {
|
|
||||||
logger.warn(this.getTag() + ' heartbeat timed out');
|
|
||||||
this.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
#handleEventClose() {
|
#handleEventClose() {
|
||||||
logger.debug(this.getTag() + ' closed socket to communication server');
|
logger.debug(this.getTag() + ' closed socket to communication server');
|
||||||
server.removeClient(this);
|
server.removeClient(this);
|
||||||
|
@ -74,6 +60,22 @@ class Client {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#handleEventError(error) {
|
||||||
|
if (error === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.debug(this.getTag() + ' encountered an error: ' + error);
|
||||||
|
}
|
||||||
|
|
||||||
|
async #handleEventData(data) {
|
||||||
|
eventparser.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleEventHeartbeatTimeout() {
|
||||||
|
logger.warn(this.getTag() + ' heartbeat timed out');
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
#handleEventNetworkStatistics(data) {
|
#handleEventNetworkStatistics(data) {
|
||||||
logger.debug(this.getTag() + ' network statistics: ' + JSON.stringify(data));
|
logger.debug(this.getTag() + ' network statistics: ' + JSON.stringify(data));
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,8 @@ class Heartbeat extends EventEmitter {
|
||||||
await sleep(this.interval);
|
await sleep(this.interval);
|
||||||
}
|
}
|
||||||
this.alive = false;
|
this.alive = false;
|
||||||
this.ping = process.hrtime.bigint();
|
// this.ping = process.hrtime.bigint();
|
||||||
|
this.ping = Date.now();
|
||||||
await new Message('ping').send(this.client);
|
await new Message('ping').send(this.client);
|
||||||
this.timeout = setTimeout(() => {
|
this.timeout = setTimeout(() => {
|
||||||
this.#sendPing();
|
this.#sendPing();
|
||||||
|
@ -39,12 +40,13 @@ class Heartbeat extends EventEmitter {
|
||||||
eventparser.on('pong', () => {
|
eventparser.on('pong', () => {
|
||||||
logger.debug(this.client.getTag() + ' handling event \'pong\'...');
|
logger.debug(this.client.getTag() + ' handling event \'pong\'...');
|
||||||
this.alive = true;
|
this.alive = true;
|
||||||
this.pong = process.hrtime.bigint();
|
// this.pong = process.hrtime.bigint();
|
||||||
|
this.pong = Date.now();
|
||||||
this.latency = this.pong - this.ping;
|
this.latency = this.pong - this.ping;
|
||||||
this.emit('network-statistics', {
|
this.emit('network-statistics', {
|
||||||
ping: this.ping.toString(),
|
ping: this.ping,
|
||||||
pong: this.pong.toString(),
|
pong: this.pong,
|
||||||
latency: this.latency.toString()
|
latency: this.latency
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ class Server {
|
||||||
this.#addClient(socket);
|
this.#addClient(socket);
|
||||||
});
|
});
|
||||||
this.server.on('error', (err) => {
|
this.server.on('error', (err) => {
|
||||||
|
logger.error('ERROR IN SERVER ' + err);
|
||||||
reject('an unexpected error occured: ' + err);
|
reject('an unexpected error occured: ' + err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
"password": "kannon"
|
"password": "kannon"
|
||||||
},
|
},
|
||||||
"audio": {
|
"audio": {
|
||||||
"threshold": 10,
|
"nodelay": false,
|
||||||
"bufferlimit": 64
|
"threshold": 100
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -15,5 +15,8 @@ module.exports = {
|
||||||
REQUEST_METHOD_GET: 'get',
|
REQUEST_METHOD_GET: 'get',
|
||||||
REQUEST_METHOD_POST: 'post',
|
REQUEST_METHOD_POST: 'post',
|
||||||
|
|
||||||
API_PLAY: '/play'
|
API_PLAY: '/play',
|
||||||
|
API_PAUSE: '/pause',
|
||||||
|
API_RESUME: '/resume',
|
||||||
|
API_STOP: '/stop'
|
||||||
}
|
}
|
Loading…
Reference in a new issue