package stream import ( "bytes" "encoding/json" "fmt" "log" "net/http" "time" "tianyan-edge/internal/config" ) // Puller 负责从云端网关动态获取拉流地址并维护心跳 type Puller struct { cfg *config.Config client *http.Client sessionID string stopCh chan struct{} } // PullResponse 云端网关返回结构 type PullResponse struct { Code int `json:"code"` Msg string `json:"msg"` Data struct { URL string `json:"url"` Protocol string `json:"protocol"` TTL int `json:"ttl"` DeviceID string `json:"device_id"` ChannelID string `json:"channel_id"` } `json:"data"` } // HeartbeatPayload 心跳请求结构 type HeartbeatPayload struct { EdgeToken string `json:"edge_token"` } // PullPayload 拉流请求结构 type PullPayload struct { EdgeToken string `json:"edge_token"` Protocol string `json:"protocol"` } func NewPuller(cfg *config.Config) *Puller { return &Puller{ cfg: cfg, client: &http.Client{ Timeout: 60 * time.Second, // WVP SIP 信令较慢,设置较长超时 }, stopCh: make(chan struct{}), } } // Pull 请求云端获取拉流 URL func (p *Puller) Pull() (string, error) { if p.cfg.StreamPullURL == "" { return "", fmt.Errorf("stream_pull_url not configured") } payload := PullPayload{ EdgeToken: p.cfg.EdgeToken, Protocol: p.cfg.StreamProtocol, } body, err := json.Marshal(payload) if err != nil { return "", err } req, err := http.NewRequest("POST", p.cfg.StreamPullURL+"/api/v1/edge/pull", bytes.NewReader(body)) if err != nil { return "", err } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Edge-Token", p.cfg.EdgeToken) resp, err := p.client.Do(req) if err != nil { return "", fmt.Errorf("pull request failed: %w", err) } defer resp.Body.Close() var pullResp PullResponse if err := json.NewDecoder(resp.Body).Decode(&pullResp); err != nil { return "", fmt.Errorf("decode pull response failed: %w", err) } if pullResp.Code != 0 { return "", fmt.Errorf("pull failed: %s", pullResp.Msg) } p.sessionID = p.cfg.EdgeToken log.Printf("puller: got stream url protocol=%s ttl=%ds", pullResp.Data.Protocol, pullResp.Data.TTL) return pullResp.Data.URL, nil } // StartHeartbeatLoop 启动后台心跳保活协程 func (p *Puller) StartHeartbeatLoop() { go func() { ticker := time.NewTicker(20 * time.Second) defer ticker.Stop() for { select { case <-p.stopCh: log.Println("puller: heartbeat loop stopped") return case <-ticker.C: if p.sessionID == "" { continue } if err := p.sendHeartbeat(); err != nil { log.Printf("puller: heartbeat failed: %v (will retry next tick)", err) } } } }() } // sendHeartbeat 发送单次心跳 func (p *Puller) sendHeartbeat() error { payload := HeartbeatPayload{EdgeToken: p.cfg.EdgeToken} body, err := json.Marshal(payload) if err != nil { return err } req, err := http.NewRequest("POST", p.cfg.StreamPullURL+"/api/v1/edge/heartbeat", bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Edge-Token", p.cfg.EdgeToken) resp, err := p.client.Do(req) if err != nil { return err } defer resp.Body.Close() var respData struct { Code int `json:"code"` Msg string `json:"msg"` } if err := json.NewDecoder(resp.Body).Decode(&respData); err != nil { return err } if respData.Code != 0 { return fmt.Errorf("heartbeat failed: %s", respData.Msg) } return nil } // Release 主动释放云端拉流会话 func (p *Puller) Release() { if p.sessionID == "" || p.cfg.StreamPullURL == "" { return } payload := HeartbeatPayload{EdgeToken: p.cfg.EdgeToken} body, err := json.Marshal(payload) if err != nil { log.Printf("puller: release marshal error: %v", err) return } req, err := http.NewRequest("POST", p.cfg.StreamPullURL+"/api/v1/edge/release", bytes.NewReader(body)) if err != nil { log.Printf("puller: release request error: %v", err) return } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Edge-Token", p.cfg.EdgeToken) resp, err := p.client.Do(req) if err != nil { log.Printf("puller: release do error: %v", err) return } defer resp.Body.Close() var respData struct { Code int `json:"code"` Msg string `json:"msg"` } if err := json.NewDecoder(resp.Body).Decode(&respData); err != nil { log.Printf("puller: release decode error: %v", err) return } if respData.Code == 0 { log.Println("puller: session released successfully") } else { log.Printf("puller: release failed: %s", respData.Msg) } p.sessionID = "" } // Stop 停止拉流管理器 func (p *Puller) Stop() { close(p.stopCh) p.Release() }