diff --git a/dist/roverd b/dist/roverd index f97409b6..d2c875c2 100755 Binary files a/dist/roverd and b/dist/roverd differ diff --git a/dist/servoverifier b/dist/servoverifier index 415019ef..9e48d212 100755 Binary files a/dist/servoverifier and b/dist/servoverifier differ diff --git a/pi/bin/audio-only-publisher.sh b/pi/bin/audio-only-publisher.sh new file mode 100644 index 00000000..7840ceb9 --- /dev/null +++ b/pi/bin/audio-only-publisher.sh @@ -0,0 +1,58 @@ +#!/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 publish audio" >&2 + exit 1 +fi + +# shellcheck disable=SC1090 +source "$ENV_FILE" +: "${AUDIO_PUBLISH_URL:?AUDIO_PUBLISH_URL not set in ${ENV_FILE}}" + +AUDIO_DEVICE="${AUDIO_DEVICE:-hw:0,0}" +AUDIO_RATE="${AUDIO_RATE:-48000}" +AUDIO_CHANNELS="${AUDIO_CHANNELS:-2}" + +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 + +run_pipeline() { + "${FFMPEG_BIN_PATH}" \ + -hide_banner \ + -loglevel warning \ + -fflags nobuffer \ + -rtbufsize 0 \ + -thread_queue_size 4096 \ + -f s32le \ + -ar "${AUDIO_RATE}" \ + -ac "${AUDIO_CHANNELS}" \ + -i <(arecord -D "${AUDIO_DEVICE}" -f S32_LE -c "${AUDIO_CHANNELS}" -r "${AUDIO_RATE}" -B 65536 -F 2048 -q -t raw) \ + -af "aresample=16000,pan=mono|c0=0.5*FL+0.5*FR" \ + -c:a libopus \ + -b:a 24000 \ + -ar:a 16000 \ + -ac:a 1 \ + -application lowdelay \ + -frame_duration 20 \ + -compression_level 0 \ + -f mpegts \ + "${AUDIO_PUBLISH_URL}" +} + +while true; do + if run_pipeline; then + exit 0 + fi + echo "Audio-only publisher exited, restarting in 2s..." >&2 + sleep 2 +done diff --git a/pi/install_roverd.sh b/pi/install_roverd.sh index c746ea3e..a1348286 100755 --- a/pi/install_roverd.sh +++ b/pi/install_roverd.sh @@ -206,6 +206,10 @@ install -D -o root -g root -m 0755 pi/bin/video-publisher.sh /usr/local/bin/vide log "Installed video-publisher helper" install -m 0644 pi/systemd/video-publisher.service /etc/systemd/system/video-publisher.service log "Installed video-publisher systemd unit" +# Install audio-only publisher assets +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 capture assets (arecord -> FIFO) [optional, not enabled by default] install -D -o root -g root -m 0755 pi/bin/audio-capture.sh /usr/local/bin/audio-capture install -m 0644 pi/systemd/audio-capture.service /etc/systemd/system/audio-capture.service @@ -214,6 +218,7 @@ 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 VIDEO_WIDTH=1280 VIDEO_HEIGHT=720 VIDEO_FPS=30 @@ -245,12 +250,14 @@ install_audio_support systemctl daemon-reload systemctl enable roverd.service systemctl enable video-publisher.service +systemctl enable audio-only-publisher.service if [[ $CONFIG_EXISTS -eq 1 ]]; then systemctl restart roverd.service systemctl restart video-publisher.service - log "Restarted roverd + video publisher" + systemctl restart audio-only-publisher.service + log "Restarted roverd + video/audio publishers" else - log "Skipped auto-start because config is the sample; edit $CONFIG_DEST then run: sudo systemctl restart roverd video-publisher" + log "Skipped auto-start because config is the sample; edit $CONFIG_DEST then run: sudo systemctl restart roverd video-publisher audio-only-publisher" fi log "Install complete" diff --git a/pi/roverd/config.go b/pi/roverd/config.go index 904757a1..3d0be548 100644 --- a/pi/roverd/config.go +++ b/pi/roverd/config.go @@ -66,16 +66,19 @@ type AudioConfig struct { } type MediaConfig struct { - PublishURL string `yaml:"publishUrl" json:"publishUrl,omitempty"` - PublishPort int `yaml:"publishPort" json:"-"` - Manage bool `yaml:"manage"` - Service string `yaml:"service"` - HealthURL string `yaml:"healthUrl"` - HealthInterval Duration `yaml:"healthInterval"` - VideoWidth int `yaml:"videoWidth" json:"-"` - VideoHeight int `yaml:"videoHeight" json:"-"` - VideoFPS int `yaml:"videoFps" json:"-"` - VideoBitrate int `yaml:"videoBitrate" json:"-"` + PublishURL string `yaml:"publishUrl" json:"publishUrl,omitempty"` + AudioPublishURL string `yaml:"audioPublishUrl" json:"audioPublishUrl,omitempty"` + PublishPort int `yaml:"publishPort" json:"-"` + Manage bool `yaml:"manage"` + ManageAudio bool `yaml:"manageAudio"` + Service string `yaml:"service"` + AudioService string `yaml:"audioService"` + HealthURL string `yaml:"healthUrl"` + HealthInterval Duration `yaml:"healthInterval"` + VideoWidth int `yaml:"videoWidth" json:"-"` + VideoHeight int `yaml:"videoHeight" json:"-"` + VideoFPS int `yaml:"videoFps" json:"-"` + VideoBitrate int `yaml:"videoBitrate" json:"-"` } type CameraServoConfig struct { @@ -201,6 +204,13 @@ func LoadConfig(path string) (*Config, error) { } cfg.Media.PublishURL = derived } + if cfg.Media.AudioPublishURL == "" { + derived, err := derivePublishURL(cfg.ServerURL, cfg.Name+"-audio", cfg.Media.PublishPort) + if err != nil { + return nil, fmt.Errorf("derive audioPublishUrl: %w", err) + } + cfg.Media.AudioPublishURL = derived + } if err := validateServoConfig(&cfg.CameraServo); err != nil { return nil, fmt.Errorf("cameraServo: %w", err) } @@ -268,9 +278,9 @@ func validateAudioConfig(cfg *AudioConfig) { } } -func derivePublishURL(serverURL, roverName string, port int) (string, error) { - if roverName == "" { - return "", errors.New("missing rover name for publishUrl") +func derivePublishURL(serverURL, streamName string, port int) (string, error) { + if streamName == "" { + return "", errors.New("missing stream name for publishUrl") } parsed, err := url.Parse(serverURL) if err != nil { @@ -283,6 +293,6 @@ func derivePublishURL(serverURL, roverName string, port int) (string, error) { if port <= 0 { port = 9000 } - streamName := url.PathEscape(roverName) - return fmt.Sprintf("srt://%s:%d?streamid=#!::r=%s,m=publish&latency=10&mode=caller&transtype=live&pkt_size=1316", host, port, streamName), nil + 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 } diff --git a/pi/roverd/media_env.go b/pi/roverd/media_env.go index b8e8374f..42cff8cc 100644 --- a/pi/roverd/media_env.go +++ b/pi/roverd/media_env.go @@ -13,6 +13,9 @@ func UpdatePublisherEnv(media MediaConfig, audio AudioConfig) error { if media.PublishURL == "" { return fmt.Errorf("media publishUrl missing") } + if media.AudioPublishURL == "" && audio.CaptureEnabled { + return fmt.Errorf("audio publishUrl missing") + } if media.VideoWidth <= 0 || media.VideoHeight <= 0 || media.VideoFPS <= 0 || media.VideoBitrate <= 0 { return fmt.Errorf("invalid media dimensions/bitrate") } @@ -21,6 +24,9 @@ func UpdatePublisherEnv(media MediaConfig, audio AudioConfig) error { } var buf bytes.Buffer fmt.Fprintf(&buf, "PUBLISH_URL=%s\n", media.PublishURL) + if audio.CaptureEnabled && media.AudioPublishURL != "" { + fmt.Fprintf(&buf, "AUDIO_PUBLISH_URL=%s\n", media.AudioPublishURL) + } fmt.Fprintf(&buf, "VIDEO_WIDTH=%d\n", media.VideoWidth) fmt.Fprintf(&buf, "VIDEO_HEIGHT=%d\n", media.VideoHeight) fmt.Fprintf(&buf, "VIDEO_FPS=%d\n", media.VideoFPS) diff --git a/pi/systemd/audio-only-publisher.service b/pi/systemd/audio-only-publisher.service new file mode 100644 index 00000000..c742324d --- /dev/null +++ b/pi/systemd/audio-only-publisher.service @@ -0,0 +1,16 @@ +[Unit] +Description=Rover Audio Publisher (ALSA -> SRT) +After=network-online.target roverd.service +Wants=network-online.target + +[Service] +Type=simple +User=roverd +Group=roverd +EnvironmentFile=/var/lib/roverd/video.env +ExecStart=/usr/local/bin/audio-only-publisher +Restart=always +RestartSec=2 + +[Install] +WantedBy=multi-user.target