120 lines
4.4 KiB
Go
120 lines
4.4 KiB
Go
package admission
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"yovision.local/sense/app/sense/adapters/onvif"
|
|
"yovision.local/sense/app/sense/adapters/rtsp"
|
|
"yovision.local/sense/app/sense/device"
|
|
"yovision.local/sense/app/sense/identity"
|
|
)
|
|
|
|
type Profile struct {
|
|
Token string `json:"token"`
|
|
Name string `json:"name"`
|
|
Width int `json:"width"`
|
|
Height int `json:"height"`
|
|
Encoding string `json:"encoding"`
|
|
StreamURI string `json:"stream_uri"`
|
|
Kind string `json:"kind"`
|
|
Verification rtsp.Result `json:"verification"`
|
|
}
|
|
type Result struct {
|
|
DeviceID string `json:"device_id"`
|
|
Address string `json:"address"`
|
|
Status string `json:"status"`
|
|
Detail string `json:"detail"`
|
|
Profiles []Profile `json:"profiles"`
|
|
CheckedAt time.Time `json:"checked_at"`
|
|
}
|
|
type Service struct {
|
|
onvif onvif.Client
|
|
rtsp rtsp.Verifier
|
|
discoveryIP string
|
|
discoveryTimeout time.Duration
|
|
mu sync.RWMutex
|
|
results map[string]Result
|
|
now func() time.Time
|
|
}
|
|
|
|
func NewService(client onvif.Client, verifier rtsp.Verifier, discoveryIP string) *Service {
|
|
return &Service{onvif: client, rtsp: verifier, discoveryIP: discoveryIP, discoveryTimeout: 3 * time.Second, results: map[string]Result{}, now: time.Now}
|
|
}
|
|
func (s *Service) Discover(ctx context.Context) ([]string, error) {
|
|
if strings.TrimSpace(s.discoveryIP) == "" {
|
|
return nil, fmt.Errorf("discovery_not_configured")
|
|
}
|
|
return onvif.Discover(ctx, s.discoveryIP, s.discoveryTimeout)
|
|
}
|
|
func (s *Service) Probe(ctx context.Context, actor identity.Principal, deviceID, address string) (Result, error) {
|
|
credential, err := device.ReadCredential(ctx, deviceID)
|
|
if err != nil {
|
|
return Result{}, fmt.Errorf("credential_required")
|
|
}
|
|
profiles, err := s.onvif.Profiles(ctx, address, onvif.Credential{Username: credential.Username, Password: credential.Password})
|
|
if err != nil {
|
|
status, detail := classify(err)
|
|
result := Result{DeviceID: deviceID, Address: address, Status: status, Detail: detail, CheckedAt: s.now().UTC()}
|
|
s.save(result)
|
|
identity.RecordAudit(ctx, actor.UserID, "admission.probe", deviceID, "failure", map[string]any{"status": status})
|
|
return result, nil
|
|
}
|
|
items := make([]Profile, 0, len(profiles))
|
|
for _, profile := range profiles {
|
|
verification, verifyErr := s.rtsp.Verify(ctx, profile.StreamURI, rtsp.Credential{Username: credential.Username, Password: credential.Password})
|
|
if verifyErr != nil {
|
|
verification = rtsp.Result{Status: "failed", Detail: "视频地址格式不正确"}
|
|
}
|
|
kind := "other"
|
|
items = append(items, Profile{Token: profile.Token, Name: profile.Name, Width: profile.Width, Height: profile.Height, Encoding: profile.Encoding, StreamURI: profile.StreamURI, Kind: kind, Verification: verification})
|
|
}
|
|
sort.Slice(items, func(i, j int) bool { return items[i].Width*items[i].Height > items[j].Width*items[j].Height })
|
|
if len(items) > 0 {
|
|
items[0].Kind = "main"
|
|
}
|
|
if len(items) > 1 {
|
|
items[len(items)-1].Kind = "sub"
|
|
}
|
|
status := "ready"
|
|
detail := "设备与视频 Profile 已验证"
|
|
for _, item := range items {
|
|
if item.Verification.Status != "ready" {
|
|
status = "profile_failed"
|
|
detail = "部分视频 Profile 验证失败"
|
|
}
|
|
}
|
|
result := Result{DeviceID: deviceID, Address: address, Status: status, Detail: detail, Profiles: items, CheckedAt: s.now().UTC()}
|
|
s.save(result)
|
|
identity.RecordAudit(ctx, actor.UserID, "admission.probe", deviceID, "success", map[string]any{"profile_count": len(items), "status": status})
|
|
return result, nil
|
|
}
|
|
func (s *Service) Get(deviceID string) (Result, bool) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
value, ok := s.results[deviceID]
|
|
return value, ok
|
|
}
|
|
func (s *Service) save(result Result) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.results[result.DeviceID] = result
|
|
}
|
|
func classify(err error) (string, string) {
|
|
value := strings.ToLower(err.Error())
|
|
switch {
|
|
case strings.Contains(value, "authentication"):
|
|
return "authentication_failed", "设备拒绝了当前凭据,请更新后重试"
|
|
case strings.Contains(value, "deadline") || strings.Contains(value, "timeout"):
|
|
return "timeout", "设备响应超时"
|
|
case strings.Contains(value, "clock") || strings.Contains(value, "time"):
|
|
return "clock_skew", "设备时间可能不准确,请校时后重试"
|
|
default:
|
|
return "unreachable", "无法读取设备信息,请检查地址和网络"
|
|
}
|
|
}
|