chore: 初始化仓库基线(AGENTS.md、git 规范、敏感文件排除)
This commit is contained in:
@@ -0,0 +1,723 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"silk-server-go/internal/config"
|
||||
)
|
||||
|
||||
// GbPlayResult GB28181 播放结果
|
||||
type GbPlayResult struct {
|
||||
DeviceID string
|
||||
ChannelID string
|
||||
HLS string
|
||||
FLV string
|
||||
WsFlv string
|
||||
Fmp4 string
|
||||
Rtsp string
|
||||
WebRtc string
|
||||
}
|
||||
|
||||
// MediaService WVP/ZLMediaKit 媒体服务代理
|
||||
type MediaService struct {
|
||||
cfg *config.Config
|
||||
httpClient *http.Client
|
||||
cachedToken string
|
||||
tokenExpiresAt time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewMediaService 根据配置创建媒体服务
|
||||
func NewMediaService(cfg *config.Config) *MediaService {
|
||||
return &MediaService{
|
||||
cfg: cfg,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 15 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// wvpAPIBase 返回去掉末尾斜杠的 WVP API 基地址
|
||||
func (m *MediaService) wvpAPIBase() string {
|
||||
return strings.TrimSuffix(m.cfg.WVPAPIBase, "/")
|
||||
}
|
||||
|
||||
// zlmAPIBase 返回去掉末尾斜杠的 ZLMediaKit API 基地址
|
||||
func (m *MediaService) zlmAPIBase() string {
|
||||
return strings.TrimSuffix(m.cfg.ZLMAPIBase, "/")
|
||||
}
|
||||
|
||||
// login 登录 WVP API 并缓存 token(密码需 MD5),缓存 55 分钟
|
||||
func (m *MediaService) login(ctx context.Context) (string, error) {
|
||||
md5Password := md5.Sum([]byte(m.cfg.WVPPassword))
|
||||
md5Hex := hex.EncodeToString(md5Password[:])
|
||||
|
||||
loginURL := fmt.Sprintf("%s/api/user/login?username=%s&password=%s",
|
||||
m.wvpAPIBase(),
|
||||
m.cfg.WVPUsername,
|
||||
md5Hex,
|
||||
)
|
||||
|
||||
slog.Info("登录 WVP API", "username", m.cfg.WVPUsername)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, loginURL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建 WVP 登录请求失败: %w", err)
|
||||
}
|
||||
|
||||
resp, err := m.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("WVP 登录请求失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("WVP 登录失败: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||
return "", fmt.Errorf("解析 WVP 登录响应失败: %w", err)
|
||||
}
|
||||
if body.Code != 0 || body.Data.AccessToken == "" {
|
||||
return "", fmt.Errorf("WVP 登录错误: %s", body.Msg)
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
m.cachedToken = body.Data.AccessToken
|
||||
m.tokenExpiresAt = time.Now().Add(55 * time.Minute)
|
||||
m.mu.Unlock()
|
||||
|
||||
slog.Info("WVP 登录成功,token 已缓存")
|
||||
return body.Data.AccessToken, nil
|
||||
}
|
||||
|
||||
// getAuthToken 获取有效的认证 token,必要时重新登录
|
||||
func (m *MediaService) getAuthToken(ctx context.Context) (string, error) {
|
||||
m.mu.Lock()
|
||||
if m.cachedToken != "" && time.Now().Before(m.tokenExpiresAt) {
|
||||
token := m.cachedToken
|
||||
m.mu.Unlock()
|
||||
return token, nil
|
||||
}
|
||||
m.mu.Unlock()
|
||||
return m.login(ctx)
|
||||
}
|
||||
|
||||
// invalidateToken 使缓存的 token 失效
|
||||
func (m *MediaService) invalidateToken() {
|
||||
m.mu.Lock()
|
||||
m.cachedToken = ""
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// StartPlay 开始播放 GB28181 设备实时流,GET /api/play/start/{deviceId}/{channelId}
|
||||
func (m *MediaService) StartPlay(deviceId, channelId string) (*GbPlayResult, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
url := fmt.Sprintf("%s/api/play/start/%s/%s", m.wvpAPIBase(), deviceId, channelId)
|
||||
slog.Info("调用 WVP play/start", "url", url)
|
||||
|
||||
body, err := m.doWVPRequest(ctx, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.extractPlayResult(body, deviceId, channelId), nil
|
||||
}
|
||||
|
||||
// StopPlay 停止播放 GB28181 设备实时流,GET /api/play/stop/{deviceId}/{channelId}
|
||||
func (m *MediaService) StopPlay(deviceId, channelId string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
url := fmt.Sprintf("%s/api/play/stop/%s/%s", m.wvpAPIBase(), deviceId, channelId)
|
||||
token, err := m.getAuthToken(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("access-token", token)
|
||||
|
||||
resp, err := m.httpClient.Do(req)
|
||||
if err != nil {
|
||||
slog.Warn("停止播放失败", "deviceId", deviceId, "channelId", channelId, "error", err)
|
||||
return nil
|
||||
}
|
||||
resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartPlayback 开始回放 GB28181 设备历史流,GET /api/playback/start/{deviceId}/{channelId}?startTime=...&endTime=...
|
||||
func (m *MediaService) StartPlayback(deviceId, channelId, startTime, endTime string) (*GbPlayResult, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
url := fmt.Sprintf("%s/api/playback/start/%s/%s?startTime=%s&endTime=%s",
|
||||
m.wvpAPIBase(), deviceId, channelId,
|
||||
startTime, endTime,
|
||||
)
|
||||
slog.Info("调用 WVP playback/start", "url", url)
|
||||
|
||||
body, err := m.doWVPRequest(ctx, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.extractPlayResult(body, deviceId, channelId), nil
|
||||
}
|
||||
|
||||
// doWVPRequest 执行 WVP API 请求(token 过期时自动重试一次)
|
||||
func (m *MediaService) doWVPRequest(ctx context.Context, reqURL string) (map[string]interface{}, error) {
|
||||
token, err := m.getAuthToken(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := m.sendWVPRequest(ctx, reqURL, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// token 过期,重试一次
|
||||
code, ok := body["code"].(float64)
|
||||
if ok && int(code) == 401 {
|
||||
m.invalidateToken()
|
||||
newToken, err := m.getAuthToken(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err = m.sendWVPRequest(ctx, reqURL, newToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
code, ok = body["code"].(float64)
|
||||
if !ok || int(code) != 0 {
|
||||
msg, _ := body["msg"].(string)
|
||||
if msg == "" {
|
||||
msg, _ = body["message"].(string)
|
||||
}
|
||||
return nil, fmt.Errorf("WVP API 错误: %s", msg)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// sendWVPRequest 发送带 access-token 的 GET 请求
|
||||
func (m *MediaService) sendWVPRequest(ctx context.Context, reqURL, token string) (map[string]interface{}, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("access-token", token)
|
||||
|
||||
resp, err := m.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("流媒体服务暂时不可用: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("WVP API 返回 HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var body map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||
return nil, fmt.Errorf("解析 WVP 响应失败: %w", err)
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// extractPlayResult 从 WVP 响应中提取播放地址,并将 ZLM 的 :80/ 替换为 :8081/
|
||||
func (m *MediaService) extractPlayResult(body map[string]interface{}, deviceId, channelId string) *GbPlayResult {
|
||||
data, _ := body["data"].(map[string]interface{})
|
||||
if data == nil {
|
||||
data = map[string]interface{}{}
|
||||
}
|
||||
return &GbPlayResult{
|
||||
DeviceID: deviceId,
|
||||
ChannelID: channelId,
|
||||
HLS: fixZlmPort(getString(data, "hls")),
|
||||
FLV: fixZlmPort(getString(data, "flv")),
|
||||
WsFlv: fixZlmPort(getString(data, "ws_flv")),
|
||||
Fmp4: fixZlmPort(getString(data, "fmp4")),
|
||||
Rtsp: getString(data, "rtsp"),
|
||||
WebRtc: fixZlmPort(getString(data, "webRtc")),
|
||||
}
|
||||
}
|
||||
|
||||
// fixZlmPort 将 ZLMediaKit 容器内地址替换为前端可通过 server.cjs 代理访问的相对路径。
|
||||
// ZLM 返回的 URL 形如 http://100.83.103.1:80/rtp/xxx.live.flv?...,
|
||||
// 改为相对路径 /rtp/xxx.live.flv?... 后,前端浏览器会以同源请求走 server.cjs 代理,
|
||||
// 避免 CORS / CSP 限制(server.cjs 已内置 /rtp/ 到 127.0.0.1:8081 的代理)。
|
||||
func fixZlmPort(u string) string {
|
||||
if u == "" {
|
||||
return u
|
||||
}
|
||||
// 提取 /rtp/ 起始的相对路径(含 query string)
|
||||
if idx := strings.Index(u, "/rtp/"); idx >= 0 {
|
||||
return u[idx:]
|
||||
}
|
||||
// 兜底:替换 Docker 内部主机名为外部可访问 IP
|
||||
u = strings.Replace(u, "polaris-media:", "100.83.103.1:", 1)
|
||||
u = strings.Replace(u, ":80/", ":8081/", 1)
|
||||
if strings.HasSuffix(u, ":80") {
|
||||
u = u[:len(u)-2] + ":8081"
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
// getString 从 map 中安全取字符串
|
||||
func getString(m map[string]interface{}, key string) string {
|
||||
if v, ok := m[key].(string); ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// --- ZLMediaKit 录制控制 ---
|
||||
|
||||
// StartRecord 开始录制(type=1 for MP4)
|
||||
func (m *MediaService) StartRecord(app, stream string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
url := fmt.Sprintf("%s/index/api/startRecord?type=1&vhost=__defaultVhost__&app=%s&stream=%s&secret=%s",
|
||||
m.zlmAPIBase(), app, stream, m.cfg.ZLMSecret,
|
||||
)
|
||||
slog.Info("ZLMediaKit startRecord", "app", app, "stream", stream)
|
||||
|
||||
return m.zlmGet(ctx, url, "startRecord")
|
||||
}
|
||||
|
||||
// StopRecord 停止录制
|
||||
func (m *MediaService) StopRecord(app, stream string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
url := fmt.Sprintf("%s/index/api/stopRecord?type=1&vhost=__defaultVhost__&app=%s&stream=%s&secret=%s",
|
||||
m.zlmAPIBase(), app, stream, m.cfg.ZLMSecret,
|
||||
)
|
||||
slog.Info("ZLMediaKit stopRecord", "app", app, "stream", stream)
|
||||
|
||||
return m.zlmGet(ctx, url, "stopRecord")
|
||||
}
|
||||
|
||||
// IsRecording 检查是否正在录制
|
||||
func (m *MediaService) IsRecording(app, stream string) bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
url := fmt.Sprintf("%s/index/api/isRecording?type=1&vhost=__defaultVhost__&app=%s&stream=%s&secret=%s",
|
||||
m.zlmAPIBase(), app, stream, m.cfg.ZLMSecret,
|
||||
)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
resp, err := m.httpClient.Do(req)
|
||||
if err != nil {
|
||||
slog.Warn("ZLM isRecording 请求失败", "error", err)
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return false
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Code int `json:"code"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||
return false
|
||||
}
|
||||
return body.Code == 0 && body.Data == true
|
||||
}
|
||||
|
||||
// zlmGet 执行 ZLMediaKit GET 请求并检查 code==0
|
||||
func (m *MediaService) zlmGet(ctx context.Context, url, action string) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建 %s 请求失败: %w", action, err)
|
||||
}
|
||||
resp, err := m.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("流媒体录制服务暂时不可用: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("ZLM %s 失败: HTTP %d", action, resp.StatusCode)
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||
return fmt.Errorf("解析 ZLM %s 响应失败: %w", action, err)
|
||||
}
|
||||
if body.Code != 0 {
|
||||
return fmt.Errorf("ZLM %s 错误: %s", action, body.Msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WvpDeviceInfo WVP 设备同步信息(silk 与 WVP 共有参数)
|
||||
type WvpDeviceInfo struct {
|
||||
Name string
|
||||
Manufacturer string
|
||||
Transport string
|
||||
StreamMode string
|
||||
Password string
|
||||
OnLine bool
|
||||
}
|
||||
|
||||
// SyncWvpDevices 查询 WVP 设备列表,返回 deviceId -> 设备信息映射(用于 WVP → silk 同步)
|
||||
func (m *MediaService) SyncWvpDevices() (map[string]*WvpDeviceInfo, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
u := fmt.Sprintf("%s/api/device/query/devices?page=1&count=100", m.wvpAPIBase())
|
||||
body, err := m.doWVPRequest(ctx, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]*WvpDeviceInfo)
|
||||
if data, ok := body["data"].(map[string]interface{}); ok {
|
||||
if devices, ok := data["list"].([]interface{}); ok {
|
||||
for _, d := range devices {
|
||||
if dev, ok := d.(map[string]interface{}); ok {
|
||||
deviceId, _ := dev["deviceId"].(string)
|
||||
if deviceId == "" {
|
||||
continue
|
||||
}
|
||||
info := &WvpDeviceInfo{
|
||||
Name: getString(dev, "name"),
|
||||
Manufacturer: getString(dev, "manufacturer"),
|
||||
Transport: getString(dev, "transport"),
|
||||
StreamMode: getString(dev, "streamMode"),
|
||||
Password: getString(dev, "password"),
|
||||
}
|
||||
if onLine, ok := dev["onLine"].(bool); ok {
|
||||
info.OnLine = onLine
|
||||
}
|
||||
result[deviceId] = info
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetServerConfig 查询 WVP 服务器配置(含 SIP 参数),供前端展示
|
||||
func (m *MediaService) GetServerConfig() (map[string]interface{}, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
url := fmt.Sprintf("%s/api/server/config", m.wvpAPIBase())
|
||||
body, err := m.doWVPRequest(ctx, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if data, ok := body["data"].(map[string]interface{}); ok {
|
||||
return data, nil
|
||||
}
|
||||
return nil, fmt.Errorf("WVP 配置响应格式异常")
|
||||
}
|
||||
|
||||
// WvpDevice WVP 设备信息
|
||||
type WvpDevice struct {
|
||||
ID int `json:"id"`
|
||||
DeviceID string `json:"deviceId"`
|
||||
Name string `json:"name"`
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
Model string `json:"model"`
|
||||
OnLine bool `json:"onLine"`
|
||||
Transport string `json:"transport"`
|
||||
StreamMode string `json:"streamMode"`
|
||||
HostAddress string `json:"hostAddress"`
|
||||
}
|
||||
|
||||
// WvpChannel WVP 通道信息
|
||||
type WvpChannel struct {
|
||||
ID int `json:"id"`
|
||||
DeviceID string `json:"deviceId"`
|
||||
ChannelID string `json:"channelId"`
|
||||
Name string `json:"name"`
|
||||
OnLine bool `json:"onLine"`
|
||||
}
|
||||
|
||||
// ListWvpDevices 查询 WVP 已注册设备列表
|
||||
func (m *MediaService) ListWvpDevices(query string) ([]WvpDevice, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
u := fmt.Sprintf("%s/api/device/query/devices?page=1&count=100", m.wvpAPIBase())
|
||||
if query != "" {
|
||||
u += "&query=" + url.QueryEscape(query)
|
||||
}
|
||||
|
||||
body, err := m.doWVPRequest(ctx, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var devices []WvpDevice
|
||||
if data, ok := body["data"].(map[string]interface{}); ok {
|
||||
if list, ok := data["list"].([]interface{}); ok {
|
||||
for _, d := range list {
|
||||
if dev, ok := d.(map[string]interface{}); ok {
|
||||
device := WvpDevice{
|
||||
DeviceID: getString(dev, "deviceId"),
|
||||
Name: getString(dev, "name"),
|
||||
Manufacturer: getString(dev, "manufacturer"),
|
||||
Model: getString(dev, "model"),
|
||||
Transport: getString(dev, "transport"),
|
||||
StreamMode: getString(dev, "streamMode"),
|
||||
HostAddress: getString(dev, "hostAddress"),
|
||||
}
|
||||
if onLine, ok := dev["onLine"].(bool); ok {
|
||||
device.OnLine = onLine
|
||||
}
|
||||
if id, ok := dev["id"].(float64); ok {
|
||||
device.ID = int(id)
|
||||
}
|
||||
devices = append(devices, device)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
// ListWvpChannels 查询 WVP 设备的通道列表
|
||||
func (m *MediaService) ListWvpChannels(deviceId string) ([]WvpChannel, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
u := fmt.Sprintf("%s/api/device/query/devices/%s/channels?page=1&count=100", m.wvpAPIBase(), url.PathEscape(deviceId))
|
||||
body, err := m.doWVPRequest(ctx, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var channels []WvpChannel
|
||||
if data, ok := body["data"].(map[string]interface{}); ok {
|
||||
if list, ok := data["list"].([]interface{}); ok {
|
||||
for _, c := range list {
|
||||
if ch, ok := c.(map[string]interface{}); ok {
|
||||
channel := WvpChannel{
|
||||
DeviceID: getString(ch, "deviceId"),
|
||||
ChannelID: getString(ch, "channelId"),
|
||||
Name: getString(ch, "name"),
|
||||
}
|
||||
if onLine, ok := ch["onLine"].(bool); ok {
|
||||
channel.OnLine = onLine
|
||||
}
|
||||
if id, ok := ch["id"].(float64); ok {
|
||||
channel.ID = int(id)
|
||||
}
|
||||
channels = append(channels, channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
// SyncWvpDevice 触发 WVP 设备通道同步
|
||||
func (m *MediaService) SyncWvpDevice(deviceId string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
u := fmt.Sprintf("%s/api/device/query/devices/%s/sync", m.wvpAPIBase(), url.PathEscape(deviceId))
|
||||
_, err := m.doWVPRequest(ctx, u)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateWvpDevice 同步设备信息到 WVP(silk → WVP,共有可编辑参数:name、manufacturer、password)
|
||||
func (m *MediaService) UpdateWvpDevice(deviceId string, name, manufacturer, password string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 先查询设备获取 WVP 内部 ID
|
||||
queryURL := fmt.Sprintf("%s/api/device/query/devices/%s", m.wvpAPIBase(), url.PathEscape(deviceId))
|
||||
body, err := m.doWVPRequest(ctx, queryURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
devData, ok := body["data"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("设备尚未在 WVP 注册,请在摄像头硬件配置设备ID后等待注册完成")
|
||||
}
|
||||
devID, ok := devData["id"].(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("WVP 设备 ID 未找到")
|
||||
}
|
||||
|
||||
// POST 更新设备信息(WVP API 仅接受 name/manufacturer/model 等可编辑字段)
|
||||
updateURL := fmt.Sprintf("%s/api/device/query/device/update", m.wvpAPIBase())
|
||||
token, err := m.getAuthToken(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"id": int(devID),
|
||||
"deviceId": deviceId, // WVP API 要求 deviceId 字段必填
|
||||
}
|
||||
if name != "" {
|
||||
updates["name"] = name
|
||||
}
|
||||
if manufacturer != "" {
|
||||
updates["manufacturer"] = manufacturer
|
||||
}
|
||||
if password != "" {
|
||||
updates["password"] = password
|
||||
}
|
||||
|
||||
updateBody, _ := json.Marshal(updates)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, updateURL, strings.NewReader(string(updateBody)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("access-token", token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := m.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("WVP 更新请求失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("WVP 更新失败: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 检查 WVP 响应体中的 code 字段
|
||||
var respBody struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil {
|
||||
return fmt.Errorf("解析 WVP 更新响应失败: %w", err)
|
||||
}
|
||||
if respBody.Code != 0 {
|
||||
return fmt.Errorf("WVP 更新失败: %s", respBody.Msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddWvpDevice 预先添加设备到 WVP(在摄像头注册前,设置独立密码等参数)
|
||||
func (m *MediaService) AddWvpDevice(deviceId, name, manufacturer, password string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
token, err := m.getAuthToken(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addURL := fmt.Sprintf("%s/api/device/query/device/add", m.wvpAPIBase())
|
||||
payload := map[string]interface{}{
|
||||
"deviceId": deviceId,
|
||||
}
|
||||
if name != "" {
|
||||
payload["name"] = name
|
||||
}
|
||||
if manufacturer != "" {
|
||||
payload["manufacturer"] = manufacturer
|
||||
}
|
||||
if password != "" {
|
||||
payload["password"] = password
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(payload)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, addURL, strings.NewReader(string(body)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("access-token", token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := m.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("WVP 添加设备请求失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var respBody struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil {
|
||||
return fmt.Errorf("解析 WVP 添加设备响应失败: %w", err)
|
||||
}
|
||||
if respBody.Code != 0 {
|
||||
return fmt.Errorf("WVP 添加设备失败: %s", respBody.Msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteWvpDevice 从 WVP 删除设备(silk 删除摄像头时同步删除 WVP 中预添加的设备)
|
||||
func (m *MediaService) DeleteWvpDevice(deviceId string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
token, err := m.getAuthToken(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("%s/api/device/query/devices/%s/delete", m.wvpAPIBase(), url.PathEscape(deviceId))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("access-token", token)
|
||||
|
||||
resp, err := m.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("WVP 删除设备请求失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var respBody struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil {
|
||||
return fmt.Errorf("解析 WVP 删除设备响应失败: %w", err)
|
||||
}
|
||||
if respBody.Code != 0 {
|
||||
return fmt.Errorf("WVP 删除设备失败: %s", respBody.Msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user