mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
oh oops
This commit is contained in:
@@ -23,13 +23,21 @@ AUDIO_DEVICE="${AUDIO_DEVICE:-plughw:0,0}"
|
||||
AUDIO_RATE="${AUDIO_RATE:-48000}"
|
||||
AUDIO_CHANNELS="${AUDIO_CHANNELS:-1}"
|
||||
AUDIO_BITRATE="${AUDIO_BITRATE:-24000}"
|
||||
AUDIO_CODEC="${AUDIO_CODEC:-pcm_mulaw}"
|
||||
|
||||
# Normalize device/rate to match the HAT capture; plughw handles any minor conversions.
|
||||
# Keep audio encode light for the Pi Zero by using 16 kHz mono Opus at a low complexity.
|
||||
AUDIO_DEVICE="plughw:0,0"
|
||||
AUDIO_RATE=16000
|
||||
AUDIO_CHANNELS=1
|
||||
AUDIO_BITRATE=16000
|
||||
# Defaults chosen to keep Pi Zero CPU low. Opus can be enabled by setting AUDIO_CODEC=libopus.
|
||||
if [[ "${AUDIO_CODEC}" == "libopus" ]]; then
|
||||
AUDIO_RATE=16000
|
||||
AUDIO_BITRATE=16000
|
||||
else
|
||||
# G.711 mu-law (or A-law if AUDIO_CODEC=pcm_alaw): very low CPU, 8 kHz mono, ~64 kbps.
|
||||
AUDIO_CODEC="pcm_mulaw"
|
||||
AUDIO_RATE=8000
|
||||
AUDIO_BITRATE=64000
|
||||
fi
|
||||
# Flip the camera 180deg (supported by rpicam-vid/libcamera-vid)
|
||||
FLIP_ARGS=(--rotation 180)
|
||||
|
||||
@@ -89,14 +97,12 @@ run_pipeline() {
|
||||
-ar "${AUDIO_RATE}" \
|
||||
-i "${AUDIO_DEVICE}" \
|
||||
-c:v copy \
|
||||
-c:a libopus \
|
||||
-c:a "${AUDIO_CODEC}" \
|
||||
-b:a "${AUDIO_BITRATE}" \
|
||||
-compression_level 0 \
|
||||
-application voip \
|
||||
-frame_duration 60 \
|
||||
$( [[ "${AUDIO_CODEC}" == "libopus" ]] && echo "-compression_level 0 -application voip -frame_duration 60" ) \
|
||||
-ac:a "${AUDIO_CHANNELS}" \
|
||||
-ar:a "${AUDIO_RATE}" \
|
||||
-af "pan=1c|c0=c0,volume=20dB" \
|
||||
-af "$( [[ "${AUDIO_CODEC}" == "libopus" ]] && echo "pan=1c|c0=c0," )volume=20dB" \
|
||||
-flush_packets 1 \
|
||||
-f mpegts \
|
||||
"${PUBLISH_URL}"
|
||||
|
||||
@@ -12,6 +12,8 @@ media:
|
||||
# http://<base>/<roverId>/whep
|
||||
# Example: http://192.168.0.86:8889/video
|
||||
whepBaseUrl: "http://192.168.0.86:8889/video"
|
||||
# Set audioBridge to false to disable server-side audio transcoding from <id>-raw -> <id>
|
||||
# audioBridge: true
|
||||
|
||||
homeAssistant:
|
||||
url: "http://homeassistant.local:8123"
|
||||
|
||||
@@ -26,3 +26,4 @@ require('./src/services/sessionService');
|
||||
require('./src/services/batteryManager');
|
||||
require('./src/services/discordBotService');
|
||||
require('./src/services/httpServer');
|
||||
require('./src/services/audioBridgeService');
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
const { spawn } = require('child_process');
|
||||
const logger = require('../globals/logger').child('audioBridge');
|
||||
const { loadConfig } = require('../helpers/configLoader');
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
const MEDIAMTX_API =
|
||||
process.env.MEDIAMTX_API || 'http://127.0.0.1:9997/v3/paths/list';
|
||||
const SRT_HOST = process.env.MEDIAMTX_SRT_HOST || '127.0.0.1';
|
||||
const POLL_MS = 5000;
|
||||
|
||||
// Track active ffmpeg bridges keyed by raw path name (e.g., rover-raw).
|
||||
const bridges = new Map();
|
||||
|
||||
function stopBridge(rawName) {
|
||||
const proc = bridges.get(rawName);
|
||||
if (!proc) return;
|
||||
bridges.delete(rawName);
|
||||
proc.kill('SIGTERM');
|
||||
logger.info('stopped bridge', { rawName });
|
||||
}
|
||||
|
||||
function startBridge(rawName, baseName) {
|
||||
if (bridges.has(rawName)) return;
|
||||
|
||||
const inputUrl = `srt://${SRT_HOST}:9000?streamid=#!::r=${rawName},m=request&mode=caller&transtype=live&latency=20`;
|
||||
const outputUrl = `srt://${SRT_HOST}:9000?streamid=#!::r=${baseName},m=publish&mode=caller&transtype=live&pkt_size=1316`;
|
||||
|
||||
const args = [
|
||||
'-hide_banner',
|
||||
'-loglevel',
|
||||
'warning',
|
||||
'-fflags',
|
||||
'nobuffer',
|
||||
'-thread_queue_size',
|
||||
'512',
|
||||
'-i',
|
||||
inputUrl,
|
||||
'-map',
|
||||
'0:v',
|
||||
'-map',
|
||||
'0:a?',
|
||||
'-c:v',
|
||||
'copy',
|
||||
'-c:a',
|
||||
'libopus',
|
||||
'-b:a',
|
||||
'24000',
|
||||
'-ac',
|
||||
'1',
|
||||
'-ar',
|
||||
'16000',
|
||||
'-application',
|
||||
'voip',
|
||||
'-frame_duration',
|
||||
'60',
|
||||
'-af',
|
||||
'pan=1c|c0=c0,volume=12dB',
|
||||
'-flush_packets',
|
||||
'1',
|
||||
'-f',
|
||||
'mpegts',
|
||||
outputUrl,
|
||||
];
|
||||
|
||||
const proc = spawn('ffmpeg', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
bridges.set(rawName, proc);
|
||||
logger.info('started bridge', { rawName, baseName });
|
||||
|
||||
proc.stdout.on('data', (data) => {
|
||||
logger.debug(data.toString().trim());
|
||||
});
|
||||
proc.stderr.on('data', (data) => {
|
||||
logger.debug(data.toString().trim());
|
||||
});
|
||||
proc.on('exit', (code, signal) => {
|
||||
bridges.delete(rawName);
|
||||
logger.info('bridge exited', { rawName, code, signal });
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchPaths() {
|
||||
try {
|
||||
const res = await fetch(MEDIAMTX_API, { signal: AbortSignal.timeout(3000) });
|
||||
if (!res.ok) {
|
||||
throw new Error(`mediamtx api ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
} catch (err) {
|
||||
logger.warn('path fetch failed: %s', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function pickRawPairs(items) {
|
||||
const names = new Set(items.map((i) => i.name));
|
||||
return items
|
||||
.filter((i) => i.name.endsWith('-raw'))
|
||||
.map((raw) => {
|
||||
const baseName = raw.name.slice(0, -4);
|
||||
return { rawName: raw.name, baseName };
|
||||
})
|
||||
.filter(({ baseName }) => !names.has(baseName));
|
||||
}
|
||||
|
||||
async function reconcile() {
|
||||
const data = await fetchPaths();
|
||||
if (!data?.items) {
|
||||
// On failure, stop all bridges so we don't run blind.
|
||||
Array.from(bridges.keys()).forEach(stopBridge);
|
||||
return;
|
||||
}
|
||||
|
||||
const activeRaw = new Set();
|
||||
pickRawPairs(data.items).forEach(({ rawName, baseName }) => {
|
||||
activeRaw.add(rawName);
|
||||
startBridge(rawName, baseName);
|
||||
});
|
||||
|
||||
// Stop bridges whose raw path disappeared.
|
||||
Array.from(bridges.keys()).forEach((rawName) => {
|
||||
if (!activeRaw.has(rawName)) {
|
||||
stopBridge(rawName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function start() {
|
||||
// Only enable when explicit media config allows it, or by default.
|
||||
const enabled = config.media?.audioBridge !== false;
|
||||
if (!enabled) {
|
||||
logger.info('audio bridge disabled by config');
|
||||
return;
|
||||
}
|
||||
logger.info('audio bridge enabled; watching for *-raw streams');
|
||||
reconcile();
|
||||
setInterval(reconcile, POLL_MS);
|
||||
}
|
||||
|
||||
start();
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
};
|
||||
Reference in New Issue
Block a user