Files
monibuca/plugin/onvif/device.go
T
2025-01-15 15:46:30 +08:00

565 lines
15 KiB
Go
Executable File

package plugin_onvif
import (
"encoding/json"
"encoding/xml"
"fmt"
"io"
"m7s.live/v5/pkg/util"
"strings"
donvif "github.com/IOTechSystems/onvif"
"github.com/IOTechSystems/onvif/gosoap"
"github.com/IOTechSystems/onvif/imaging"
"github.com/IOTechSystems/onvif/media"
"github.com/IOTechSystems/onvif/ptz"
"github.com/IOTechSystems/onvif/xsd"
"github.com/IOTechSystems/onvif/xsd/onvif"
onvifTypes "github.com/IOTechSystems/onvif/xsd/onvif"
"github.com/beevik/etree"
"m7s.live/v5/pkg/config"
rtsp "m7s.live/v5/plugin/rtsp/pkg"
)
// 设备状态常量
const (
StatusInitOk = iota
StatusInitError
StatusAddError
StatusProfileError
StatusGetStreamUriOk
StatusGetStreamUriError
StatusPullRtspOk
StatusPullRtspError
StatusGetImagingSetting
StatusSetImagingSetting
StatusGetPtzPreset
StatusSetPtzPreset
StatusGotoPtzPreset
StatusPtzMove
)
// PTZ移动模式
const (
PtzMoveAbs = iota
PtzMoveRelative
PtzMoveContinue
)
type DeviceStatus struct {
Device *donvif.Device
Xaddr string `json:"xaddr"` // onvif 设备地址
IP string `json:"ip"` // 设备IP
Port string `json:"port"` // 设备端口
Username string `json:"username"` // 设备用户名
Password string `json:"password"` // 设备密码
Path string `json:"path"` // onvif device_service 路径
MediaUrl string `json:"mediaUrl"` // rtsp 流
Channel int `json:"channel"` // 设备通道
Stream string `json:"stream"` // 设备流
Status int `json:"status"` // 设备状态
Description string `json:"description"` // 状态描述
Profiles []onvif.Profile
}
func (d *DeviceStatus) GetKey() string {
if d.Xaddr == "" {
d.Xaddr = d.IP + ":" + d.Port
}
return d.Xaddr
}
type AuthConfig struct {
Interfaces map[string]deviceAuth
Devices map[string]deviceAuth
}
type deviceAuth struct {
Username string
Password string
}
var authCfg = &AuthConfig{
Interfaces: make(map[string]deviceAuth),
Devices: make(map[string]deviceAuth),
}
func GenStreamPath(device *donvif.Device, ifname string) string {
streamPath := strings.ReplaceAll(device.GetDeviceParams().Xaddr, ".", "_")
streamPath = "onvif/" + util.ConvertRuneToEn(ifname) + "/" + strings.ReplaceAll(streamPath, ":", "_")
return streamPath
}
func NewDeviceStatus(ip, user, passwd, port, path string, channel int) (*DeviceStatus, int, error) {
param := donvif.DeviceParams{Xaddr: ip + ":" + port, Username: user, Password: passwd, EndpointRefAddress: path}
device, err := donvif.NewDevice(param)
if err != nil {
return nil, StatusAddError, err
}
profiles, err := GetProfiles(device)
if err != nil {
return nil, StatusProfileError, err
}
return &DeviceStatus{Device: device,
Channel: channel, Path: path, Profiles: profiles,
IP: ip, Port: port,
Username: user,
Password: passwd,
}, 0, nil
}
// MarshalJSON 实现设备状态的JSON序列化
func (d *DeviceStatus) MarshalJSON() ([]byte, error) {
type Alias DeviceStatus
return json.Marshal(&struct {
*Alias
Status int `json:"status"`
StatusText string `json:"status_text"`
Profiles int `json:"profiles,omitempty"`
StreamToken string `json:"stream_token,omitempty"`
}{
Alias: (*Alias)(d),
Status: d.Status,
StatusText: getStatusText(d.Status),
})
}
// GetImaging 获取图像设置
func (d *DeviceStatus) GetImaging() (*onvifTypes.ImagingSettings20, error) {
dev, err := donvif.NewDevice(donvif.DeviceParams{
Xaddr: fmt.Sprintf("http://%s/onvif/device_service", d.IP),
Username: d.Username,
Password: d.Password,
})
if err != nil {
return nil, err
}
if len(d.Profiles) == 0 {
return nil, fmt.Errorf("no profiles found")
}
token := onvifTypes.ReferenceToken(d.Profiles[d.Channel].Token)
result, err := dev.CallMethod(imaging.GetImagingSettings{
VideoSourceToken: token,
})
if err != nil {
return nil, err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return nil, err
}
var imagingSetting imaging.GetImagingSettingsResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&imagingSetting)
err = xml.Unmarshal(contents, responseEnvelope)
if err != nil {
return nil, err
}
return &imagingSetting.ImagingSettings, nil
}
// SetImaging 设置图像参数
func (d *DeviceStatus) SetImaging(settings *onvifTypes.ImagingSettings20, forcePersistence bool) error {
dev, err := donvif.NewDevice(donvif.DeviceParams{
Xaddr: fmt.Sprintf("http://%s/onvif/device_service", d.IP),
Username: d.Username,
Password: d.Password,
})
if err != nil {
return err
}
if len(d.Profiles) == 0 {
return fmt.Errorf("no profiles found")
}
token := onvifTypes.ReferenceToken(d.Profiles[d.Channel].Token)
result, err := dev.CallMethod(imaging.SetImagingSettings{
VideoSourceToken: token,
ImagingSettings: *settings,
ForcePersistence: xsd.Boolean(forcePersistence),
})
if err != nil {
return err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return err
}
var imagingSetting imaging.SetImagingSettingsResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&imagingSetting)
err = xml.Unmarshal(contents, responseEnvelope)
if err != nil {
return err
}
return nil
}
// GetPtzPresets 获取预置点列表
func (d *DeviceStatus) GetPtzPresets() ([]onvifTypes.PTZPreset, error) {
dev, err := donvif.NewDevice(donvif.DeviceParams{
Xaddr: fmt.Sprintf("http://%s/onvif/device_service", d.IP),
Username: d.Username,
Password: d.Password,
})
if err != nil {
return nil, err
}
if len(d.Profiles) == 0 {
return nil, fmt.Errorf("no profiles found")
}
token := onvifTypes.ReferenceToken(d.Profiles[d.Channel].Token)
result, err := dev.CallMethod(ptz.GetPresets{
ProfileToken: token,
})
if err != nil {
return nil, err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return nil, err
}
var presets ptz.GetPresetsResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&presets)
err = xml.Unmarshal(contents, responseEnvelope)
if err != nil {
return nil, err
}
return presets.Preset, nil
}
// SetPtzPreset 设置预置点
func (d *DeviceStatus) SetPtzPreset(name string, presetToken string) (*onvif.ReferenceToken, error) {
token := d.Profiles[d.Channel].Token
ptzName := xsd.String(name)
ptzPresetToken := onvif.ReferenceToken(presetToken)
result, err := d.Device.CallMethod(ptz.SetPreset{
ProfileToken: &token,
PresetToken: &ptzPresetToken,
PresetName: &ptzName,
})
if err != nil {
return nil, err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return nil, err
}
var presets ptz.SetPresetResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&presets)
err = xml.Unmarshal(contents, responseEnvelope)
if err != nil {
return nil, err
}
return &presets.PresetToken, nil
}
// PtzMove PTZ移动控制
func (d *DeviceStatus) PtzMove(mode int, move ptz.Vector, speed ptz.Speed) error {
dev, err := donvif.NewDevice(donvif.DeviceParams{
Xaddr: fmt.Sprintf("http://%s/onvif/device_service", d.IP),
Username: d.Username,
Password: d.Password,
})
if err != nil {
return err
}
if len(d.Profiles) == 0 {
return fmt.Errorf("no profiles found")
}
token := onvifTypes.ReferenceToken(d.Profiles[d.Channel].Token)
switch mode {
case PtzMoveAbs:
result, err := dev.CallMethod(ptz.AbsoluteMove{
ProfileToken: token,
Position: move,
Speed: speed,
})
if err != nil {
return err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return err
}
var moveResponse ptz.AbsoluteMoveResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&moveResponse)
return xml.Unmarshal(contents, responseEnvelope)
case PtzMoveRelative:
result, err := dev.CallMethod(ptz.RelativeMove{
ProfileToken: token,
Translation: move,
Speed: speed,
})
if err != nil {
return err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return err
}
var moveResponse ptz.RelativeMoveResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&moveResponse)
return xml.Unmarshal(contents, responseEnvelope)
case PtzMoveContinue:
result, err := dev.CallMethod(ptz.ContinuousMove{
ProfileToken: &token,
Velocity: &onvifTypes.PTZSpeed{
PanTilt: move.PanTilt,
Zoom: move.Zoom,
},
})
if err != nil {
return err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return err
}
var moveResponse ptz.ContinuousMoveResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&moveResponse)
return xml.Unmarshal(contents, responseEnvelope)
default:
return fmt.Errorf("unknown ptz move mode: %d", mode)
}
}
// GotoPtzPreset 跳转到预置点
func (d *DeviceStatus) GotoPtzPreset(presetToken string, speed *onvifTypes.PTZSpeed) error {
dev, err := donvif.NewDevice(donvif.DeviceParams{
Xaddr: fmt.Sprintf("http://%s/onvif/device_service", d.IP),
Username: d.Username,
Password: d.Password,
})
if err != nil {
return err
}
if len(d.Profiles) == 0 {
return fmt.Errorf("no profiles found")
}
token := onvifTypes.ReferenceToken(d.Profiles[d.Channel].Token)
ptzPresetToken := onvifTypes.ReferenceToken(presetToken)
result, err := dev.CallMethod(ptz.GotoPreset{
ProfileToken: &token,
PresetToken: &ptzPresetToken,
Speed: speed,
})
if err != nil {
return err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return err
}
var presets ptz.GotoPresetResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&presets)
err = xml.Unmarshal(contents, responseEnvelope)
if err != nil {
return err
}
return nil
}
func (d *DeviceStatus) PullStream(ifname string, channel int) error {
// 生成流路径
streamPath := GenStreamPath(d.Device, ifname)
var rtspUrl string
var err error
if d.MediaUrl != "" {
rtspUrl = d.MediaUrl
} else {
// 获取 RTSP 流地址
rtspUrl, err = GetStreamUri(d.Device, d.Profiles[channel].Token)
if err != nil {
d.Status = StatusGetStreamUriError
return fmt.Errorf("get stream uri error: %v", err)
}
d.Status = StatusGetStreamUriOk
}
pullConf := config.Pull{
URL: rtspUrl,
}
// pubConf := config.Publish{}
puller := rtsp.NewPuller(pullConf)
puller.GetPullJob().Init(puller, &deviceList.plugin.Plugin, streamPath, pullConf, nil)
d.Status = StatusPullRtspOk
d.Stream = streamPath
return nil
}
func (d *DeviceStatus) GetPtzPreset() ([]onvif.PTZPreset, error) {
token := d.Profiles[d.Channel].Token
result, err := d.Device.CallMethod(ptz.GetPresets{ProfileToken: token})
if err != nil {
return nil, err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return nil, err
}
var presets ptz.GetPresetsResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&presets)
err = xml.Unmarshal(contents, responseEnvelope)
if err != nil {
return nil, err
}
return presets.Preset, nil
}
func (d *DeviceStatus) RemovePtzPreset(presetToken string) error {
ptzPresetToken := onvif.ReferenceToken(presetToken)
token := d.Profiles[d.Channel].Token
result, err := d.Device.CallMethod(ptz.RemovePreset{
ProfileToken: token,
PresetToken: ptzPresetToken,
})
if err != nil {
return err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return err
}
var presets ptz.RemovePresetResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&presets)
err = xml.Unmarshal(contents, responseEnvelope)
if err != nil {
return err
}
return nil
}
func (d *DeviceStatus) PtzContinueMove(vel *onvif.PTZSpeed, tmout *xsd.Duration) error {
token := d.Profiles[d.Channel].Token
result, err := d.Device.CallMethod(ptz.ContinuousMove{
ProfileToken: &token,
Velocity: vel,
Timeout: tmout,
})
if err != nil {
return err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return err
}
var resp ptz.ContinuousMoveResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&resp)
err = xml.Unmarshal(contents, responseEnvelope)
if err != nil {
return err
}
return nil
}
func getStatusText(status int) string {
switch status {
case StatusInitOk:
return "初始化成功"
case StatusInitError:
return "初始化失败"
case StatusAddError:
return "添加设备失败"
case StatusProfileError:
return "获取配置文件失败"
case StatusGetStreamUriOk:
return "获取流地址成功"
case StatusGetStreamUriError:
return "获取流地址失败"
case StatusPullRtspOk:
return "拉流成功"
case StatusPullRtspError:
return "拉流失败"
case StatusGetImagingSetting:
return "获取图像设置"
case StatusSetImagingSetting:
return "设置图像参数"
case StatusGetPtzPreset:
return "获取预置点"
case StatusSetPtzPreset:
return "设置预置点"
case StatusGotoPtzPreset:
return "跳转预置点"
case StatusPtzMove:
return "云台控制"
default:
return "未知状态"
}
}
func GetStreamUri(dev *donvif.Device, profileToken onvifTypes.ReferenceToken) (string, error) {
response, err := dev.CallMethod(media.GetStreamUri{ProfileToken: &profileToken})
if err != nil {
return "", err
}
resp, err := io.ReadAll(response.Body)
if err != nil {
return "", err
}
doc := etree.NewDocument()
if err := doc.ReadFromBytes(resp); err != nil {
return "", fmt.Errorf("error:%s", err.Error())
}
endpoints := doc.Root().FindElements("./Body/GetStreamUriResponse/MediaUri/Uri")
if len(endpoints) == 0 {
return "", fmt.Errorf("error:%s", "no media uri")
}
mediaUri := endpoints[0].Text()
if !strings.Contains(mediaUri, "rtsp") {
fmt.Println("mediaUri:", mediaUri)
return "", fmt.Errorf("error:%s", "media uri is not rtsp")
}
if !strings.Contains(mediaUri, "@") && dev.GetDeviceParams().Username != "" {
//如果返回的rtsp里没有账号密码,则自己拼接
mediaUri = strings.Replace(mediaUri, "//", fmt.Sprintf("//%s:%s@", dev.GetDeviceParams().Username, dev.GetDeviceParams().Password), 1)
}
if strings.Contains(mediaUri, "udp") {
mediaUri = strings.Replace(mediaUri, "udp", "rtsp", 1)
}
return mediaUri, nil
}
// 获取设备的账号密码
func getDeviceAuth(interfaceName string, ip string, config *AuthConfig) deviceAuth {
var auth deviceAuth
if a, ok := config.Interfaces[interfaceName]; ok {
auth = a
}
if a, ok := config.Devices[ip]; ok {
auth = a
}
return auth
}
func GetProfiles(dev *donvif.Device) ([]onvifTypes.Profile, error) {
result, err := dev.CallMethod(media.GetProfiles{})
if err != nil {
return nil, err
}
contents, err := io.ReadAll(result.Body)
if err != nil {
return nil, err
}
var profiles media.GetProfilesResponse
responseEnvelope := gosoap.NewSOAPEnvelope(&profiles)
err = xml.Unmarshal(contents, responseEnvelope)
if err != nil {
return nil, err
}
return profiles.Profiles, nil
}
func getAuth(iface, ip string) *deviceAuth {
if auth, ok := authCfg.Devices[ip]; ok {
return &auth
}
if auth, ok := authCfg.Interfaces[iface]; ok {
return &auth
}
return nil
}