From 64f41b93aeb7d31719ecf10551ba450d5acb8cf7 Mon Sep 17 00:00:00 2001 From: Amateur Audio Dude <168192910+AmateurAudioDude@users.noreply.github.com> Date: Sun, 20 Apr 2025 22:31:14 +1000 Subject: [PATCH 1/4] bkram ffmpeg removal implementation --- server/stream/checkFFmpeg.js | 23 +++++ server/stream/index.js | 191 ++++++++++++++++++++++++----------- 2 files changed, 156 insertions(+), 58 deletions(-) create mode 100644 server/stream/checkFFmpeg.js diff --git a/server/stream/checkFFmpeg.js b/server/stream/checkFFmpeg.js new file mode 100644 index 0000000..96d385e --- /dev/null +++ b/server/stream/checkFFmpeg.js @@ -0,0 +1,23 @@ +const { spawn } = require('child_process'); + +function checkFFmpeg() { + return new Promise((resolve, reject) => { + const checkFFmpegProcess = spawn('ffmpeg', ['-version'], { + stdio: ['ignore', 'ignore', 'ignore'], + }); + + checkFFmpegProcess.on('error', () => { + resolve(require('ffmpeg-static')); + }); + + checkFFmpegProcess.on('exit', (code) => { + if (code === 0) { + resolve('ffmpeg'); + } else { + resolve(require('ffmpeg-static')); + } + }); + }); +} + +module.exports = checkFFmpeg; diff --git a/server/stream/index.js b/server/stream/index.js index 6a97b58..10b3105 100644 --- a/server/stream/index.js +++ b/server/stream/index.js @@ -1,59 +1,134 @@ -const { spawn } = require('child_process'); -const ffmpeg = require('ffmpeg-static'); -const { configName, serverConfig, configUpdate, configSave, configExists } = require('../server_config'); -const { logDebug, logError, logInfo, logWarn, logFfmpeg } = require('../console'); - -function enableAudioStream() { - var ffmpegParams, ffmpegCommand; - serverConfig.webserver.webserverPort = Number(serverConfig.webserver.webserverPort); - - const flags = `-fflags +nobuffer+flush_packets -flags low_delay -rtbufsize 6192 -probesize 32`; - const codec = `-acodec pcm_s16le -ar 48000 -ac ${serverConfig.audio.audioChannels}`; - const output = `${serverConfig.audio.audioBoost == true ? '-af "volume=3.5"' : ''} -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 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 48000 -channels ${serverConfig.audio.audioChannels}`; - } - - logInfo("Trying to start audio stream on device: \x1b[35m" + serverConfig.audio.audioDevice); - logInfo(`Using internal audio network port ${serverConfig.webserver.webserverPort + 10}.`); - - // If an audio device is configured, start the stream - if(serverConfig.audio.audioDevice.length > 2) { - let startupSuccess = false; - const childProcess = spawn(ffmpegCommand, [ffmpegParams], { shell: true }); - - childProcess.stdout.on('data', (data) => { - logFfmpeg(`stdout: ${data}`); - }); - - childProcess.stderr.on('data', (data) => { - logFfmpeg(`stderr: ${data}`); - if(data.includes('I/O error')) { - logError('Audio device \x1b[35m' + serverConfig.audio.audioDevice + '\x1b[0m failed to start. Start server with the command \x1b[33mnode . --ffmpegdebug \x1b[0mfor more info.'); - } - if(data.includes('size=') && startupSuccess === false) { - logInfo('Audio stream started up successfully.'); - startupSuccess = true; - } - }); - - childProcess.on('close', (code) => { - logFfmpeg(`Child process exited with code ${code}`); - }); - - childProcess.on('error', (err) => { - logFfmpeg(`Error starting child process: ${err}`); - }); - } -} - -if(configExists()) { - enableAudioStream(); +const { spawn, execSync } = require('child_process'); +const { configName, serverConfig, configUpdate, configSave, configExists } = require('../server_config'); +const { logDebug, logError, logInfo, logWarn, logFfmpeg } = require('../console'); +const checkFFmpeg = require('./checkFFmpeg'); + +let ffmpeg, ffmpegCommand, ffmpegParams; + +function checkAudioUtilities() { + if (process.platform === 'darwin') { + try { + execSync('which rec'); + //console.log('[Audio Utility Check] SoX ("rec") found.'); + } catch (error) { + logError('[Audio Utility Check] Error: SoX ("rec") not found. Please install SoX (e.g., using `brew install sox`).'); + process.exit(1); // Exit the process with an error code + } + } else if (process.platform === 'linux') { + try { + execSync('which arecord'); + //console.log('[Audio Utility Check] ALSA ("arecord") found.'); + } catch (error) { + logError('[Audio Utility Check] Error: ALSA ("arecord") not found. Please ensure ALSA utilities are installed (e.g., using `sudo apt-get install alsa-utils` or `sudo yum install alsa-utils`).'); + process.exit(1); // Exit the process with an error code + } + } else { + //console.log(`[Audio Utility Check] Platform "${process.platform}" does not require explicit checks for rec or arecord.`); + } +} + +function buildCommand() { + // Common audio options for FFmpeg + const baseOptions = { + flags: '-fflags +nobuffer+flush_packets -flags low_delay -rtbufsize 6192 -probesize 32', + codec: `-acodec pcm_s16le -ar 48000 -ac ${serverConfig.audio.audioChannels}`, + output: '-f s16le -fflags +nobuffer+flush_packets -packetsize 384 -flush_packets 1 -bufsize 960' + }; + + if (process.platform === 'win32') { + // Windows: ffmpeg using dshow + logInfo('[Audio Stream] Platform: Windows (win32). Using "dshow" input.'); + ffmpegCommand = `"${ffmpeg.replace(/\\/g, '\\\\')}"`; + return `${ffmpegCommand} ${baseOptions.flags} -f dshow -audio_buffer_size 200 -i audio="${serverConfig.audio.audioDevice}" ` + + `${baseOptions.codec} ${baseOptions.output} pipe:1 | node server/stream/3las.server.js -port ` + + `${serverConfig.webserver.webserverPort + 10} -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`; + } else if (process.platform === 'darwin') { + // macOS: using SoX's rec with coreaudio + if (!serverConfig.audio.ffmpeg) { + logInfo('[Audio Stream] Platform: macOS (darwin) using "coreaudio" with the default audio device.'); + const recCommand = `rec -t coreaudio -b 32 -r 48000 -c ${serverConfig.audio.audioChannels} -t raw -b 16 -r 48000 -c ${serverConfig.audio.audioChannels} -`; + return `${recCommand} | node server/stream/3las.server.js -port ${serverConfig.webserver.webserverPort + 10}` + + ` -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`; + } else { + ffmpegCommand = ffmpeg; + ffmpegParams = `${baseOptions.flags} -f alsa -i "${serverConfig.audio.softwareMode && serverConfig.audio.softwareMode == true ? 'plug' : ''}${serverConfig.audio.audioDevice}" ${baseOptions.codec}`; + ffmpegParams += ` ${baseOptions.output} -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 10 pipe:1 | node server/stream/3las.server.js -port ${serverConfig.webserver.webserverPort + 10} -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`; + return `${ffmpegCommand} ${ffmpegParams}`; + } + } else { + // Linux: use alsa with arecord + // If softwareMode is enabled, prefix the device with 'plug' + if (!serverConfig.audio.ffmpeg) { + const audioDevicePrefix = (serverConfig.audio.softwareMode && serverConfig.audio.softwareMode === true) ? 'plug' : ''; + logInfo('[Audio Stream] Platform: Linux. Using "alsa" input.'); + const recCommand = `while true; do arecord -D "${audioDevicePrefix}${serverConfig.audio.audioDevice}" -f S16_LE -r 48000 -c ${serverConfig.audio.audioChannels} -t raw -; done`; + return `${recCommand} | node server/stream/3las.server.js -port ${serverConfig.webserver.webserverPort + 10}` + + ` -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`; + } else { + ffmpegCommand = ffmpeg; + ffmpegParams = `${baseOptions.flags} -f alsa -i "${serverConfig.audio.softwareMode && serverConfig.audio.softwareMode == true ? 'plug' : ''}${serverConfig.audio.audioDevice}" ${baseOptions.codec}`; + ffmpegParams += ` ${baseOptions.output} -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 10 pipe:1 | node server/stream/3las.server.js -port ${serverConfig.webserver.webserverPort + 10} -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`; + return `${ffmpegCommand} ${ffmpegParams}`; + } + } +} + +function enableAudioStream() { + // Ensure the webserver port is a number. + serverConfig.webserver.webserverPort = Number(serverConfig.webserver.webserverPort); + let startupSuccess = false; + const command = buildCommand(); + + // Only log audio device details if the platform is not macOS. + if (process.platform !== 'darwin') { + logInfo(`Trying to start audio stream on device: \x1b[35m${serverConfig.audio.audioDevice}\x1b[0m`); + } + else { + // For macOS, log the default audio device. + logInfo(`Trying to start audio stream on default input device.`); + } + + logInfo(`Using internal audio network port: ${serverConfig.webserver.webserverPort + 10}`); + logInfo('Using', ffmpeg === 'ffmpeg' ? 'system-installed FFmpeg' : 'ffmpeg-static'); + logDebug(`[Audio Stream] Full command:\n${command}`); + + // Start the stream only if a valid audio device is configured. + if (serverConfig.audio.audioDevice && serverConfig.audio.audioDevice.length > 2) { + const childProcess = spawn(command, { shell: true }); + + childProcess.stdout.on('data', (data) => { + logFfmpeg(`[stream:stdout] ${data}`); + }); + + childProcess.stderr.on('data', (data) => { + logFfmpeg(`[stream:stderr] ${data}`); + + if (data.includes('I/O error')) { + logError(`[Audio Stream] Audio device "${serverConfig.audio.audioDevice}" failed to start.`); + logError('Please start the server with: node . --ffmpegdebug for more info.'); + } + if (data.includes('size=') && !startupSuccess) { + logInfo('[Audio Stream] Audio stream started up successfully.'); + startupSuccess = true; + } + }); + + childProcess.on('close', (code) => { + logFfmpeg(`[Audio Stream] Child process exited with code: ${code}`); + }); + + childProcess.on('error', (err) => { + logFfmpeg(`[Audio Stream] Error starting child process: ${err}`); + }); + } else { + logWarn('[Audio Stream] No valid audio device configured. Skipping audio stream initialization.'); + } +} + +if(configExists()) { + checkFFmpeg().then((ffmpegResult) => { + ffmpeg = ffmpegResult; + if (!serverConfig.audio.ffmpeg) checkAudioUtilities(); + enableAudioStream(); + }); } \ No newline at end of file From 2c73d7ef19e1ca95d082af329ee8f5c7b30080a1 Mon Sep 17 00:00:00 2001 From: Amateur Audio Dude <168192910+AmateurAudioDude@users.noreply.github.com> Date: Sun, 20 Apr 2025 22:41:50 +1000 Subject: [PATCH 2/4] check available ffmpeg, check -ar offset setting --- server/stream/3las.server.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/server/stream/3las.server.js b/server/stream/3las.server.js index e12bbef..b2efd05 100644 --- a/server/stream/3las.server.js +++ b/server/stream/3las.server.js @@ -1,8 +1,11 @@ "use strict"; var fs = require('fs'); -const ffmpegStaticPath = require('ffmpeg-static'); -const {serverConfig} = require('../server_config') +const checkFFmpeg = require('./checkFFmpeg'); +const {serverConfig} = require('../server_config'); +let ffmpegStaticPath; + +function runStream() { /* Stdin streamer is part of 3LAS (Low Latency Live Audio Streaming) https://github.com/JoJoBond/3LAS @@ -243,7 +246,7 @@ class FallbackProviderMp3 extends AFallbackProvider { return [ "-fflags", "+nobuffer+flush_packets", "-flags", "low_delay", "-rtbufsize", "32", "-probesize", "32", "-f", "s16le", - "-ar", this.Server.SampleRate.toString(), + "-ar", Number(this.Server.SampleRate.toString()) + Number(serverConfig.audio.samplerateOffset), "-ac", this.Server.Channels.toString(), "-i", "pipe:0", "-c:a", "libmp3lame", @@ -274,7 +277,7 @@ class FallbackProviderWav extends AFallbackProvider { return [ "-fflags", "+nobuffer+flush_packets", "-flags", "low_delay", "-rtbufsize", "32", "-probesize", "32", "-f", "s16le", - "-ar", this.Server.SampleRate.toString(), + "-ar", Number(this.Server.SampleRate.toString()) + Number(serverConfig.audio.samplerateOffset), "-ac", this.Server.Channels.toString(), "-i", "pipe:0", "-c:a", "pcm_s16le", @@ -327,4 +330,10 @@ for (let i = 2; i < (process.argv.length - 1); i += 2) { } const Server = StreamServer.Create(Options); Server.Run(); -//# sourceMappingURL=3las.server.js.map \ No newline at end of file +//# sourceMappingURL=3las.server.js.map +} + +checkFFmpeg().then((ffmpegResult) => { + ffmpegStaticPath = ffmpegResult; + runStream(); +}); \ No newline at end of file From 4c43daa206ee0e1638499aeed1230320f8f9d9c7 Mon Sep 17 00:00:00 2001 From: Amateur Audio Dude <168192910+AmateurAudioDude@users.noreply.github.com> Date: Sun, 20 Apr 2025 23:32:26 +1000 Subject: [PATCH 3/4] add config options for 'ffmpeg', 'samplerate offset' --- server/server_config.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/server_config.js b/server/server_config.js index 1be8796..b4e75c0 100644 --- a/server/server_config.js +++ b/server/server_config.js @@ -45,7 +45,9 @@ let serverConfig = { audioBitrate: "128k", audioBoost: false, softwareMode: false, - startupVolume: "0.95" + startupVolume: "0.95", + ffmpeg: false, + samplerateOffset: "0" }, identification: { token: null, From 4f9a926e6a9e29e8dbde60a246a87073e1e5b9d2 Mon Sep 17 00:00:00 2001 From: Amateur Audio Dude <168192910+AmateurAudioDude@users.noreply.github.com> Date: Sun, 20 Apr 2025 23:33:06 +1000 Subject: [PATCH 4/4] setup options for 'ffmpeg', 'samplerate offset' --- web/setup.ejs | 12 ++++++++++++ web/wizard.ejs | 12 ++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/web/setup.ejs b/web/setup.ejs index f63edfd..2b5719e 100644 --- a/web/setup.ejs +++ b/web/setup.ejs @@ -217,6 +217,18 @@ <%- include('_components', {component: 'checkbox', cssClass: '', label: 'ALSA Software mode', id: 'audio-softwareMode'}) %> +
+
+

