Compare commits
2 Commits
966ef512ab
...
831412f063
| Author | SHA1 | Date | |
|---|---|---|---|
| 831412f063 | |||
| 6ff71fded0 |
@@ -21,8 +21,8 @@ func main() {
|
|||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
cfg := config.Load(*cfgPath)
|
cfg := config.Load(*cfgPath)
|
||||||
log.Printf("edge-agent start id=%s uuid=%s cloud=%s mqtt=%s",
|
log.Printf("edge-agent start id=%s uuid=%s cloud=%s mqtt=%s auto_pull=%v",
|
||||||
cfg.EdgeID, cfg.GetDeviceIdentity(), cfg.CloudURL, cfg.MqttBroker)
|
cfg.EdgeID, cfg.GetDeviceIdentity(), cfg.CloudURL, cfg.MqttBroker, cfg.AutoPull)
|
||||||
|
|
||||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -43,8 +43,17 @@ func main() {
|
|||||||
mqttMgr := control.NewMqttManager(cfg)
|
mqttMgr := control.NewMqttManager(cfg)
|
||||||
run(mqttMgr.Run)
|
run(mqttMgr.Run)
|
||||||
|
|
||||||
|
// 1.5 Stream Puller (Optional: Dynamic URL fetching)
|
||||||
|
var puller *stream.Puller
|
||||||
|
if cfg.AutoPull {
|
||||||
|
puller = stream.NewPuller(cfg)
|
||||||
|
puller.StartHeartbeatLoop()
|
||||||
|
defer puller.Stop()
|
||||||
|
log.Println("puller: auto-pull mode enabled")
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Stream Ingestion (Dynamic)
|
// 2. Stream Ingestion (Dynamic)
|
||||||
run(stream.NewStreamManager(cfg, frames, mqttMgr.GetUpdates()).Run)
|
run(stream.NewStreamManager(cfg, puller, frames, mqttMgr.GetUpdates()).Run)
|
||||||
|
|
||||||
// 3. Inference (Dynamic Workers)
|
// 3. Inference (Dynamic Workers)
|
||||||
run(infer.NewClient(cfg, frames, events, mqttMgr.GetUpdates()).Run)
|
run(infer.NewClient(cfg, frames, events, mqttMgr.GetUpdates()).Run)
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ cloud_url: http://101.36.73.102:8004
|
|||||||
mqtt_broker: tcp://101.36.73.102:1883
|
mqtt_broker: tcp://101.36.73.102:1883
|
||||||
mqtt_user: ""
|
mqtt_user: ""
|
||||||
mqtt_pass: ""
|
mqtt_pass: ""
|
||||||
edge_token: ""
|
edge_token: "edge-token-001"
|
||||||
rtsp_urls:
|
rtsp_urls: []
|
||||||
- http://101.36.73.102:8080/rtp/34020000002000000003_34020000001310000001.live.flv
|
# 自动按需拉流配置
|
||||||
|
stream_pull_url: http://101.36.73.102:9000
|
||||||
|
stream_protocol: flv
|
||||||
|
auto_pull: true
|
||||||
infer_socket: /tmp/edge-infer.sock
|
infer_socket: /tmp/edge-infer.sock
|
||||||
infer_fps: 2
|
infer_fps: 2
|
||||||
infer_workers: 3
|
infer_workers: 3
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ type Config struct {
|
|||||||
EdgeToken string `yaml:"edge_token"`
|
EdgeToken string `yaml:"edge_token"`
|
||||||
StreamEnabled bool `yaml:"stream_enabled"`
|
StreamEnabled bool `yaml:"stream_enabled"`
|
||||||
RTSPURLs []string `yaml:"rtsp_urls"`
|
RTSPURLs []string `yaml:"rtsp_urls"`
|
||||||
|
// 新增:自动按需拉流配置
|
||||||
|
StreamPullURL string `yaml:"stream_pull_url"` // 云端拉流网关地址
|
||||||
|
StreamProtocol string `yaml:"stream_protocol"` // 拉流协议: flv, rtsp, ws_flv
|
||||||
|
AutoPull bool `yaml:"auto_pull"` // 是否启用自动按需拉流
|
||||||
InferSocket string `yaml:"infer_socket"`
|
InferSocket string `yaml:"infer_socket"`
|
||||||
InferFPS int `yaml:"infer_fps"`
|
InferFPS int `yaml:"infer_fps"`
|
||||||
InferWorkers int `yaml:"infer_workers"`
|
InferWorkers int `yaml:"infer_workers"`
|
||||||
|
|||||||
@@ -29,15 +29,17 @@ type streamProcess struct {
|
|||||||
|
|
||||||
type StreamManager struct {
|
type StreamManager struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
|
puller *Puller // 可选的动态拉流客户端
|
||||||
frames chan<- Frame
|
frames chan<- Frame
|
||||||
processes map[string]*streamProcess // url -> {cmd, fps}
|
processes map[string]*streamProcess // url -> {cmd, fps}
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
updates <-chan *config.Config
|
updates <-chan *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStreamManager(cfg *config.Config, frames chan<- Frame, updates <-chan *config.Config) *StreamManager {
|
func NewStreamManager(cfg *config.Config, puller *Puller, frames chan<- Frame, updates <-chan *config.Config) *StreamManager {
|
||||||
return &StreamManager{
|
return &StreamManager{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
|
puller: puller,
|
||||||
frames: frames,
|
frames: frames,
|
||||||
processes: make(map[string]*streamProcess),
|
processes: make(map[string]*streamProcess),
|
||||||
updates: updates,
|
updates: updates,
|
||||||
@@ -45,7 +47,14 @@ func NewStreamManager(cfg *config.Config, frames chan<- Frame, updates <-chan *c
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sm *StreamManager) Run(ctx context.Context) {
|
func (sm *StreamManager) Run(ctx context.Context) {
|
||||||
// Initial start
|
// 1. 动态拉流模式
|
||||||
|
if sm.puller != nil {
|
||||||
|
log.Println("stream: running in auto-pull mode")
|
||||||
|
sm.runAutoPullMode(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 静态配置模式
|
||||||
sm.applyStreams(ctx, sm.cfg.RTSPURLs, sm.cfg.InferFPS)
|
sm.applyStreams(ctx, sm.cfg.RTSPURLs, sm.cfg.InferFPS)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -60,6 +69,39 @@ func (sm *StreamManager) Run(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runAutoPullMode 动态拉流模式主循环
|
||||||
|
func (sm *StreamManager) runAutoPullMode(ctx context.Context) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
sm.stopAll()
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从云端获取拉流地址
|
||||||
|
url, err := sm.puller.Pull()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("stream: pull failed: %v, retry in 10s", err)
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("stream: got dynamic url=%s, starting ingestion", url)
|
||||||
|
|
||||||
|
// 启动该 URL 的拉流
|
||||||
|
sm.applyStreams(ctx, []string{url}, sm.cfg.InferFPS)
|
||||||
|
|
||||||
|
// 等待上下文结束或 URL 被移除(通常意味着需要重新 Pull)
|
||||||
|
// 在 auto-pull 模式下,如果 ffmpeg 断开,streamLoop 会自动重试,
|
||||||
|
// 但我们需要在这里监听 ctx,或者等待 puller 心跳失败后重新拉取。
|
||||||
|
// 简单起见,这里阻塞直到 ctx 取消,streamLoop 内部会处理 ffmpeg 重连。
|
||||||
|
// 如果 streamLoop 发现 URL 失效,会退出并删除 process。
|
||||||
|
<-ctx.Done()
|
||||||
|
sm.stopAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (sm *StreamManager) applyStreams(ctx context.Context, urls []string, fps int) {
|
func (sm *StreamManager) applyStreams(ctx context.Context, urls []string, fps int) {
|
||||||
sm.mu.Lock()
|
sm.mu.Lock()
|
||||||
defer sm.mu.Unlock()
|
defer sm.mu.Unlock()
|
||||||
|
|||||||
205
internal/stream/puller.go
Normal file
205
internal/stream/puller.go
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user