config reshape

This commit is contained in:
legop3
2026-06-30 20:17:43 -04:00
parent 2eb6c00f9c
commit 31eacea7bc
21 changed files with 520 additions and 200 deletions
+2 -2
View File
@@ -21,7 +21,7 @@ func main() {
if err != nil {
log.Fatalf("load config: %v", err)
}
if err := roverd.UpdatePublisherEnv(cfg.Media, cfg.Audio); err != nil {
if err := roverd.UpdatePublisherEnv(cfg.Media); err != nil {
log.Fatalf("prepare media env: %v", err)
}
@@ -55,7 +55,7 @@ func main() {
adapter := roverd.NewSerialAdapter(serialPort, logger)
mediaSupervisor := roverd.NewMediaSupervisor(cfg.Media, cfg.Audio, logger)
mediaSupervisor := roverd.NewMediaSupervisor(cfg.Media, logger)
if mediaSupervisor != nil {
mediaSupervisor.Start(ctx)
}
+195 -66
View File
@@ -56,13 +56,13 @@ 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"`
TTSEnabled bool `yaml:"ttsEnabled" json:"ttsEnabled"`
DefaultEngine string `yaml:"defaultEngine" json:"defaultEngine,omitempty"`
DefaultVoice string `yaml:"defaultVoice" json:"defaultVoice,omitempty"`
DefaultPitch int `yaml:"defaultPitch" json:"defaultPitch,omitempty"`
// AudioConfig is only for sounds generated by roverd itself. Live capture
// and playback are media-publisher concerns, so those settings live under
// MediaConfig where they can be written directly into media.env.
TTSEnabled bool `yaml:"ttsEnabled" json:"ttsEnabled"`
DefaultEngine string `yaml:"defaultEngine" json:"defaultEngine,omitempty"`
DefaultVoice string `yaml:"defaultVoice" json:"defaultVoice,omitempty"`
DefaultPitch int `yaml:"defaultPitch" json:"defaultPitch,omitempty"`
}
type HornConfig struct {
@@ -77,21 +77,59 @@ 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:"-"`
CameraInverted bool `yaml:"cameraInverted" 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:"-"`
// PublishPort is shared by the derived video, microphone, and forwarded-audio
// SRT URLs. Keeping it at this level prevents each nested block from needing
// to repeat the same server port when the common MediaMTX listener is used.
PublishPort int `yaml:"publishPort" json:"-"`
Manage bool `yaml:"manage" json:"manage"`
HealthURL string `yaml:"healthUrl" json:"healthUrl,omitempty"`
HealthInterval Duration `yaml:"healthInterval" json:"-"`
Video VideoMediaConfig `yaml:"video" json:"video"`
AudioCapture AudioCaptureConfig `yaml:"audioCapture" json:"audioCapture"`
AudioPlayback AudioPlaybackConfig `yaml:"audioPlayback" json:"audioPlayback"`
}
type VideoMediaConfig struct {
// Publisher selects the installed publisher script/pipeline family. The
// first pass uses pi-libcamera for current rovers; laptop-v4l2 can be added
// without changing the server-facing media shape again.
Enabled bool `yaml:"enabled" json:"enabled"`
Service string `yaml:"service" json:"service,omitempty"`
Publisher string `yaml:"publisher" json:"publisher,omitempty"`
PublishURL string `yaml:"publishUrl" json:"publishUrl,omitempty"`
Device string `yaml:"device" json:"device,omitempty"`
Width int `yaml:"width" json:"-"`
Height int `yaml:"height" json:"-"`
FPS int `yaml:"fps" json:"-"`
Bitrate int `yaml:"bitrate" json:"-"`
Inverted bool `yaml:"inverted" json:"-"`
SensorMode string `yaml:"sensorMode" json:"-"`
}
type AudioCaptureConfig struct {
// AudioCapture describes the rover microphone stream that browsers can
// subscribe to as "<rover>-audio". A disabled capture block still has
// normalized defaults so enabling it only requires flipping enabled: true.
Enabled bool `yaml:"enabled" json:"enabled"`
Service string `yaml:"service" json:"service,omitempty"`
PublishURL string `yaml:"publishUrl" json:"publishUrl,omitempty"`
Device string `yaml:"device" json:"device,omitempty"`
SampleRate int `yaml:"sampleRate" json:"-"`
Channels int `yaml:"channels" json:"-"`
Bitrate int `yaml:"bitrate" json:"-"`
}
type AudioPlaybackConfig struct {
// AudioPlayback describes the reverse stream that VIP users publish into
// MediaMTX for playback on the rover speaker. The URL is a request/read URL
// for the rover listener, while the server converts it to publish mode when
// it needs to inject audio.
Enabled bool `yaml:"enabled" json:"enabled"`
Service string `yaml:"service" json:"service,omitempty"`
ForwardURL string `yaml:"forwardUrl" json:"forwardUrl,omitempty"`
Device string `yaml:"device" json:"device,omitempty"`
Normalize bool `yaml:"normalize" json:"-"`
NormalizeFilter string `yaml:"normalizeFilter" json:"-"`
}
type CameraServoConfig struct {
@@ -189,9 +227,33 @@ func LoadConfig(path string) (*Config, error) {
},
Media: MediaConfig{
PublishPort: 9000,
CameraInverted: true,
HealthInterval: Duration{Duration: 30 * time.Second},
VideoBitrate: 2000000,
Video: VideoMediaConfig{
Enabled: true,
Service: "video-publisher.service",
Publisher: "pi-libcamera",
Width: 640,
Height: 480,
FPS: 30,
Bitrate: 2000000,
Inverted: true,
SensorMode: "1296:972",
},
AudioCapture: AudioCaptureConfig{
Enabled: false,
Service: "audio-only-publisher.service",
Device: "hw:0,0",
SampleRate: 48000,
Channels: 2,
Bitrate: 510000,
},
AudioPlayback: AudioPlaybackConfig{
Enabled: true,
Service: "audio-forward-listener.service",
Device: "forward",
Normalize: true,
NormalizeFilter: "dynaudnorm=f=75:g=15:m=10:p=0.9,alimiter=limit=0.85:level=disabled",
},
},
CameraServo: CameraServoConfig{
Pin: 12,
@@ -205,13 +267,10 @@ func LoadConfig(path string) (*Config, error) {
NudgeDegrees: 2,
},
Audio: AudioConfig{
CaptureEnabled: false,
CaptureDevice: "rovermic",
PlaybackDevice: "forward",
TTSEnabled: false,
DefaultEngine: "flite",
DefaultVoice: "rms",
DefaultPitch: 50,
TTSEnabled: false,
DefaultEngine: "flite",
DefaultVoice: "rms",
DefaultPitch: 50,
},
Horn: HornConfig{
Enabled: false,
@@ -283,38 +342,11 @@ func LoadConfig(path string) (*Config, error) {
if cfg.BRC.GPIOChip == "" {
cfg.BRC.GPIOChip = "gpiochip0"
}
if cfg.Media.Manage && cfg.Media.Service == "" {
return nil, errors.New("media.manage requires media.service")
}
if cfg.Media.Manage && cfg.Media.HealthInterval.Duration <= 0 {
cfg.Media.HealthInterval = Duration{Duration: 30 * time.Second}
}
if cfg.Media.VideoBitrate <= 0 {
cfg.Media.VideoBitrate = 3000000
}
if cfg.Media.PublishPort <= 0 {
cfg.Media.PublishPort = 9000
}
if cfg.Media.PublishURL == "" {
derived, err := derivePublishURL(cfg.ServerURL, cfg.Name, cfg.Media.PublishPort)
if err != nil {
return nil, fmt.Errorf("derive publishUrl: %w", err)
}
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 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 := validateMediaConfig(&cfg.Media, cfg.ServerURL, cfg.Name); err != nil {
return nil, fmt.Errorf("media: %w", err)
}
if err := validateServoConfig(&cfg.CameraServo); err != nil {
return nil, fmt.Errorf("cameraServo: %w", err)
@@ -375,12 +407,6 @@ func clampFloat(value, min, max float64) float64 {
}
func validateAudioConfig(cfg *AudioConfig) {
if cfg.CaptureEnabled && cfg.CaptureDevice == "" {
cfg.CaptureDevice = "hw:0,0"
}
if cfg.PlaybackDevice == "" || cfg.PlaybackDevice == "default" {
cfg.PlaybackDevice = "forward"
}
if cfg.DefaultEngine == "" {
cfg.DefaultEngine = "flite"
}
@@ -392,6 +418,109 @@ func validateAudioConfig(cfg *AudioConfig) {
}
}
func validateMediaConfig(cfg *MediaConfig, serverURL string, roverName string) error {
/*
The nested media config is the source of truth for the publisher scripts,
so defaults that used to live in shell are normalized here before any env
file is written. This keeps the Pi behavior stable while making laptop
and future publisher variants explicit configuration choices.
*/
if cfg.PublishPort <= 0 {
cfg.PublishPort = 9000
}
if cfg.HealthInterval.Duration <= 0 {
cfg.HealthInterval = Duration{Duration: 30 * time.Second}
}
if err := validateVideoMediaConfig(&cfg.Video, serverURL, roverName, cfg.PublishPort); err != nil {
return fmt.Errorf("video: %w", err)
}
if err := validateAudioCaptureConfig(&cfg.AudioCapture, serverURL, roverName, cfg.PublishPort); err != nil {
return fmt.Errorf("audioCapture: %w", err)
}
if err := validateAudioPlaybackConfig(&cfg.AudioPlayback, serverURL, roverName, cfg.PublishPort); err != nil {
return fmt.Errorf("audioPlayback: %w", err)
}
return nil
}
func validateVideoMediaConfig(cfg *VideoMediaConfig, serverURL string, roverName string, publishPort int) error {
if cfg.Service == "" {
cfg.Service = "video-publisher.service"
}
if cfg.Publisher == "" {
cfg.Publisher = "pi-libcamera"
}
if cfg.Width <= 0 {
cfg.Width = 640
}
if cfg.Height <= 0 {
cfg.Height = 480
}
if cfg.FPS <= 0 {
cfg.FPS = 30
}
if cfg.Bitrate <= 0 {
cfg.Bitrate = 2000000
}
if cfg.SensorMode == "" && cfg.Publisher == "pi-libcamera" {
cfg.SensorMode = "1296:972"
}
if cfg.PublishURL == "" {
derived, err := derivePublishURL(serverURL, roverName, publishPort)
if err != nil {
return fmt.Errorf("derive publishUrl: %w", err)
}
cfg.PublishURL = derived
}
return nil
}
func validateAudioCaptureConfig(cfg *AudioCaptureConfig, serverURL string, roverName string, publishPort int) error {
if cfg.Service == "" {
cfg.Service = "audio-only-publisher.service"
}
if cfg.Device == "" {
cfg.Device = "hw:0,0"
}
if cfg.SampleRate <= 0 {
cfg.SampleRate = 48000
}
if cfg.Channels <= 0 {
cfg.Channels = 2
}
if cfg.Bitrate <= 0 {
cfg.Bitrate = 510000
}
if cfg.PublishURL == "" {
derived, err := derivePublishURL(serverURL, roverName+"-audio", publishPort)
if err != nil {
return fmt.Errorf("derive publishUrl: %w", err)
}
cfg.PublishURL = derived
}
return nil
}
func validateAudioPlaybackConfig(cfg *AudioPlaybackConfig, serverURL string, roverName string, publishPort int) error {
if cfg.Service == "" {
cfg.Service = "audio-forward-listener.service"
}
if cfg.Device == "" {
cfg.Device = "forward"
}
if cfg.NormalizeFilter == "" {
cfg.NormalizeFilter = "dynaudnorm=f=75:g=15:m=10:p=0.9,alimiter=limit=0.85:level=disabled"
}
if cfg.ForwardURL == "" {
derived, err := deriveReadURL(serverURL, roverName+"-fwd", publishPort)
if err != nil {
return fmt.Errorf("derive forwardUrl: %w", err)
}
cfg.ForwardURL = derived
}
return nil
}
func validateHornConfig(cfg *HornConfig) {
if cfg.Volume <= 0 {
cfg.Volume = 0.25
+38 -37
View File
@@ -7,51 +7,52 @@ import (
"path/filepath"
)
const publisherEnvPath = "/var/lib/roverd/video.env"
const publisherEnvPath = "/var/lib/roverd/media.env"
func UpdatePublisherEnv(media MediaConfig, audio AudioConfig) error {
if media.PublishURL == "" {
return fmt.Errorf("media publishUrl missing")
func UpdatePublisherEnv(media MediaConfig) error {
if media.Video.Enabled && media.Video.PublishURL == "" {
return fmt.Errorf("media video publishUrl missing")
}
if media.AudioPublishURL == "" && audio.CaptureEnabled {
return fmt.Errorf("audio publishUrl missing")
if media.AudioCapture.Enabled && media.AudioCapture.PublishURL == "" {
return fmt.Errorf("media audioCapture publishUrl missing")
}
if media.VideoWidth < 0 || media.VideoHeight < 0 || media.VideoFPS < 0 || media.VideoBitrate <= 0 {
return fmt.Errorf("invalid media dimensions/bitrate")
if media.AudioPlayback.Enabled && media.AudioPlayback.ForwardURL == "" {
return fmt.Errorf("media audioPlayback forwardUrl missing")
}
if media.Video.Width <= 0 || media.Video.Height <= 0 || media.Video.FPS <= 0 || media.Video.Bitrate <= 0 {
return fmt.Errorf("invalid media video dimensions/bitrate")
}
if err := os.MkdirAll(filepath.Dir(publisherEnvPath), 0o755); err != nil {
return err
}
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)
}
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)
}
if media.VideoHeight > 0 {
fmt.Fprintf(&buf, "VIDEO_HEIGHT=%d\n", media.VideoHeight)
}
if media.VideoFPS > 0 {
fmt.Fprintf(&buf, "VIDEO_FPS=%d\n", media.VideoFPS)
}
fmt.Fprintf(&buf, "VIDEO_BITRATE=%d\n", media.VideoBitrate)
fmt.Fprintf(&buf, "VIDEO_INVERT=%d\n", boolToInt(media.CameraInverted))
audioDevice := audio.CaptureDevice
if audioDevice == "" || audioDevice == "rovermic" {
audioDevice = "hw:0,0"
}
fmt.Fprintf(&buf, "AUDIO_ENABLE=%d\n", boolToInt(audio.CaptureEnabled))
fmt.Fprintf(&buf, "AUDIO_DEVICE=%s\n", audioDevice)
playbackDevice := audio.PlaybackDevice
if playbackDevice == "" {
playbackDevice = "forward"
}
fmt.Fprintf(&buf, "AUDIO_PLAYBACK_DEVICE=%s\n", playbackDevice)
/*
The env file is intentionally verbose: every publisher receives concrete
values instead of silently falling back to script-local defaults. That
makes roverd.yaml the owner of media behavior while keeping the shell
scripts as small pipeline launchers.
*/
fmt.Fprintf(&buf, "ROVERD_VIDEO_ENABLE=%d\n", boolToInt(media.Video.Enabled))
fmt.Fprintf(&buf, "ROVERD_VIDEO_PUBLISHER=%s\n", media.Video.Publisher)
fmt.Fprintf(&buf, "ROVERD_VIDEO_PUBLISH_URL=%s\n", media.Video.PublishURL)
fmt.Fprintf(&buf, "ROVERD_VIDEO_DEVICE=%s\n", media.Video.Device)
fmt.Fprintf(&buf, "ROVERD_VIDEO_WIDTH=%d\n", media.Video.Width)
fmt.Fprintf(&buf, "ROVERD_VIDEO_HEIGHT=%d\n", media.Video.Height)
fmt.Fprintf(&buf, "ROVERD_VIDEO_FPS=%d\n", media.Video.FPS)
fmt.Fprintf(&buf, "ROVERD_VIDEO_BITRATE=%d\n", media.Video.Bitrate)
fmt.Fprintf(&buf, "ROVERD_VIDEO_INVERT=%d\n", boolToInt(media.Video.Inverted))
fmt.Fprintf(&buf, "ROVERD_VIDEO_SENSOR_MODE=%s\n", media.Video.SensorMode)
fmt.Fprintf(&buf, "ROVERD_AUDIO_CAPTURE_ENABLE=%d\n", boolToInt(media.AudioCapture.Enabled))
fmt.Fprintf(&buf, "ROVERD_AUDIO_CAPTURE_PUBLISH_URL=%s\n", media.AudioCapture.PublishURL)
fmt.Fprintf(&buf, "ROVERD_AUDIO_CAPTURE_DEVICE=%s\n", media.AudioCapture.Device)
fmt.Fprintf(&buf, "ROVERD_AUDIO_CAPTURE_SAMPLE_RATE=%d\n", media.AudioCapture.SampleRate)
fmt.Fprintf(&buf, "ROVERD_AUDIO_CAPTURE_CHANNELS=%d\n", media.AudioCapture.Channels)
fmt.Fprintf(&buf, "ROVERD_AUDIO_CAPTURE_BITRATE=%d\n", media.AudioCapture.Bitrate)
fmt.Fprintf(&buf, "ROVERD_AUDIO_PLAYBACK_ENABLE=%d\n", boolToInt(media.AudioPlayback.Enabled))
fmt.Fprintf(&buf, "ROVERD_AUDIO_PLAYBACK_FORWARD_URL=%s\n", media.AudioPlayback.ForwardURL)
fmt.Fprintf(&buf, "ROVERD_AUDIO_PLAYBACK_DEVICE=%s\n", media.AudioPlayback.Device)
fmt.Fprintf(&buf, "ROVERD_AUDIO_PLAYBACK_NORMALIZE=%d\n", boolToInt(media.AudioPlayback.Normalize))
fmt.Fprintf(&buf, "ROVERD_AUDIO_PLAYBACK_NORMALIZE_FILTER=%s\n", media.AudioPlayback.NormalizeFilter)
if err := os.WriteFile(publisherEnvPath, buf.Bytes(), 0o640); err != nil {
return err
}
+36 -15
View File
@@ -13,17 +13,16 @@ import (
type MediaSupervisor struct {
cfg MediaConfig
audio AudioConfig
logger *log.Logger
client *http.Client
checkInterval time.Duration
}
func NewMediaSupervisor(cfg MediaConfig, audio AudioConfig, logger *log.Logger) *MediaSupervisor {
if err := UpdatePublisherEnv(cfg, audio); err != nil {
func NewMediaSupervisor(cfg MediaConfig, logger *log.Logger) *MediaSupervisor {
if err := UpdatePublisherEnv(cfg); err != nil {
logger.Printf("media supervisor: update env failed: %v", err)
}
if !cfg.Manage || cfg.Service == "" {
if !cfg.Manage {
return nil
}
interval := cfg.HealthInterval.Duration
@@ -36,7 +35,6 @@ func NewMediaSupervisor(cfg MediaConfig, audio AudioConfig, logger *log.Logger)
}
return &MediaSupervisor{
cfg: cfg,
audio: audio,
logger: logger,
client: client,
checkInterval: interval,
@@ -47,7 +45,7 @@ func (m *MediaSupervisor) Start(ctx context.Context) {
if m == nil {
return
}
if err := UpdatePublisherEnv(m.cfg, m.audio); err != nil {
if err := UpdatePublisherEnv(m.cfg); err != nil {
m.logger.Printf("media supervisor: update env failed: %v", err)
}
if m.cfg.HealthURL == "" || m.client == nil {
@@ -78,7 +76,7 @@ func (m *MediaSupervisor) HandleAction(ctx context.Context, action string) error
if m == nil {
return errors.New("media supervisor disabled")
}
if err := UpdatePublisherEnv(m.cfg, m.audio); err != nil {
if err := UpdatePublisherEnv(m.cfg); err != nil {
return err
}
switch action {
@@ -96,9 +94,9 @@ func (m *MediaSupervisor) checkAndRepair() error {
if m.checkHealth(ctx) {
return nil
}
m.logger.Printf("media supervisor: health check failed, restarting %s", m.cfg.Service)
m.logger.Printf("media supervisor: health check failed, restarting configured media services")
if err := m.runSystemctl(ctx, "restart"); err != nil {
return fmt.Errorf("restart mediamtx: %w", err)
return fmt.Errorf("restart media services: %w", err)
}
return nil
}
@@ -127,16 +125,39 @@ func (m *MediaSupervisor) checkHealth(ctx context.Context) bool {
}
func (m *MediaSupervisor) runSystemctl(ctx context.Context, action string) error {
if m.cfg.Service == "" {
return errors.New("no media service configured")
services := m.managedServices()
if len(services) == 0 {
return errors.New("no media services configured")
}
runCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
cmd := exec.CommandContext(runCtx, "systemctl", action, m.cfg.Service)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("systemctl %s %s: %w (%s)", action, m.cfg.Service, err, string(output))
for _, service := range services {
cmd := exec.CommandContext(runCtx, "systemctl", action, service)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("systemctl %s %s: %w (%s)", action, service, err, string(output))
}
}
return nil
}
func (m *MediaSupervisor) managedServices() []string {
/*
Only enabled streams are service-managed. This matters for audio capture:
the service may be installed everywhere, but a rover with capture disabled
should not have remote media commands starting a publisher that the config
says should stay off.
*/
services := []string{}
if m.cfg.Video.Enabled && m.cfg.Video.Service != "" {
services = append(services, m.cfg.Video.Service)
}
if m.cfg.AudioCapture.Enabled && m.cfg.AudioCapture.Service != "" {
services = append(services, m.cfg.AudioCapture.Service)
}
if m.cfg.AudioPlayback.Enabled && m.cfg.AudioPlayback.Service != "" {
services = append(services, m.cfg.AudioPlayback.Service)
}
return services
}
+26 -6
View File
@@ -17,16 +17,36 @@ battery:
urgent: 1650
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=request&latency=10&mode=caller&transtype=live&pkt_size=1316
publishPort: 9000
# Default assumes camera is mounted upside down; set false for upright mounts.
cameraInverted: true
videoBitrate: 2000000
manage: true
service: video-publisher.service
healthUrl: ""
healthInterval: 30s
video:
enabled: true
service: video-publisher.service
publisher: pi-libcamera
publishUrl: srt://192.168.0.86:9000?streamid=#!::r=roomba-alpha,m=publish&latency=10&mode=caller&transtype=live&pkt_size=1316
width: 640
height: 480
fps: 30
bitrate: 2000000
# Default assumes camera is mounted upside down; set false for upright mounts.
inverted: true
sensorMode: 1296:972
audioCapture:
enabled: false
service: audio-only-publisher.service
publishUrl: srt://192.168.0.86:9000?streamid=#!::r=roomba-alpha-audio,m=publish&latency=10&mode=caller&transtype=live&pkt_size=1316
device: hw:0,0
sampleRate: 48000
channels: 2
bitrate: 510000
audioPlayback:
enabled: true
service: audio-forward-listener.service
forwardUrl: srt://192.168.0.86:9000?streamid=#!::r=roomba-alpha-fwd,m=request&latency=10&mode=caller&transtype=live&pkt_size=1316
device: forward
normalize: true
cameraServo:
enabled: false
pin: 12
+10 -1
View File
@@ -17,9 +17,18 @@ battery:
maxWheelSpeed: 350
media:
manage: false
service: mediamtx.service
healthUrl: http://127.0.0.1:9997/v3/paths/list
healthInterval: 30s
video:
enabled: true
service: video-publisher.service
publisher: pi-libcamera
audioCapture:
enabled: false
service: audio-only-publisher.service
audioPlayback:
enabled: true
service: audio-forward-listener.service
cameraServo:
enabled: false
pin: 19