package agent import ( "embed" "fmt" "log" "os" "sync" "time" ) type Config struct { NodeName string OriginURL string DiscoveryURL string HlsPort int ApiPort int Subnet string DataDir string Weight int } type Agent struct { cfg Config mediamtxFS embed.FS mtx *MediaMTXManager hb *Heartbeat monitor *Monitor nodeID string stopCh chan struct{} connected bool mu sync.RWMutex } func New(cfg Config, mtxFS embed.FS) (*Agent, error) { return &Agent{ cfg: cfg, mediamtxFS: mtxFS, nodeID: generateNodeID(cfg), stopCh: make(chan struct{}), }, nil } func (a *Agent) Start() error { if err := os.MkdirAll(a.cfg.DataDir, 0755); err != nil { return fmt.Errorf("create data dir: %w", err) } a.hb = NewHeartbeat(a.cfg, a.nodeID) if err := a.hb.Discover(); err != nil { log.Printf("Warning: initial discovery failed: %v (will retry every 10 seconds)", err) } else { a.cfg.OriginURL = a.hb.OriginURL() } a.mtx = NewMediaMTXManager(a.cfg, a.mediamtxFS) if err := a.mtx.Extract(); err != nil { return fmt.Errorf("extract mediamtx: %w", err) } if err := a.mtx.WriteConfig(); err != nil { return fmt.Errorf("write config: %w", err) } if err := a.mtx.Start(); err != nil { return fmt.Errorf("start mediamtx: %w", err) } a.monitor = NewMonitor() time.Sleep(2 * time.Second) if err := a.connect(); err != nil { log.Printf("Warning: connection failed: %v (will retry every 10 seconds)", err) } go a.heartbeatLoop() go a.monitorLoop() return nil } func (a *Agent) Stop() { log.Println("Stopping agent...") close(a.stopCh) if a.mtx != nil { a.mtx.Stop() } log.Println("Agent stopped") } func (a *Agent) heartbeatLoop() { ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() for { select { case <-a.stopCh: return case <-ticker.C: if !a.Connected() { if err := a.connect(); err != nil { log.Printf("Connection retry failed: %v", err) continue } } metrics := a.monitor.Snapshot() if err := a.hb.Send(metrics); err != nil { a.setConnected(false) log.Printf("Heartbeat error: %v", err) } } } } func (a *Agent) monitorLoop() { ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() for { select { case <-a.stopCh: return case <-ticker.C: a.monitor.Tick() if a.mtx != nil { a.monitor.SetMediaMetrics(a.mtx.Metrics()) } } } } func (a *Agent) NodeID() string { return a.nodeID } func (a *Agent) Config() Config { return a.cfg } func (a *Agent) Connected() bool { a.mu.RLock() defer a.mu.RUnlock() return a.connected } func (a *Agent) setConnected(connected bool) { a.mu.Lock() a.connected = connected a.mu.Unlock() } func (a *Agent) connect() error { if err := a.hb.Discover(); err != nil { return err } if err := a.hb.Register(); err != nil { return err } a.setConnected(true) return nil } func (a *Agent) Metrics() Metrics { if a.monitor != nil { return a.monitor.Snapshot() } return Metrics{} } func generateNodeID(cfg Config) string { host, _ := os.Hostname() if host == "" { host = "unknown" } return fmt.Sprintf("%s-%s", host, cfg.NodeName) }