This commit is contained in:
legop3
2026-03-15 22:43:05 -04:00
parent 2ea222355f
commit 95972af558
20 changed files with 552 additions and 134 deletions
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
set -euo pipefail
set +H
ENV_FILE="${VIDEO_ENV_FILE:-/var/lib/roverd/video.env}"
if [[ ! -f "$ENV_FILE" ]]; then
echo "Environment file ${ENV_FILE} missing; cannot start audio forward listener" >&2
exit 1
fi
# shellcheck disable=SC1090
source "$ENV_FILE"
: "${AUDIO_FORWARD_URL:?AUDIO_FORWARD_URL not set in ${ENV_FILE}}"
PLAYBACK_DEVICE="${AUDIO_PLAYBACK_DEVICE:-default}"
if [[ -n "${FFMPEG_BIN:-}" ]]; then
FFMPEG_BIN_PATH="$FFMPEG_BIN"
elif command -v ffmpeg >/dev/null 2>&1; then
FFMPEG_BIN_PATH="$(command -v ffmpeg)"
else
echo "ffmpeg not found; install it via apt install ffmpeg." >&2
exit 1
fi
if command -v aplay >/dev/null 2>&1; then
APLAY_BIN_PATH="$(command -v aplay)"
else
echo "aplay not found; install it via apt install alsa-utils." >&2
exit 1
fi
run_pipeline() {
"${FFMPEG_BIN_PATH}" \
-hide_banner \
-loglevel warning \
-fflags nobuffer \
-flags low_delay \
-analyzeduration 0 \
-probesize 32 \
-i "${AUDIO_FORWARD_URL}" \
-vn \
-ac 1 \
-ar 16000 \
-f s16le \
pipe:1 \
| "${APLAY_BIN_PATH}" \
-q \
-D "${PLAYBACK_DEVICE}" \
-f S16_LE \
-r 16000 \
-c 1
}
trap 'kill 0 2>/dev/null' EXIT INT TERM
while true; do
if run_pipeline; then
exit 0
fi
echo "Audio forward listener exited ffmpeg=${PIPESTATUS[0]} aplay=${PIPESTATUS[1]}, restarting in 2s..." >&2
sleep 2
done
+12 -4
View File
@@ -20,8 +20,8 @@ Options:
The script must run from the repository root and as root (sudo). It will:
* create system users/groups if needed
* install /usr/local/bin/roverd and /etc/roverd.yaml
* install /usr/local/bin/video-publisher and its systemd unit
* enable roverd.service and video-publisher.service
* install /usr/local/bin/video/audio helpers and systemd units
* enable roverd.service and media publisher/listener services
USAGE
}
@@ -210,14 +210,20 @@ log "Installed video-publisher systemd unit"
install -D -o root -g root -m 0755 pi/bin/audio-only-publisher.sh /usr/local/bin/audio-only-publisher
install -m 0644 pi/systemd/audio-only-publisher.service /etc/systemd/system/audio-only-publisher.service
log "Installed audio-only publisher helper + systemd unit"
# Install audio-forward listener assets
install -D -o root -g root -m 0755 pi/bin/audio-forward-listener.sh /usr/local/bin/audio-forward-listener
install -m 0644 pi/systemd/audio-forward-listener.service /etc/systemd/system/audio-forward-listener.service
log "Installed audio-forward listener helper + systemd unit"
install -d -o roverd -g roverd /var/lib/roverd
cat > /var/lib/roverd/video.env <<'ENV'
# Managed by roverd; placeholder values will be overwritten at runtime.
PUBLISH_URL=srt://192.168.0.86:9000?streamid=#!::r=CHANGE_ME,m=publish&latency=10&mode=caller&transtype=live&pkt_size=1316
AUDIO_PUBLISH_URL=srt://192.168.0.86:9000?streamid=#!::r=CHANGE_ME-audio,m=publish&latency=10&mode=caller&transtype=live&pkt_size=1316
AUDIO_FORWARD_URL=srt://192.168.0.86:9000?streamid=#!::r=CHANGE_ME-fwd,m=read&latency=10&mode=caller&transtype=live&pkt_size=1316
VIDEO_BITRATE=2000000
AUDIO_ENABLE=0
AUDIO_DEVICE=hw:0,0
AUDIO_PLAYBACK_DEVICE=default
AUDIO_RATE=48000
AUDIO_CHANNELS=2
ENV
@@ -244,13 +250,15 @@ systemctl daemon-reload
systemctl enable roverd.service
systemctl enable video-publisher.service
systemctl enable audio-only-publisher.service
systemctl enable audio-forward-listener.service
if [[ $CONFIG_EXISTS -eq 1 ]]; then
systemctl restart roverd.service
systemctl restart video-publisher.service
systemctl restart audio-only-publisher.service
log "Restarted roverd + video/audio publishers"
systemctl restart audio-forward-listener.service
log "Restarted roverd + media publishers/listener"
else
log "Skipped auto-start because config is the sample; edit $CONFIG_DEST then run: sudo systemctl restart roverd video-publisher audio-only-publisher"
log "Skipped auto-start because config is the sample; edit $CONFIG_DEST then run: sudo systemctl restart roverd video-publisher audio-only-publisher audio-forward-listener"
fi
log "Install complete"
+25 -1
View File
@@ -56,6 +56,7 @@ type BatteryConfig struct {
type AudioConfig struct {
CaptureEnabled bool `yaml:"captureEnabled" json:"captureEnabled"`
CaptureDevice string `yaml:"captureDevice" json:"captureDevice,omitempty"`
PlaybackDevice string `yaml:"playbackDevice" json:"playbackDevice,omitempty"`
SampleRate int `yaml:"sampleRate" json:"sampleRate,omitempty"`
Channels int `yaml:"channels" json:"channels,omitempty"`
Bitrate int `yaml:"bitrate" json:"bitrate,omitempty"`
@@ -79,6 +80,7 @@ type HornConfig struct {
type MediaConfig struct {
PublishURL string `yaml:"publishUrl" json:"publishUrl,omitempty"`
AudioPublishURL string `yaml:"audioPublishUrl" json:"audioPublishUrl,omitempty"`
AudioForwardURL string `yaml:"audioForwardUrl" json:"audioForwardUrl,omitempty"`
PublishPort int `yaml:"publishPort" json:"-"`
Manage bool `yaml:"manage"`
ManageAudio bool `yaml:"manageAudio"`
@@ -170,6 +172,7 @@ func LoadConfig(path string) (*Config, error) {
Audio: AudioConfig{
CaptureEnabled: false,
CaptureDevice: "rovermic",
PlaybackDevice: "default",
SampleRate: 48000,
Channels: 2,
Bitrate: 24000,
@@ -245,6 +248,13 @@ func LoadConfig(path string) (*Config, error) {
}
cfg.Media.AudioPublishURL = derived
}
if cfg.Media.AudioForwardURL == "" {
derived, err := deriveReadURL(cfg.ServerURL, cfg.Name+"-fwd", cfg.Media.PublishPort)
if err != nil {
return nil, fmt.Errorf("derive audioForwardUrl: %w", err)
}
cfg.Media.AudioForwardURL = derived
}
if err := validateServoConfig(&cfg.CameraServo); err != nil {
return nil, fmt.Errorf("cameraServo: %w", err)
}
@@ -304,6 +314,9 @@ func validateAudioConfig(cfg *AudioConfig) {
if cfg.CaptureEnabled && cfg.CaptureDevice == "" {
cfg.CaptureDevice = "hw:0,0"
}
if cfg.PlaybackDevice == "" {
cfg.PlaybackDevice = "default"
}
if cfg.SampleRate <= 0 {
cfg.SampleRate = 48000
}
@@ -369,9 +382,20 @@ func validateAutoSideBrushConfig(cfg *AutoSideBrushConfig) {
}
func derivePublishURL(serverURL, streamName string, port int) (string, error) {
return deriveSRTURL(serverURL, streamName, port, "publish")
}
func deriveReadURL(serverURL, streamName string, port int) (string, error) {
return deriveSRTURL(serverURL, streamName, port, "read")
}
func deriveSRTURL(serverURL, streamName string, port int, mode string) (string, error) {
if streamName == "" {
return "", errors.New("missing stream name for publishUrl")
}
if mode == "" {
mode = "publish"
}
parsed, err := url.Parse(serverURL)
if err != nil {
return "", err
@@ -384,5 +408,5 @@ func derivePublishURL(serverURL, streamName string, port int) (string, error) {
port = 9000
}
escaped := url.PathEscape(streamName)
return fmt.Sprintf("srt://%s:%d?streamid=#!::r=%s,m=publish&latency=10&mode=caller&transtype=live&pkt_size=1316", host, port, escaped), nil
return fmt.Sprintf("srt://%s:%d?streamid=#!::r=%s,m=%s&latency=10&mode=caller&transtype=live&pkt_size=1316", host, port, escaped, mode), nil
}
+8
View File
@@ -27,6 +27,9 @@ func UpdatePublisherEnv(media MediaConfig, audio AudioConfig) error {
if audio.CaptureEnabled && media.AudioPublishURL != "" {
fmt.Fprintf(&buf, "AUDIO_PUBLISH_URL=%s\n", media.AudioPublishURL)
}
if media.AudioForwardURL != "" {
fmt.Fprintf(&buf, "AUDIO_FORWARD_URL=%s\n", media.AudioForwardURL)
}
if media.VideoWidth > 0 {
fmt.Fprintf(&buf, "VIDEO_WIDTH=%d\n", media.VideoWidth)
}
@@ -49,6 +52,11 @@ func UpdatePublisherEnv(media MediaConfig, audio AudioConfig) error {
}
fmt.Fprintf(&buf, "AUDIO_ENABLE=%d\n", boolToInt(audio.CaptureEnabled))
fmt.Fprintf(&buf, "AUDIO_DEVICE=%s\n", audioDevice)
playbackDevice := audio.PlaybackDevice
if playbackDevice == "" {
playbackDevice = "default"
}
fmt.Fprintf(&buf, "AUDIO_PLAYBACK_DEVICE=%s\n", playbackDevice)
fmt.Fprintf(&buf, "AUDIO_RATE=%d\n", audio.SampleRate)
fmt.Fprintf(&buf, "AUDIO_CHANNELS=%d\n", audio.Channels)
if err := os.WriteFile(publisherEnvPath, buf.Bytes(), 0o640); err != nil {
+2
View File
@@ -16,6 +16,7 @@ battery:
maxWheelSpeed: 350
media:
publishUrl: srt://192.168.0.86:9000?streamid=#!::r=roomba-alpha,m=publish&latency=10&mode=caller&transtype=live&pkt_size=1316
audioForwardUrl: srt://192.168.0.86:9000?streamid=#!::r=roomba-alpha-fwd,m=read&latency=10&mode=caller&transtype=live&pkt_size=1316
publishPort: 9000
videoBitrate: 2000000
manage: true
@@ -38,6 +39,7 @@ cameraServo:
audio:
captureEnabled: false
captureDevice: hw:0,0
playbackDevice: default
sampleRate: 48000
channels: 2
bitrate: 24000
+18
View File
@@ -0,0 +1,18 @@
[Unit]
Description=Rover Audio Forward Listener (SRT -> ALSA)
After=network-online.target roverd.service
Wants=network-online.target
[Service]
Type=simple
User=roverd
Group=audio
EnvironmentFile=/var/lib/roverd/video.env
ExecStart=/usr/local/bin/audio-forward-listener
KillMode=control-group
TimeoutStopSec=5
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target