Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 26 additions & 7 deletions biome.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
{
"files": {
"ignore": [
"ui/node_modules/**/*",
"ui/vendor/**/*",
"ui/dist/**/*",
"config.json",
"gsm_operator_cache.json"
"includes": [
"./package.json",
"**/server/**/*",
"**/ui/**/*",
"**/*.json",
"!**/ui/node_modules/**/*",
"!**/ui/vendor/**/*",
"!**/ui/dist/**/*",
"!**/config.json",
"!**/gsm_operator_cache.json"
],
"include": ["server/**/*", "ui/**/*", "*.json"],
"ignoreUnknown": true
},
"linter": {
"rules": {
"style": {
"noParameterAssign": "error",
"useAsConstAssertion": "error",
"useDefaultParameterLast": "error",
"useEnumInitializers": "error",
"useSelfClosingElements": "error",
"useSingleVarDeclarator": "error",
"noUnusedTemplateLiteral": "error",
"useNumberNamespace": "error",
"noInferrableTypes": "error",
"noUselessElse": "error"
}
}
}
}
Binary file modified bun.lockb
Binary file not shown.
12 changes: 9 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{
"name": "@belaui/root",
"type": "module",
"workspaces": ["server", "ui"],
"workspaces": [
"server",
"ui"
],
"dependencies": {},
"devDependencies": {
"@types/bun": "^1.2.4",
Expand All @@ -11,7 +14,7 @@
},
"scripts": {
"check": "bun run --bun --filter '*' check && run-p check:*",
"check:biome": "bun x --bun @biomejs/biome check",
"check:biome": "bun x --bun @biomejs/biome check .",
"dev": "bun run --bun --filter '*' dev",
"fix": "bun run --bun --filter '*' fix",
"lint": "bun run --bun --filter '*' lint",
Expand All @@ -20,5 +23,8 @@
"build:copy-ui": "mkdir -p ./dist/public && rm -rf ./dist/public/* && cp -r ./ui/dist/* ./dist/public/",
"build:copy-server": "mkdir -p ./dist && cp ./server/belaUI ./dist/"
},
"trustedDependencies": ["@biomejs/biome", "esbuild"]
"trustedDependencies": [
"@biomejs/biome",
"esbuild"
]
}
4 changes: 2 additions & 2 deletions server/helpers/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export async function execPNR(cmd: string) {
try {
const res = await execP(cmd);
return { stdout: res.stdout, stderr: res.stderr, code: 0 };
} catch (err) {
} catch (_err) {
return { stdout: "", stderr: "", code: 1 };
}
}
Expand All @@ -21,7 +21,7 @@ export function checkExecPathSafe(path: string) {
try {
fs.accessSync(path, fs.constants.R_OK);
return true;
} catch (err) {
} catch (_err) {
logger.error(
`\n\n${path} not found, double check the settings in setup.json`,
);
Expand Down
79 changes: 50 additions & 29 deletions server/modules/ingest/rtmp.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,69 @@
import { parseStringPromise as parseXmlStringPromise } from "xml2js";

import { httpGet } from "../network/internet";

/* Monitor the RTMP server */
let rtmpIngestStats: Record<string, string> = {};
let prevRtmpBytesIn: Record<string, number> = {};
async function updateRtmpStats() {
const statsReq = await httpGet({
// Define specific types for the RTMP statistics
type StreamBandwidth = `${number} Kbps`;
type StreamName = `RTMP ingest - ${string}`;

// Define more specific record types
type RtmpStats = Record<StreamName, StreamBandwidth>;
type BytesCounter = Record<StreamName, number>;

// Track RTMP server stats
let currentStreamBandwidths: RtmpStats = {};
let previousBytesCounted: BytesCounter = {};

async function updateRtmpStats(): Promise<void> {
const serverResponse = await httpGet({
host: "localhost",
port: 1936,
});
if (statsReq.code !== 200) return;

const newStats: Record<string, string> = {};
const bytesIn: Record<string, number> = {};

let stats = await parseXmlStringPromise(statsReq.body);
stats = stats.rtmp.server[0].application[0].live[0];

if (stats.stream) {
for (const s of stats.stream) {
const name = `RTMP ingest - ${s.name[0]}`;
bytesIn[name] = Number.parseInt(s.bytes_in[0]);
const bw = Math.round(
((bytesIn[name] - (prevRtmpBytesIn[name] || 0)) * 8) / 1024,
);
newStats[name] = `${bw} Kbps`;

// Exit if request failed
if (serverResponse.code !== 200) return;

const newStreamBandwidths: RtmpStats = {};
const currentBytesCounted: BytesCounter = {};

// Parse XML response and navigate to live streams data
const xmlData = await parseXmlStringPromise(serverResponse.body);
const liveStreamData = xmlData.rtmp.server[0].application[0].live[0];

// Process each active stream if any exist
if (liveStreamData.stream) {
for (const stream of liveStreamData.stream) {
// Create standardized stream name
const streamName = `RTMP ingest - ${stream.name[0]}` as StreamName;

// Count total bytes received for this stream
currentBytesCounted[streamName] = Number.parseInt(stream.bytes_in[0]);

// Calculate bandwidth based on difference since last check
const previousBytes = previousBytesCounted[streamName] || 0;
const bytesDifference = currentBytesCounted[streamName] - previousBytes;
const bandwidthKbps = Math.round((bytesDifference * 8) / 1024);

// Store formatted bandwidth value
newStreamBandwidths[streamName] =
`${bandwidthKbps} Kbps` as StreamBandwidth;
}
}

rtmpIngestStats = newStats;
prevRtmpBytesIn = bytesIn;
// Update global state with new values
currentStreamBandwidths = newStreamBandwidths;
previousBytesCounted = currentBytesCounted;
}

export function initRTMPIngestStats() {
export function initRTMPIngestStats(): void {
setInterval(async () => {
try {
await updateRtmpStats();
} catch (err) {
console.log(err);
} catch (error) {
console.log(error);
}
}, 1000);
}

export function getRTMPIngestStats() {
return rtmpIngestStats;
export function getRTMPIngestStats(): RtmpStats {
return currentStreamBandwidths;
}
86 changes: 63 additions & 23 deletions server/modules/ingest/srt.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,87 @@
import { spawn } from "node:child_process";

/* Use srt-live-transmit to convert from SRT to UDP (usable by udpsrc in gstreamer), with stats */
let ingestStats: string | null = null;
function runSLT() {
const proc = spawn(
// Define types for better clarity
type ConnectionStats = `${number} Kbps, ${number} ms RTT` | "" | null;
interface SrtStatsData {
recv: {
mbitRate: number;
};
link: {
rtt: number;
};
}

/**
* Manages SRT to UDP conversion using srt-live-transmit
* Collects and formats connection statistics
*/
let currentConnectionStats: ConnectionStats = null;

/**
* Starts the SRT Live Transmit process to convert SRT stream to UDP
* Collects and processes statistics about the connection
*/
function startSrtTransmitter(): void {
// Launch the srt-live-transmit process with appropriate parameters
const transmitProcess = spawn(
"srt-live-transmit",
[
"-st:yes",
"-stats-report-frequency:500",
"-statspf:json",
"srt://:4000",
"udp://127.0.0.1:4001",
"srt://:4000", // SRT input on port 4000
"udp://127.0.0.1:4001", // UDP output on localhost:4001
],
{
detached: true,
stdio: ["ignore", "pipe", "pipe"],
},
);

let hasInConn = false;
proc.stdout.on("data", (data) => {
if (!hasInConn) return;
let hasActiveConnection = false;

// Process statistics output from stdout
transmitProcess.stdout.on("data", (data) => {
if (!hasActiveConnection) return;

try {
const stats = JSON.parse(data.toString("utf8"));
ingestStats = `${Math.round(stats.recv.mbitRate * 1024)} Kbps, ${Math.round(stats.link.rtt)} ms RTT`;
} catch (err) {}
// Parse JSON stats and format connection information
const statsData: SrtStatsData = JSON.parse(data.toString("utf8"));
const bitrate = Math.round(statsData.recv.mbitRate * 1024);
const roundTripTime = Math.round(statsData.link.rtt);

currentConnectionStats = `${bitrate} Kbps, ${roundTripTime} ms RTT`;
} catch (_err) {
// Silently handle parsing errors
}
});

proc.stderr.on("data", (data) => {
const datStr = data.toString("utf8");
if (datStr.match("SRT source disconnected")) {
ingestStats = "";
hasInConn = false;
} else if (datStr.match("Accepted SRT source connection")) {
hasInConn = true;
// Monitor connection status from stderr
transmitProcess.stderr.on("data", (data) => {
const logMessage = data.toString("utf8");

if (logMessage.match("SRT source disconnected")) {
// Handle disconnection
currentConnectionStats = "";
hasActiveConnection = false;
} else if (logMessage.match("Accepted SRT source connection")) {
// Handle the new connection
hasActiveConnection = true;
}
});
}

export function initSRTIngest() {
runSLT();
/**
* Initialize the SRT ingest system
*/
export function initSRTIngest(): void {
startSrtTransmitter();
}

export function getSRTIngestStats() {
return ingestStats;
/**
* Get the current SRT connection statistics
* @returns Current connection stats: bitrate and round-trip time, or empty if disconnected
*/
export function getSRTIngestStats(): ConnectionStats {
return currentConnectionStats;
}
2 changes: 1 addition & 1 deletion server/modules/modems/modem-network-scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ import { broadcastMsg } from "../ui/websocket-server.ts";
import { mmNetworkScan } from "./mmcli.ts";
import {
type AvailableNetwork,
type Modem,
getAvailableNetworksForModem,
getModem,
getModemIds,
type Modem,
} from "./modems-state.ts";

function modemBuildAvailableNetworksMessage(id: number) {
Expand Down
4 changes: 2 additions & 2 deletions server/modules/modems/modem-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ import { broadcastMsg } from "../ui/websocket-server.ts";
import type { ModemId } from "./mmcli.ts";
import {
type AvailableNetwork,
type Modem,
type ModemConfig,
getAvailableNetworksForModem,
getModem,
getModemIds,
type Modem,
type ModemConfig,
} from "./modems-state.ts";

type ModemsResponseModemStatus = {
Expand Down
8 changes: 4 additions & 4 deletions server/modules/modems/modem-update-loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import {
type NetworkManagerConnection,
type NetworkManagerConnectionModemConfig,
nmConnAdd,
nmConnGetFields,
nmConnect,
nmConnGetFields,
} from "../network/network-manager.ts";

import { setup } from "../setup.ts";
Expand All @@ -32,20 +32,20 @@ import { getGsmConnections, resetGsmConnections } from "./gsm-connections.ts";
import {
type ModemId,
type ModemInfo,
type SimInfo,
mmConvertAccessTech,
mmConvertNetworkType,
mmConvertNetworkTypes,
mmGetModem,
mmGetSim,
mmList,
type SimInfo,
} from "./mmcli.ts";
import { broadcastModems } from "./modem-status.ts";
import {
type Modem,
type ModemConfig,
getModem,
getModems,
type Modem,
type ModemConfig,
removeModem,
setModem,
} from "./modems-state.ts";
Expand Down
9 changes: 6 additions & 3 deletions server/modules/modems/modems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { mmSetNetworkTypes } from "./mmcli.ts";
import { modemNetworkScan } from "./modem-network-scan.ts";
import { broadcastModems } from "./modem-status.ts";
import { sanitizeModemConfigForNetworkManager } from "./modem-update-loop.ts";
import { type ModemConfig, getModem } from "./modems-state.ts";
import { getModem, type ModemConfig } from "./modems-state.ts";

type ModemConfigMessage = {
config: {
Expand Down Expand Up @@ -71,7 +71,7 @@ async function updateModemConnection(
}

async function handleModemConfig(
conn: WebSocket,
_conn: WebSocket,
msg: ModemConfigMessage["config"],
) {
if (!msg.device) {
Expand Down Expand Up @@ -186,7 +186,10 @@ async function handleModemConfig(
broadcastModems({ [msg.device]: true });
}

async function handleModemScan(conn: WebSocket, msg: ModemScanMessage["scan"]) {
async function handleModemScan(
_conn: WebSocket,
msg: ModemScanMessage["scan"],
) {
const modemId = Number.parseInt(msg.device, 10);
if (!msg || !getModem(modemId)) return;

Expand Down
2 changes: 1 addition & 1 deletion server/modules/network/dns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ let dnsCache: Record<string, DnsCacheEntry> = {};
const dnsResults: Record<string, ResolveResult> = {};
try {
dnsCache = JSON.parse(fs.readFileSync(DNS_CACHE_FILE, "utf8"));
} catch (err) {
} catch (_err) {
logger.warn(
"Failed to load the persistent DNS cache, starting with an empty cache",
);
Expand Down
Loading