feat(control): 视频源状态检测与展示
- 新增 service.FetchVideoSourceStatus: 调 agent /v1/graphs 解析 input_rtsp 节点 (custom_metrics: streaming/last_error/fail_count) - 设备详情页运行与服务区块展示每个视频源: 在线/离线 + 失败原因(如 RTSP 打开失败: 404) + 帧率 + 历史失败次数 - 视频源离线时红色提示,与媒体服务状态区分
This commit is contained in:
parent
dca065e6cc
commit
c92ad41bd1
82
control/internal/service/device_video_sources.go
Normal file
82
control/internal/service/device_video_sources.go
Normal file
@ -0,0 +1,82 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"safesight-control/internal/models"
|
||||
)
|
||||
|
||||
// VideoSourceStatus 设备上一个视频源(input_rtsp 节点)的运行状态。
|
||||
// 数据来自 edge /v1/graphs 节点快照的 custom_metrics(media 端 input_rtsp 上报)。
|
||||
type VideoSourceStatus struct {
|
||||
GraphName string `json:"graph_name"`
|
||||
NodeID string `json:"node_id"`
|
||||
URL string `json:"url"`
|
||||
Streaming bool `json:"streaming"`
|
||||
Online bool `json:"online"`
|
||||
InputFPS float64 `json:"input_fps"`
|
||||
FailCount int64 `json:"fail_count"`
|
||||
LastError string `json:"last_error"`
|
||||
LastErrorAtMS int64 `json:"last_error_at_ms"`
|
||||
LastOKAtMS int64 `json:"last_ok_at_ms"`
|
||||
}
|
||||
|
||||
// FetchVideoSourceStatus 调用 edge agent /v1/graphs,解析所有 input_rtsp 节点的
|
||||
// 拉流状态(在线/离线/失败原因)。设备离线或接口不可用时返回 error。
|
||||
func FetchVideoSourceStatus(agent *AgentClient, device *models.Device) ([]VideoSourceStatus, error) {
|
||||
if agent == nil || device == nil || strings.TrimSpace(device.IP) == "" || device.AgentPort <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
body, status, err := agent.Do("GET", device.IP, device.AgentPort, "/v1/graphs", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if status != 200 {
|
||||
return nil, fmt.Errorf("agent returned status %d", status)
|
||||
}
|
||||
var graphs []struct {
|
||||
Name string `json:"name"`
|
||||
Running bool `json:"running"`
|
||||
Nodes []struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
InputFPS float64 `json:"input_fps"`
|
||||
CustomMetrics struct {
|
||||
Type string `json:"type"`
|
||||
URL string `json:"url"`
|
||||
Streaming bool `json:"streaming"`
|
||||
FailCount float64 `json:"fail_count"`
|
||||
LastError string `json:"last_error"`
|
||||
LastErrorAtMS float64 `json:"last_error_at_ms"`
|
||||
LastOKAtMS float64 `json:"last_ok_at_ms"`
|
||||
} `json:"custom_metrics"`
|
||||
} `json:"nodes"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &graphs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var out []VideoSourceStatus
|
||||
for _, g := range graphs {
|
||||
for _, n := range g.Nodes {
|
||||
if n.Type != "input_rtsp" {
|
||||
continue
|
||||
}
|
||||
out = append(out, VideoSourceStatus{
|
||||
GraphName: g.Name,
|
||||
NodeID: n.ID,
|
||||
URL: n.CustomMetrics.URL,
|
||||
Streaming: n.CustomMetrics.Streaming,
|
||||
Online: n.CustomMetrics.Streaming,
|
||||
InputFPS: n.InputFPS,
|
||||
FailCount: int64(n.CustomMetrics.FailCount),
|
||||
LastError: n.CustomMetrics.LastError,
|
||||
LastErrorAtMS: int64(n.CustomMetrics.LastErrorAtMS),
|
||||
LastOKAtMS: int64(n.CustomMetrics.LastOKAtMS),
|
||||
})
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@ -144,6 +144,8 @@ type PageData struct {
|
||||
DeviceResourceStatuses []service.InstalledResourceStatus
|
||||
DeviceVersion *service.DeviceVersionInfo
|
||||
DeviceVersionErr string
|
||||
VideoSources []service.VideoSourceStatus
|
||||
VideoSourceErr string
|
||||
Templates []service.Template
|
||||
Template *service.Template
|
||||
AssetTab string
|
||||
@ -2024,6 +2026,11 @@ func (u *UI) deviceDetailPageData(dev *models.Device) PageData {
|
||||
} else {
|
||||
data.DeviceVersionErr = err.Error()
|
||||
}
|
||||
if vs, err := service.FetchVideoSourceStatus(u.agent, dev); err == nil {
|
||||
data.VideoSources = vs
|
||||
} else {
|
||||
data.VideoSourceErr = err.Error()
|
||||
}
|
||||
if items, err := service.FetchInstalledModelStatuses(u.agent, dev); err == nil {
|
||||
data.DeviceModelStatuses = items
|
||||
}
|
||||
|
||||
@ -478,3 +478,9 @@ body[data-mode=expert] .mode-track{background:var(--primary-strong);border-color
|
||||
.mode-thumb{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:var(--sidebar-text);transition:transform .16s ease}
|
||||
body[data-mode=expert] .mode-thumb{transform:translateX(16px)}
|
||||
body:not([data-mode=expert]) .expert-only{display:none}
|
||||
|
||||
/* 视频源状态(设备详情页) */
|
||||
.video-source-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:12px}
|
||||
.video-source-item{padding:10px 12px;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface-soft)}
|
||||
.video-source-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px}
|
||||
.video-source-error{margin-top:6px;padding:6px 8px;border-radius:var(--radius);background:rgba(220,53,69,.08);color:var(--danger);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
|
||||
@ -115,6 +115,23 @@
|
||||
<div class="info-list compact-list">
|
||||
<div><span>媒体服务</span><strong>{{if and .ConfigStatus .ConfigStatus.MediaServer.Running}}运行中{{else}}{{if .ConfigStatus.MediaServer.LastError}}{{.ConfigStatus.MediaServer.LastError}}{{else}}状态未知{{end}}{{end}}</strong></div>
|
||||
</div>
|
||||
{{if .VideoSources}}
|
||||
<div class="video-source-list">
|
||||
{{range .VideoSources}}
|
||||
<div class="video-source-item">
|
||||
<div class="video-source-head">
|
||||
<strong>{{.GraphName}}</strong>
|
||||
{{if .Streaming}}<span class="pill ok">在线</span>{{else}}<span class="pill bad">离线</span>{{end}}
|
||||
</div>
|
||||
<div class="mono small muted" title="{{.URL}}">{{.URL}}</div>
|
||||
{{if .LastError}}<div class="video-source-error" title="{{.LastError}}">⚠ {{.LastError}}</div>{{end}}
|
||||
{{if not .LastError}}<div class="muted small">帧率 {{printf "%.1f" .InputFPS}} fps{{if gt .FailCount 0}} · 历史失败 {{.FailCount}} 次{{end}}</div>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else if .VideoSourceErr}}
|
||||
<div class="muted small" style="padding:8px 0">视频源状态获取失败:{{.VideoSourceErr}}</div>
|
||||
{{end}}
|
||||
<div class="actions service-actions-row">
|
||||
<form method="post" action="/devices/{{.Device.DeviceID}}/action">
|
||||
<input type="hidden" name="action" value="media_start" />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user