Compare commits

...

10 Commits

Author SHA1 Message Date
dexter
682aec656b Merge pull request #90 from rufftio/v4
ptz 控制接口,采用更易理解和使用的参数
2023-06-05 18:40:32 +08:00
ogofly
fbd8683f5b Merge branch 'Monibuca:v4' into v4 2023-06-05 18:18:50 +08:00
liuyancong
b15e4ee89c add: ptz 控制接口,采用更易理解和使用的参数 2023-06-05 18:17:46 +08:00
langhuihui
3c7b3a042d fix: list接口为空时返回[] 而不是null 2023-05-25 14:12:57 +08:00
dexter
858df1377e Merge pull request #88 from ogofly/v4
录像查询重构为在当前查询的http响应中返回
2023-05-24 11:36:14 +08:00
liuyancong
60021d3cd9 录像查询重构为在当前查询的http响应中返回 2023-05-24 11:30:50 +08:00
langhuihui
d8061cd7c3 channel结构体反转 2023-05-23 20:56:24 +08:00
langhuihui
ed397063c4 chroe: update log format 2023-05-21 22:12:09 +08:00
langhuihui
5853120d30 update readme 2023-05-17 09:07:12 +08:00
langhuihui
4c47df0695 fix: update dep ps version to 4.0.1 2023-05-16 23:11:26 +08:00
12 changed files with 400 additions and 211 deletions

View File

