diff --git a/server/index.js b/server/index.js index 0fc1ab7..3435283 100644 --- a/server/index.js +++ b/server/index.js @@ -74,8 +74,9 @@ function connectToSerial() { serialport.write('x\n'); serialport.on('data', (data) => { - if (data && data.startsWith('OK')) { - okReceived = true; + console.log(data); + const receivedData = data.toString(); + if (receivedData.startsWith('OK')) { // Send the remaining commands serialport.write('Q0\n'); diff --git a/server/stream/3las.server.js b/server/stream/3las.server.js index 330cce6..e12bbef 100644 --- a/server/stream/3las.server.js +++ b/server/stream/3las.server.js @@ -1,11 +1,46 @@ "use strict"; -const fs = require('fs'); -const ws = require('ws'); -const { serverConfig } = require('../server_config'); - -// Load settings from config -const Settings = JSON.parse(fs.readFileSync('server/stream/settings.json', 'utf-8')); +var fs = require('fs'); +const ffmpegStaticPath = require('ffmpeg-static'); +const {serverConfig} = require('../server_config') +/* + Stdin streamer is part of 3LAS (Low Latency Live Audio Streaming) + https://github.com/JoJoBond/3LAS +*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs_1 = require("fs"); +const child_process_1 = require("child_process"); +const ws = __importStar(require("ws")); +const Settings = JSON.parse((0, fs_1.readFileSync)('server/stream/settings.json', 'utf-8')); +const FFmpeg_command = ffmpegStaticPath; class StreamClient { constructor(server, socket) { this.Server = server; @@ -16,127 +51,155 @@ class StreamClient { }; this.Socket.on('error', this.OnError.bind(this)); this.Socket.on('message', this.OnMessage.bind(this)); - this.Socket.on('close', this.OnClose.bind(this)); } - - OnMessage(message) { + OnMessage(message, isBinary) { try { let request = JSON.parse(message.toString()); - if (request.type === "answer") { - // Handle answer type messages if needed - } else if (request.type === "fallback") { - // Assuming SetFallback is not needed or replace with the correct method - console.warn('SetFallback method not defined. Fallback request ignored.'); - } else if (request.type === "stats") { - if (Settings.AdminKey && request.data === Settings.AdminKey) { + if (request.type == "answer") { + + } + else if (request.type == "fallback") { + this.Server.SetFallback(this, request.data); + } + else if (request.type == "stats") { + if (Settings.AdminKey && request.data == Settings.AdminKey) { this.SendText(JSON.stringify({ "type": "stats", "data": this.Server.GetStats(), })); } - } else { - this.OnError(new Error("Invalid message type")); } - } catch (error) { - this.OnError(error); + else { + this.OnError(null); + return; + } + } + catch (_a) { + this.OnError(null); + return; } } - - OnError(error) { - console.error('WebSocket error:', error); + OnError(_err) { this.Server.DestroyClient(this); } - - OnClose() { - console.log('WebSocket connection closed'); - this.Server.DestroyClient(this); - } - Destroy() { - if (this.Socket.readyState === ws.OPEN) { - try { - this.Socket.close(); - } catch (ex) { - console.error('Error while closing socket:', ex); - } + try { + this.Socket.close(); + } + catch (ex) { } } - SendBinary(buffer) { - if (this.Socket.readyState !== ws.OPEN) { - this.OnError(new Error('Socket is not open')); + if (this.Socket.readyState != ws.OPEN) { + this.OnError(null); return; } this.Socket.send(buffer, this.BinaryOptions); } - SendText(text) { - if (this.Socket.readyState !== ws.OPEN) { - this.OnError(new Error('Socket is not open')); + if (this.Socket.readyState != ws.OPEN) { + this.OnError(null); return; } this.Socket.send(text); } + OnIceCandidate(e) { + if (e.candidate) { + this.SendText(JSON.stringify({ + "type": "candidate", + "data": e.candidate + })); + } + } } - class StreamServer { constructor(port, channels, sampleRate) { this.Port = port; this.Channels = channels; this.SampleRate = sampleRate; this.Clients = new Set(); + this.FallbackClients = { + "wav": new Set(), + "mp3": new Set() + }; + this.FallbackProvider = {}; + if (Settings.FallbackUseMp3) { + this.FallbackProvider["mp3"] = AFallbackProvider.Create(this, "mp3"); + } + if (Settings.FallbackUseWav) { + this.FallbackProvider["wav"] = AFallbackProvider.Create(this, "wav"); + } this.StdIn = process.stdin; - this.Buffer = Buffer.alloc(0); + this.SamplesCount = this.SampleRate / 100; + this.Samples = new Int16Array(this.Channels * this.SamplesCount); + this.SamplesPosition = 0; } - Run() { this.Server = new ws.Server({ - host: ["127.0.0.1", "::1"], - port: this.Port, - clientTracking: true, - perMessageDeflate: false + "host": ["127.0.0.1", "::1"], + "port": this.Port, + "clientTracking": true, + "perMessageDeflate": false }); this.Server.on('connection', this.OnServerConnection.bind(this)); this.StdIn.on('data', this.OnStdInData.bind(this)); this.StdIn.resume(); } - + BroadcastBinary(format, buffer) { + this.FallbackClients[format].forEach((function each(client) { + client.SendBinary(buffer); + }).bind(this)); + } OnStdInData(buffer) { - this.Buffer = Buffer.concat([this.Buffer, buffer]); - if (this.Buffer.length >= 8192) { // Adjust the buffer size as needed - this.SendAudioData(this.Buffer); - this.Buffer = Buffer.alloc(0); + for (let i = 0; i < buffer.length; i += 2) { + this.Samples[this.SamplesPosition] = buffer.readInt16LE(i); + this.SamplesPosition++; + if (this.SamplesPosition >= this.Samples.length) { + let data = { + "samples": this.Samples, + "sampleRate": this.SampleRate, + "bitsPerSample": 16, + "channelCount": this.Channels, + "numberOfFrames": this.SamplesCount, + }; + this.Samples = new Int16Array(this.Channels * this.SamplesCount); + this.SamplesPosition = 0; + } + } + for (let format in this.FallbackProvider) { + this.FallbackProvider[format].InsertData(buffer); } } - - SendAudioData(buffer) { - this.Clients.forEach(client => { - try { - client.SendBinary(buffer); - } catch (error) { - console.error('Error sending data to client:', error); - } - }); + OnServerConnection(socket, _request) { + this.Clients.add(new StreamClient(this, socket)); } - - OnServerConnection(socket) { - const client = new StreamClient(this, socket); - this.Clients.add(client); - console.log('New client connected'); + SetFallback(client, format) { + if (format != "mp3" && format != "wav") { + this.DestroyClient(client); + return; + } + this.FallbackClients[format].add(client); + this.FallbackProvider[format].PrimeClient(client); } - DestroyClient(client) { + this.FallbackClients["mp3"].delete(client); + this.FallbackClients["wav"].delete(client); this.Clients.delete(client); client.Destroy(); - console.log('Client connection destroyed'); } - GetStats() { + let fallback = { + "wav": (this.FallbackClients["wav"] ? this.FallbackClients["wav"].size : 0), + "mp3": (this.FallbackClients["mp3"] ? this.FallbackClients["mp3"].size : 0), + }; + for (let format in fallback) { + total += fallback[format]; + } return { - "Total": this.Clients.size, + "Total": total, + "Fallback": fallback, }; } - static Create(options) { if (!options["-port"]) throw new Error("Port undefined. Please use -port to define the port."); @@ -145,7 +208,7 @@ class StreamServer { if (!options["-channels"]) throw new Error("Channels undefined. Please use -channels to define the number of channels."); if (typeof options["-channels"] !== "number" || options["-channels"] !== Math.floor(options["-channels"]) || - !(options["-channels"] === 1 || options["-channels"] === 2)) + !(options["-channels"] == 1 || options["-channels"] == 2)) throw new Error("Invalid channels. Must be either 1 or 2."); if (!options["-samplerate"]) throw new Error("Sample rate undefined. Please use -samplerate to define the sample rate."); @@ -154,21 +217,114 @@ class StreamServer { return new StreamServer(options["-port"], options["-channels"], options["-samplerate"]); } } - +class AFallbackProvider { + constructor(server) { + this.Server = server; + this.Process = (0, child_process_1.spawn)(FFmpeg_command, this.GetFFmpegArguments(), { shell: false, detached: false, stdio: ['pipe', 'pipe', 'ignore'] }); + this.Process.stdout.addListener('data', this.OnData.bind(this)); + } + InsertData(buffer) { + this.Process.stdin.write(buffer); + } + static Create(server, format) { + if (format == "mp3") { + return new FallbackProviderMp3(server); + } + else if (format == "wav") { + return new FallbackProviderWav(server, 384); + } + } +} +class FallbackProviderMp3 extends AFallbackProvider { + constructor(server) { + super(server); + } + GetFFmpegArguments() { + return [ + "-fflags", "+nobuffer+flush_packets", "-flags", "low_delay", "-rtbufsize", "32", "-probesize", "32", + "-f", "s16le", + "-ar", this.Server.SampleRate.toString(), + "-ac", this.Server.Channels.toString(), + "-i", "pipe:0", + "-c:a", "libmp3lame", + "-b:a", serverConfig.audio.audioBitrate, + "-ac", this.Server.Channels.toString(), + "-reservoir", "0", + "-f", "mp3", "-write_xing", "0", "-id3v2_version", "0", + "-fflags", "+nobuffer", "-flush_packets", "1", + "pipe:1" + ]; + } + OnData(chunk) { + this.Server.BroadcastBinary("mp3", chunk); + } + PrimeClient(_) { + } +} +class FallbackProviderWav extends AFallbackProvider { + constructor(server, chunkSize) { + super(server); + if (typeof chunkSize !== "number" || chunkSize !== Math.floor(chunkSize) || chunkSize < 1) + throw new Error("Invalid ChunkSize. Must be natural number greater than or equal to 1."); + this.ChunkSize = chunkSize; + this.ChunkBuffer = Buffer.alloc(0); + this.HeaderBuffer = new Array(); + } + GetFFmpegArguments() { + return [ + "-fflags", "+nobuffer+flush_packets", "-flags", "low_delay", "-rtbufsize", "32", "-probesize", "32", + "-f", "s16le", + "-ar", this.Server.SampleRate.toString(), + "-ac", this.Server.Channels.toString(), + "-i", "pipe:0", + "-c:a", "pcm_s16le", + "-ar", Settings.FallbackWavSampleRate.toString(), + "-ac", "1", + "-f", "wav", + "-flush_packets", "1", "-fflags", "+nobuffer", "-chunk_size", "384", "-packetsize", "384", + "pipe:1" + ]; + } + OnData(chunk) { + // Check if riff for wav + if (this.HeaderBuffer.length == 0) { + // Check if chunk is a header page + let isHeader = (chunk[0] == 0x52 && chunk[1] == 0x49 && chunk[2] == 0x46 && chunk[3] == 0x46); + if (isHeader) { + this.HeaderBuffer.push(chunk); + this.Server.BroadcastBinary("wav", chunk); + } + } + else { + this.ChunkBuffer = Buffer.concat(new Array(this.ChunkBuffer, chunk), this.ChunkBuffer.length + chunk.length); + if (this.ChunkBuffer.length >= this.ChunkSize) { + let chunkBuffer = this.ChunkBuffer; + this.ChunkBuffer = Buffer.alloc(0); + this.Server.BroadcastBinary("wav", chunkBuffer); + } + } + } + PrimeClient(client) { + let headerBuffer = this.HeaderBuffer; + for (let i = 0; i < headerBuffer.length; i++) { + client.SendBinary(headerBuffer[i]); + } + } +} const OptionParser = { - "-port": txt => parseInt(txt, 10), - "-channels": txt => parseInt(txt, 10), - "-samplerate": txt => parseInt(txt, 10) + "-port": function (txt) { return parseInt(txt, 10); }, + "-channels": function (txt) { return parseInt(txt, 10); }, + "-samplerate": function (txt) { return parseInt(txt, 10); } }; - const Options = {}; -for (let i = 2; i < process.argv.length; i += 2) { +// Parse parameters +for (let i = 2; i < (process.argv.length - 1); i += 2) { if (!OptionParser[process.argv[i]]) throw new Error("Invalid argument: '" + process.argv[i] + "'."); if (Options[process.argv[i]]) throw new Error("Redefined argument: '" + process.argv[i] + "'. Please use '" + process.argv[i] + "' only ONCE"); Options[process.argv[i]] = OptionParser[process.argv[i]](process.argv[i + 1]); } - const Server = StreamServer.Create(Options); Server.Run(); +//# sourceMappingURL=3las.server.js.map \ No newline at end of file diff --git a/server/stream/index.js b/server/stream/index.js index 6a84dde..9979df9 100644 --- a/server/stream/index.js +++ b/server/stream/index.js @@ -8,17 +8,17 @@ function enableAudioStream() { serverConfig.webserver.webserverPort = Number(serverConfig.webserver.webserverPort); const flags = `-fflags +nobuffer+flush_packets -flags low_delay -rtbufsize 6192 -probesize 32`; - const codec = `-acodec libmp3lame -ar 48000 -ac ${serverConfig.audio.audioChannels} -b:a ${serverConfig.audio.audioBitrate}`; - const output = `-f mp3 -fflags +nobuffer+flush_packets -flush_packets 1 -bufsize 960`; + const codec = `-acodec pcm_s16le -ar 48000 -ac ${serverConfig.audio.audioChannels}`; + const output = `-f s16le -fflags +nobuffer+flush_packets -packetsize 384 -flush_packets 1 -bufsize 960`; if (process.platform === 'win32') { // Windows ffmpegCommand = "\"" + ffmpeg.replace(/\\/g, '\\\\') + "\""; - ffmpegParams = `${flags} -f dshow -audio_buffer_size 200 -i audio="${serverConfig.audio.audioDevice}" ${codec} ${output} pipe:1 | node server/stream/3las.server.js -port ${serverConfig.webserver.webserverPort + 10} -samplerate 44100 -channels ${serverConfig.audio.audioChannels}`; - } else { + ffmpegParams = `${flags} -f dshow -audio_buffer_size 200 -i audio="${serverConfig.audio.audioDevice}" ${codec} ${output} pipe:1 | node server/stream/3las.server.js -port ${serverConfig.webserver.webserverPort + 10} -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`; + } else { // Linux ffmpegCommand = 'ffmpeg'; - ffmpegParams = `${flags} -f alsa -i "${serverConfig.audio.softwareMode && serverConfig.audio.softwareMode == true ? 'plug' : ''}${serverConfig.audio.audioDevice}" ${codec} ${output} pipe:1 | node server/stream/3las.server.js -port ${serverConfig.webserver.webserverPort + 10} -samplerate 44100 -channels ${serverConfig.audio.audioChannels}`; + ffmpegParams = `${flags} -f alsa -i "${serverConfig.audio.softwareMode && serverConfig.audio.softwareMode == true ? 'plug' : ''}${serverConfig.audio.audioDevice}" ${codec} ${output} pipe:1 | node server/stream/3las.server.js -port ${serverConfig.webserver.webserverPort + 10} -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`; } logInfo("Trying to start audio stream on device: \x1b[35m" + serverConfig.audio.audioDevice);