mirror of
https://github.com/langhuihui/monibuca.git
synced 2025-10-24 19:30:22 +08:00
842 lines
26 KiB
Go
842 lines
26 KiB
Go
package plugin_gb28181pro
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"m7s.live/v5"
|
||
|
||
"github.com/emiago/sipgo"
|
||
"github.com/emiago/sipgo/sip"
|
||
"github.com/icholy/digest"
|
||
"m7s.live/v5/pkg/task"
|
||
gb28181 "m7s.live/v5/plugin/gb28181pro/pkg"
|
||
)
|
||
|
||
// Platform 表示GB28181平台的运行时实例
|
||
type Platform struct {
|
||
task.Job `gorm:"-:all"` // 使用TickTask,并且排除 gorm 序列化
|
||
PlatformModel *gb28181.PlatformModel
|
||
|
||
// SIP相关字段,不存储到数据库
|
||
Client *sipgo.Client `gorm:"-" json:"-"` // SIP客户端
|
||
DialogClient *sipgo.DialogClient `gorm:"-" json:"-"` // SIP对话客户端
|
||
Recipient sip.Uri `gorm:"-" json:"-"` // 接收者地址
|
||
ContactHDR *sip.ContactHeader `gorm:"-" json:"-"` // 联系人头部
|
||
UserAgentHDR sip.Header `gorm:"-" json:"-"`
|
||
MaxForwardsHDR sip.MaxForwardsHeader `gorm:"-" json:"-"`
|
||
|
||
// 运行时字段
|
||
KeepAliveReply int `gorm:"-" json:"keepAliveReply"` // KeepAliveReply表示心跳未回复次数
|
||
CallID string `gorm:"-" json:"callId"` // CallID表示SIP会话的标识符
|
||
SN int
|
||
|
||
eventChan chan any
|
||
// 插件配置
|
||
plugin *GB28181ProPlugin
|
||
ctx context.Context
|
||
}
|
||
|
||
func (p *Platform) init() {
|
||
p.ctx = context.Background()
|
||
client, err := sipgo.NewClient(p.plugin.ua, sipgo.WithClientHostname(p.PlatformModel.DeviceIP), sipgo.WithClientPort(p.PlatformModel.DevicePort))
|
||
if err != nil {
|
||
p.Error("failed to create sip client: %v", err)
|
||
}
|
||
p.Client = client
|
||
userAgentHeader := sip.NewHeader("User-Agent", "M7S/"+m7s.Version)
|
||
p.UserAgentHDR = userAgentHeader
|
||
|
||
// 创建注册请求的目标URI,使用上级平台的信息
|
||
recipient := sip.Uri{
|
||
User: p.PlatformModel.ServerGBID,
|
||
Host: p.PlatformModel.ServerIP,
|
||
Port: p.PlatformModel.ServerPort,
|
||
}
|
||
|
||
p.Recipient = recipient
|
||
|
||
// 设置联系人头部,使用本地平台的信息
|
||
contactHdr := sip.ContactHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.DeviceGBID,
|
||
Host: p.PlatformModel.DeviceIP,
|
||
Port: p.PlatformModel.DevicePort,
|
||
},
|
||
}
|
||
p.ContactHDR = &contactHdr
|
||
|
||
// 创建对话客户端
|
||
p.DialogClient = sipgo.NewDialogClient(p.Client, *p.ContactHDR)
|
||
|
||
p.MaxForwardsHDR = sip.MaxForwardsHeader(70)
|
||
p.plugin.platforms.Add(p)
|
||
}
|
||
|
||
func (p *Platform) Start() error {
|
||
|
||
if _, ok := p.plugin.platforms.Get(p.ID); !ok {
|
||
p.init()
|
||
}
|
||
register := NewRegister(p, "firstRegister")
|
||
register.OnStart(func() {
|
||
register.Tick(nil)
|
||
})
|
||
p.AddTask(register)
|
||
return nil
|
||
}
|
||
|
||
// getResponse 从事务中获取响应
|
||
func (p *Platform) getResponse(tx sip.ClientTransaction) (*sip.Response, error) {
|
||
select {
|
||
case <-tx.Done():
|
||
return nil, fmt.Errorf("事务已终止")
|
||
case res := <-tx.Responses():
|
||
return res, nil
|
||
}
|
||
}
|
||
|
||
// Keepalive 发送心跳请求到上级平台
|
||
func (p *Platform) Keepalive(ctx context.Context) (*sipgo.DialogClientSession, error) {
|
||
|
||
req := sip.NewRequest("MESSAGE", p.Recipient)
|
||
req.SetTransport(strings.ToUpper(p.PlatformModel.Transport))
|
||
customCallID := fmt.Sprintf("%s-%d@%s", p.PlatformModel.DeviceGBID, time.Now().Unix(), p.PlatformModel.ServerIP)
|
||
callID := sip.CallIDHeader(customCallID)
|
||
req.AppendHeader(&callID)
|
||
|
||
csqHeader := sip.CSeqHeader{
|
||
SeqNo: uint32(p.SN),
|
||
MethodName: "REGISTER",
|
||
}
|
||
p.SN++
|
||
req.AppendHeader(&csqHeader)
|
||
|
||
// 添加From头部
|
||
fromHeader := sip.FromHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.DeviceGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
Params: sip.NewParams(),
|
||
}
|
||
fromHeader.Params.Add("tag", sip.GenerateTagN(16))
|
||
req.AppendHeader(&fromHeader)
|
||
|
||
// 添加To头部
|
||
toHeader := sip.ToHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.ServerGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
}
|
||
req.AppendHeader(&toHeader)
|
||
|
||
viaHeader := sip.ViaHeader{
|
||
ProtocolName: "SIP",
|
||
ProtocolVersion: "2.0",
|
||
Transport: p.PlatformModel.Transport,
|
||
Host: p.PlatformModel.DeviceIP,
|
||
Port: p.PlatformModel.DevicePort,
|
||
Params: sip.NewParams(),
|
||
}
|
||
viaHeader.Params.Add("branch", sip.GenerateBranchN(16)).Add("rport", "")
|
||
req.AppendHeader(&viaHeader)
|
||
|
||
req.SetBody(gb28181.BuildKeepAliveXML(p.SN, p.PlatformModel.DeviceGBID))
|
||
p.SN++
|
||
tx, err := p.Client.TransactionRequest(ctx, req)
|
||
if err != nil {
|
||
p.Error("keepalive", "error", err.Error())
|
||
return nil, fmt.Errorf("创建事务失败: %v", err)
|
||
}
|
||
defer tx.Terminate()
|
||
|
||
res, err := p.getResponse(tx)
|
||
if err != nil {
|
||
p.Error("keepalive", "error", err.Error())
|
||
return nil, err
|
||
}
|
||
|
||
if res.StatusCode != 200 {
|
||
p.Error("keepalive", "status", res.StatusCode)
|
||
return nil, fmt.Errorf("心跳失败,状态码: %d", res.StatusCode)
|
||
}
|
||
|
||
p.Info("keepalive", "response", res.String())
|
||
return nil, nil
|
||
}
|
||
|
||
// Unregister 发送注销请求到上级平台
|
||
func (p *Platform) Unregister(ctx context.Context) (*sipgo.DialogClientSession, error) {
|
||
// 创建注销请求的目标URI
|
||
recipient := sip.Uri{
|
||
User: p.PlatformModel.ServerGBID,
|
||
Host: p.PlatformModel.ServerIP,
|
||
Port: p.PlatformModel.ServerPort,
|
||
}
|
||
|
||
// 创建基本的REGISTER请求
|
||
req := sip.NewRequest(sip.REGISTER, recipient)
|
||
|
||
// 添加Contact头部
|
||
contactStr := fmt.Sprintf("<sip:%s@%s:%d>", p.PlatformModel.DeviceGBID, p.PlatformModel.DeviceIP, p.PlatformModel.DevicePort)
|
||
req.AppendHeader(sip.NewHeader("Contact", contactStr))
|
||
|
||
// 添加From头部
|
||
fromHeader := sip.FromHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.DeviceGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
Params: sip.NewParams(),
|
||
}
|
||
fromHeader.Params.Add("tag", sip.GenerateTagN(16))
|
||
req.AppendHeader(&fromHeader)
|
||
|
||
// 添加To头部
|
||
toHeader := sip.ToHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.DeviceGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
}
|
||
req.AppendHeader(&toHeader)
|
||
|
||
// 添加Expires头部,设置为0表示注销
|
||
req.AppendHeader(sip.NewHeader("Expires", "0"))
|
||
|
||
// 设置传输协议
|
||
req.SetTransport(strings.ToUpper(p.PlatformModel.Transport))
|
||
|
||
// 发送请求并获取响应
|
||
tx, err := p.Client.TransactionRequest(ctx, req)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("创建事务失败: %v", err)
|
||
}
|
||
defer tx.Terminate()
|
||
|
||
// 获取响应
|
||
res, err := p.getResponse(tx)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("获取响应失败: %v", err)
|
||
}
|
||
|
||
// 检查响应状态
|
||
if res.StatusCode != 200 {
|
||
return nil, fmt.Errorf("注销失败,状态码: %d", res.StatusCode)
|
||
}
|
||
|
||
return nil, nil
|
||
}
|
||
|
||
// PlatformKeepAliveTask 任务
|
||
type PlatformKeepAliveTask struct {
|
||
task.TickTask
|
||
platform *Platform
|
||
}
|
||
|
||
func (k *PlatformKeepAliveTask) GetTickInterval() time.Duration {
|
||
return time.Second * time.Duration(k.platform.PlatformModel.KeepTimeout)
|
||
}
|
||
|
||
func (k *PlatformKeepAliveTask) Tick(any) {
|
||
if !k.platform.PlatformModel.Enable {
|
||
return
|
||
}
|
||
|
||
ctx := context.Background()
|
||
_, err := k.platform.Keepalive(ctx)
|
||
if err != nil {
|
||
k.platform.KeepAliveReply++
|
||
k.Error("keepalive", "error", err.Error())
|
||
if k.platform.KeepAliveReply >= 3 {
|
||
k.platform.PlatformModel.Status = false
|
||
k.Stop(fmt.Errorf("max keepalive retries reached"))
|
||
// 重新启动注册任务
|
||
//k.platform.Start()
|
||
}
|
||
} else {
|
||
k.platform.KeepAliveReply = 0
|
||
}
|
||
}
|
||
|
||
// OnMessage 处理来自平台的消息
|
||
func (p *Platform) OnMessage(req *sip.Request, tx sip.ServerTransaction, msg *gb28181.Message) error {
|
||
// 更新平台状态
|
||
p.PlatformModel.UpdateTime = time.Now().Format("2006-01-02 15:04:05")
|
||
|
||
// 根据消息类型处理不同的消息
|
||
switch msg.CmdType {
|
||
case "Catalog":
|
||
// 处理目录请求
|
||
return p.handleCatalog(req, tx, msg)
|
||
case "DeviceControl":
|
||
// 处理设备控制请求
|
||
return p.handleDeviceControl(req, tx, msg)
|
||
case "DeviceInfo":
|
||
// 处理设备信息请求
|
||
return p.handleDeviceInfo(req, tx, msg)
|
||
case "Alarm":
|
||
// 处理报警消息
|
||
return p.handleAlarm(req, tx, msg)
|
||
case "MobilePosition":
|
||
// 处理移动位置信息
|
||
return p.handleMobilePosition(req, tx, msg)
|
||
default:
|
||
// 不支持的消息类型,返回错误
|
||
response := sip.NewResponseFromRequest(req, sip.StatusUnsupportedMediaType, "Unsupported message type", nil)
|
||
if err := tx.Respond(response); err != nil {
|
||
return fmt.Errorf("respond error: %v", err)
|
||
}
|
||
return fmt.Errorf("unsupported message type: %s", msg.CmdType)
|
||
}
|
||
}
|
||
|
||
// handleCatalog 处理目录请求
|
||
func (p *Platform) handleCatalog(req *sip.Request, tx sip.ServerTransaction, msg *gb28181.Message) error {
|
||
// 回复 200 OK
|
||
err := tx.Respond(sip.NewResponseFromRequest(req, http.StatusOK, "OK", nil))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 获取 SN 和 FromTag
|
||
sn := strconv.Itoa(msg.SN)
|
||
fromTag, _ := req.From().Params.Get("tag")
|
||
p.plugin.Info("catalog", "sn", sn, "fromTag", fromTag)
|
||
|
||
// 打印平台ID
|
||
p.plugin.Info("catalog query platform_id", "platform_id", p.PlatformModel.ID)
|
||
|
||
// 查询通道列表
|
||
var channels []gb28181.CommonGBChannel
|
||
if p.plugin.DB != nil {
|
||
if err := p.plugin.DB.Table("channel_gb28181pro c").
|
||
Select(`c.id as gb_id,
|
||
c.device_db_id as gb_device_db_id,
|
||
c.stream_push_id,
|
||
c.stream_proxy_id,
|
||
c.create_time,
|
||
c.update_time,
|
||
COALESCE(nullif(pc.custom_device_id,''), nullif(c.gb_device_id,''), nullif(c.device_id,'')) as gb_device_id,
|
||
COALESCE(nullif(pc.custom_name,''), nullif(c.gb_name,''), nullif(c.name,'')) as gb_name,
|
||
COALESCE(nullif(pc.custom_manufacturer,''), nullif(c.gb_manufacturer,''), nullif(c.manufacturer,'')) as gb_manufacturer,
|
||
COALESCE(nullif(pc.custom_model,''), nullif(c.gb_model,''), nullif(c.model,'')) as gb_model,
|
||
COALESCE(nullif(pc.custom_owner,''), nullif(c.gb_owner,''), nullif(c.owner,'')) as gb_owner,
|
||
COALESCE(nullif(pc.custom_civil_code,''), nullif(c.gb_civil_code,''), nullif(c.civil_code,'')) as gb_civil_code,
|
||
COALESCE(nullif(pc.custom_block,''), nullif(c.gb_block,''), nullif(c.block,'')) as gb_block,
|
||
COALESCE(nullif(pc.custom_address,''), nullif(c.gb_address,''), nullif(c.address,'')) as gb_address,
|
||
COALESCE(nullif(pc.custom_parental,''), nullif(c.gb_parental,''), nullif(c.parental,'')) as gb_parental,
|
||
COALESCE(nullif(pc.custom_parent_id,''), nullif(c.gb_parent_id,''), nullif(c.parent_id,'')) as gb_parent_id,
|
||
COALESCE(nullif(pc.custom_safety_way,''), nullif(c.gb_safety_way,''), nullif(c.safety_way,'')) as gb_safety_way,
|
||
COALESCE(nullif(pc.custom_register_way,''), nullif(c.gb_register_way,''), nullif(c.register_way,'')) as gb_register_way,
|
||
COALESCE(nullif(pc.custom_cert_num,''), nullif(c.gb_cert_num,''), nullif(c.cert_num,'')) as gb_cert_num,
|
||
COALESCE(nullif(pc.custom_certifiable,''), nullif(c.gb_certifiable,''), nullif(c.certifiable,'')) as gb_certifiable,
|
||
COALESCE(nullif(pc.custom_err_code,''), nullif(c.gb_err_code,''), nullif(c.err_code,'')) as gb_err_code,
|
||
COALESCE(nullif(pc.custom_end_time,''), nullif(c.gb_end_time,''), nullif(c.end_time,'')) as gb_end_time,
|
||
COALESCE(nullif(pc.custom_secrecy,''), nullif(c.gb_secrecy,''), nullif(c.secrecy,'')) as gb_secrecy,
|
||
COALESCE(nullif(pc.custom_ip_address,''), nullif(c.gb_ip_address,''), nullif(c.ip_address,'')) as gb_ip_address,
|
||
COALESCE(nullif(pc.custom_port,''), nullif(c.gb_port,''), nullif(c.port,'')) as gb_port,
|
||
COALESCE(nullif(pc.custom_password,''), nullif(c.gb_password,''), nullif(c.password,'')) as gb_password,
|
||
COALESCE(nullif(pc.custom_status,''), nullif(c.gb_status,''), nullif(c.status,'')) as gb_status,
|
||
COALESCE(nullif(pc.custom_longitude,''), nullif(c.gb_longitude,''), nullif(c.longitude,'')) as gb_longitude,
|
||
COALESCE(nullif(pc.custom_latitude,''), nullif(c.gb_latitude,''), nullif(c.latitude,'')) as gb_latitude,
|
||
COALESCE(nullif(pc.custom_ptz_type,''), nullif(c.gb_ptz_type,''), nullif(c.ptz_type,'')) as gb_ptz_type,
|
||
COALESCE(nullif(pc.custom_position_type,''), nullif(c.gb_position_type,''), nullif(c.position_type,'')) as gb_position_type,
|
||
COALESCE(nullif(pc.custom_room_type,''), nullif(c.gb_room_type,''), nullif(c.room_type,'')) as gb_room_type,
|
||
COALESCE(nullif(pc.custom_use_type,''), nullif(c.gb_use_type,''), nullif(c.use_type,'')) as gb_use_type,
|
||
COALESCE(nullif(pc.custom_supply_light_type,''), nullif(c.gb_supply_light_type,''), nullif(c.supply_light_type,'')) as gb_supply_light_type,
|
||
COALESCE(nullif(pc.custom_direction_type,''), nullif(c.gb_direction_type,''), nullif(c.direction_type,'')) as gb_direction_type,
|
||
COALESCE(nullif(pc.custom_resolution,''), nullif(c.gb_resolution,''), nullif(c.resolution,'')) as gb_resolution,
|
||
COALESCE(nullif(pc.custom_business_group_id,''), nullif(c.gb_business_group_id,''), nullif(c.business_group_id,'')) as gb_business_group_id,
|
||
COALESCE(nullif(pc.custom_download_speed,''), nullif(c.gb_download_speed,''), nullif(c.download_speed,'')) as gb_download_speed,
|
||
COALESCE(nullif(pc.custom_svc_space_support_mod,''), nullif(c.gb_svc_space_support_mod,''), nullif(c.svc_space_support_mod,'')) as gb_svc_space_support_mod,
|
||
COALESCE(nullif(pc.custom_svc_time_support_mode,''), nullif(c.gb_svc_time_support_mode,''), nullif(c.svc_time_support_mode,'')) as gb_svc_time_support_mode`).
|
||
Joins("left join platform_channel_gb28181pro pc on c.id = pc.device_channel_id").
|
||
Where("pc.platform_id = ?", p.PlatformModel.ID).
|
||
Find(&channels).Error; err != nil {
|
||
return fmt.Errorf("query channels error: %v", err)
|
||
}
|
||
}
|
||
|
||
// 发送目录响应,无论是否有通道
|
||
p.plugin.Info("get channels success", channels)
|
||
return p.sendCatalogResponse(req, sn, fromTag, channels)
|
||
}
|
||
|
||
// CreateRequest 创建 SIP 请求
|
||
func (p *Platform) CreateRequest(method string) *sip.Request {
|
||
request := sip.NewRequest(sip.RequestMethod(method), p.Recipient)
|
||
//request.SetDestination(p.Recipient.String())
|
||
return request
|
||
}
|
||
|
||
// sendCatalogResponse 发送目录响应
|
||
func (p *Platform) sendCatalogResponse(req *sip.Request, sn string, fromTag string, channels []gb28181.CommonGBChannel) error {
|
||
// 如果没有通道,发送一个空的目录列表
|
||
if len(channels) == 0 {
|
||
request := p.CreateRequest("MESSAGE")
|
||
|
||
// 设置From头部
|
||
fromHeader := sip.FromHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.DeviceGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
Params: sip.NewParams(),
|
||
}
|
||
fromHeader.Params.Add("tag", fromTag)
|
||
request.AppendHeader(&fromHeader)
|
||
|
||
// 添加To头部
|
||
toHeader := sip.ToHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.ServerGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
}
|
||
request.AppendHeader(&toHeader)
|
||
|
||
// 添加Via头部
|
||
viaHeader := sip.ViaHeader{
|
||
ProtocolName: "SIP",
|
||
ProtocolVersion: "2.0",
|
||
Transport: p.PlatformModel.Transport,
|
||
Host: p.PlatformModel.DeviceIP,
|
||
Port: p.PlatformModel.DevicePort,
|
||
Params: sip.NewParams(),
|
||
}
|
||
viaHeader.Params.Add("branch", sip.GenerateBranchN(16)).Add("rport", "")
|
||
request.AppendHeader(&viaHeader)
|
||
|
||
request.SetTransport(req.Transport())
|
||
contentTypeHeader := sip.ContentTypeHeader("Application/MANSCDP+xml")
|
||
request.AppendHeader(&contentTypeHeader)
|
||
|
||
// 空目录列表XML
|
||
xmlContent := fmt.Sprintf(`<?xml version="1.0" encoding="GB2312"?>
|
||
<Response>
|
||
<CmdType>Catalog</CmdType>
|
||
<SN>%s</SN>
|
||
<DeviceID>%s</DeviceID>
|
||
<SumNum>0</SumNum>
|
||
<DeviceList Num="0">
|
||
</DeviceList>
|
||
</Response>`, sn, p.PlatformModel.DeviceGBID)
|
||
request.SetBody([]byte(xmlContent))
|
||
_, err := p.Client.Do(p.ctx, request)
|
||
if err != nil {
|
||
p.plugin.Error("p.Client.Do", err)
|
||
}
|
||
return err
|
||
}
|
||
|
||
// 有通道时,为每个通道单独发送一个XML
|
||
for i, channel := range channels {
|
||
request := p.CreateRequest("MESSAGE")
|
||
|
||
// 设置From头部
|
||
fromHeader := sip.FromHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.DeviceGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
Params: sip.NewParams(),
|
||
}
|
||
fromHeader.Params.Add("tag", fromTag)
|
||
request.AppendHeader(&fromHeader)
|
||
|
||
// 添加To头部
|
||
toHeader := sip.ToHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.ServerGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
}
|
||
request.AppendHeader(&toHeader)
|
||
|
||
// 添加Via头部
|
||
viaHeader := sip.ViaHeader{
|
||
ProtocolName: "SIP",
|
||
ProtocolVersion: "2.0",
|
||
Transport: p.PlatformModel.Transport,
|
||
Host: p.PlatformModel.DeviceIP,
|
||
Port: p.PlatformModel.DevicePort,
|
||
Params: sip.NewParams(),
|
||
}
|
||
viaHeader.Params.Add("branch", sip.GenerateBranchN(16)).Add("rport", "")
|
||
request.AppendHeader(&viaHeader)
|
||
|
||
request.SetTransport(req.Transport())
|
||
contentTypeHeader := sip.ContentTypeHeader("Application/MANSCDP+xml")
|
||
request.AppendHeader(&contentTypeHeader)
|
||
|
||
// 为单个通道创建XML
|
||
channelXML := p.buildChannelItem(channel)
|
||
xmlContent := fmt.Sprintf(`<?xml version="1.0" encoding="GB2312"?>
|
||
<Response>
|
||
<CmdType>Catalog</CmdType>
|
||
<SN>%s</SN>
|
||
<DeviceID>%s</DeviceID>
|
||
<SumNum>%d</SumNum>
|
||
<DeviceList Num="1">
|
||
%s
|
||
</DeviceList>
|
||
</Response>`, sn, p.PlatformModel.DeviceGBID, len(channels), channelXML)
|
||
|
||
request.SetBody([]byte(xmlContent))
|
||
_, err := p.Client.Do(p.ctx, request)
|
||
if err != nil {
|
||
p.Error("send catalog response", "error", err.Error(), "channel_index", i)
|
||
return err
|
||
}
|
||
|
||
// 添加短暂延迟以防止发送过快
|
||
time.Sleep(time.Millisecond * 50)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// buildChannelItem 构建单个通道的XML项
|
||
func (p *Platform) buildChannelItem(channel gb28181.CommonGBChannel) string {
|
||
// 确保字符串字段不为空
|
||
deviceID := channel.GbDeviceID
|
||
if deviceID == "" {
|
||
deviceID = "unknown_device" // 如果没有设备ID,使用默认值
|
||
}
|
||
name := channel.GbName
|
||
if name == "" {
|
||
name = "未命名设备"
|
||
}
|
||
manufacturer := channel.GbManufacturer
|
||
if manufacturer == "" {
|
||
manufacturer = "未知厂商"
|
||
}
|
||
model := channel.GbModel
|
||
if model == "" {
|
||
model = "未知型号"
|
||
}
|
||
owner := channel.GbOwner
|
||
if owner == "" {
|
||
owner = "未知所有者"
|
||
}
|
||
address := channel.GbAddress
|
||
if address == "" {
|
||
address = "未知地址"
|
||
}
|
||
parentID := channel.GbParentID
|
||
if parentID == "" {
|
||
parentID = p.PlatformModel.DeviceGBID // 使用平台ID作为父ID
|
||
}
|
||
|
||
return fmt.Sprintf(`<Item>
|
||
<DeviceID>%s</DeviceID>
|
||
<Name>%s</Name>
|
||
<Manufacturer>%s</Manufacturer>
|
||
<Model>%s</Model>
|
||
<Owner>%s</Owner>
|
||
<Address>%s</Address>
|
||
<RegisterWay>%d</RegisterWay>
|
||
<Secrecy>%d</Secrecy>
|
||
<ParentID>%s</ParentID>
|
||
<Parental>%d</Parental>
|
||
<SafetyWay>%d</SafetyWay>
|
||
<Status>ON</Status>
|
||
<Info>
|
||
</Info>
|
||
</Item>`, deviceID, name, manufacturer, model,
|
||
owner, address,
|
||
channel.GbRegisterWay, // 直接使用整数值
|
||
channel.GbSecrecy, // 直接使用整数值
|
||
parentID,
|
||
channel.GbParental, // 直接使用整数值
|
||
channel.GbSafetyWay) // 直接使用整数值
|
||
}
|
||
|
||
// handleDeviceControl 处理设备控制请求
|
||
func (p *Platform) handleDeviceControl(req *sip.Request, tx sip.ServerTransaction, msg *gb28181.Message) error {
|
||
// TODO: 实现设备控制请求处理
|
||
response := sip.NewResponseFromRequest(req, sip.StatusOK, "OK", nil)
|
||
return tx.Respond(response)
|
||
}
|
||
|
||
// handleDeviceInfo 处理设备信息查询请求
|
||
func (p *Platform) handleDeviceInfo(req *sip.Request, tx sip.ServerTransaction, msg *gb28181.Message) error {
|
||
// 先回复200 OK
|
||
err := tx.Respond(sip.NewResponseFromRequest(req, http.StatusOK, "OK", nil))
|
||
if err != nil {
|
||
return fmt.Errorf("respond error: %v", err)
|
||
}
|
||
|
||
// 获取 SN 和 FromTag
|
||
sn := strconv.Itoa(msg.SN)
|
||
fromTag, _ := req.From().Params.Get("tag")
|
||
|
||
// 获取请求的设备ID
|
||
channelId := msg.DeviceID
|
||
|
||
// 1. 判断是否是查询平台自身信息
|
||
if p.PlatformModel.DeviceGBID == channelId {
|
||
// 如果是查询平台信息,直接返回平台信息
|
||
return p.sendDeviceInfoResponse(req, nil, sn, fromTag)
|
||
}
|
||
|
||
// 2. 查询通道信息
|
||
var channel gb28181.CommonGBChannel
|
||
if p.plugin.DB != nil {
|
||
if err := p.plugin.DB.Where("platform_id = ? AND gb_device_id = ?", p.PlatformModel.ID, channelId).First(&channel).Error; err != nil {
|
||
// 通道不存在,返回404
|
||
response := sip.NewResponseFromRequest(req, sip.StatusNotFound, "channel not found or offline", nil)
|
||
return tx.Respond(response)
|
||
}
|
||
}
|
||
|
||
// 3. 判断通道类型
|
||
if channel.GbDeviceDbID == 0 {
|
||
// 非国标通道不支持设备信息查询
|
||
response := sip.NewResponseFromRequest(req, sip.StatusForbidden, "non-gb channel not supported", nil)
|
||
return tx.Respond(response)
|
||
}
|
||
|
||
// 4. 查询设备信息
|
||
var device Device
|
||
if p.plugin.DB != nil {
|
||
if err := p.plugin.DB.First(&device, channel.GbDeviceDbID).Error; err != nil {
|
||
// 设备不存在,返回404
|
||
response := sip.NewResponseFromRequest(req, sip.StatusNotFound, "device not found", nil)
|
||
return tx.Respond(response)
|
||
}
|
||
}
|
||
|
||
// 5. 发送设备信息响应
|
||
return p.sendDeviceInfoResponse(req, &device, sn, fromTag)
|
||
}
|
||
|
||
// sendDeviceInfoResponse 发送设备信息响应
|
||
func (p *Platform) sendDeviceInfoResponse(req *sip.Request, device *Device, sn string, fromTag string) error {
|
||
request := p.CreateRequest("MESSAGE")
|
||
// 设置From头部
|
||
fromHeader := sip.FromHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.DeviceGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
Params: sip.NewParams(),
|
||
}
|
||
fromHeader.Params.Add("tag", fromTag)
|
||
request.AppendHeader(&fromHeader)
|
||
// 添加To头部
|
||
toHeader := sip.ToHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.ServerGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
}
|
||
req.AppendHeader(&toHeader)
|
||
// 添加Via头部
|
||
viaHeader := sip.ViaHeader{
|
||
ProtocolName: "SIP",
|
||
ProtocolVersion: "2.0",
|
||
Transport: p.PlatformModel.Transport,
|
||
Host: p.PlatformModel.DeviceIP,
|
||
Port: p.PlatformModel.DevicePort,
|
||
Params: sip.NewParams(),
|
||
}
|
||
viaHeader.Params.Add("branch", sip.GenerateBranchN(16)).Add("rport", "")
|
||
req.AppendHeader(&viaHeader)
|
||
contentTypeHeader := sip.ContentTypeHeader("Application/MANSCDP+xml")
|
||
request.AppendHeader(&contentTypeHeader)
|
||
|
||
// 构建响应XML
|
||
var xmlContent string
|
||
if device == nil {
|
||
// 返回平台信息
|
||
xmlContent = fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
||
<Response>
|
||
<CmdType>DeviceInfo</CmdType>
|
||
<SN>%s</SN>
|
||
<DeviceID>%s</DeviceID>
|
||
<Result>OK</Result>
|
||
<Manufacturer>%s</Manufacturer>
|
||
<Model>%s</Model>
|
||
<Firmware>%s</Firmware>
|
||
<Channel>%d</Channel>
|
||
</Response>`, sn, p.PlatformModel.DeviceGBID, p.PlatformModel.Manufacturer, p.PlatformModel.Model, "", p.PlatformModel.ChannelCount)
|
||
} else {
|
||
// 返回设备信息
|
||
xmlContent = fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
||
<Response>
|
||
<CmdType>DeviceInfo</CmdType>
|
||
<SN>%s</SN>
|
||
<DeviceID>%s</DeviceID>
|
||
<Result>OK</Result>
|
||
<Manufacturer>%s</Manufacturer>
|
||
<Model>%s</Model>
|
||
<Firmware>%s</Firmware>
|
||
<Channel>%d</Channel>
|
||
</Response>`, sn, device.DeviceID, device.Manufacturer, device.Model, device.Firmware, device.ChannelCount)
|
||
}
|
||
|
||
request.SetBody([]byte(xmlContent))
|
||
_, err := p.Client.Do(p, request)
|
||
return err
|
||
}
|
||
|
||
// handleAlarm 处理报警消息
|
||
func (p *Platform) handleAlarm(req *sip.Request, tx sip.ServerTransaction, msg *gb28181.Message) error {
|
||
// TODO: 实现报警消息处理
|
||
response := sip.NewResponseFromRequest(req, sip.StatusOK, "OK", nil)
|
||
return tx.Respond(response)
|
||
}
|
||
|
||
// handleMobilePosition 处理移动位置信息
|
||
func (p *Platform) handleMobilePosition(req *sip.Request, tx sip.ServerTransaction, msg *gb28181.Message) error {
|
||
// TODO: 实现移动位置信息处理
|
||
response := sip.NewResponseFromRequest(req, sip.StatusOK, "OK", nil)
|
||
return tx.Respond(response)
|
||
}
|
||
|
||
// GetKey 返回平台的唯一标识符
|
||
func (p *Platform) GetKey() uint32 {
|
||
return p.PlatformModel.ID
|
||
}
|
||
|
||
// Register 执行注册流程
|
||
func (p *Platform) DoRegister(ctx context.Context) error {
|
||
// 创建基本的REGISTER请求
|
||
req := sip.NewRequest(sip.REGISTER, p.Recipient)
|
||
|
||
//callid
|
||
customCallID := fmt.Sprintf("%d@%s", time.Now().Unix(), p.PlatformModel.DeviceIP)
|
||
callID := sip.CallIDHeader(customCallID)
|
||
req.AppendHeader(&callID)
|
||
|
||
//cseqheader
|
||
csqHeader := sip.CSeqHeader{
|
||
SeqNo: uint32(p.SN),
|
||
MethodName: "REGISTER",
|
||
}
|
||
p.SN++
|
||
req.AppendHeader(&csqHeader)
|
||
// 设置From头部,使用本地平台的信息
|
||
fromHdr := sip.FromHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.DeviceGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
Params: sip.NewParams(),
|
||
}
|
||
fromHdr.Params.Add("tag", sip.GenerateTagN(16))
|
||
req.AppendHeader(&fromHdr)
|
||
|
||
// 添加To头部
|
||
toHeader := sip.ToHeader{
|
||
Address: sip.Uri{
|
||
User: p.PlatformModel.DeviceGBID,
|
||
Host: p.PlatformModel.ServerGBDomain,
|
||
},
|
||
}
|
||
req.AppendHeader(&toHeader)
|
||
|
||
// 添加Via头部
|
||
viaHeader := sip.ViaHeader{
|
||
ProtocolName: "SIP",
|
||
ProtocolVersion: "2.0",
|
||
Transport: p.PlatformModel.Transport,
|
||
Host: p.PlatformModel.DeviceIP,
|
||
Port: p.PlatformModel.DevicePort,
|
||
Params: sip.NewParams(),
|
||
}
|
||
viaHeader.Params.Add("branch", sip.GenerateBranchN(16)).Add("rport", "")
|
||
req.AppendHeader(&viaHeader)
|
||
|
||
req.AppendHeader(&p.MaxForwardsHDR)
|
||
|
||
// 添加Contact头部
|
||
req.AppendHeader(p.ContactHDR)
|
||
|
||
req.AppendHeader(p.UserAgentHDR)
|
||
// 添加Expires头部
|
||
req.AppendHeader(sip.NewHeader("Expires", fmt.Sprintf("%d", p.PlatformModel.Expires)))
|
||
|
||
contentLengthHeader := sip.ContentLengthHeader(0)
|
||
req.AppendHeader(&contentLengthHeader)
|
||
// 设置传输协议
|
||
req.SetTransport(strings.ToUpper(p.PlatformModel.Transport))
|
||
|
||
tx, err := p.Client.TransactionRequest(ctx, req)
|
||
if err != nil {
|
||
p.Error("register", "error", err.Error())
|
||
return fmt.Errorf("创建事务失败: %v", err)
|
||
}
|
||
defer tx.Terminate()
|
||
|
||
// 获取响应
|
||
res, err := p.getResponse(tx)
|
||
if err != nil {
|
||
p.Error("register", "error", err.Error())
|
||
return err
|
||
}
|
||
|
||
// 处理401未授权响应
|
||
if res.StatusCode == 401 {
|
||
// 获取WWW-Authenticate头部
|
||
wwwAuth := res.GetHeader("WWW-Authenticate")
|
||
if wwwAuth == nil {
|
||
p.Error("register", "error", "no auth challenge")
|
||
return fmt.Errorf("no auth challenge")
|
||
}
|
||
|
||
// 解析认证质询
|
||
chal, err := digest.ParseChallenge(wwwAuth.Value())
|
||
if err != nil {
|
||
p.Error("register", "error", err.Error())
|
||
return err
|
||
}
|
||
|
||
// 生成认证响应
|
||
cred, _ := digest.Digest(chal, digest.Options{
|
||
Method: req.Method.String(),
|
||
URI: p.Recipient.Host,
|
||
Username: p.PlatformModel.Username,
|
||
Password: p.PlatformModel.Password,
|
||
})
|
||
|
||
// 创建新的带认证信息的请求
|
||
newReq := req.Clone()
|
||
newReq.RemoveHeader("Via") // 必须由传输层重新生成
|
||
newReq.AppendHeader(sip.NewHeader("Authorization", cred.String()))
|
||
|
||
// 发送认证请求
|
||
tx, err = p.Client.TransactionRequest(ctx, newReq, sipgo.ClientRequestAddVia)
|
||
if err != nil {
|
||
p.Error("register", "error", err.Error())
|
||
return err
|
||
}
|
||
defer tx.Terminate()
|
||
|
||
// 获取认证响应
|
||
res, err = p.getResponse(tx)
|
||
if err != nil {
|
||
p.Error("register", "error", err.Error())
|
||
return err
|
||
}
|
||
}
|
||
|
||
// 检查最终响应状态
|
||
if res.StatusCode != 200 {
|
||
p.Error("register", "status", res.StatusCode)
|
||
return fmt.Errorf("注册失败,状态码: %d", res.StatusCode)
|
||
}
|
||
|
||
p.Info("register", "status", "success")
|
||
p.PlatformModel.Status = true
|
||
return nil
|
||
}
|