mirror of
https://github.com/langhuihui/monibuca.git
synced 2025-12-24 13:48:04 +08:00
565 lines
15 KiB
Go
Executable File
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
|
||
}
|