Files
gb28181/internal/adapter/onvifadapter/onvif.go
T
2026-01-08 13:14:12 +08:00

293 lines
7.9 KiB
Go

package onvifadapter
import (
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"strings"
"time"
"github.com/gowvp/onvif"
devicemodel "github.com/gowvp/onvif/device"
m "github.com/gowvp/onvif/media"
sdkdevice "github.com/gowvp/onvif/sdk/device"
sdkmedia "github.com/gowvp/onvif/sdk/media"
xsdonvif "github.com/gowvp/onvif/xsd/onvif"
"github.com/gowvp/owl/internal/core/ipc"
"github.com/gowvp/owl/internal/core/sms"
"github.com/ixugo/goddd/pkg/conc"
"github.com/ixugo/goddd/pkg/orm"
)
var _ ipc.Protocoler = (*Adapter)(nil)
// Adapter ONVIF 协议适配器
//
// 设计说明:
// - 适配器实现 ipc.Protocol 接口(Port 在 ipc 包内)
// - 适配器直接依赖领域模型 (ipc.Device, ipc.Channel)
// - 适配器依赖 ipc.Adapter 来访问存储和通用功能
// - 这符合清晰架构: 外层(适配器)依赖内层(领域)
type Adapter struct {
devices conc.Map[string, *Device] // ONVIF 设备连接缓存
adapter ipc.Adapter // 通用适配器,提供 SaveChannels 等方法
client *http.Client
sms sms.Core
}
// Device ONVIF 设备包装(内存状态 + ONVIF 连接)
type Device struct {
*onvif.Device
KeepaliveAt orm.Time // 最后心跳时间
IsOnline bool // 在线状态(内存缓存)
}
// DeleteDevice implements ipc.Protocoler.
func (a *Adapter) DeleteDevice(ctx context.Context, device *ipc.Device) error {
a.devices.Delete(device.ID)
return nil
}
func NewAdapter(adapter ipc.Adapter, sms sms.Core) *Adapter {
cli := *http.DefaultClient
cli.Timeout = time.Millisecond * 3000
a := Adapter{
adapter: adapter,
client: &cli,
sms: sms,
}
a.init()
// 启动健康检查
go a.startHealthCheck(context.Background())
return &a
}
func (a *Adapter) init() {
devices, err := a.adapter.FindDevices(context.TODO())
if err != nil {
panic(err)
}
for _, device := range devices {
if device.IsOnvif() {
go func(device *ipc.Device) {
onvifDev, err := onvif.NewDevice(onvif.DeviceParams{
Xaddr: fmt.Sprintf("%s:%d", device.IP, device.Port),
Username: device.GetUsername(),
Password: device.Password,
HttpClient: a.client,
})
if err != nil {
_ = a.adapter.Edit(device.ID, func(d *ipc.Device) {
d.IsOnline = false
})
slog.Error("初始化 ONVIF 设备失败", "err", err, "device_id", device.ID)
}
if onvifDev == nil {
return
}
a.devices.Store(device.ID, &Device{
Device: onvifDev,
IsOnline: err == nil,
})
}(device)
}
}
}
// ValidateDevice 实现 ipc.Protocol 接口 - ONVIF 设备验证
func (a *Adapter) ValidateDevice(ctx context.Context, dev *ipc.Device) error {
onvifDev, err := onvif.NewDevice(onvif.DeviceParams{
Xaddr: fmt.Sprintf("%s:%d", dev.IP, dev.Port),
Username: dev.GetUsername(),
Password: dev.Password,
HttpClient: a.client,
})
if err != nil {
return fmt.Errorf("IP 或 PORT 错误: %w", err)
}
// 获取设备信息并填充到领域模型
resp, err := sdkdevice.Call_GetDeviceInformation(ctx, onvifDev, devicemodel.GetDeviceInformation{})
if err != nil {
return fmt.Errorf("账号或密码错误: %w", err)
}
dev.Transport = "tcp"
dev.Ext.Firmware = resp.FirmwareVersion
dev.Ext.Manufacturer = resp.Manufacturer
dev.Ext.Model = resp.Model
dev.IsOnline = true
dev.Address = fmt.Sprintf("%s:%d", dev.IP, dev.Port)
return nil
}
// InitDevice 实现 ipc.Protocol 接口 - 初始化 ONVIF 设备
// ONVIF 设备初始化时,自动查询 Profiles 并创建为通道
func (a *Adapter) InitDevice(ctx context.Context, dev *ipc.Device) error {
// 创建 ONVIF 连接
onvifDev, err := onvif.NewDevice(onvif.DeviceParams{
Xaddr: fmt.Sprintf("%s:%d", dev.IP, dev.Port),
Username: dev.GetUsername(),
Password: dev.Password,
HttpClient: a.client,
})
if err != nil {
return err
}
// 缓存设备连接
d := Device{
Device: onvifDev,
IsOnline: true,
}
a.devices.Store(dev.ID, &d)
// 自动查询 Profiles 作为通道
return a.queryAndSaveProfiles(ctx, dev, &d)
}
// QueryCatalog 实现 ipc.Protocol 接口 - ONVIF 查询 Profiles
func (a *Adapter) QueryCatalog(ctx context.Context, dev *ipc.Device) error {
onvifDev, ok := a.devices.Load(dev.ID)
if !ok {
// 设备连接不在缓存中,尝试重新连接
var err error
d, err := onvif.NewDevice(onvif.DeviceParams{
Xaddr: fmt.Sprintf("%s:%d", dev.IP, dev.Port),
Username: dev.GetUsername(),
Password: dev.Password,
})
if err != nil {
return fmt.Errorf("ONVIF 设备未初始化: %w", err)
}
onvifDev = &Device{
Device: d,
IsOnline: true,
}
a.devices.Store(dev.ID, onvifDev)
}
return a.queryAndSaveProfiles(ctx, dev, onvifDev)
}
// StartPlay 实现 ipc.Protocol 接口 - ONVIF 播放
func (a *Adapter) StartPlay(ctx context.Context, dev *ipc.Device, ch *ipc.Channel) (*ipc.PlayResponse, error) {
onvifDev, ok := a.devices.Load(dev.ID)
if !ok {
return nil, fmt.Errorf("ONVIF 设备未初始化")
}
// 获取 RTSP 地址
streamURI, err := a.getStreamURI(ctx, onvifDev, ch.ChannelID)
if err != nil {
return nil, err
}
return &ipc.PlayResponse{
RTSP: streamURI,
}, nil
}
// StopPlay 实现 ipc.Protocol 接口 - ONVIF 停止播放
func (a *Adapter) StopPlay(ctx context.Context, dev *ipc.Device, ch *ipc.Channel) error {
// ONVIF 通常不需要显式停止播放
return nil
}
// queryAndSaveProfiles 查询 ONVIF Profiles 并保存为通道
//
// 使用统一的 SaveChannels 方法,自动处理增量更新和删除
func (a *Adapter) queryAndSaveProfiles(ctx context.Context, device *ipc.Device, onvifDev *Device) error {
resp, err := sdkmedia.Call_GetProfiles(ctx, onvifDev.Device, m.GetProfiles{})
if err != nil {
return fmt.Errorf("账号或密码错误: %w", err)
}
// 将 Profiles 转换为通道列表
channels := make([]*ipc.Channel, 0, len(resp.Profiles))
for _, profile := range resp.Profiles {
channel := &ipc.Channel{
DeviceID: device.ID,
ChannelID: string(profile.Token),
Name: string(profile.Name),
DID: device.ID,
IsOnline: true,
Type: ipc.TypeOnvif,
}
channels = append(channels, channel)
}
if len(channels) == 0 {
return fmt.Errorf("没有找到 ONVIF 通道")
}
// 使用统一的 SaveChannels 方法保存(自动处理增删改)
if err := a.adapter.SaveChannels(channels); err != nil {
return fmt.Errorf("保存 ONVIF 通道失败: %w", err)
}
slog.InfoContext(ctx, "ONVIF Profiles 同步完成",
"device_id", device.ID,
"profile_count", len(channels))
return nil
}
// getStreamURI 获取 RTSP 流地址
func (a *Adapter) getStreamURI(ctx context.Context, dev *Device, profileToken string) (string, error) {
var param m.GetStreamUri
param.StreamSetup.Transport.Protocol = "RTSP"
param.StreamSetup.Stream = "RTP-Unicast"
param.ProfileToken = xsdonvif.ReferenceToken(profileToken)
resp, err := sdkmedia.Call_GetStreamUri(ctx, dev.Device, param)
if err != nil {
return "", err
}
playURL := buildPlayURL(string(resp.MediaUri.Uri), dev.Device.GetDeviceParams().Username, dev.Device.GetDeviceParams().Password)
return playURL, nil
}
func buildPlayURL(rawurl, username, password string) string {
if username != "" && password != "" {
return strings.Replace(rawurl, "rtsp://", fmt.Sprintf("rtsp://%s:%s@", username, password), 1)
}
return rawurl
}
func (a *Adapter) Discover(ctx context.Context, w io.Writer) error {
recv, err := onvif.AllAvailableDevicesAtSpecificEthernetInterfaces()
if err != nil {
return err
}
for {
select {
case dev, ok := <-recv:
if !ok {
return nil
}
var exists bool
a.devices.Range(func(key string, value *Device) bool {
if value.GetDeviceParams().Xaddr == dev.GetDeviceParams().Xaddr {
exists = true
return false
}
return true
})
if exists {
continue
}
b, _ := json.Marshal(toDiscoverResponse(dev))
_, _ = w.Write(b)
case <-ctx.Done():
return nil
case <-time.After(3 * time.Second):
slog.DebugContext(ctx, "discover timeout")
return nil
}
}
}