mirror of
https://github.com/KubaPro010/fm-dx-webserver.git
synced 2026-08-01 09:49:18 +02:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34363b6c3b
|
||
|
|
f87f3c8eaa
|
||
|
|
d3ab634846
|
||
|
|
ddff8f51e5
|
@@ -1,61 +0,0 @@
|
|||||||
name: Fetch and Commit librdsparser
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: 'Release version'
|
|
||||||
required: true
|
|
||||||
default: 'v1.1'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
fetch-and-commit:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Download all librdsparser release assets
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
cd server/libraries
|
|
||||||
|
|
||||||
VERSION="${{ github.event.inputs.version }}"
|
|
||||||
echo "Fetching librdsparser release: $VERSION"
|
|
||||||
|
|
||||||
platforms=(
|
|
||||||
"aarch64"
|
|
||||||
"arm"
|
|
||||||
"macos"
|
|
||||||
"mingw32"
|
|
||||||
"mingw64"
|
|
||||||
"x86"
|
|
||||||
"x86_64"
|
|
||||||
)
|
|
||||||
|
|
||||||
base_url="https://github.com/kkonradpl/librdsparser/releases/download/$VERSION"
|
|
||||||
|
|
||||||
for platform in "${platforms[@]}"; do
|
|
||||||
filename="librdsparser-${platform}.zip"
|
|
||||||
dir="${platform}"
|
|
||||||
mkdir -p "$dir"
|
|
||||||
|
|
||||||
echo "Downloading $filename..."
|
|
||||||
curl -sSL -o "$dir/$filename" "$base_url/$filename"
|
|
||||||
|
|
||||||
echo "Extracting from $filename..."
|
|
||||||
unzip -j "$dir/$filename" "*.dll" "*.so" "*.dylib" -d "$dir" || echo "No binaries in $filename"
|
|
||||||
rm "$dir/$filename"
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Commit and push binaries to the repo
|
|
||||||
run: |
|
|
||||||
git config user.name "github-actions"
|
|
||||||
git config user.email "github-actions@github.com"
|
|
||||||
|
|
||||||
git add server/libraries
|
|
||||||
git diff --cached --quiet || git commit -m "Add librdsparser ${{ github.event.inputs.version }}"
|
|
||||||
git push
|
|
||||||
+1
-1
@@ -2,7 +2,7 @@ node_modules/
|
|||||||
/*.json
|
/*.json
|
||||||
/serverlog.txt
|
/serverlog.txt
|
||||||
/web/js/plugins/
|
/web/js/plugins/
|
||||||
/libraries/
|
/libraries/**
|
||||||
/plugins/*
|
/plugins/*
|
||||||
!/plugins/example/frontend.js
|
!/plugins/example/frontend.js
|
||||||
!/plugins/example.js
|
!/plugins/example.js
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ This project utilizes these libraries:
|
|||||||
|
|
||||||
- [3LAS](https://github.com/jojobond/3LAS) library by JoJoBond for Low Latency Audio Streaming.
|
- [3LAS](https://github.com/jojobond/3LAS) library by JoJoBond for Low Latency Audio Streaming.
|
||||||
- [flat-flags](https://github.com/luishdez/flat-flags) library by luishdez for RDS country flags.
|
- [flat-flags](https://github.com/luishdez/flat-flags) library by luishdez for RDS country flags.
|
||||||
- [librdsparser](https://github.com/kkonradpl/librdsparser) library by Konrad Kosmatka for RDS parsing.
|
|
||||||
|
|
||||||
All of these libraries are already bundled with the webserver.
|
All of these libraries are already bundled with the webserver.
|
||||||
|
|
||||||
|
|||||||
Generated
+40
-7
@@ -16,7 +16,6 @@
|
|||||||
"express-session": "1.18.2",
|
"express-session": "1.18.2",
|
||||||
"ffmpeg-static": "5.2.0",
|
"ffmpeg-static": "5.2.0",
|
||||||
"http": "0.0.1-security",
|
"http": "0.0.1-security",
|
||||||
"koffi": "2.7.2",
|
|
||||||
"net": "1.0.2",
|
"net": "1.0.2",
|
||||||
"serialport": "12.0.0",
|
"serialport": "12.0.0",
|
||||||
"ws": "8.18.1"
|
"ws": "8.18.1"
|
||||||
@@ -516,6 +515,20 @@
|
|||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/bufferutil": {
|
||||||
|
"version": "4.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz",
|
||||||
|
"integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"node-gyp-build": "^4.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.14.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bytes": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
@@ -1181,12 +1194,6 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/koffi": {
|
|
||||||
"version": "2.7.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/koffi/-/koffi-2.7.2.tgz",
|
|
||||||
"integrity": "sha512-AWcsEKETQuELxK0Wq/aXDkDiNFFY41TxZQSrKm2Nd6HO/KTHeohPOOIlh2OfQnBXJbRjx5etpWt8cbqMUZo2sg==",
|
|
||||||
"hasInstallScript": true
|
|
||||||
},
|
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
@@ -1339,6 +1346,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-gyp-build": {
|
||||||
|
"version": "4.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz",
|
||||||
|
"integrity": "sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"bin": {
|
||||||
|
"node-gyp-build": "bin.js",
|
||||||
|
"node-gyp-build-optional": "optional.js",
|
||||||
|
"node-gyp-build-test": "build-test.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nopt": {
|
"node_modules/nopt": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz",
|
||||||
@@ -1865,6 +1884,20 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/utf-8-validate": {
|
||||||
|
"version": "5.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
|
||||||
|
"integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"node-gyp-build": "^4.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.14.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
"express-session": "1.18.2",
|
"express-session": "1.18.2",
|
||||||
"ffmpeg-static": "5.2.0",
|
"ffmpeg-static": "5.2.0",
|
||||||
"http": "0.0.1-security",
|
"http": "0.0.1-security",
|
||||||
"koffi": "2.7.2",
|
|
||||||
"net": "1.0.2",
|
"net": "1.0.2",
|
||||||
"serialport": "12.0.0",
|
"serialport": "12.0.0",
|
||||||
"ws": "8.18.1"
|
"ws": "8.18.1"
|
||||||
|
|||||||
-121
@@ -1,121 +0,0 @@
|
|||||||
const WebSocket = require('ws');
|
|
||||||
const { serverConfig } = require('./server_config');
|
|
||||||
const { logChat } = require('./console');
|
|
||||||
const helpers = require('./helpers');
|
|
||||||
|
|
||||||
function createChatServer(storage) {
|
|
||||||
if (!serverConfig.webserver.chatEnabled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatWss = new WebSocket.Server({ noServer: true });
|
|
||||||
|
|
||||||
chatWss.on('connection', (ws, request) => {
|
|
||||||
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
|
|
||||||
const userCommandHistory = {};
|
|
||||||
|
|
||||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
|
||||||
ws.close(1008, 'Banned IP');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send chat history safely
|
|
||||||
storage.chatHistory.forEach((message) => {
|
|
||||||
const historyMessage = { ...message, history: true };
|
|
||||||
|
|
||||||
if (!request.session?.isAdminAuthenticated) {
|
|
||||||
delete historyMessage.ip;
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.send(JSON.stringify(historyMessage));
|
|
||||||
});
|
|
||||||
|
|
||||||
const ipMessage = {
|
|
||||||
type: 'clientIp',
|
|
||||||
ip: clientIp,
|
|
||||||
admin: request.session?.isAdminAuthenticated
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.send(JSON.stringify(ipMessage));
|
|
||||||
|
|
||||||
const userCommands = {};
|
|
||||||
let lastWarn = { time: 0 };
|
|
||||||
|
|
||||||
ws.on('message', (message) => {
|
|
||||||
helpers.antispamProtection(
|
|
||||||
message,
|
|
||||||
clientIp,
|
|
||||||
ws,
|
|
||||||
userCommands,
|
|
||||||
lastWarn,
|
|
||||||
userCommandHistory,
|
|
||||||
'5',
|
|
||||||
'chat'
|
|
||||||
);
|
|
||||||
|
|
||||||
let messageData;
|
|
||||||
|
|
||||||
try {
|
|
||||||
messageData = JSON.parse(message);
|
|
||||||
} catch {
|
|
||||||
ws.send(JSON.stringify({ error: "Invalid message format" }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Chat message:", messageData);
|
|
||||||
|
|
||||||
delete messageData.admin;
|
|
||||||
delete messageData.ip;
|
|
||||||
delete messageData.time;
|
|
||||||
|
|
||||||
if (messageData.nickname != null) {
|
|
||||||
messageData.nickname = helpers.escapeHtml(String(messageData.nickname));
|
|
||||||
}
|
|
||||||
|
|
||||||
messageData.ip = clientIp;
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
messageData.time =
|
|
||||||
String(now.getHours()).padStart(2, '0') +
|
|
||||||
":" +
|
|
||||||
String(now.getMinutes()).padStart(2, '0');
|
|
||||||
|
|
||||||
if (serverConfig.webserver.banlist?.includes(clientIp)) return;
|
|
||||||
|
|
||||||
if (request.session?.isAdminAuthenticated === true) {
|
|
||||||
messageData.admin = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messageData.nickname?.length > 32) {
|
|
||||||
messageData.nickname = messageData.nickname.substring(0, 32);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messageData.message?.length > 255) {
|
|
||||||
messageData.message = messageData.message.substring(0, 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
storage.chatHistory.push(messageData);
|
|
||||||
if (storage.chatHistory.length > 50) {
|
|
||||||
storage.chatHistory.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
logChat(messageData);
|
|
||||||
|
|
||||||
chatWss.clients.forEach((client) => {
|
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
|
||||||
const responseMessage = { ...messageData };
|
|
||||||
|
|
||||||
if (!request.session?.isAdminAuthenticated) {
|
|
||||||
delete responseMessage.ip;
|
|
||||||
}
|
|
||||||
|
|
||||||
client.send(JSON.stringify(responseMessage));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return chatWss; // ← VERY IMPORTANT
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { createChatServer };
|
|
||||||
+16
-211
@@ -1,213 +1,7 @@
|
|||||||
/* Libraries / Imports */
|
/* Libraries / Imports */
|
||||||
const fs = require('fs');
|
|
||||||
const https = require('https');
|
|
||||||
const koffi = require('koffi');
|
|
||||||
const path = require('path');
|
|
||||||
const os = require('os');
|
|
||||||
const platform = os.platform();
|
|
||||||
const cpuArchitecture = os.arch();
|
|
||||||
const { configName, serverConfig, configUpdate, configSave } = require('./server_config');
|
const { configName, serverConfig, configUpdate, configSave } = require('./server_config');
|
||||||
let unicode_type;
|
const RDSDecoder = require("./rds.js");
|
||||||
let shared_Library;
|
|
||||||
|
|
||||||
if (platform === 'win32') {
|
|
||||||
unicode_type = 'int16_t';
|
|
||||||
arch_type = (cpuArchitecture === 'x64' ? 'mingw64' : 'mingw32');
|
|
||||||
shared_Library=path.join(__dirname, "libraries", arch_type, "librdsparser.dll");
|
|
||||||
} else if (platform === 'linux') {
|
|
||||||
unicode_type = 'int32_t';
|
|
||||||
arch_type = (cpuArchitecture === 'x64' ? 'x86_64' :
|
|
||||||
(cpuArchitecture === 'ia32' ? 'x86' :
|
|
||||||
(cpuArchitecture === 'arm64' ? 'aarch64' : cpuArchitecture)));
|
|
||||||
shared_Library=path.join(__dirname, "libraries", arch_type, "librdsparser.so");
|
|
||||||
} else if (platform === 'darwin') {
|
|
||||||
unicode_type = 'int32_t';
|
|
||||||
shared_Library=path.join(__dirname, "libraries", "macos", "librdsparser.dylib");
|
|
||||||
}
|
|
||||||
|
|
||||||
const lib = koffi.load(shared_Library);
|
|
||||||
const { fetchTx } = require('./tx_search.js');
|
const { fetchTx } = require('./tx_search.js');
|
||||||
|
|
||||||
koffi.proto('void callback_pi(void *rds, void *user_data)');
|
|
||||||
koffi.proto('void callback_pty(void *rds, void *user_data)');
|
|
||||||
koffi.proto('void callback_tp(void *rds, void *user_data)');
|
|
||||||
koffi.proto('void callback_ta(void *rds, void *user_data)');
|
|
||||||
koffi.proto('void callback_ms(void *rds, void *user_data)');
|
|
||||||
koffi.proto('void callback_ecc(void *rds, void *user_data)');
|
|
||||||
koffi.proto('void callback_country(void *rds, void *user_data)');
|
|
||||||
koffi.proto('void callback_af(void *rds, uint32_t af, void *user_data)');
|
|
||||||
koffi.proto('void callback_ps(void *rds, void *user_data)');
|
|
||||||
koffi.proto('void callback_rt(void *rds, int flag, void *user_data)');
|
|
||||||
koffi.proto('void callback_ptyn(void *rds, void *user_data)');
|
|
||||||
koffi.proto('void callback_ct(void *rds, void *ct, void *user_data)');
|
|
||||||
|
|
||||||
const rdsparser = {
|
|
||||||
new: lib.func('void* rdsparser_new()'),
|
|
||||||
free: lib.func('void rdsparser_free(void *rds)'),
|
|
||||||
clear: lib.func('void rdsparser_clear(void *rds)'),
|
|
||||||
parse_string: lib.func('bool rdsparser_parse_string(void *rds, const char *input)'),
|
|
||||||
set_text_correction: lib.func('void rdsparser_set_text_correction(void *rds, uint8_t text, uint8_t type, uint8_t error)'),
|
|
||||||
set_text_progressive: lib.func('void rdsparser_set_text_progressive(void *rds, uint8_t string, uint8_t state)'),
|
|
||||||
get_pi: lib.func('int32_t rdsparser_get_pi(void *rds)'),
|
|
||||||
get_pty: lib.func('int8_t rdsparser_get_pty(void *rds)'),
|
|
||||||
get_tp: lib.func('int8_t rdsparser_get_tp(void *rds)'),
|
|
||||||
get_ta: lib.func('int8_t rdsparser_get_ta(void *rds)'),
|
|
||||||
get_ms: lib.func('int8_t rdsparser_get_ms(void *rds)'),
|
|
||||||
get_ecc: lib.func('int16_t rdsparser_get_ecc(void *rds)'),
|
|
||||||
get_country: lib.func('int rdsparser_get_country(void *rds)'),
|
|
||||||
get_ps: lib.func('void* rdsparser_get_ps(void *rds)'),
|
|
||||||
get_rt: lib.func('void* rdsparser_get_rt(void *rds, int flag)'),
|
|
||||||
get_ptyn: lib.func('void* rdsparser_get_ptyn(void *rds)'),
|
|
||||||
register_pi: lib.func('void rdsparser_register_pi(void *rds, void *cb)'),
|
|
||||||
register_pty: lib.func('void rdsparser_register_pty(void *rds, void *cb)'),
|
|
||||||
register_tp: lib.func('void rdsparser_register_tp(void *rds, void *cb)'),
|
|
||||||
register_ta: lib.func('void rdsparser_register_ta(void *rds, void *cb)'),
|
|
||||||
register_ms: lib.func('void rdsparser_register_ms(void *rds, void *cb)'),
|
|
||||||
register_ecc: lib.func('void rdsparser_register_ecc(void *rds, void *cb)'),
|
|
||||||
register_country: lib.func('void rdsparser_register_country(void *rds, void *cb)'),
|
|
||||||
register_af: lib.func('void rdsparser_register_af(void *rds, void *cb)'),
|
|
||||||
register_ps: lib.func('void rdsparser_register_ps(void *rds, void *cb)'),
|
|
||||||
register_rt: lib.func('void rdsparser_register_rt(void *rds, void *cb)'),
|
|
||||||
register_ptyn: lib.func('void rdsparser_register_ptyn(void *rds, void *cb)'),
|
|
||||||
register_ct: lib.func('void rdsparser_register_ct(void *rds, void *cb)'),
|
|
||||||
string_get_content: lib.func(unicode_type + '* rdsparser_string_get_content(void *string)'),
|
|
||||||
string_get_errors: lib.func('uint8_t* rdsparser_string_get_errors(void *string)'),
|
|
||||||
string_get_length: lib.func('uint8_t rdsparser_string_get_length(void *string)'),
|
|
||||||
ct_get_year: lib.func('uint16_t rdsparser_ct_get_year(void *ct)'),
|
|
||||||
ct_get_month: lib.func('uint8_t rdsparser_ct_get_month(void *ct)'),
|
|
||||||
ct_get_day: lib.func('uint8_t rdsparser_ct_get_day(void *ct)'),
|
|
||||||
ct_get_hour: lib.func('uint8_t rdsparser_ct_get_hour(void *ct)'),
|
|
||||||
ct_get_minute: lib.func('uint8_t rdsparser_ct_get_minute(void *ct)'),
|
|
||||||
ct_get_offset: lib.func('int8_t rdsparser_ct_get_offset(void *ct)'),
|
|
||||||
pty_lookup_short: lib.func('const char* rdsparser_pty_lookup_short(int8_t pty, bool rbds)'),
|
|
||||||
pty_lookup_long: lib.func('const char* rdsparser_pty_lookup_long(int8_t pty, bool rbds)'),
|
|
||||||
country_lookup_name: lib.func('const char* rdsparser_country_lookup_name(int country)'),
|
|
||||||
country_lookup_iso: lib.func('const char* rdsparser_country_lookup_iso(int country)')
|
|
||||||
}
|
|
||||||
|
|
||||||
const callbacks = {
|
|
||||||
pi: koffi.register(rds => (
|
|
||||||
value = rdsparser.get_pi(rds)
|
|
||||||
//console.log('PI: ' + value.toString(16).toUpperCase())
|
|
||||||
), 'callback_pi*'),
|
|
||||||
|
|
||||||
pty: koffi.register(rds => (
|
|
||||||
value = rdsparser.get_pty(rds),
|
|
||||||
dataToSend.pty = value
|
|
||||||
), 'callback_pty*'),
|
|
||||||
|
|
||||||
tp: koffi.register(rds => (
|
|
||||||
value = rdsparser.get_tp(rds),
|
|
||||||
dataToSend.tp = value
|
|
||||||
), 'callback_tp*'),
|
|
||||||
|
|
||||||
ta: koffi.register(rds => (
|
|
||||||
value = rdsparser.get_ta(rds),
|
|
||||||
dataToSend.ta = value
|
|
||||||
), 'callback_ta*'),
|
|
||||||
|
|
||||||
ms: koffi.register(rds => (
|
|
||||||
value = rdsparser.get_ms(rds),
|
|
||||||
dataToSend.ms = value
|
|
||||||
), 'callback_ms*'),
|
|
||||||
|
|
||||||
af: koffi.register((rds, value) => (
|
|
||||||
dataToSend.af.push(value)
|
|
||||||
), 'callback_af*'),
|
|
||||||
|
|
||||||
ecc: koffi.register(rds => (
|
|
||||||
value = rdsparser.get_ecc(rds),
|
|
||||||
dataToSend.ecc = value
|
|
||||||
), 'callback_ecc*'),
|
|
||||||
|
|
||||||
country: koffi.register(rds => (
|
|
||||||
value = rdsparser.get_country(rds),
|
|
||||||
display = rdsparser.country_lookup_name(value),
|
|
||||||
iso = rdsparser.country_lookup_iso(value),
|
|
||||||
dataToSend.country_name = display,
|
|
||||||
dataToSend.country_iso = iso
|
|
||||||
), 'callback_country*'),
|
|
||||||
|
|
||||||
ps: koffi.register(rds => (
|
|
||||||
ps = rdsparser.get_ps(rds),
|
|
||||||
dataToSend.ps = decode_unicode(ps),
|
|
||||||
dataToSend.ps_errors = decode_errors(ps)
|
|
||||||
), 'callback_ps*'),
|
|
||||||
|
|
||||||
rt: koffi.register((rds, flag) => {
|
|
||||||
const rt = rdsparser.get_rt(rds, flag);
|
|
||||||
|
|
||||||
if (flag === 0) {
|
|
||||||
dataToSend.rt0 = decode_unicode(rt);
|
|
||||||
dataToSend.rt0_errors = decode_errors(rt);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flag === 1) {
|
|
||||||
dataToSend.rt1 = decode_unicode(rt);
|
|
||||||
dataToSend.rt1_errors = decode_errors(rt);
|
|
||||||
}
|
|
||||||
dataToSend.rt_flag = flag;
|
|
||||||
}, 'callback_rt*'),
|
|
||||||
|
|
||||||
ptyn: koffi.register((rds, flag) => (
|
|
||||||
value = decode_unicode(rdsparser.get_ptyn(rds))
|
|
||||||
/*console.log('PTYN: ' + value)*/
|
|
||||||
), 'callback_ptyn*'),
|
|
||||||
|
|
||||||
ct: koffi.register((rds, ct) => (
|
|
||||||
year = rdsparser.ct_get_year(ct),
|
|
||||||
month = String(rdsparser.ct_get_month(ct)).padStart(2, '0'),
|
|
||||||
day = String(rdsparser.ct_get_day(ct)).padStart(2, '0'),
|
|
||||||
hour = String(rdsparser.ct_get_hour(ct)).padStart(2, '0'),
|
|
||||||
minute = String(rdsparser.ct_get_minute(ct)).padStart(2, '0'),
|
|
||||||
offset = rdsparser.ct_get_offset(ct),
|
|
||||||
tz_sign = (offset >= 0 ? '+' : '-'),
|
|
||||||
tz_hour = String(Math.abs(Math.floor(offset / 60))).padStart(2, '0'),
|
|
||||||
tz_minute = String(Math.abs(offset % 60)).padStart(2, '0')
|
|
||||||
//console.log('CT: ' + year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ' (' + tz_sign + tz_hour + ':' + tz_minute + ')')
|
|
||||||
), 'callback_ct*')
|
|
||||||
};
|
|
||||||
|
|
||||||
let rds = rdsparser.new()
|
|
||||||
rdsparser.set_text_correction(rds, 0, 0, 2);
|
|
||||||
rdsparser.set_text_correction(rds, 0, 1, 2);
|
|
||||||
rdsparser.set_text_correction(rds, 1, 0, 2);
|
|
||||||
rdsparser.set_text_correction(rds, 1, 1, 2);
|
|
||||||
rdsparser.set_text_progressive(rds, 0, 1)
|
|
||||||
rdsparser.set_text_progressive(rds, 1, 1)
|
|
||||||
rdsparser.register_pi(rds, callbacks.pi);
|
|
||||||
rdsparser.register_pty(rds, callbacks.pty);
|
|
||||||
rdsparser.register_tp(rds, callbacks.tp);
|
|
||||||
rdsparser.register_ta(rds, callbacks.ta);
|
|
||||||
rdsparser.register_ms(rds, callbacks.ms);
|
|
||||||
rdsparser.register_ecc(rds, callbacks.ecc);
|
|
||||||
rdsparser.register_country(rds, callbacks.country);
|
|
||||||
rdsparser.register_af(rds, callbacks.af);
|
|
||||||
rdsparser.register_ps(rds, callbacks.ps);
|
|
||||||
rdsparser.register_rt(rds, callbacks.rt);
|
|
||||||
rdsparser.register_ptyn(rds, callbacks.ptyn);
|
|
||||||
rdsparser.register_ct(rds, callbacks.ct);
|
|
||||||
|
|
||||||
const decode_unicode = function(string) {
|
|
||||||
let length = rdsparser.string_get_length(string);
|
|
||||||
if (length) {
|
|
||||||
let content = rdsparser.string_get_content(string);
|
|
||||||
let array = koffi.decode(content, unicode_type + ' [' + length + ']');
|
|
||||||
return String.fromCodePoint.apply(String, array);
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const decode_errors = function(string) {
|
|
||||||
let length = rdsparser.string_get_length(string);
|
|
||||||
if (length) {
|
|
||||||
let errors = rdsparser.string_get_errors(string);
|
|
||||||
let array = koffi.decode(errors, 'uint8_t [' + length + ']');
|
|
||||||
return Uint8Array.from(array).toString();
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateInterval = 75;
|
const updateInterval = 75;
|
||||||
|
|
||||||
// Initialize the data object
|
// Initialize the data object
|
||||||
@@ -229,7 +23,9 @@ var dataToSend = {
|
|||||||
ecc: null,
|
ecc: null,
|
||||||
af: [],
|
af: [],
|
||||||
rt0: '',
|
rt0: '',
|
||||||
|
rt0_errors: '',
|
||||||
rt1: '',
|
rt1: '',
|
||||||
|
rt1_errors: '',
|
||||||
rt_flag: '',
|
rt_flag: '',
|
||||||
ims: 0,
|
ims: 0,
|
||||||
eq: 0,
|
eq: 0,
|
||||||
@@ -251,6 +47,8 @@ var dataToSend = {
|
|||||||
users: 0,
|
users: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rds = new RDSDecoder(dataToSend);
|
||||||
|
|
||||||
const filterMappings = {
|
const filterMappings = {
|
||||||
'G11': { eq: 1, ims: 1 },
|
'G11': { eq: 1, ims: 1 },
|
||||||
'G01': { eq: 0, ims: 1 },
|
'G01': { eq: 0, ims: 1 },
|
||||||
@@ -283,7 +81,7 @@ function rdsReceived() {
|
|||||||
function rdsReset() {
|
function rdsReset() {
|
||||||
resetToDefault(dataToSend);
|
resetToDefault(dataToSend);
|
||||||
dataToSend.af.length = 0;
|
dataToSend.af.length = 0;
|
||||||
rdsparser.clear(rds);
|
rds.clear();
|
||||||
if (rdsTimeoutTimer) {
|
if (rdsTimeoutTimer) {
|
||||||
clearTimeout(rdsTimeoutTimer);
|
clearTimeout(rdsTimeoutTimer);
|
||||||
rdsTimeoutTimer = null;
|
rdsTimeoutTimer = null;
|
||||||
@@ -400,11 +198,18 @@ function handleData(wss, receivedData, rdsWss) {
|
|||||||
data += (((errors & 0x03) == 0) ? modifiedData.slice(12, 16) : '----');
|
data += (((errors & 0x03) == 0) ? modifiedData.slice(12, 16) : '----');
|
||||||
|
|
||||||
const newDataString = "G:\r\n" + data + "\r\n\r\n";
|
const newDataString = "G:\r\n" + data + "\r\n\r\n";
|
||||||
const finalBuffer = Buffer.from(newDataString, 'utf-8');
|
client.send(newDataString);
|
||||||
client.send(finalBuffer);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
rdsparser.parse_string(rds, modifiedData);
|
var data2 = "";
|
||||||
|
for(var i = 0; i < modifiedData.length; i++) {
|
||||||
|
data2 += modifiedData[i]
|
||||||
|
if ((i + 1) % 4 === 0 && i !== modifiedData.length - 1) data2 += " ";
|
||||||
|
}
|
||||||
|
var data = [];
|
||||||
|
data2.split(" ").forEach((i) => {data.push(parseInt(i, 16))})
|
||||||
|
|
||||||
|
rds.decodeGroup(data[0], data[1], data[2], data[3], data[4])
|
||||||
legacyRdsPiBuffer = null;
|
legacyRdsPiBuffer = null;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
+114
-11
@@ -9,6 +9,7 @@ const app = express();
|
|||||||
const httpServer = http.createServer(app);
|
const httpServer = http.createServer(app);
|
||||||
const WebSocket = require('ws');
|
const WebSocket = require('ws');
|
||||||
const wss = new WebSocket.Server({ noServer: true, perMessageDeflate: true });
|
const wss = new WebSocket.Server({ noServer: true, perMessageDeflate: true });
|
||||||
|
const chatWss = new WebSocket.Server({ noServer: true });
|
||||||
const rdsWss = new WebSocket.Server({ noServer: true });
|
const rdsWss = new WebSocket.Server({ noServer: true });
|
||||||
const pluginsWss = new WebSocket.Server({ noServer: true, perMessageDeflate: true });
|
const pluginsWss = new WebSocket.Server({ noServer: true, perMessageDeflate: true });
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
@@ -16,17 +17,16 @@ const path = require('path');
|
|||||||
const net = require('net');
|
const net = require('net');
|
||||||
const client = new net.Socket();
|
const client = new net.Socket();
|
||||||
const { SerialPort } = require('serialport');
|
const { SerialPort } = require('serialport');
|
||||||
|
const audioServer = require('./stream/3las.server');
|
||||||
const tunnel = require('./tunnel');
|
const tunnel = require('./tunnel');
|
||||||
const { createChatServer } = require('./chat');
|
|
||||||
const { createAudioServer } = require('./stream/ws.js');
|
|
||||||
|
|
||||||
// File imports
|
// File imports
|
||||||
const helpers = require('./helpers');
|
const helpers = require('./helpers');
|
||||||
const dataHandler = require('./datahandler');
|
const dataHandler = require('./datahandler');
|
||||||
const fmdxList = require('./fmdx_list');
|
const fmdxList = require('./fmdx_list');
|
||||||
const { logError, logInfo, logWarn } = require('./console');
|
const { logDebug, logError, logInfo, logWarn, logChat } = require('./console');
|
||||||
const storage = require('./storage');
|
const storage = require('./storage');
|
||||||
const { serverConfig, configExists } = require('./server_config');
|
const { serverConfig, configExists, configSave } = require('./server_config');
|
||||||
const pjson = require('../package.json');
|
const pjson = require('../package.json');
|
||||||
|
|
||||||
// Function to find server files based on the plugins listed in config
|
// Function to find server files based on the plugins listed in config
|
||||||
@@ -90,8 +90,6 @@ console.log('\x1b[32m\x1b[2mby Noobish @ \x1b[4mFMDX.org\x1b[0m');
|
|||||||
console.log("v" + pjson.version)
|
console.log("v" + pjson.version)
|
||||||
console.log('\x1b[90m' + '─'.repeat(terminalWidth - 1) + '\x1b[0m');
|
console.log('\x1b[90m' + '─'.repeat(terminalWidth - 1) + '\x1b[0m');
|
||||||
|
|
||||||
const chatWss = createChatServer(storage);
|
|
||||||
const audioWss = createAudioServer();
|
|
||||||
// Start ffmpeg
|
// Start ffmpeg
|
||||||
require('./stream/index');
|
require('./stream/index');
|
||||||
require('./plugins');
|
require('./plugins');
|
||||||
@@ -582,6 +580,84 @@ wss.on('connection', (ws, request) => {
|
|||||||
ws.on('error', console.error);
|
ws.on('error', console.error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// CHAT WEBSOCKET BLOCK
|
||||||
|
chatWss.on('connection', (ws, request) => {
|
||||||
|
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
|
||||||
|
const userCommandHistory = {};
|
||||||
|
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||||
|
ws.close(1008, 'Banned IP');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send chat history to the newly connected client
|
||||||
|
storage.chatHistory.forEach(function(message) {
|
||||||
|
message.history = true;
|
||||||
|
!request.session.isAdminAuthenticated ? delete message.ip : null;
|
||||||
|
ws.send(JSON.stringify(message));
|
||||||
|
});
|
||||||
|
|
||||||
|
const ipMessage = {
|
||||||
|
type: 'clientIp',
|
||||||
|
ip: clientIp,
|
||||||
|
admin: request.session.isAdminAuthenticated
|
||||||
|
};
|
||||||
|
ws.send(JSON.stringify(ipMessage));
|
||||||
|
|
||||||
|
// Anti-spam tracking for each client
|
||||||
|
const userCommands = {};
|
||||||
|
let lastWarn = { time: 0 };
|
||||||
|
|
||||||
|
ws.on('message', function incoming(message) {
|
||||||
|
// Anti-spam
|
||||||
|
const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '5', 'chat');
|
||||||
|
|
||||||
|
let messageData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
messageData = JSON.parse(message);
|
||||||
|
} catch (error) {
|
||||||
|
ws.send(JSON.stringify({ error: "Invalid message format" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape nickname and other potentially unsafe fields
|
||||||
|
if (messageData.nickname) {
|
||||||
|
messageData.nickname = helpers.escapeHtml(messageData.nickname);
|
||||||
|
}
|
||||||
|
|
||||||
|
messageData.ip = clientIp;
|
||||||
|
const currentTime = new Date();
|
||||||
|
|
||||||
|
const hours = String(currentTime.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(currentTime.getMinutes()).padStart(2, '0');
|
||||||
|
messageData.time = `${hours}:${minutes}`; // Adding current time to the message object in hours:minutes format
|
||||||
|
|
||||||
|
if (serverConfig.webserver.banlist?.includes(clientIp)) { return; }
|
||||||
|
if (request.session.isAdminAuthenticated === true) { messageData.admin = true; }
|
||||||
|
if (messageData.message.length > 255) { messageData.message = messageData.message.substring(0, 255); }
|
||||||
|
|
||||||
|
storage.chatHistory.push(messageData);
|
||||||
|
if (storage.chatHistory.length > 50) { storage.chatHistory.shift(); }
|
||||||
|
logChat(messageData);
|
||||||
|
|
||||||
|
chatWss.clients.forEach(function each(client) {
|
||||||
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
|
// Only include IP for admin clients
|
||||||
|
let responseMessage = { ...messageData };
|
||||||
|
|
||||||
|
if (request.session.isAdminAuthenticated !== true) {
|
||||||
|
delete responseMessage.ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifiedMessage = JSON.stringify(responseMessage);
|
||||||
|
client.send(modifiedMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('close', function close() {});
|
||||||
|
});
|
||||||
|
|
||||||
// Additional web socket for using plugins
|
// Additional web socket for using plugins
|
||||||
pluginsWss.on('connection', (ws, request) => {
|
pluginsWss.on('connection', (ws, request) => {
|
||||||
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
|
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
|
||||||
@@ -617,11 +693,35 @@ pluginsWss.on('connection', (ws, request) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ws.on('close', () => {
|
||||||
|
// logInfo('WebSocket Extra connection closed'); // Use custom logInfo function
|
||||||
|
});
|
||||||
|
|
||||||
ws.on('error', error => {
|
ws.on('error', error => {
|
||||||
logError('WebSocket Extra error: ' + error); // Use custom logError function
|
logError('WebSocket Extra error: ' + error); // Use custom logError function
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function isPortOpen(host, port, timeout = 1000) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const socket = new net.Socket();
|
||||||
|
|
||||||
|
const onError = () => {
|
||||||
|
socket.destroy();
|
||||||
|
resolve(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.setTimeout(timeout);
|
||||||
|
socket.once('error', onError);
|
||||||
|
socket.once('timeout', onError);
|
||||||
|
|
||||||
|
socket.connect(port, host, () => {
|
||||||
|
socket.end();
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Websocket register for /text, /audio and /chat paths
|
// Websocket register for /text, /audio and /chat paths
|
||||||
httpServer.on('upgrade', (request, socket, head) => {
|
httpServer.on('upgrade', (request, socket, head) => {
|
||||||
if (request.url === '/text') {
|
if (request.url === '/text') {
|
||||||
@@ -631,12 +731,15 @@ httpServer.on('upgrade', (request, socket, head) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (request.url === '/audio') {
|
} else if (request.url === '/audio') {
|
||||||
sessionMiddleware(request, {}, () => {
|
if (typeof audioServer?.handleAudioUpgrade === 'function') {
|
||||||
audioWss.handleUpgrade(request, socket, head, (ws) => {
|
audioServer.handleAudioUpgrade(request, socket, head, (ws) => {
|
||||||
audioWss.emit('connection', ws, request);
|
audioServer.Server?.Server?.emit?.('connection', ws, request);
|
||||||
});
|
});
|
||||||
});
|
} else {
|
||||||
} else if (request.url === '/chat' && serverConfig.webserver.chatEnabled === true) {
|
logWarn('[Audio WebSocket] Audio server not ready — dropping client connection.');
|
||||||
|
socket.destroy();
|
||||||
|
}
|
||||||
|
} else if (request.url === '/chat') {
|
||||||
sessionMiddleware(request, {}, () => {
|
sessionMiddleware(request, {}, () => {
|
||||||
chatWss.handleUpgrade(request, socket, head, (ws) => {
|
chatWss.handleUpgrade(request, socket, head, (ws) => {
|
||||||
chatWss.emit('connection', ws, request);
|
chatWss.emit('connection', ws, request);
|
||||||
|
|||||||
+175
@@ -0,0 +1,175 @@
|
|||||||
|
const { rdsEccLookup, iso, countries } = require("./rds_country.js")
|
||||||
|
|
||||||
|
class RDSDecoder {
|
||||||
|
constructor(data) {
|
||||||
|
this.data = data;
|
||||||
|
this.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.data.pi = '?';
|
||||||
|
this.ps = Array(8).fill(' ');
|
||||||
|
this.ps_errors = Array(8).fill("0");
|
||||||
|
this.rt0 = Array(64).fill(' ');
|
||||||
|
this.rt0_errors = Array(64).fill("0");
|
||||||
|
this.rt1 = Array(64).fill(' ');
|
||||||
|
this.rt1_errors = Array(64).fill("0");
|
||||||
|
this.data.ps = '';
|
||||||
|
this.data.rt1 = '';
|
||||||
|
this.data.rt0 = '';
|
||||||
|
this.data.pty = 0;
|
||||||
|
this.data.tp = 0;
|
||||||
|
this.data.ta = 0;
|
||||||
|
this.data.ms = -1;
|
||||||
|
this.data.rt_flag = 0;
|
||||||
|
this.rt1_to_clear = false;
|
||||||
|
this.rt0_to_clear = false;
|
||||||
|
this.data.ecc = null;
|
||||||
|
this.data.country_name = ""
|
||||||
|
this.data.country_iso = "UN"
|
||||||
|
|
||||||
|
this.af_len = 0;
|
||||||
|
this.data.af = []
|
||||||
|
this.af_am_follows = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeGroup(blockA, blockB, blockC, blockD, error) {
|
||||||
|
if(error > 2) return; // TODO: handle errors
|
||||||
|
// console.log(error)
|
||||||
|
this.data.pi = blockA.toString(16).toUpperCase().padStart(4, '0');
|
||||||
|
|
||||||
|
const group = (blockB >> 12) & 0xF;
|
||||||
|
const version = (blockB >> 11) & 0x1;
|
||||||
|
this.data.tp = Number((blockB >> 10) & 1);
|
||||||
|
this.data.pty = (blockB >> 5) & 0b11111;
|
||||||
|
|
||||||
|
if (group === 0) {
|
||||||
|
this.data.ta = (blockB >> 4) & 1;
|
||||||
|
this.data.ms = (blockB >> 3) & 1;
|
||||||
|
|
||||||
|
if(version === 0) {
|
||||||
|
var af_high = blockC >> 8;
|
||||||
|
var af_low = blockC & 0xFF;
|
||||||
|
var BASE = 224;
|
||||||
|
var FILLER = 205;
|
||||||
|
var AM_FOLLOWS = 250;
|
||||||
|
|
||||||
|
if(af_high >= BASE && af_high <= (BASE+25)) {
|
||||||
|
this.af_len = af_high-BASE;
|
||||||
|
if(this.af_len !== this.data.af.length) {
|
||||||
|
this.data.af = [];
|
||||||
|
this.af_am_follows = false;
|
||||||
|
|
||||||
|
if(af_low != FILLER && af_low != AM_FOLLOWS) this.data.af.push((af_low+875)*100)
|
||||||
|
else if(af_low == AM_FOLLOWS) this.af_am_follows = true;
|
||||||
|
}
|
||||||
|
} else if(this.data.af.length != this.af_len) {
|
||||||
|
if(!(af_high == AM_FOLLOWS || this.af_am_follows)) {
|
||||||
|
var freq = (af_high+875)*100;
|
||||||
|
if(!this.data.af.includes(freq)) this.data.af.push(freq);
|
||||||
|
}
|
||||||
|
if(this.af_am_follows) this.af_am_follows = false;
|
||||||
|
if(!(af_high == AM_FOLLOWS || af_low == FILLER || af_low == AM_FOLLOWS)) {
|
||||||
|
var freq = (af_low+875)*100;
|
||||||
|
if(!this.data.af.includes(freq)) this.data.af.push(freq);
|
||||||
|
}
|
||||||
|
if(af_low == AM_FOLLOWS) this.af_am_follows = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const idx = blockB & 0x3;
|
||||||
|
this.ps[idx * 2] = String.fromCharCode(blockD >> 8);
|
||||||
|
this.ps[idx * 2 + 1] = String.fromCharCode(blockD & 0xFF);
|
||||||
|
this.ps_errors[idx * 2] = error;
|
||||||
|
this.ps_errors[idx * 2 + 1] = error;
|
||||||
|
this.data.ps = this.ps.join('');
|
||||||
|
this.data.ps_errors = this.ps_errors.join(',');
|
||||||
|
} else if (group === 1 && version === 0) {
|
||||||
|
var la = Boolean(blockC & 0x8000);
|
||||||
|
var variant_code = (blockC >> 12) & 0x7;
|
||||||
|
switch (variant_code) {
|
||||||
|
case 0:
|
||||||
|
this.data.ecc = blockC & 0xff;
|
||||||
|
this.data.country_name = rdsEccLookup(blockA, this.data.ecc);
|
||||||
|
if(this.data.country_name.length === 0) this.data.country_iso = "UN";
|
||||||
|
else this.data.country_iso = iso[countries.indexOf(this.data.country_name)]
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (group === 2) {
|
||||||
|
const idx = blockB & 0b1111;
|
||||||
|
this.rt_ab = Boolean((blockB >> 4) & 1);
|
||||||
|
// TODO: rt_errors
|
||||||
|
if(this.rt_ab) {
|
||||||
|
if(this.rt1_to_clear) {
|
||||||
|
this.rt1 = Array(64).fill(' ');
|
||||||
|
this.rt1_errors = Array(64).fill("0");
|
||||||
|
this.rt1_to_clear = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(version == 0) {
|
||||||
|
this.rt1[idx * 4] = String.fromCharCode(blockC >> 8);
|
||||||
|
this.rt1[idx * 4 + 1] = String.fromCharCode(blockC & 0xFF);
|
||||||
|
this.rt1[idx * 4 + 2] = String.fromCharCode(blockD >> 8);
|
||||||
|
this.rt1[idx * 4 + 3] = String.fromCharCode(blockD & 0xFF);
|
||||||
|
this.rt1_errors[idx * 4] = error;
|
||||||
|
this.rt1_errors[idx * 4 + 1] = error;
|
||||||
|
this.rt1_errors[idx * 4 + 2] = error;
|
||||||
|
this.rt1_errors[idx * 4 + 3] = error;
|
||||||
|
} else {
|
||||||
|
this.rt1[idx * 2] = String.fromCharCode(blockD >> 8);
|
||||||
|
this.rt1[idx * 2 + 1] = String.fromCharCode(blockD & 0xFF);
|
||||||
|
this.rt1_errors[idx * 2] = error;
|
||||||
|
this.rt1_errors[idx * 2 + 1] = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
var i = this.rt1.indexOf("\r")
|
||||||
|
while(i != -1) {
|
||||||
|
this.rt1[i] = " ";
|
||||||
|
i = this.rt1.indexOf("\r");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.data.rt1 = this.rt1.join('');
|
||||||
|
this.data.rt1_errors = this.rt1_errors.join(',');
|
||||||
|
this.data.rt_flag = 1;
|
||||||
|
this.rt0_to_clear = true;
|
||||||
|
} else {
|
||||||
|
if(this.rt0_to_clear) {
|
||||||
|
this.rt0 = Array(64).fill(' ');
|
||||||
|
this.rt0_errors = Array(64).fill("0");
|
||||||
|
this.rt0_to_clear = false;
|
||||||
|
}
|
||||||
|
if(version == 0) {
|
||||||
|
this.rt0[idx * 4] = String.fromCharCode(blockC >> 8);
|
||||||
|
this.rt0[idx * 4 + 1] = String.fromCharCode(blockC & 0xFF);
|
||||||
|
this.rt0[idx * 4 + 2] = String.fromCharCode(blockD >> 8);
|
||||||
|
this.rt0[idx * 4 + 3] = String.fromCharCode(blockD & 0xFF);
|
||||||
|
this.rt0_errors[idx * 4] = error;
|
||||||
|
this.rt0_errors[idx * 4 + 1] = error;
|
||||||
|
this.rt0_errors[idx * 4 + 2] = error;
|
||||||
|
this.rt0_errors[idx * 4 + 3] = error;
|
||||||
|
} else {
|
||||||
|
this.rt0[idx * 2] = String.fromCharCode(blockD >> 8);
|
||||||
|
this.rt0[idx * 2 + 1] = String.fromCharCode(blockD & 0xFF);
|
||||||
|
this.rt0_errors[idx * 2] = error;
|
||||||
|
this.rt0_errors[idx * 2 + 1] = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
var i = this.rt0.indexOf("\r");
|
||||||
|
while(i != -1) {
|
||||||
|
this.rt0[i] = " ";
|
||||||
|
i = this.rt0.indexOf("\r");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.data.rt0 = this.rt0.join('');
|
||||||
|
this.data.rt0_errors = this.rt0_errors.join(',');
|
||||||
|
this.data.rt_flag = 0;
|
||||||
|
this.rt1_to_clear = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// console.log(group, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = RDSDecoder;
|
||||||
@@ -0,0 +1,632 @@
|
|||||||
|
var countries = [
|
||||||
|
"Albania",
|
||||||
|
"Estonia",
|
||||||
|
"Algeria",
|
||||||
|
"Ethiopia",
|
||||||
|
"Andorra",
|
||||||
|
"Angola",
|
||||||
|
"Finland",
|
||||||
|
"Armenia",
|
||||||
|
"France",
|
||||||
|
"Ascension Island",
|
||||||
|
"Gabon",
|
||||||
|
"Austria",
|
||||||
|
"Gambia",
|
||||||
|
"Azerbaijan",
|
||||||
|
"Georgia",
|
||||||
|
"Germany",
|
||||||
|
"Bahrein",
|
||||||
|
"Ghana",
|
||||||
|
"Belarus",
|
||||||
|
"Gibraltar",
|
||||||
|
"Belgium",
|
||||||
|
"Greece",
|
||||||
|
"Benin",
|
||||||
|
"Guinea",
|
||||||
|
"Bosnia Herzegovina",
|
||||||
|
"Guinea-Bissau",
|
||||||
|
"Botswana",
|
||||||
|
"Hungary",
|
||||||
|
"Bulgaria",
|
||||||
|
"Iceland",
|
||||||
|
"Burkina Faso",
|
||||||
|
"Iraq",
|
||||||
|
"Burundi",
|
||||||
|
"Ireland",
|
||||||
|
"Cabinda",
|
||||||
|
"Israel",
|
||||||
|
"Cameroon",
|
||||||
|
"Italy",
|
||||||
|
"Jordan",
|
||||||
|
"Cape Verde",
|
||||||
|
"Kazakhstan",
|
||||||
|
"Central African Republic",
|
||||||
|
"Kenya",
|
||||||
|
"Chad",
|
||||||
|
"Kosovo",
|
||||||
|
"Comoros",
|
||||||
|
"Kuwait",
|
||||||
|
"DR Congo",
|
||||||
|
"Kyrgyzstan",
|
||||||
|
"Republic of Congo",
|
||||||
|
"Latvia",
|
||||||
|
"Cote d'Ivoire",
|
||||||
|
"Lebanon",
|
||||||
|
"Croatia",
|
||||||
|
"Lesotho",
|
||||||
|
"Cyprus",
|
||||||
|
"Liberia",
|
||||||
|
"Czechia",
|
||||||
|
"Libya",
|
||||||
|
"Denmark",
|
||||||
|
"Liechtenstein",
|
||||||
|
"Djiboutia",
|
||||||
|
"Lithuania",
|
||||||
|
"Egypt",
|
||||||
|
"Luxembourg",
|
||||||
|
"Equatorial Guinea",
|
||||||
|
"Macedonia",
|
||||||
|
"Eritrea",
|
||||||
|
"Madagascar",
|
||||||
|
"Seychelles",
|
||||||
|
"Malawi",
|
||||||
|
"Sierra Leone",
|
||||||
|
"Mali",
|
||||||
|
"Slovakia",
|
||||||
|
"Malta",
|
||||||
|
"Slovenia",
|
||||||
|
"Mauritania",
|
||||||
|
"Somalia",
|
||||||
|
"Mauritius",
|
||||||
|
"South Africa",
|
||||||
|
"Moldova",
|
||||||
|
"South Sudan",
|
||||||
|
"Monaco",
|
||||||
|
"Spain",
|
||||||
|
"Mongolia",
|
||||||
|
"Sudan",
|
||||||
|
"Montenegro",
|
||||||
|
"Swaziland",
|
||||||
|
"Morocco",
|
||||||
|
"Sweden",
|
||||||
|
"Mozambique",
|
||||||
|
"Switzerland",
|
||||||
|
"Namibia",
|
||||||
|
"Syria",
|
||||||
|
"Netherlands",
|
||||||
|
"Tajikistan",
|
||||||
|
"Niger",
|
||||||
|
"Tanzania",
|
||||||
|
"Nigeria",
|
||||||
|
"Togo",
|
||||||
|
"Norway",
|
||||||
|
"Tunisia",
|
||||||
|
"Oman",
|
||||||
|
"Turkey",
|
||||||
|
"Palestine",
|
||||||
|
"Turkmenistan",
|
||||||
|
"Poland",
|
||||||
|
"Uganda",
|
||||||
|
"Portugal",
|
||||||
|
"Ukraine",
|
||||||
|
"Qatar",
|
||||||
|
"United Arab Emirates",
|
||||||
|
"Romania",
|
||||||
|
"United Kingdom",
|
||||||
|
"Russia",
|
||||||
|
"Uzbekistan",
|
||||||
|
"Rwanda",
|
||||||
|
"Vatican",
|
||||||
|
"San Marino",
|
||||||
|
"Western Sahara",
|
||||||
|
"Sao Tome and Principe",
|
||||||
|
"Yemen",
|
||||||
|
"Saudi Arabia",
|
||||||
|
"Zambia",
|
||||||
|
"Senegal",
|
||||||
|
"Zimbabwe",
|
||||||
|
"Serbia",
|
||||||
|
"Anguilla",
|
||||||
|
"Guyana",
|
||||||
|
"Antigua and Barbuda",
|
||||||
|
"Haiti",
|
||||||
|
"Argentina",
|
||||||
|
"Honduras",
|
||||||
|
"Aruba",
|
||||||
|
"Jamaica",
|
||||||
|
"Bahamas",
|
||||||
|
"Martinique",
|
||||||
|
"Barbados",
|
||||||
|
"Mexico",
|
||||||
|
"Belize",
|
||||||
|
"Montserrat",
|
||||||
|
"Brazil/Bermuda",
|
||||||
|
"Brazil/AN",
|
||||||
|
"Bolivia",
|
||||||
|
"Nicaragua",
|
||||||
|
"Brazil",
|
||||||
|
"Panama",
|
||||||
|
"Canada",
|
||||||
|
"Paraguay",
|
||||||
|
"Cayman Islands",
|
||||||
|
"Peru",
|
||||||
|
"Chile",
|
||||||
|
"USA/VI/PR",
|
||||||
|
"Colombia",
|
||||||
|
"St. Kitts",
|
||||||
|
"Costa Rica",
|
||||||
|
"St. Lucia",
|
||||||
|
"Cuba",
|
||||||
|
"St. Pierre and Miquelon",
|
||||||
|
"Dominica",
|
||||||
|
"St. Vincent",
|
||||||
|
"Dominican Republic",
|
||||||
|
"Suriname",
|
||||||
|
"El Salvador",
|
||||||
|
"Trinidad and Tobago",
|
||||||
|
"Turks and Caicos islands",
|
||||||
|
"Falkland Islands",
|
||||||
|
"Greenland",
|
||||||
|
"Uruguay",
|
||||||
|
"Grenada",
|
||||||
|
"Venezuela",
|
||||||
|
"Guadeloupe",
|
||||||
|
"Virgin Islands",
|
||||||
|
"Guatemala",
|
||||||
|
"Afghanistan",
|
||||||
|
"South Korea",
|
||||||
|
"Laos",
|
||||||
|
"Australia Capital Territory",
|
||||||
|
"Macao",
|
||||||
|
"Australia New South Wales",
|
||||||
|
"Malaysia",
|
||||||
|
"Australia Victoria",
|
||||||
|
"Maldives",
|
||||||
|
"Australia Queensland",
|
||||||
|
"Marshall Islands",
|
||||||
|
"Australia South Australia",
|
||||||
|
"Micronesia",
|
||||||
|
"Australia Western Australia",
|
||||||
|
"Myanmar",
|
||||||
|
"Australia Tasmania",
|
||||||
|
"Nauru",
|
||||||
|
"Australia Northern Territory",
|
||||||
|
"Nepal",
|
||||||
|
"Bangladesh",
|
||||||
|
"New Zealand",
|
||||||
|
"Bhutan",
|
||||||
|
"Pakistan",
|
||||||
|
"Brunei Darussalam",
|
||||||
|
"Papua New Guinea",
|
||||||
|
"Cambodia",
|
||||||
|
"Philippines",
|
||||||
|
"China",
|
||||||
|
"Samoa",
|
||||||
|
"Singapore",
|
||||||
|
"Solomon Islands",
|
||||||
|
"Fiji",
|
||||||
|
"Sri Lanka",
|
||||||
|
"Hong Kong",
|
||||||
|
"Taiwan",
|
||||||
|
"India",
|
||||||
|
"Thailand",
|
||||||
|
"Indonesia",
|
||||||
|
"Tonga",
|
||||||
|
"Iran",
|
||||||
|
"Vanuatu",
|
||||||
|
"Japan",
|
||||||
|
"Vietnam",
|
||||||
|
"Kiribati",
|
||||||
|
"North Korea",
|
||||||
|
"Brazil/Equator"
|
||||||
|
]
|
||||||
|
|
||||||
|
var iso = [
|
||||||
|
"AL",
|
||||||
|
"EE",
|
||||||
|
"DZ",
|
||||||
|
"ET",
|
||||||
|
"AD",
|
||||||
|
"AO",
|
||||||
|
"FI",
|
||||||
|
"AM",
|
||||||
|
"FR",
|
||||||
|
"SH",
|
||||||
|
"GA",
|
||||||
|
"AT",
|
||||||
|
"GM",
|
||||||
|
"AZ",
|
||||||
|
"GE",
|
||||||
|
"DE",
|
||||||
|
"BH",
|
||||||
|
"GH",
|
||||||
|
"BY",
|
||||||
|
"GI",
|
||||||
|
"BE",
|
||||||
|
"GR",
|
||||||
|
"BJ",
|
||||||
|
"GN",
|
||||||
|
"BA",
|
||||||
|
"GW",
|
||||||
|
"BW",
|
||||||
|
"HU",
|
||||||
|
"BG",
|
||||||
|
"IS",
|
||||||
|
"BF",
|
||||||
|
"IQ",
|
||||||
|
"BI",
|
||||||
|
"IE",
|
||||||
|
"--",
|
||||||
|
"IL",
|
||||||
|
"CM",
|
||||||
|
"IT",
|
||||||
|
"JO",
|
||||||
|
"CV",
|
||||||
|
"KZ",
|
||||||
|
"CF",
|
||||||
|
"KE",
|
||||||
|
"TD",
|
||||||
|
"XK",
|
||||||
|
"KM",
|
||||||
|
"KW",
|
||||||
|
"CD",
|
||||||
|
"KG",
|
||||||
|
"CG",
|
||||||
|
"LV",
|
||||||
|
"CI",
|
||||||
|
"LB",
|
||||||
|
"HR",
|
||||||
|
"LS",
|
||||||
|
"CY",
|
||||||
|
"LR",
|
||||||
|
"CZ",
|
||||||
|
"LY",
|
||||||
|
"DK",
|
||||||
|
"LI",
|
||||||
|
"DJ",
|
||||||
|
"LT",
|
||||||
|
"EG",
|
||||||
|
"LU",
|
||||||
|
"GQ",
|
||||||
|
"MK",
|
||||||
|
"ER",
|
||||||
|
"MG",
|
||||||
|
"SC",
|
||||||
|
"MW",
|
||||||
|
"SL",
|
||||||
|
"ML",
|
||||||
|
"SK",
|
||||||
|
"MT",
|
||||||
|
"SI",
|
||||||
|
"MR",
|
||||||
|
"SO",
|
||||||
|
"MU",
|
||||||
|
"ZA",
|
||||||
|
"MD",
|
||||||
|
"SS",
|
||||||
|
"MC",
|
||||||
|
"ES",
|
||||||
|
"MN",
|
||||||
|
"SD",
|
||||||
|
"ME",
|
||||||
|
"SZ",
|
||||||
|
"MA",
|
||||||
|
"SE",
|
||||||
|
"MZ",
|
||||||
|
"CH",
|
||||||
|
"NA",
|
||||||
|
"SY",
|
||||||
|
"NL",
|
||||||
|
"TJ",
|
||||||
|
"NE",
|
||||||
|
"TZ",
|
||||||
|
"NG",
|
||||||
|
"TG",
|
||||||
|
"NO",
|
||||||
|
"TN",
|
||||||
|
"OM",
|
||||||
|
"TR",
|
||||||
|
"PS",
|
||||||
|
"TM",
|
||||||
|
"PL",
|
||||||
|
"UG",
|
||||||
|
"PT",
|
||||||
|
"UA",
|
||||||
|
"QA",
|
||||||
|
"AE",
|
||||||
|
"RO",
|
||||||
|
"GB",
|
||||||
|
"RU",
|
||||||
|
"UZ",
|
||||||
|
"RW",
|
||||||
|
"VA",
|
||||||
|
"SM",
|
||||||
|
"EH",
|
||||||
|
"ST",
|
||||||
|
"YE",
|
||||||
|
"SA",
|
||||||
|
"ZM",
|
||||||
|
"SN",
|
||||||
|
"ZW",
|
||||||
|
"RS",
|
||||||
|
"AI",
|
||||||
|
"GY",
|
||||||
|
"AG",
|
||||||
|
"HT",
|
||||||
|
"AR",
|
||||||
|
"HN",
|
||||||
|
"AW",
|
||||||
|
"JM",
|
||||||
|
"BS",
|
||||||
|
"MQ",
|
||||||
|
"BB",
|
||||||
|
"MX",
|
||||||
|
"BZ",
|
||||||
|
"MS",
|
||||||
|
"--",
|
||||||
|
"--",
|
||||||
|
"BO",
|
||||||
|
"NI",
|
||||||
|
"BR",
|
||||||
|
"PA",
|
||||||
|
"CA",
|
||||||
|
"PY",
|
||||||
|
"KY",
|
||||||
|
"PE",
|
||||||
|
"CL",
|
||||||
|
"--",
|
||||||
|
"CO",
|
||||||
|
"KN",
|
||||||
|
"CR",
|
||||||
|
"LC",
|
||||||
|
"CU",
|
||||||
|
"PM",
|
||||||
|
"DM",
|
||||||
|
"VC",
|
||||||
|
"DO",
|
||||||
|
"SR",
|
||||||
|
"SN",
|
||||||
|
"TT",
|
||||||
|
"TB",
|
||||||
|
"FK",
|
||||||
|
"GL",
|
||||||
|
"UY",
|
||||||
|
"GD",
|
||||||
|
"VE",
|
||||||
|
"GP",
|
||||||
|
"VG",
|
||||||
|
"GT",
|
||||||
|
"AF",
|
||||||
|
"KR",
|
||||||
|
"LA",
|
||||||
|
"AU",
|
||||||
|
"MO",
|
||||||
|
"AU",
|
||||||
|
"MY",
|
||||||
|
"AU",
|
||||||
|
"MV",
|
||||||
|
"AU",
|
||||||
|
"MH",
|
||||||
|
"AU",
|
||||||
|
"FM",
|
||||||
|
"AU",
|
||||||
|
"MM",
|
||||||
|
"AU",
|
||||||
|
"NR",
|
||||||
|
"AU",
|
||||||
|
"NP",
|
||||||
|
"BD",
|
||||||
|
"NZ",
|
||||||
|
"BT",
|
||||||
|
"PK",
|
||||||
|
"BN",
|
||||||
|
"PG",
|
||||||
|
"KH",
|
||||||
|
"PH",
|
||||||
|
"CN",
|
||||||
|
"WS",
|
||||||
|
"SG",
|
||||||
|
"SB",
|
||||||
|
"FJ",
|
||||||
|
"LK",
|
||||||
|
"HK",
|
||||||
|
"TW",
|
||||||
|
"IN",
|
||||||
|
"TH",
|
||||||
|
"ID",
|
||||||
|
"TO",
|
||||||
|
"IR",
|
||||||
|
"VU",
|
||||||
|
"JP",
|
||||||
|
"VN",
|
||||||
|
"KI",
|
||||||
|
"KP",
|
||||||
|
"--"
|
||||||
|
]
|
||||||
|
|
||||||
|
// RDS ECC Lookup Tables - Converted from C to JavaScript
|
||||||
|
|
||||||
|
const rdsEccA0A6Lut = [
|
||||||
|
// A0
|
||||||
|
[
|
||||||
|
"USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR",
|
||||||
|
"USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR",
|
||||||
|
"USA/VI/PR", null, "USA/VI/PR", "USA/VI/PR", null
|
||||||
|
],
|
||||||
|
// A1
|
||||||
|
[
|
||||||
|
null, null, null, null, null,
|
||||||
|
null, null, null, null, null,
|
||||||
|
"Canada", "Canada", "Canada", "Canada", "Greenland"
|
||||||
|
],
|
||||||
|
// A2
|
||||||
|
[
|
||||||
|
"Anguilla", "Antigua and Barbuda", "Brazil/Equator", "Falkland Islands", "Barbados",
|
||||||
|
"Belize", "Cayman Islands", "Costa Rica", "Cuba", "Argentina",
|
||||||
|
"Brazil", "Brazil/Bermuda", "Brazil/AN", "Guadeloupe", "Bahamas"
|
||||||
|
],
|
||||||
|
// A3
|
||||||
|
[
|
||||||
|
"Bolivia", "Colombia", "Jamaica", "Martinique", null,
|
||||||
|
"Paraguay", "Nicaragua", null, "Panama", "Dominica",
|
||||||
|
"Dominican Republic", "Chile", "Grenada", "Turks and Caicos islands", "Guyana"
|
||||||
|
],
|
||||||
|
// A4
|
||||||
|
[
|
||||||
|
"Guatemala", "Honduras", "Aruba", null, "Montserrat",
|
||||||
|
"Trinidad and Tobago", "Peru", "Suriname", "Uruguay", "St. Kitts",
|
||||||
|
"St. Lucia", "El Salvador", "Haiti", "Venezuela", "Virgin Islands"
|
||||||
|
],
|
||||||
|
// A5
|
||||||
|
[
|
||||||
|
null, null, null, null, null,
|
||||||
|
null, null, null, null, null,
|
||||||
|
"Mexico", "St. Vincent", "Mexico", "Mexico", "Mexico"
|
||||||
|
],
|
||||||
|
// A6
|
||||||
|
[
|
||||||
|
null, null, null, null, null,
|
||||||
|
null, null, null, null, null,
|
||||||
|
null, null, null, null, "St. Pierre and Miquelon"
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
const rdsEccD0D4Lut = [
|
||||||
|
// D0
|
||||||
|
[
|
||||||
|
"Cameroon", "Central African Republic", "Djiboutia", "Madagascar", "Mali",
|
||||||
|
"Angola", "Equatorial Guinea", "Gabon", "Guinea", "South Africa",
|
||||||
|
"Burkina Faso", "Republic of Congo", "Togo", "Benin", "Malawi"
|
||||||
|
],
|
||||||
|
// D1
|
||||||
|
[
|
||||||
|
"Namibia", "Liberia", "Ghana", "Mauritania", "Sao Tome and Principe",
|
||||||
|
"Cape Verde", "Senegal", "Gambia", "Burundi", "Ascension Island",
|
||||||
|
"Botswana", "Comoros", "Tanzania", "Ethiopia", "Nigeria"
|
||||||
|
],
|
||||||
|
// D2
|
||||||
|
[
|
||||||
|
"Sierra Leone", "Zimbabwe", "Mozambique", "Uganda", "Swaziland",
|
||||||
|
"Kenya", "Somalia", "Niger", "Chad", "Guinea-Bissau",
|
||||||
|
"DR Congo", "Cote d'Ivoire", null, "Zambia", "Eritrea"
|
||||||
|
],
|
||||||
|
// D3
|
||||||
|
[
|
||||||
|
null, null, "Western Sahara", "Cabinda", "Rwanda",
|
||||||
|
"Lesotho", null, "Seychelles", null, "Mauritius",
|
||||||
|
null, "Sudan", null, null, null
|
||||||
|
],
|
||||||
|
// D4
|
||||||
|
[
|
||||||
|
null, null, null, null, null,
|
||||||
|
null, null, null, null, "South Sudan",
|
||||||
|
null, null, null, null, null
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
const rdsEccE0E5Lut = [
|
||||||
|
// E0
|
||||||
|
[
|
||||||
|
"Germany", "Algeria", "Andorra", "Israel", "Italy",
|
||||||
|
"Belgium", "Russia", "Palestine", "Albania", "Austria",
|
||||||
|
"Hungary", "Malta", "Germany", null, "Egypt"
|
||||||
|
],
|
||||||
|
// E1
|
||||||
|
[
|
||||||
|
"Greece", "Cyprus", "San Marino", "Switzerland", "Jordan",
|
||||||
|
"Finland", "Luxembourg", "Bulgaria", "Denmark", "Gibraltar",
|
||||||
|
"Iraq", "United Kingdom", "Libya", "Romania", "France"
|
||||||
|
],
|
||||||
|
// E2
|
||||||
|
[
|
||||||
|
"Morocco", "Czechia", "Poland", "Vatican", "Slovakia",
|
||||||
|
"Syria", "Tunisia", null, "Liechtenstein", "Iceland",
|
||||||
|
"Monaco", "Lithuania", "Serbia", "Spain", "Norway"
|
||||||
|
],
|
||||||
|
// E3
|
||||||
|
[
|
||||||
|
"Montenegro", "Ireland", "Turkey", null, "Tajikistan",
|
||||||
|
null, null, "Netherlands", "Latvia", "Lebanon",
|
||||||
|
"Azerbaijan", "Croatia", "Kazakhstan", "Sweden", "Belarus"
|
||||||
|
],
|
||||||
|
// E4
|
||||||
|
[
|
||||||
|
"Moldova", "Estonia", "Macedonia", null, null,
|
||||||
|
"Ukraine", "Kosovo", "Portugal", "Slovenia", "Armenia",
|
||||||
|
"Uzbekistan", "Georgia", null, "Turkmenistan", "Bosnia Herzegovina"
|
||||||
|
],
|
||||||
|
// E5
|
||||||
|
[
|
||||||
|
null, null, "Kyrgyzstan", null, null,
|
||||||
|
null, null, null, null, null,
|
||||||
|
null, null, null, null, null
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
const rdsEccF0F4Lut = [
|
||||||
|
// F0
|
||||||
|
[
|
||||||
|
"Australia Capital Territory", "Australia New South Wales", "Australia Victoria", "Australia Queensland", "Australia South Australia",
|
||||||
|
"Australia Western Australia", "Australia Tasmania", "Australia Northern Territory", "Saudi Arabia", "Afghanistan",
|
||||||
|
"Myanmar", "China", "North Korea", "Bahrein", "Malaysia"
|
||||||
|
],
|
||||||
|
// F1
|
||||||
|
[
|
||||||
|
"Kiribati", "Bhutan", "Bangladesh", "Pakistan", "Fiji",
|
||||||
|
"Oman", "Nauru", "Iran", "New Zealand", "Solomon Islands",
|
||||||
|
"Brunei Darussalam", "Sri Lanka", "Taiwan", "South Korea", "Hong Kong"
|
||||||
|
],
|
||||||
|
// F2
|
||||||
|
[
|
||||||
|
"Kuwait", "Qatar", "Cambodia", "Samoa", "India",
|
||||||
|
"Macao", "Vietnam", "Philippines", "Japan", "Singapore",
|
||||||
|
"Maldives", "Indonesia", "United Arab Emirates", "Nepal", "Vanuatu"
|
||||||
|
],
|
||||||
|
// F3
|
||||||
|
[
|
||||||
|
"Laos", "Thailand", "Tonga", null, null,
|
||||||
|
null, null, "China", "Papua New Guinea", null,
|
||||||
|
"Yemen", null, null, "Micronesia", "Mongolia"
|
||||||
|
],
|
||||||
|
// F4
|
||||||
|
[
|
||||||
|
null, null, null, null, null,
|
||||||
|
null, null, null, "China", null,
|
||||||
|
"Marshall Islands", null, null, null, null
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
function rdsEccLookup(pi, ecc) {
|
||||||
|
const PI_UNKNOWN = -1;
|
||||||
|
|
||||||
|
const piCountry = (pi >> 12) & 0xF;
|
||||||
|
|
||||||
|
if (pi === PI_UNKNOWN || piCountry === 0) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const piId = piCountry - 1;
|
||||||
|
|
||||||
|
const eccRanges = [
|
||||||
|
{ min: 0xA0, max: 0xA6, lut: rdsEccA0A6Lut },
|
||||||
|
{ min: 0xD0, max: 0xD4, lut: rdsEccD0D4Lut },
|
||||||
|
{ min: 0xE0, max: 0xE5, lut: rdsEccE0E5Lut },
|
||||||
|
{ min: 0xF0, max: 0xF4, lut: rdsEccF0F4Lut }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check each range
|
||||||
|
for (const range of eccRanges) {
|
||||||
|
if (ecc >= range.min && ecc <= range.max) {
|
||||||
|
const eccId = ecc - range.min;
|
||||||
|
return range.lut[eccId][piId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
rdsEccLookup,
|
||||||
|
iso,
|
||||||
|
countries
|
||||||
|
};
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
"use strict";
|
||||||
|
/*
|
||||||
|
Stdin streamer is part of 3LAS (Low Latency Live Audio Streaming)
|
||||||
|
https://github.com/JoJoBond/3LAS
|
||||||
|
*/
|
||||||
|
var fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const checkFFmpeg = require('./checkFFmpeg');
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
const { logDebug, logError, logInfo, logWarn, logFfmpeg } = require('../console');
|
||||||
|
const { serverConfig } = require('../server_config');
|
||||||
|
|
||||||
|
let ffmpegStaticPath = 'ffmpeg'; // fallback value
|
||||||
|
|
||||||
|
let ServerInstance;
|
||||||
|
let handleAudioUpgradeFn;
|
||||||
|
|
||||||
|
let readyResolve;
|
||||||
|
const waitUntilReady = new Promise((resolve) => {
|
||||||
|
readyResolve = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
checkFFmpeg().then((resolvedPath) => {
|
||||||
|
ffmpegStaticPath = resolvedPath;
|
||||||
|
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(fs.readFileSync(path.resolve(__dirname, 'settings.json'), 'utf-8'));
|
||||||
|
const FFmpeg_command = ffmpegStaticPath;
|
||||||
|
class StreamClient {
|
||||||
|
constructor(server, socket) {
|
||||||
|
this.Server = server;
|
||||||
|
this.Socket = socket;
|
||||||
|
this.BinaryOptions = {
|
||||||
|
compress: false,
|
||||||
|
binary: true
|
||||||
|
};
|
||||||
|
this.Socket.on('error', this.OnError.bind(this));
|
||||||
|
this.Socket.on('message', this.OnMessage.bind(this));
|
||||||
|
}
|
||||||
|
OnMessage(message, isBinary) {
|
||||||
|
try {
|
||||||
|
let request = JSON.parse(message.toString());
|
||||||
|
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(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (_a) {
|
||||||
|
this.OnError(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OnError(_err) {
|
||||||
|
this.Server.DestroyClient(this);
|
||||||
|
}
|
||||||
|
Destroy() {
|
||||||
|
try {
|
||||||
|
this.Socket.close();
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SendBinary(buffer) {
|
||||||
|
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(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 || null;
|
||||||
|
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.SamplesCount = this.SampleRate / 100;
|
||||||
|
this.Samples = new Int16Array(this.Channels * this.SamplesCount);
|
||||||
|
this.SamplesPosition = 0;
|
||||||
|
}
|
||||||
|
Run() {
|
||||||
|
this.Server = new ws.Server({
|
||||||
|
noServer: true,
|
||||||
|
clientTracking: true,
|
||||||
|
perMessageDeflate: false,
|
||||||
|
});
|
||||||
|
// Allow manual upgrade handling from index.js
|
||||||
|
this.handleUpgrade = (req, socket, head) => {
|
||||||
|
this.Server.handleUpgrade(req, socket, head, (ws) => {
|
||||||
|
this.Server.emit('connection', ws, req);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
this.Server.on('connection', this.OnServerConnection.bind(this));
|
||||||
|
if (!this.StdIn) {
|
||||||
|
logError('[Stream] No audio input stream defined (this.StdIn is null)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OnServerConnection(socket, _request) {
|
||||||
|
this.Clients.add(new StreamClient(this, socket));
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
GetStats() {
|
||||||
|
let fallback = {
|
||||||
|
"wav": (this.FallbackClients["wav"] ? this.FallbackClients["wav"].size : 0),
|
||||||
|
"mp3": (this.FallbackClients["mp3"] ? this.FallbackClients["mp3"].size : 0),
|
||||||
|
};
|
||||||
|
let total = 0;
|
||||||
|
for (let format in fallback) {
|
||||||
|
total += fallback[format];
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"Total": total,
|
||||||
|
"Fallback": fallback,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
static Create(options) {
|
||||||
|
// Allow Port to be omitted
|
||||||
|
const port = options["-port"] || null;
|
||||||
|
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))
|
||||||
|
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.");
|
||||||
|
if (typeof options["-samplerate"] !== "number" || options["-samplerate"] !== Math.floor(options["-samplerate"]) || options["-samplerate"] < 1)
|
||||||
|
throw new Error("Invalid sample rate. Must be natural number greater than 0.");
|
||||||
|
return new StreamServer(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", Number(this.Server.SampleRate.toString()) + Number(serverConfig.audio.samplerateOffset),
|
||||||
|
"-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", Number(this.Server.SampleRate.toString()) + Number(serverConfig.audio.samplerateOffset),
|
||||||
|
"-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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Parsing parameters no longer required for Server variable but we'll keep the old code here as a reference
|
||||||
|
const OptionParser = {
|
||||||
|
"-port": function (txt) { return parseInt(txt, 10); },
|
||||||
|
"-channels": function (txt) { return parseInt(txt, 10); },
|
||||||
|
"-samplerate": function (txt) { return parseInt(txt, 10); }
|
||||||
|
};
|
||||||
|
const Options = {};
|
||||||
|
// 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 audioChannels = serverConfig.audio.audioChannels || 2;
|
||||||
|
const Server = new StreamServer(null, audioChannels, 48000);
|
||||||
|
|
||||||
|
ServerInstance = Server;
|
||||||
|
|
||||||
|
handleAudioUpgradeFn = function (request, socket, head, cb) {
|
||||||
|
if (Server.Server && Server.Server.handleUpgrade) {
|
||||||
|
Server.Server.handleUpgrade(request, socket, head, cb);
|
||||||
|
} else {
|
||||||
|
socket.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
readyResolve();
|
||||||
|
|
||||||
|
}).catch((err) => {
|
||||||
|
logError('[Stream] Error:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
get Server() {
|
||||||
|
return ServerInstance;
|
||||||
|
},
|
||||||
|
get handleAudioUpgrade() {
|
||||||
|
return handleAudioUpgradeFn;
|
||||||
|
},
|
||||||
|
waitUntilReady
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=3las.server.js.map
|
||||||
+368
-112
@@ -1,8 +1,8 @@
|
|||||||
const { spawn } = require('child_process');
|
const { spawn, execSync } = require('child_process');
|
||||||
const { serverConfig } = require('../server_config');
|
const { configName, serverConfig, configUpdate, configSave, configExists } = require('../server_config');
|
||||||
const { logDebug, logError, logInfo, logWarn, logFfmpeg } = require('../console');
|
const { logDebug, logError, logInfo, logWarn, logFfmpeg } = require('../console');
|
||||||
const checkFFmpeg = require('./checkFFmpeg');
|
const checkFFmpeg = require('./checkFFmpeg');
|
||||||
const { PassThrough } = require('stream');
|
const audioServer = require('./3las.server');
|
||||||
|
|
||||||
const consoleLogTitle = '[Audio Stream]';
|
const consoleLogTitle = '[Audio Stream]';
|
||||||
|
|
||||||
@@ -15,128 +15,384 @@ function connectMessage(message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const audio_pipe = new PassThrough();
|
function checkAudioUtilities() {
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
try {
|
||||||
|
execSync('which sox');
|
||||||
|
} catch (error) {
|
||||||
|
logError(`${consoleLogTitle} Error: SoX ("sox") not found, Please install sox.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} else if (process.platform === 'linux') {
|
||||||
|
try {
|
||||||
|
execSync('which arecord');
|
||||||
|
} catch (error) {
|
||||||
|
logError(`${consoleLogTitle} Error: ALSA ("arecord") not found. Please install ALSA utils.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCommand(ffmpegPath) {
|
||||||
|
const inputDevice = serverConfig.audio.audioDevice || 'Stereo Mix';
|
||||||
|
const audioChannels = serverConfig.audio.audioChannels || 2;
|
||||||
|
const webPort = Number(serverConfig.webserver.webserverPort);
|
||||||
|
|
||||||
|
// 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', `${audioChannels}`],
|
||||||
|
output: ['-f', 's16le', '-fflags', '+nobuffer+flush_packets', '-packetsize', '384', '-flush_packets', '1', '-bufsize', '960', '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '10', 'pipe:1']
|
||||||
|
};
|
||||||
|
|
||||||
|
// Windows
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
logInfo(`${consoleLogTitle} Platform: Windows (win32). Using "dshow" input.`);
|
||||||
|
return {
|
||||||
|
command: ffmpegPath,
|
||||||
|
args: [
|
||||||
|
...baseOptions.flags,
|
||||||
|
'-f', 'dshow',
|
||||||
|
'-audio_buffer_size', '200',
|
||||||
|
'-i', `audio=${inputDevice}`,
|
||||||
|
...baseOptions.codec,
|
||||||
|
...baseOptions.output
|
||||||
|
]
|
||||||
|
};
|
||||||
|
} else if (process.platform === 'darwin') {
|
||||||
|
// macOS
|
||||||
|
if (!serverConfig.audio.ffmpeg) {
|
||||||
|
logInfo(`${consoleLogTitle} Platform: macOS (darwin) using "coreaudio"`);
|
||||||
|
return {
|
||||||
|
args: [],
|
||||||
|
soxArgs: [
|
||||||
|
'-t', 'coreaudio', `${inputDevice}`,
|
||||||
|
'-b', '32',
|
||||||
|
'-r', '48000',
|
||||||
|
'-c', `${audioChannels}`,
|
||||||
|
'-t', 'raw',
|
||||||
|
'-b', '16',
|
||||||
|
'-r', '48000',
|
||||||
|
'-c', `${audioChannels}`
|
||||||
|
, '-'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const device = serverConfig.audio.audioDevice;
|
||||||
|
return {
|
||||||
|
command: ffmpegPath,
|
||||||
|
args: [
|
||||||
|
...baseOptions.flags,
|
||||||
|
'-f', 'avfoundation',
|
||||||
|
'-i', `${device || ':0'}`,
|
||||||
|
...baseOptions.codec,
|
||||||
|
...baseOptions.output
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Linux
|
||||||
|
if (!serverConfig.audio.ffmpeg) {
|
||||||
|
const prefix = serverConfig.audio.softwareMode ? 'plug' : '';
|
||||||
|
const device = `${prefix}${serverConfig.audio.audioDevice}`;
|
||||||
|
logInfo(`${consoleLogTitle} Platform: Linux. Using "alsa" input.`);
|
||||||
|
return {
|
||||||
|
// command not used if arecordArgs are used
|
||||||
|
command: `while true; do arecord -D "${device}" -f S16_LE -r 48000 -c ${audioChannels} -t raw; done`,
|
||||||
|
args: [],
|
||||||
|
arecordArgs: [
|
||||||
|
'-D', device,
|
||||||
|
'-f', 'S16_LE',
|
||||||
|
'-r', '48000',
|
||||||
|
'-c', audioChannels,
|
||||||
|
'-t', 'raw'
|
||||||
|
],
|
||||||
|
ffmpegArgs: []
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const device = serverConfig.audio.audioDevice;
|
||||||
|
return {
|
||||||
|
command: ffmpegPath,
|
||||||
|
args: [
|
||||||
|
...baseOptions.flags,
|
||||||
|
'-f', 'alsa',
|
||||||
|
'-i', `${device}`,
|
||||||
|
...baseOptions.codec,
|
||||||
|
...baseOptions.output
|
||||||
|
],
|
||||||
|
arecordArgs: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
checkFFmpeg().then((ffmpegPath) => {
|
checkFFmpeg().then((ffmpegPath) => {
|
||||||
logInfo(`${consoleLogTitle} Using ${ffmpegPath === 'ffmpeg' ? 'system-installed FFmpeg' : 'ffmpeg-static'}`);
|
if (!serverConfig.audio.ffmpeg) checkAudioUtilities();
|
||||||
logInfo(`${consoleLogTitle} Starting audio stream on device: \x1b[35m${serverConfig.audio.audioDevice}\x1b[0m`);
|
let audioErrorLogged = false;
|
||||||
|
|
||||||
const sampleRate = Number(this?.Server?.SampleRate || serverConfig.audio.sampleRate || 44100) + Number(serverConfig.audio.samplerateOffset || 0);
|
logInfo(`${consoleLogTitle} Using`, ffmpegPath === 'ffmpeg' ? 'system-installed FFmpeg' : 'ffmpeg-static');
|
||||||
|
|
||||||
const channels = Number(this?.Server?.Channels || serverConfig.audio.audioChannels || 2);
|
if (process.platform !== 'darwin') {
|
||||||
|
logInfo(`${consoleLogTitle} Starting audio stream on device: \x1b[35m${serverConfig.audio.audioDevice}\x1b[0m`);
|
||||||
let ffmpeg = null;
|
} else {
|
||||||
let restartTimer = null;
|
logInfo(`${consoleLogTitle} Starting audio stream on default input device.`);
|
||||||
let lastTimestamp = null;
|
|
||||||
let staleCount = 0;
|
|
||||||
let lastCheckTime = Date.now();
|
|
||||||
|
|
||||||
function buildArgs() {
|
|
||||||
const device = serverConfig.audio.audioDevice;
|
|
||||||
|
|
||||||
let inputArgs;
|
|
||||||
|
|
||||||
if (process.platform === 'win32') inputArgs = ["-f", "dshow", "-i", `audio=${device}`];
|
|
||||||
else if (process.platform === 'darwin') inputArgs = ["-f", "avfoundation", "-i", device || ":0"];
|
|
||||||
else inputArgs = ["-f", "alsa", "-i", device];
|
|
||||||
|
|
||||||
return [
|
|
||||||
"-fflags", "+flush_packets",
|
|
||||||
"-flags", "low_delay",
|
|
||||||
"-rtbufsize", "4096",
|
|
||||||
"-probesize", "128",
|
|
||||||
|
|
||||||
...inputArgs,
|
|
||||||
|
|
||||||
"-thread_queue_size", "1024",
|
|
||||||
"-ar", String(sampleRate),
|
|
||||||
"-ac", String(channels),
|
|
||||||
|
|
||||||
"-c:a", "libmp3lame",
|
|
||||||
"-b:a", serverConfig.audio.audioBitrate,
|
|
||||||
"-ac", String(channels),
|
|
||||||
"-reservoir", "0",
|
|
||||||
|
|
||||||
"-f", "mp3",
|
|
||||||
"-write_xing", "0",
|
|
||||||
"-id3v2_version", "0",
|
|
||||||
|
|
||||||
"-fflags", "+nobuffer",
|
|
||||||
"-flush_packets", "1",
|
|
||||||
|
|
||||||
"pipe:1"
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function launchFFmpeg() {
|
if (process.platform === 'win32') {
|
||||||
const args = buildArgs();
|
// Windows (FFmpeg DirectShow Capture)
|
||||||
|
let ffmpeg;
|
||||||
|
let restartTimer = null;
|
||||||
|
let lastTimestamp = null;
|
||||||
|
let lastCheckTime = Date.now();
|
||||||
|
let audioErrorLogged = false;
|
||||||
|
let staleCount = 0;
|
||||||
|
|
||||||
logDebug(`${consoleLogTitle} Launching FFmpeg with args: ${args.join(' ')}`);
|
function launchFFmpeg() {
|
||||||
|
const commandDef = buildCommand(ffmpegPath);
|
||||||
|
let ffmpegArgs = commandDef.args;
|
||||||
|
|
||||||
ffmpeg = spawn(ffmpegPath, args, {stdio: ['ignore', 'pipe', 'pipe']});
|
// Apply audio boost if enabled
|
||||||
|
if (serverConfig.audio.audioBoost) {
|
||||||
ffmpeg.stdout.pipe(audio_pipe, { end: false });
|
ffmpegArgs.splice(ffmpegArgs.indexOf('pipe:1'), 0, '-af', 'volume=1.7');
|
||||||
|
|
||||||
connectMessage(`${consoleLogTitle} Connected FFmpeg → MP3 → audioWss`);
|
|
||||||
|
|
||||||
ffmpeg.stderr.on('data', (data) => {
|
|
||||||
const msg = data.toString();
|
|
||||||
logFfmpeg(`[FFmpeg stderr]: ${msg}`);
|
|
||||||
|
|
||||||
// Detect frozen timestamps
|
|
||||||
const match = msg.match(/time=(\d\d):(\d\d):(\d\d\.\d+)/);
|
|
||||||
if (match) {
|
|
||||||
const [_, hh, mm, ss] = match;
|
|
||||||
const totalSec = parseInt(hh) * 3600 + parseInt(mm) * 60 + parseFloat(ss);
|
|
||||||
|
|
||||||
if (lastTimestamp !== null && totalSec === lastTimestamp) {
|
|
||||||
staleCount++;
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
if (staleCount >= 10 && now - lastCheckTime > 10000 && !restartTimer) {
|
|
||||||
logWarn(`${consoleLogTitle} FFmpeg appears frozen. Restarting...`);
|
|
||||||
|
|
||||||
restartTimer = setTimeout(() => {
|
|
||||||
restartTimer = null;
|
|
||||||
staleCount = 0;
|
|
||||||
try {
|
|
||||||
ffmpeg.kill('SIGKILL');
|
|
||||||
} catch (e) {
|
|
||||||
logWarn(`${consoleLogTitle} Failed to kill FFmpeg: ${e.message}`);
|
|
||||||
}
|
|
||||||
launchFFmpeg();
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
lastTimestamp = totalSec;
|
|
||||||
lastCheckTime = Date.now();
|
|
||||||
staleCount = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logDebug(`${consoleLogTitle} Launching FFmpeg with args: ${ffmpegArgs.join(' ')}`);
|
||||||
|
ffmpeg = spawn(ffmpegPath, ffmpegArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||||
|
|
||||||
|
audioServer.waitUntilReady.then(() => {
|
||||||
|
audioServer.Server.StdIn = ffmpeg.stdout;
|
||||||
|
audioServer.Server.Run();
|
||||||
|
connectMessage(`${consoleLogTitle} Connected FFmpeg (capture) \u2192 FFmpeg (process) \u2192 Server.StdIn${serverConfig.audio.audioBoost ? ' (audio boost)' : ''}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
ffmpeg.stderr.on('data', (data) => {
|
||||||
|
const msg = data.toString();
|
||||||
|
logFfmpeg(`[FFmpeg stderr]: ${msg}`);
|
||||||
|
|
||||||
|
if (msg.includes('I/O error') && !audioErrorLogged) {
|
||||||
|
audioErrorLogged = true;
|
||||||
|
logError(`${consoleLogTitle} Audio device "${serverConfig.audio.audioDevice}" failed to start.`);
|
||||||
|
logError('Please start the server with: node . --ffmpegdebug for more info.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect frozen timestamp
|
||||||
|
const match = msg.match(/time=(\d\d):(\d\d):(\d\d\.\d+)/);
|
||||||
|
if (match) {
|
||||||
|
const [_, hh, mm, ss] = match;
|
||||||
|
const totalSec = parseInt(hh) * 3600 + parseInt(mm) * 60 + parseFloat(ss);
|
||||||
|
|
||||||
|
if (lastTimestamp !== null && totalSec === lastTimestamp) {
|
||||||
|
const now = Date.now();
|
||||||
|
staleCount++;
|
||||||
|
if (staleCount >= 10 && now - lastCheckTime > 10000 && !restartTimer) {
|
||||||
|
restartTimer = setTimeout(() => {
|
||||||
|
restartTimer = null;
|
||||||
|
staleCount = 0;
|
||||||
|
try {
|
||||||
|
ffmpeg.kill('SIGKILL');
|
||||||
|
} catch (e) {
|
||||||
|
logWarn(`${consoleLogTitle} Failed to kill FFmpeg process: ${e.message}`);
|
||||||
|
}
|
||||||
|
launchFFmpeg(); // Restart FFmpeg
|
||||||
|
}, 0);
|
||||||
|
setTimeout(() => logWarn(`${consoleLogTitle} FFmpeg appears frozen. Restarting...`), 100);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lastTimestamp = totalSec;
|
||||||
|
lastCheckTime = Date.now();
|
||||||
|
staleCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ffmpeg.on('exit', (code, signal) => {
|
||||||
|
if (signal) {
|
||||||
|
logFfmpeg(`[FFmpeg exited] with signal ${signal}`);
|
||||||
|
logWarn(`${consoleLogTitle} FFmpeg was killed with signal ${signal}`);
|
||||||
|
} else {
|
||||||
|
logFfmpeg(`[FFmpeg exited] with code ${code}`);
|
||||||
|
if (code !== 0) {
|
||||||
|
logWarn(`${consoleLogTitle} FFmpeg exited unexpectedly with code ${code}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry on device fail
|
||||||
|
if (audioErrorLogged) {
|
||||||
|
logWarn(`${consoleLogTitle} Retrying in 10 seconds...`);
|
||||||
|
setTimeout(() => {
|
||||||
|
audioErrorLogged = false;
|
||||||
|
launchFFmpeg();
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
launchFFmpeg(); // Initial launch
|
||||||
|
} else if (process.platform === 'darwin') {
|
||||||
|
// macOS (sox --> 3las.server.js --> FFmpeg)
|
||||||
|
const commandDef = buildCommand(ffmpegPath);
|
||||||
|
|
||||||
|
// Apply audio boost if enabled and FFmpeg is used
|
||||||
|
if (serverConfig.audio.audioBoost && serverConfig.audio.ffmpeg) {
|
||||||
|
commandDef.args.splice(commandDef.soxArgs.indexOf('pipe:1'), 0, '-af', 'volume=1.7');
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentSox = null;
|
||||||
|
|
||||||
|
process.on('exit', () => {
|
||||||
|
if (currentSox) currentSox.kill('SIGINT');
|
||||||
});
|
});
|
||||||
|
|
||||||
ffmpeg.on('exit', (code, signal) => {
|
process.on('SIGINT', () => {
|
||||||
if (signal) logWarn(`${consoleLogTitle} FFmpeg killed with signal ${signal}`);
|
if (currentSox) currentSox.kill('SIGINT');
|
||||||
else if (code !== 0) logWarn(`${consoleLogTitle} FFmpeg exited with code ${code}`);
|
process.exit();
|
||||||
|
|
||||||
logWarn(`${consoleLogTitle} Restarting FFmpeg in 5 seconds...`);
|
|
||||||
setTimeout(launchFFmpeg, 5000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function startSox() {
|
||||||
|
if (!serverConfig.audio.ffmpeg) {
|
||||||
|
// Spawn sox
|
||||||
|
logDebug(`${consoleLogTitle} Launching sox with args: ${commandDef.soxArgs.join(' ')}`);
|
||||||
|
|
||||||
|
const sox = spawn('sox', commandDef.soxArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||||
|
currentSox = sox;
|
||||||
|
|
||||||
|
audioServer.waitUntilReady.then(() => {
|
||||||
|
audioServer.Server.StdIn = sox.stdout;
|
||||||
|
audioServer.Server.Run();
|
||||||
|
connectMessage(`${consoleLogTitle} Connected SoX \u2192 FFmpeg \u2192 Server.StdIn${serverConfig.audio.audioBoost && serverConfig.audio.ffmpeg ? ' (audio boost)' : ''}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
sox.stderr.on('data', (data) => {
|
||||||
|
logFfmpeg(`[sox stderr]: ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
sox.on('exit', (code) => {
|
||||||
|
logFfmpeg(`[sox exited] with code ${code}`);
|
||||||
|
if (code !== 0) {
|
||||||
|
setTimeout(startSox, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startSox();
|
||||||
|
|
||||||
|
if (serverConfig.audio.ffmpeg) {
|
||||||
|
logDebug(`${consoleLogTitle} Launching FFmpeg with args: ${commandDef.args.join(' ')}`);
|
||||||
|
const ffmpeg = spawn(ffmpegPath, commandDef.args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||||
|
|
||||||
|
// Pipe FFmpeg output to 3las.server.js
|
||||||
|
audioServer.waitUntilReady.then(() => {
|
||||||
|
audioServer.Server.StdIn = ffmpeg.stdout;
|
||||||
|
audioServer.Server.Run();
|
||||||
|
connectMessage(`${consoleLogTitle} Connected FFmpeg stdout \u2192 Server.StdIn${serverConfig.audio.audioBoost ? ' (audio boost)' : ''}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
ffmpeg.kill('SIGINT');
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('exit', () => {
|
||||||
|
ffmpeg.kill('SIGINT');
|
||||||
|
});
|
||||||
|
|
||||||
|
// FFmpeg stderr handling
|
||||||
|
ffmpeg.stderr.on('data', (data) => {
|
||||||
|
logFfmpeg(`[FFmpeg stderr]: ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// FFmpeg exit handling
|
||||||
|
ffmpeg.on('exit', (code) => {
|
||||||
|
logFfmpeg(`[FFmpeg exited] with code ${code}`);
|
||||||
|
if (code !== 0) {
|
||||||
|
logWarn(`${consoleLogTitle} FFmpeg exited unexpectedly with code ${code}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Linux (arecord --> 3las.server.js --> FFmpeg)
|
||||||
|
const commandDef = buildCommand(ffmpegPath);
|
||||||
|
|
||||||
|
// Apply audio boost if enabled and FFmpeg is used
|
||||||
|
if (serverConfig.audio.audioBoost && serverConfig.audio.ffmpeg) {
|
||||||
|
commandDef.args.splice(commandDef.args.indexOf('pipe:1'), 0, '-af', 'volume=1.7');
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentArecord = null;
|
||||||
|
|
||||||
|
process.on('exit', () => {
|
||||||
|
if (currentArecord) currentArecord.kill('SIGINT');
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
if (currentArecord) currentArecord.kill('SIGINT');
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
|
||||||
|
function startArecord() {
|
||||||
|
if (!serverConfig.audio.ffmpeg) {
|
||||||
|
// Spawn the arecord loop
|
||||||
|
logDebug(`${consoleLogTitle} Launching arecord with args: ${commandDef.arecordArgs.join(' ')}`);
|
||||||
|
|
||||||
|
//const arecord = spawn(commandDef.command, { shell: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||||
|
const arecord = spawn('arecord', commandDef.arecordArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||||
|
currentArecord = arecord;
|
||||||
|
|
||||||
|
audioServer.waitUntilReady.then(() => {
|
||||||
|
audioServer.Server.StdIn = arecord.stdout;
|
||||||
|
audioServer.Server.Run();
|
||||||
|
connectMessage(`${consoleLogTitle} Connected arecord \u2192 FFmpeg \u2192 Server.StdIn${serverConfig.audio.audioBoost && serverConfig.audio.ffmpeg ? ' (audio boost)' : ''}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
arecord.stderr.on('data', (data) => {
|
||||||
|
logFfmpeg(`[arecord stderr]: ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
arecord.on('exit', (code) => {
|
||||||
|
logFfmpeg(`[arecord exited] with code ${code}`);
|
||||||
|
if (code !== 0) {
|
||||||
|
setTimeout(startArecord, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startArecord();
|
||||||
|
|
||||||
|
if (serverConfig.audio.ffmpeg) {
|
||||||
|
logDebug(`${consoleLogTitle} Launching FFmpeg with args: ${commandDef.args.join(' ')}`);
|
||||||
|
const ffmpeg = spawn(ffmpegPath, commandDef.args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||||
|
|
||||||
|
// Pipe FFmpeg output to 3las.server.js
|
||||||
|
audioServer.waitUntilReady.then(() => {
|
||||||
|
audioServer.Server.StdIn = ffmpeg.stdout;
|
||||||
|
audioServer.Server.Run();
|
||||||
|
connectMessage(`${consoleLogTitle} Connected FFmpeg stdout \u2192 Server.StdIn${serverConfig.audio.audioBoost ? ' (audio boost)' : ''}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
ffmpeg.kill('SIGINT');
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('exit', () => {
|
||||||
|
ffmpeg.kill('SIGINT');
|
||||||
|
});
|
||||||
|
|
||||||
|
// FFmpeg stderr handling
|
||||||
|
ffmpeg.stderr.on('data', (data) => {
|
||||||
|
logFfmpeg(`[FFmpeg stderr]: ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// FFmpeg exit handling
|
||||||
|
ffmpeg.on('exit', (code) => {
|
||||||
|
logFfmpeg(`[FFmpeg exited] with code ${code}`);
|
||||||
|
if (code !== 0) {
|
||||||
|
logWarn(`${consoleLogTitle} FFmpeg exited unexpectedly with code ${code}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
if (ffmpeg) ffmpeg.kill('SIGINT');
|
|
||||||
process.exit();
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on('exit', () => {
|
|
||||||
if (ffmpeg) ffmpeg.kill('SIGINT');
|
|
||||||
});
|
|
||||||
|
|
||||||
launchFFmpeg();
|
|
||||||
|
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
logError(`${consoleLogTitle} Error: ${err.message}`);
|
logError(`${consoleLogTitle} Error: ${err.message}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports.audio_pipe = audio_pipe;
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"RtcConfig" : null,
|
||||||
|
"FallbackFFmpegPath": "ffmpeg.exe",
|
||||||
|
"FallbackUseMp3": true,
|
||||||
|
"FallbackUseWav": false,
|
||||||
|
"FallbackMp3Bitrate": 192,
|
||||||
|
"FallbackWavSampleRate": 16000,
|
||||||
|
"AdminKey": ""
|
||||||
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
const WebSocket = require('ws');
|
|
||||||
const { serverConfig } = require('../server_config');
|
|
||||||
const { audio_pipe } = require('./index.js');
|
|
||||||
const { PassThrough } = require('stream');
|
|
||||||
|
|
||||||
function createAudioServer() {
|
|
||||||
const audioWss = new WebSocket.Server({ noServer: true });
|
|
||||||
|
|
||||||
audioWss.on('connection', (ws, request) => {
|
|
||||||
const clientIp =
|
|
||||||
request.headers['x-forwarded-for'] ||
|
|
||||||
request.connection.remoteAddress;
|
|
||||||
|
|
||||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
|
||||||
ws.close(1008, 'Banned IP');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
audio_pipe.on('data', (chunk) => {
|
|
||||||
audioWss.clients.forEach((client) => {
|
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
|
||||||
client.send(chunk, {
|
|
||||||
binary: true,
|
|
||||||
compress: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
audio_pipe.on('end', () => {
|
|
||||||
audioWss.clients.forEach((client) => {
|
|
||||||
client.close(1001, "Audio stream ended");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return audioWss;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { createAudioServer };
|
|
||||||
Reference in New Issue
Block a user