FFmpeg

+

Legacy option for Linux / macOS that could resolve audio issues, but will consume additional CPU and RAM usage.

+ <%- include('_components', {component: 'checkbox', cssClass: '', label: 'Additional FFmpeg', id: 'audio-ffmpeg'}) %> +
+
+

Samplerate Offset

+

Using a negative value could eliminate audio buffering issues during long periods of listening. However, a value that’s too low might increase the buffer over time.

+

+
+
diff --git a/web/wizard.ejs b/web/wizard.ejs index 3c3a976..0f4b87f 100644 --- a/web/wizard.ejs +++ b/web/wizard.ejs @@ -146,8 +146,16 @@
-

If you use an USB audio card on Linux, enabling this option might fix your audio issues.

- <%- include('_components', {component: 'checkbox', cssClass: 'panel-100 flex-container flex-center', label: 'ALSA Software mode', id: 'audio-softwareMode'}) %> +
+
+

If you use an USB audio card on Linux, enabling this option might fix your audio issues.

+ <%- include('_components', {component: 'checkbox', cssClass: 'panel-100 flex-container flex-center', label: 'ALSA Software mode', id: 'audio-softwareMode'}) %> +
+
+

Legacy option for Linux / macOS that could resolve audio issues, but will consume additional CPU and RAM usage.

+ <%- include('_components', {component: 'checkbox', cssClass: 'panel-100 flex-container flex-center', label: 'Additional FFmpeg', id: 'audio-ffmpeg'}) %> +
+