@@ -18,16 +18,13 @@ _ "m7s.live/plugin/gb28181/v4"
```yaml
gb28181:
autoinvite: true #表示自动发起invite当ServerSIP接收到设备信息时立即向设备发送invite命令获取流
invitemode: 1 #0、手动invite 1、表示自动发起invite当ServerSIP接收到设备信息时立即向设备发送invite命令获取流,2、按需拉流既等待订阅者触发
position:
autosubposition: false #是否自动订阅定位
expires: 3600s #订阅周期(单位:秒)默认3600
interval: 6s #订阅间隔单位默认6
prefetchrecord: false
udpcachesize: 0 #表示UDP缓存大小默认为0不开启。仅当TCP关闭切缓存大于0时才开启
sipnetwork: udp
sipip: "" #sip服务器地址 默认 自动适配设备网段
sipport: 5060
serial: "34020000002000000001"
realm: "3402000000"
username: ""
@@ -36,10 +33,9 @@ gb28181:
registervalidity: 60s #注册有效期
mediaip: "" #媒体服务器地址 默认 自动适配设备网段
mediaport: 58200 #媒体服务器端口,用于接收设备的流
medianetwork: tcp
mediaportmin: 0 #媒体服务器端口范围最小值,设置后将开启端口范围模式
mediaportmax: 0 #媒体服务器端口范围最大值,设置后将开启端口范围模式
port:
sip: udp:5060 #sip服务器端口
media: tcp:58200 #媒体服务器端口,用于接收设备的流,范围端口表示法udp:50000-60000
removebaninterval: 10m #定时移除注册失败的设备黑名单单位秒默认10分钟600秒
loglevel: info
@@ -106,7 +102,7 @@ type Device struct {
| startTime | 否 | 开始时间纯数字Unix时间戳 |
| endTime | 否 | 结束时间纯数字Unix时间戳 |
返回200代表成功
返回200代表成功, 304代表已经在拉取中不能重复拉仅仅针对直播流
### 停止从设备拉流
@@ -117,6 +113,8 @@ type Device struct {
| id | 是 | 设备ID |
| channel | 是 | 通道编号 |
http 200 表示成功404流不存在
### 发送控制命令
`/gb28181/api/control`

View File

@@ -12,10 +12,13 @@ import (
"github.com/ghettovoice/gosip/sip"
"go.uber.org/zap"
. "m7s.live/engine/v4"
"m7s.live/engine/v4/log"
"m7s.live/plugin/gb28181/v4/utils"
"m7s.live/plugin/ps/v4"
)
var QUERY_RECORD_TIMEOUT = time.Second * 5
type PullStream struct {
opt *InviteOptions
channel *Channel
@@ -40,28 +43,25 @@ func (p *PullStream) Bye() int {
p.opt.recyclePort(p.opt.MediaPort)
}
if err != nil {
return ServerInternalError
return http.StatusInternalServerError
}
return int(resp.StatusCode())
}
type ChannelEx struct {
device *Device // 所属设备
status atomic.Int32 // 通道状态,0:空闲,1:正在invite,2:正在播放
LiveSubSP string // 实时子码流通过rtsp
Records []*Record
RecordStartTime string
RecordEndTime string
recordStartTime time.Time
recordEndTime time.Time
GpsTime time.Time //gps时间
Longitude string //经度
Latitude string //纬度
type Channel struct {
device *Device // 所属设备
status atomic.Int32 // 通道状态,0:空闲,1:正在invite,2:正在播放
LiveSubSP string // 实时子码流通过rtsp
GpsTime time.Time //gps时间
Longitude string //经度
Latitude string //纬度
*log.Logger `json:"-" yaml:"-"`
ChannelInfo
}
// Channel 通道
type Channel struct {
DeviceID string
type ChannelInfo struct {
DeviceID string // 通道ID
ParentID string
Name string
Manufacturer string
@@ -75,8 +75,6 @@ type Channel struct {
RegisterWay int
Secrecy int
Status string
Children []*Channel `json:"-" yaml:"-"`
ChannelEx //自定义属性
}
func (channel *Channel) CreateRequst(Method sip.RequestMethod) (req sip.Request) {
@@ -138,13 +136,9 @@ func (channel *Channel) CreateRequst(Method sip.RequestMethod) (req sip.Request)
req.SetDestination(d.NetAddr)
return req
}
func (channel *Channel) QueryRecord(startTime, endTime string) int {
func (channel *Channel) QueryRecord(startTime, endTime string) ([]*Record, error) {
d := channel.device
channel.RecordStartTime = startTime
channel.RecordEndTime = endTime
channel.recordStartTime, _ = time.Parse(TIME_LAYOUT, startTime)
channel.recordEndTime, _ = time.Parse(TIME_LAYOUT, endTime)
channel.Records = nil
request := d.CreateRequest(sip.MESSAGE)
contentType := sip.ContentType("Application/MANSCDP+xml")
request.AppendHeader(&contentType)
@@ -159,12 +153,21 @@ func (channel *Channel) QueryRecord(startTime, endTime string) int {
<Type>all</Type>
</Query>`, d.sn, channel.DeviceID, startTime, endTime)
request.SetBody(body, true)
resultCh := RecordQueryLink.WaitResult(d.ID, channel.DeviceID, d.sn, QUERY_RECORD_TIMEOUT)
resp, err := d.SipRequestForResponse(request)
if err != nil {
return http.StatusRequestTimeout
return nil, fmt.Errorf("query error: %s", err)
}
return int(resp.StatusCode())
if resp.StatusCode() != http.StatusOK {
return nil, fmt.Errorf("query error, status=%d", resp.StatusCode())
}
// RecordQueryLink 中加了超时机制,该结果一定会返回
// 所以此处不用再增加超时等保护机制
r := <-resultCh
return r.list, r.err
}
func (channel *Channel) Control(PTZCmd string) int {
d := channel.device
request := d.CreateRequest(sip.MESSAGE)
@@ -239,6 +242,7 @@ func (channel *Channel) Invite(opt *InviteOptions) (code int, err error) {
}
defer func() {
if err != nil {
GB28181Plugin.Error("Invite", zap.Error(err))
channel.status.Store(0)
if conf.InviteMode == 1 {
// 5秒后重试
@@ -317,13 +321,13 @@ func (channel *Channel) Invite(opt *InviteOptions) (code int, err error) {
invite.AppendHeader(&subject)
inviteRes, err := d.SipRequestForResponse(invite)
if err != nil {
plugin.Error(fmt.Sprintf("SIP->Invite %s :%s invite error: %s", channel.DeviceID, invite.String(), err.Error()))
channel.Error("invite", zap.Error(err), zap.String("msg", invite.String()))
return http.StatusInternalServerError, err
}
code = int(inviteRes.StatusCode())
plugin.Info(fmt.Sprintf("Channel :%s invite response status code: %d", channel.DeviceID, code))
channel.Info("invite response", zap.Int("status code", code))
if code == OK {
if code == http.StatusOK {
ds := strings.Split(inviteRes.Body(), "\r\n")
for _, l := range ds {
if ls := strings.Split(l, "="); len(ls) > 1 {
@@ -331,7 +335,7 @@ func (channel *Channel) Invite(opt *InviteOptions) (code int, err error) {
if _ssrc, err := strconv.ParseInt(ls[1], 10, 0); err == nil {
opt.SSRC = uint32(_ssrc)
} else {
plugin.Error("read invite response y ", zap.Error(err))
channel.Error("read invite response y ", zap.Error(err))
}
break
}
@@ -366,7 +370,7 @@ func (channel *Channel) Bye(streamPath string) int {
}
func (channel *Channel) TryAutoInvite(opt *InviteOptions) {
if conf.InviteMode == 1 && channel.CanInvite() {
if channel.CanInvite() {
go channel.Invite(opt)
}
}

View File

@@ -1,60 +1,5 @@
package gb28181
const (
Trying = 100
Ringing = 180
CallIsBeingForwarded = 181
Queued = 182
SessionProgress = 183
OK = 200
Accepted = 202
MultipleChoices = 300
MovedPermanently = 301
MovedTemporarily = 302
UseProxy = 305
AlternativeService = 380
BadRequest = 400
Unauthorized = 401
PaymentRequired = 402
Forbidden = 403
NotFound = 404
MethodNotAllowed = 405
NotAcceptable = 406
ProxyAuthenticationRequired = 407
RequestTimeout = 408
Gone = 410
RequestEntityTooLarge = 413
RequestURITooLong = 414
UnsupportedMediaType = 415
UnsupportedURIScheme = 416
BadExtension = 420
ExtensionRequired = 421
IntervalTooBrief = 423
TemporarilyUnavailable = 480
CallTransactionDoesNotExist = 481
LoopDetected = 482
TooManyHops = 483
AddressIncomplete = 484
Ambiguous = 485
BusyHere = 486
RequestTerminated = 487
NotAcceptableHere = 488
BadEvent = 489
RequestPending = 491
Undecipherable = 493
ServerInternalError = 500
NotImplemented = 501
BadGateway = 502
ServiceUnavailable = 503
ServerTim = 504
VersionNotSupported = 505
MessageTooLarge = 513
BusyEverywhere = 600
Decline = 603
DoesNotExistAnywhere = 604
SessionNotAcceptable = 606
)
var reasons = map[int]string{
100: "Trying",
180: "Ringing",
@@ -113,3 +58,9 @@ var reasons = map[int]string{
func Explain(statusCode int) string {
return reasons[statusCode]
}
const (
INVIDE_MODE_MANUAL = iota
INVIDE_MODE_AUTO
INVIDE_MODE_ONSUBSCRIBE
)

130
device.go
View File

@@ -12,6 +12,7 @@ import (
"go.uber.org/zap"
"m7s.live/engine/v4"
"m7s.live/engine/v4/log"
"m7s.live/plugin/gb28181/v4/utils"
// . "github.com/logrusorgru/aurora"
@@ -23,7 +24,6 @@ const TIME_LAYOUT = "2006-01-02T15:04:05"
// Record 录像
type Record struct {
//channel *Channel
DeviceID string
Name string
FilePath string
@@ -69,19 +69,20 @@ type Device struct {
GpsTime time.Time //gps时间
Longitude string //经度
Latitude string //纬度
*log.Logger `json:"-" yaml:"-"`
}
func (d *Device) MarshalJSON() ([]byte, error) {
type Alias Device
data := &struct {
Channels []*Channel
Channels []*ChannelInfo
*Alias
}{
Alias: (*Alias)(d),
}
d.channelMap.Range(func(key, value interface{}) bool {
c := value.(*Channel)
data.Channels = append(data.Channels, c)
data.Channels = append(data.Channels, &c.ChannelInfo)
return true
})
return json.Marshal(data)
@@ -112,7 +113,7 @@ func (c *GB28181Config) RecoverDevice(d *Device, req sip.Request) {
if c.MediaIP != "" {
mediaIp = c.MediaIP
}
plugin.Info("RecoverDevice", zap.String("id", d.ID), zap.String("deviceIp", deviceIp), zap.String("servIp", servIp), zap.String("sipIP", sipIP), zap.String("mediaIp", mediaIp))
d.Info("RecoverDevice", zap.String("deviceIp", deviceIp), zap.String("servIp", servIp), zap.String("sipIP", sipIP), zap.String("mediaIp", mediaIp))
d.Status = string(sip.REGISTER)
d.sipIP = sipIP
d.mediaIP = mediaIp
@@ -132,7 +133,7 @@ func (c *GB28181Config) StoreDevice(id string, req sip.Request) (d *Device) {
d.UpdateTime = time.Now()
d.NetAddr = deviceIp
d.addr = deviceAddr
plugin.Debug("UpdateDevice", zap.String("id", id), zap.String("netaddr", d.NetAddr))
d.Debug("UpdateDevice", zap.String("netaddr", d.NetAddr))
} else {
servIp := req.Recipient().Host()
//根据网卡ip获取对应的公网ip
@@ -153,7 +154,6 @@ func (c *GB28181Config) StoreDevice(id string, req sip.Request) (d *Device) {
if c.MediaIP != "" {
mediaIp = c.MediaIP
}
plugin.Info("StoreDevice", zap.String("id", id), zap.String("deviceIp", deviceIp), zap.String("servIp", servIp), zap.String("sipIP", sipIP), zap.String("mediaIp", mediaIp))
d = &Device{
ID: id,
RegisterTime: time.Now(),
@@ -163,7 +163,9 @@ func (c *GB28181Config) StoreDevice(id string, req sip.Request) (d *Device) {
sipIP: sipIP,
mediaIP: mediaIp,
NetAddr: deviceIp,
Logger: GB28181Plugin.With(zap.String("id", id)),
}
d.Info("StoreDevice", zap.String("deviceIp", deviceIp), zap.String("servIp", servIp), zap.String("sipIP", sipIP), zap.String("mediaIp", mediaIp))
Devices.Store(id, d)
c.SaveDevices()
}
@@ -177,6 +179,7 @@ func (c *GB28181Config) ReadDevices() {
for _, item := range items {
if time.Since(item.UpdateTime) < conf.RegisterValidity {
item.Status = "RECOVER"
item.Logger = GB28181Plugin.With(zap.String("id", item.ID))
Devices.Store(item.ID, item)
}
}
@@ -197,31 +200,31 @@ func (c *GB28181Config) SaveDevices() {
}
}
func (d *Device) addOrUpdateChannel(channel *Channel) {
if old, ok := d.channelMap.Load(channel.DeviceID); ok {
channel.ChannelEx = old.(*Channel).ChannelEx
func (d *Device) addOrUpdateChannel(info ChannelInfo) (c *Channel) {
if old, ok := d.channelMap.Load(info.DeviceID); ok {
c = old.(*Channel)
c.ChannelInfo = info
} else {
c = &Channel{
device: d,
ChannelInfo: info,
Logger: d.Logger.With(zap.String("channel", info.DeviceID)),
}
if s := engine.Streams.Get(fmt.Sprintf("%s/%s/rtsp", c.device.ID, c.DeviceID)); s != nil {
c.LiveSubSP = s.Path
} else {
c.LiveSubSP = ""
}
d.channelMap.Store(info.DeviceID, c)
}
channel.device = d
d.channelMap.Store(channel.DeviceID, channel)
return
}
func (d *Device) deleteChannel(DeviceID string) {
d.channelMap.Delete(DeviceID)
}
func (d *Device) CheckSubStream() {
d.channelMap.Range(func(key, value any) bool {
c := value.(*Channel)
if s := engine.Streams.Get("sub/" + c.DeviceID); s != nil {
c.LiveSubSP = s.Path
} else {
c.LiveSubSP = ""
}
return true
})
}
func (d *Device) UpdateChannels(list []*Channel) {
func (d *Device) UpdateChannels(list ...ChannelInfo) {
for _, c := range list {
if _, ok := conf.Ignores[c.DeviceID]; ok {
continue
@@ -244,32 +247,18 @@ func (d *Device) UpdateChannels(list []*Channel) {
}
}
//本设备增加通道
d.addOrUpdateChannel(c)
channel := d.addOrUpdateChannel(c)
//预取和邀请
if conf.PreFetchRecord {
n := time.Now()
n = time.Date(n.Year(), n.Month(), n.Day(), 0, 0, 0, 0, time.Local)
if len(c.Records) == 0 || (n.Format(TIME_LAYOUT) == c.RecordStartTime &&
n.Add(time.Hour*24-time.Second).Format(TIME_LAYOUT) == c.RecordEndTime) {
go c.QueryRecord(n.Format(TIME_LAYOUT), n.Add(time.Hour*24-time.Second).Format(TIME_LAYOUT))
}
if conf.InviteMode == INVIDE_MODE_AUTO {
channel.TryAutoInvite(&InviteOptions{})
}
c.TryAutoInvite(&InviteOptions{})
if s := engine.Streams.Get("sub/" + c.DeviceID); s != nil {
c.LiveSubSP = s.Path
channel.LiveSubSP = s.Path
} else {
c.LiveSubSP = ""
channel.LiveSubSP = ""
}
}
}
func (d *Device) UpdateRecord(channelId string, list []*Record) {
d.channelMap.Range(func(key, value any) bool {
c := value.(*Channel)
c.Records = append(c.Records, list...)
return true
})
}
func (d *Device) CreateRequest(Method sip.RequestMethod) (req sip.Request) {
d.sn++
@@ -353,7 +342,7 @@ func (d *Device) Subscribe() int {
response, err := d.SipRequestForResponse(request)
if err == nil && response != nil {
if response.StatusCode() == OK {
if response.StatusCode() == http.StatusOK {
callId, _ := request.CallID()
d.subscriber.CallID = string(*callId)
} else {
@@ -375,13 +364,13 @@ func (d *Device) Catalog() int {
request.AppendHeader(&expires)
request.SetBody(BuildCatalogXML(d.sn, d.ID), true)
// 输出Sip请求设备通道信息信令
plugin.Sugar().Debugf("SIP->Catalog:%s", request)
GB28181Plugin.Sugar().Debugf("SIP->Catalog:%s", request)
resp, err := d.SipRequestForResponse(request)
if err == nil && resp != nil {
plugin.Sugar().Debugf("SIP<-Catalog Response: %s", resp.String())
GB28181Plugin.Sugar().Debugf("SIP<-Catalog Response: %s", resp.String())
return int(resp.StatusCode())
} else if err != nil {
plugin.Error("SIP<-Catalog error:", zap.Error(err))
GB28181Plugin.Error("SIP<-Catalog error:", zap.Error(err))
}
return http.StatusRequestTimeout
}
@@ -403,8 +392,8 @@ func (d *Device) QueryDeviceInfo() {
// received, _ := via.Params.Get("received")
// d.SipIP = received.String()
// }
plugin.Info(fmt.Sprintf("QueryDeviceInfo:%s ipaddr:%s response code:%d", d.ID, d.NetAddr, response.StatusCode()))
if response.StatusCode() == OK {
d.Info("QueryDeviceInfo", zap.Uint16("status code", uint16(response.StatusCode())))
if response.StatusCode() == http.StatusOK {
break
}
}
@@ -432,7 +421,7 @@ func (d *Device) MobilePositionSubscribe(id string, expires time.Duration, inter
response, err := d.SipRequestForResponse(mobilePosition)
if err == nil && response != nil {
if response.StatusCode() == OK {
if response.StatusCode() == http.StatusOK {
callId, _ := mobilePosition.CallID()
d.subscriber.CallID = callId.String()
} else {
@@ -447,16 +436,16 @@ func (d *Device) MobilePositionSubscribe(id string, expires time.Duration, inter
func (d *Device) UpdateChannelPosition(channelId string, gpsTime string, lng string, lat string) {
if v, ok := d.channelMap.Load(channelId); ok {
c := v.(*Channel)
c.ChannelEx.GpsTime = time.Now() //时间取系统收到的时间,避免设备时间和格式问题
c.ChannelEx.Longitude = lng
c.ChannelEx.Latitude = lat
plugin.Sugar().Debugf("更新通道[%s]坐标成功\n", c.Name)
c.GpsTime = time.Now() //时间取系统收到的时间,避免设备时间和格式问题
c.Longitude = lng
c.Latitude = lat
c.Debug("update channel position success")
} else {
//如果未找到通道,则更新到设备上
d.GpsTime = time.Now() //时间取系统收到的时间,避免设备时间和格式问题
d.Longitude = lng
d.Latitude = lat
plugin.Sugar().Debugf("未找到通道[%s],更新设备[%s]坐标成功\n", channelId, d.ID)
d.Debug("update device position success", zap.String("channelId", channelId))
}
}
@@ -465,20 +454,20 @@ func (d *Device) UpdateChannelStatus(deviceList []*notifyMessage) {
for _, v := range deviceList {
switch v.Event {
case "ON":
plugin.Debug("收到通道上线通知")
d.Debug("receive channel online notify")
d.channelOnline(v.DeviceID)
case "OFF":
plugin.Debug("收到通道离线通知")
d.Debug("receive channel offline notify")
d.channelOffline(v.DeviceID)
case "VLOST":
plugin.Debug("收到通道视频丢失通知")
d.Debug("receive channel video lost notify")
d.channelOffline(v.DeviceID)
case "DEFECT":
plugin.Debug("收到通道故障通知")
d.Debug("receive channel video defect notify")
d.channelOffline(v.DeviceID)
case "ADD":
plugin.Debug("收到通道新增通知")
channel := Channel{
d.Debug("receive channel add notify")
channel := ChannelInfo{
DeviceID: v.DeviceID,
ParentID: v.ParentID,
Name: v.Name,
@@ -494,15 +483,15 @@ func (d *Device) UpdateChannelStatus(deviceList []*notifyMessage) {
Secrecy: v.Secrecy,
Status: v.Status,
}
d.addOrUpdateChannel(&channel)
d.addOrUpdateChannel(channel)
case "DEL":
//删除
plugin.Debug("收到通道删除通知")
d.Debug("receive channel delete notify")
d.deleteChannel(v.DeviceID)
case "UPDATE":
plugin.Debug("收到通道更新通知")
d.Debug("receive channel update notify")
// 更新通道
channel := &Channel{
channel := ChannelInfo{
DeviceID: v.DeviceID,
ParentID: v.ParentID,
Name: v.Name,
@@ -518,8 +507,7 @@ func (d *Device) UpdateChannelStatus(deviceList []*notifyMessage) {
Secrecy: v.Secrecy,
Status: v.Status,
}
channels := []*Channel{channel}
d.UpdateChannels(channels)
d.UpdateChannels(channel)
}
}
}
@@ -528,9 +516,9 @@ func (d *Device) channelOnline(DeviceID string) {
if v, ok := d.channelMap.Load(DeviceID); ok {
c := v.(*Channel)
c.Status = "ON"
plugin.Sugar().Debugf("通道[%s]在线\n", c.Name)
c.Debug("online")
} else {
plugin.Sugar().Debugf("更新通道[%s]状态失败,未找到\n", DeviceID)
d.Debug("update channel status failed, not found", zap.String("channelId", DeviceID))
}
}
@@ -538,8 +526,8 @@ func (d *Device) channelOffline(DeviceID string) {
if v, ok := d.channelMap.Load(DeviceID); ok {
c := v.(*Channel)
c.Status = "OFF"
plugin.Sugar().Debugf("通道[%s]离线\n", c.Name)
c.Debug("offline")
} else {
plugin.Sugar().Debugf("更新通道[%s]状态失败,未找到\n", DeviceID)
d.Debug("update channel status failed, not found", zap.String("channelId", DeviceID))
}
}

7
go.mod
View File

@@ -8,10 +8,10 @@ require (
github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/pion/rtp v1.7.13
go.uber.org/zap v1.23.0
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db
golang.org/x/net v0.8.0
golang.org/x/text v0.8.0
m7s.live/engine/v4 v4.12.0
m7s.live/engine/v4 v4.12.8
m7s.live/plugin/ps/v4 v4.0.1
)
require (
@@ -49,11 +49,12 @@ require (
github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.0 // indirect
github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect
github.com/yapingcat/gomedia v0.0.0-20230222121919-c67df405bf33 // indirect
github.com/yapingcat/gomedia v0.0.0-20230426092936-387031404274 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
golang.org/x/crypto v0.4.0 // indirect
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.6.0 // indirect

10
go.sum
View File

@@ -188,8 +188,8 @@ github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYm
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
github.com/yapingcat/gomedia v0.0.0-20230222121919-c67df405bf33 h1:uyZY++dluUg7iTSsNzuOVln/mC2U2KXwgKLfKLCJ74Y=
github.com/yapingcat/gomedia v0.0.0-20230222121919-c67df405bf33/go.mod h1:WSZ59bidJOO40JSJmLqlkBJrjZCtjbKKkygEMfzY/kc=
github.com/yapingcat/gomedia v0.0.0-20230426092936-387031404274 h1:cj4I+bvWX9I+Hg6tnZ7DAiOVxzhyLhdvYVKp+WpM/2c=
github.com/yapingcat/gomedia v0.0.0-20230426092936-387031404274/go.mod h1:WSZ59bidJOO40JSJmLqlkBJrjZCtjbKKkygEMfzY/kc=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@@ -332,5 +332,7 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
m7s.live/engine/v4 v4.12.0 h1:CRPbJ0jhHVZArc5mvV7e6Seb4Ye816kGzs3FOVKnfHw=
m7s.live/engine/v4 v4.12.0/go.mod h1:AiJPBwdA77DM3fymlcH2qYPR8ivL6ib9UVLm1Rft/to=
m7s.live/engine/v4 v4.12.8 h1:cNGyajzEkbUzIcPtcedGbxvMlIuScWxDb/raYFgAHKE=
m7s.live/engine/v4 v4.12.8/go.mod h1:LoALBfV5rmsz5TJQr6cmLxM33mfUE5BKBq/sMtXOVlc=
m7s.live/plugin/ps/v4 v4.0.1 h1:iKgo9D4g6vo3I97Je1hG8v/6+IDRei7sHnTCYBEyasY=
m7s.live/plugin/ps/v4 v4.0.1/go.mod h1:lAPr3gGIFoU4ctMRnPeyjbcREueyT6TfiKhWBgDrOGM=

View File

@@ -6,7 +6,6 @@ import (
"encoding/xml"
"fmt"
"github.com/logrusorgru/aurora"
"go.uber.org/zap"
"m7s.live/plugin/gb28181/v4/utils"
@@ -32,7 +31,7 @@ func (a *Authorization) Verify(username, passwd, realm, nonce string) bool {
r2 := a.getDigest(s2)
if r1 == "" || r2 == "" {
plugin.Error("Authorization algorithm wrong")
GB28181Plugin.Error("Authorization algorithm wrong")
return false
}
//3、将密文 1nonce 和密文 2 依次组合获取 1 个字符串,并对这个字符串使用算法加密,获得密文 r3即Response
@@ -53,12 +52,15 @@ func (a *Authorization) getDigest(raw string) string {
}
func (c *GB28181Config) OnRegister(req sip.Request, tx sip.ServerTransaction) {
from, _ := req.From()
from, ok := req.From()
if !ok {
GB28181Plugin.Error("OnRegister", zap.String("error", "no from"))
return
}
id := from.Address.User().String()
plugin.Sugar().Infof("OnRegister: %s, %s, from: %s", req.Destination(), id, req.Source())
GB28181Plugin.Info("OnRegister", zap.String("id", id), zap.String("source", req.Source()), zap.String("destination", req.Destination()))
if len(id) != 20 {
plugin.Sugar().Infof("Wrong GB-28181 id: %s", id)
GB28181Plugin.Info("Wrong GB-28181", zap.String("id", id))
return
}
passAuth := false
@@ -148,7 +150,7 @@ func (d *Device) syncChannels() {
func (c *GB28181Config) OnMessage(req sip.Request, tx sip.ServerTransaction) {
from, _ := req.From()
id := from.Address.User().String()
plugin.Sugar().Debugf("SIP<-OnMessage from %s : %s", req.Source(), req.String())
GB28181Plugin.Debug("SIP<-OnMessage", zap.String("id", id), zap.String("source", req.Source()), zap.String("req", req.String()))
if v, ok := Devices.Load(id); ok {
d := v.(*Device)
switch d.Status {
@@ -163,13 +165,15 @@ func (c *GB28181Config) OnMessage(req sip.Request, tx sip.ServerTransaction) {
temp := &struct {
XMLName xml.Name
CmdType string
SN int // 请求序列号,一般用于对应 request 和 response
DeviceID string
DeviceName string
Manufacturer string
Model string
Channel string
DeviceList []*Channel `xml:"DeviceList>Item"`
RecordList []*Record `xml:"RecordList>Item"`
DeviceList []ChannelInfo `xml:"DeviceList>Item"`
RecordList []*Record `xml:"RecordList>Item"`
SumNum int // 录像结果的总数 SumNum录像结果会按照多条消息返回可用于判断是否全部返回
}{}
decoder := xml.NewDecoder(bytes.NewReader([]byte(req.Body())))
decoder.CharsetReader = charset.NewReaderLabel
@@ -177,7 +181,7 @@ func (c *GB28181Config) OnMessage(req sip.Request, tx sip.ServerTransaction) {
if err != nil {
err = utils.DecodeGbk(temp, []byte(req.Body()))
if err != nil {
plugin.Error("decode catelog err", zap.Error(err))
GB28181Plugin.Error("decode catelog err", zap.Error(err))
}
}
var body string
@@ -189,22 +193,21 @@ func (c *GB28181Config) OnMessage(req sip.Request, tx sip.ServerTransaction) {
go d.syncChannels()
} else {
d.channelMap.Range(func(key, value interface{}) bool {
channel := value.(*Channel)
channel.TryAutoInvite(&InviteOptions{})
if conf.InviteMode == INVIDE_MODE_AUTO {
value.(*Channel).TryAutoInvite(&InviteOptions{})
}
return true
})
}
//为什么要查找子码流?
//d.CheckSubStream()
//在KeepLive 进行位置订阅的处理,如果开启了自动订阅位置,则去订阅位置
if c.Position.AutosubPosition && time.Since(d.GpsTime) > c.Position.Interval*2 {
d.MobilePositionSubscribe(d.ID, c.Position.Expires, c.Position.Interval)
plugin.Sugar().Debugf("位置自动订阅,设备[%s]成功\n", d.ID)
GB28181Plugin.Debug("Mobile Position Subscribe", zap.String("deviceID", d.ID))
}
case "Catalog":
d.UpdateChannels(temp.DeviceList)
d.UpdateChannels(temp.DeviceList...)
case "RecordInfo":
d.UpdateRecord(temp.DeviceID, temp.RecordList)
RecordQueryLink.Put(d.ID, temp.DeviceID, temp.SN, temp.SumNum, temp.RecordList)
case "DeviceInfo":
// 主设备信息
d.Name = temp.DeviceName
@@ -214,7 +217,7 @@ func (c *GB28181Config) OnMessage(req sip.Request, tx sip.ServerTransaction) {
d.Status = "Alarmed"
body = BuildAlarmResponseXML(d.ID)
default:
plugin.Sugar().Warnf("DeviceID:", aurora.Red(d.ID), " Not supported CmdType : "+temp.CmdType+" body:\n", req.Body)
d.Warn("Not supported CmdType", zap.String("CmdType", temp.CmdType), zap.String("body", req.Body()))
response := sip.NewResponseFromRequest("", req, http.StatusBadRequest, "", "")
tx.Respond(response)
return
@@ -252,7 +255,7 @@ func (c *GB28181Config) OnNotify(req sip.Request, tx sip.ServerTransaction) {
if err != nil {
err = utils.DecodeGbk(temp, []byte(req.Body()))
if err != nil {
plugin.Error("decode catelog err", zap.Error(err))
GB28181Plugin.Error("decode catelog err", zap.Error(err))
}
}
var body string
@@ -266,7 +269,7 @@ func (c *GB28181Config) OnNotify(req sip.Request, tx sip.ServerTransaction) {
// case "Alarm":
// //报警事件通知 TODO
default:
plugin.Sugar().Warnf("DeviceID:", aurora.Red(d.ID), " Not supported CmdType : "+temp.CmdType+" body:", req.Body)
d.Warn("Not supported CmdType", zap.String("CmdType", temp.CmdType), zap.String("body", req.Body()))
response := sip.NewResponseFromRequest("", req, http.StatusBadRequest, "", "")
tx.Respond(response)
return

124
link.go Normal file
View File

@@ -0,0 +1,124 @@
package gb28181
import (
"fmt"
"sync"
"time"
"go.uber.org/zap"
)
// 对于录像查询,通过 queryKey (即 deviceId + channelId + sn) 唯一区分一次请求和响应
// 并将其关联起来,以实现异步响应的目的
// 提供单例实例供调用
var RecordQueryLink = NewRecordQueryLink(time.Second * 60)
type recordQueryLink struct {
pendingResult map[string]recordQueryResult // queryKey 查询结果缓存
pendingResp map[string]recordQueryResp // queryKey 待回复的查询请求
timeout time.Duration // 查询结果的过期时间
sync.RWMutex
}
type recordQueryResult struct {
time time.Time
err error
sum int
finished bool
list []*Record
}
type recordQueryResp struct {
respChan chan<- recordQueryResult
timeout time.Duration
startTime time.Time
}
func NewRecordQueryLink(resultTimeout time.Duration) *recordQueryLink {
c := &recordQueryLink{
timeout: resultTimeout,
pendingResult: make(map[string]recordQueryResult),
pendingResp: make(map[string]recordQueryResp),
}
go c.cleanTimeout()
return c
}
// 唯一区分一次录像查询
func recordQueryKey(deviceId, channelId string, sn int) string {
return fmt.Sprintf("%s-%s-%d", deviceId, channelId, sn)
}
// 定期清理过期的查询结果和请求
func (c *recordQueryLink) cleanTimeout() {
tick := time.NewTicker(time.Millisecond * 100)
for {
<-tick.C
for k, s := range c.pendingResp {
if time.Since(s.startTime) > s.timeout {
if r, ok := c.pendingResult[k]; ok {
c.notify(k, r)
} else {
c.notify(k, recordQueryResult{err: fmt.Errorf("query time out")})
}
}
}
for k, r := range c.pendingResult {
if time.Since(r.time) > c.timeout {
delete(c.pendingResult, k)
}
}
}
}
func (c *recordQueryLink) Put(deviceId, channelId string, sn int, sum int, record []*Record) {
key, r := c.doPut(deviceId, channelId, sn, sum, record)
if r.finished {
c.notify(key, r)
}
}
func (c *recordQueryLink) doPut(deviceId, channelId string, sn, sum int, record []*Record) (key string, r recordQueryResult) {
c.Lock()
defer c.Unlock()
key = recordQueryKey(deviceId, channelId, sn)
if v, ok := c.pendingResult[key]; ok {
r = v
} else {
r = recordQueryResult{time: time.Now(), sum: sum, list: make([]*Record, 0)}
}
r.list = append(r.list, record...)
if len(r.list) == sum {
r.finished = true
}
c.pendingResult[key] = r
GB28181Plugin.Logger.Debug("put record",
zap.String("key", key),
zap.Int("sum", sum),
zap.Int("count", len(r.list)))
return
}
func (c *recordQueryLink) WaitResult(
deviceId, channelId string, sn int,
timeout time.Duration) (resultCh <-chan recordQueryResult) {
key := recordQueryKey(deviceId, channelId, sn)
c.Lock()
defer c.Unlock()
respCh := make(chan recordQueryResult, 1)
resultCh = respCh
c.pendingResp[key] = recordQueryResp{startTime: time.Now(), timeout: timeout, respChan: respCh}
return
}
func (c *recordQueryLink) notify(key string, r recordQueryResult) {
if s, ok := c.pendingResp[key]; ok {
s.respChan <- r
}
c.Lock()
defer c.Unlock()
delete(c.pendingResp, key)
delete(c.pendingResult, key)
GB28181Plugin.Logger.Debug("record notify", zap.String("key", key))
}

23
main.go
View File

@@ -1,13 +1,13 @@
package gb28181
import (
"fmt"
"os"
"strings"
"sync"
"time"
myip "github.com/husanpao/ip"
"go.uber.org/zap"
. "m7s.live/engine/v4"
"m7s.live/engine/v4/util"
)
@@ -19,11 +19,9 @@ type GB28181PositionConfig struct {
}
type GB28181Config struct {
// AutoInvite bool `default:"true"`
InviteMode int `default:"1"` //邀请模式0:手动拉流1:预拉流2:按需拉流
PreFetchRecord bool
InviteIDs string //按照国标gb28181协议允许邀请的设备类型:132 摄像机 NVR
ListenAddr string `default:"0.0.0.0"`
InviteMode int `default:"1"` //邀请模式0:手动拉流1:预拉流2:按需拉流
InviteIDs string //按照国标gb28181协议允许邀请的设备类型:132 摄像机 NVR
ListenAddr string `default:"0.0.0.0"`
//sip服务器的配置
SipNetwork string `default:"udp"` //传输协议默认UDP可选TCP
SipIP string //sip 服务器公网IP
@@ -73,7 +71,7 @@ func (c *GB28181Config) initRoutes() {
c.routes[k[0:lastdot]] = k
}
}
plugin.Info(fmt.Sprintf("LocalAndInternalIPs detail: %s", c.routes))
GB28181Plugin.Info("LocalAndInternalIPs", zap.Any("routes", c.routes))
}
func (c *GB28181Config) OnEvent(event any) {
@@ -99,12 +97,19 @@ func (c *GB28181Config) OnEvent(event any) {
go c.initRoutes()
c.startServer()
case *Stream:
if c.InviteMode == 2 {
if c.InviteMode == INVIDE_MODE_ONSUBSCRIBE {
if channel := FindChannel(e.AppName, e.StreamName); channel != nil {
channel.TryAutoInvite(&InviteOptions{})
}
}
case SEpublish:
if channel := FindChannel(e.Target.AppName, strings.TrimSuffix(e.Target.StreamName, "/rtsp")); channel != nil {
channel.LiveSubSP = e.Target.Path
}
case SEclose:
if channel := FindChannel(e.Target.AppName, strings.TrimSuffix(e.Target.StreamName, "/rtsp")); channel != nil {
channel.LiveSubSP = ""
}
if v, ok := PullStreams.LoadAndDelete(e.Target.Path); ok {
go v.(*PullStream).Bye()
}
@@ -117,5 +122,5 @@ func (c *GB28181Config) IsMediaNetworkTCP() bool {
var conf GB28181Config
var plugin = InstallPlugin(&conf)
var GB28181Plugin = InstallPlugin(&conf)
var PullStreams sync.Map //拉流

47
ptz.go Normal file
View File

@@ -0,0 +1,47 @@
package gb28181
import "fmt"
var (
name2code = map[string]uint8{
"stop": 0,
"right": 1,
"left": 2,
"down": 4,
"downright": 5,
"downleft": 6,
"up": 8,
"upright": 9,
"upleft": 10,
"zoomin": 16,
"zoomout": 32,
}
)
func toPtzStrByCmdName(cmdName string, horizontalSpeed, verticalSpeed, zoomSpeed uint8) (string, error) {
c, err := toPtzCode(cmdName)
if err != nil {
return "", err
}
return toPtzStr(c, horizontalSpeed, verticalSpeed, zoomSpeed), nil
}
func toPtzStr(cmdCode, horizontalSpeed, verticalSpeed, zoomSpeed uint8) string {
checkCode := uint16(0xA5+0x0F+0x01+cmdCode+horizontalSpeed+verticalSpeed+(zoomSpeed&0xF0)) % 0x100
return fmt.Sprintf("A50F01%02X%02X%02X%01X0%02X",
cmdCode,
horizontalSpeed,
verticalSpeed,
zoomSpeed>>4, // 根据 GB28181 协议zoom 只取 4 bit
checkCode,
)
}
func toPtzCode(cmd string) (uint8, error) {
if code, ok := name2code[cmd]; ok {
return code, nil
} else {
return 0, fmt.Errorf("invalid ptz cmd %q", cmd)
}
}

View File

@@ -1,8 +1,11 @@
package gb28181
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"m7s.live/engine/v4/util"
@@ -10,6 +13,7 @@ import (
func (c *GB28181Config) API_list(w http.ResponseWriter, r *http.Request) {
util.ReturnJson(func() (list []*Device) {
list = make([]*Device, 0)
Devices.Range(func(key, value interface{}) bool {
device := value.(*Device)
if time.Since(device.UpdateTime) > c.RegisterValidity {
@@ -24,12 +28,23 @@ func (c *GB28181Config) API_list(w http.ResponseWriter, r *http.Request) {
}
func (c *GB28181Config) API_records(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
channel := r.URL.Query().Get("channel")
startTime := r.URL.Query().Get("startTime")
endTime := r.URL.Query().Get("endTime")
query := r.URL.Query()
id := query.Get("id")
channel := query.Get("channel")
startTime := query.Get("startTime")
endTime := query.Get("endTime")
trange := strings.Split(query.Get("range"), "-")
if len(trange) == 2 {
startTime = trange[0]
endTime = trange[1]
}
if c := FindChannel(id, channel); c != nil {
w.WriteHeader(c.QueryRecord(startTime, endTime))
res, err := c.QueryRecord(startTime, endTime)
if err == nil {
WriteJSONOk(w, res)
} else {
WriteJSON(w, err.Error(), http.StatusInternalServerError)
}
} else {
http.NotFound(w, r)
}
@@ -46,6 +61,40 @@ func (c *GB28181Config) API_control(w http.ResponseWriter, r *http.Request) {
}
}
func (c *GB28181Config) API_ptz(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
id := q.Get("id")
channel := q.Get("channel")
cmd := q.Get("cmd") // 命令名称,见 ptz.go name2code 定义
hs := q.Get("hSpeed") // 水平速度
vs := q.Get("vSpeed") // 垂直速度
zs := q.Get("zSpeed") // 缩放速度
hsN, err := strconv.ParseUint(hs, 10, 8)
if err != nil {
WriteJSON(w, "hSpeed parameter is invalid", 400)
}
vsN, err := strconv.ParseUint(vs, 10, 8)
if err != nil {
WriteJSON(w, "vSpeed parameter is invalid", 400)
}
zsN, err := strconv.ParseUint(zs, 10, 8)
if err != nil {
WriteJSON(w, "zSpeed parameter is invalid", 400)
}
ptzcmd, err := toPtzStrByCmdName(cmd, uint8(hsN), uint8(vsN), uint8(zsN))
if err != nil {
WriteJSON(w, err.Error(), 400)
}
if c := FindChannel(id, channel); c != nil {
code := c.Control(ptzcmd)
WriteJSON(w, "device received", code)
} else {
WriteJSON(w, fmt.Sprintf("device %q channel %q not found", id, channel), 404)
}
}
func (c *GB28181Config) API_invite(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
id := query.Get("id")
@@ -57,11 +106,18 @@ func (c *GB28181Config) API_invite(w http.ResponseWriter, r *http.Request) {
MediaPort: uint16(port),
StreamPath: streamPath,
}
opt.Validate(query.Get("startTime"), query.Get("endTime"))
startTime := query.Get("startTime")
endTime := query.Get("endTime")
trange := strings.Split(query.Get("range"), "-")
if len(trange) == 2 {
startTime = trange[0]
endTime = trange[1]
}
opt.Validate(startTime, endTime)
if c := FindChannel(id, channel); c == nil {
http.NotFound(w, r)
} else if opt.IsLive() && c.status.Load() > 0 {
w.WriteHeader(304) //直播流已存在
http.Error(w, "live stream already exists", http.StatusNotModified)
} else if code, err := c.Invite(&opt); err == nil {
w.WriteHeader(code)
} else {
@@ -135,3 +191,13 @@ func (c *GB28181Config) API_get_position(w http.ResponseWriter, r *http.Request)
return
}, c.Position.Interval, w, r)
}
func WriteJSONOk(w http.ResponseWriter, data interface{}) {
WriteJSON(w, data, 200)
}
func WriteJSON(w http.ResponseWriter, data interface{}, status int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}

View File

@@ -118,7 +118,7 @@ func RequestForResponse(transport string, request sip.Request,
func (c *GB28181Config) startServer() {
addr := c.ListenAddr + ":" + strconv.Itoa(int(c.SipPort))
logger := utils.NewZapLogger(plugin.Logger, "GB SIP Server", nil)
logger := utils.NewZapLogger(GB28181Plugin.Logger, "GB SIP Server", nil)
logger.SetLevel(levelMap[c.LogLevel])
// logger := log.NewDefaultLogrusLogger().WithPrefix("GB SIP Server")
srvConf := gosip.ServerConfig{}
@@ -132,9 +132,9 @@ func (c *GB28181Config) startServer() {
srv.OnRequest(sip.BYE, c.OnBye)
err := srv.Listen(strings.ToLower(c.SipNetwork), addr)
if err != nil {
plugin.Logger.Error("gb28181 server listen", zap.Error(err))
GB28181Plugin.Logger.Error("gb28181 server listen", zap.Error(err))
} else {
plugin.Info(fmt.Sprint(aurora.Green("Server gb28181 start at"), aurora.BrightBlue(addr)))
GB28181Plugin.Info(fmt.Sprint(aurora.Green("Server gb28181 start at"), aurora.BrightBlue(addr)))
}
if c.MediaNetwork == "tcp" {