Compare commits

...

51 Commits

Author SHA1 Message Date
langhuihui
d2cc62ff9e 修改一个变量名拼写错误 2023-07-07 14:50:09 +08:00
dexter
2e8aa47bc5 Merge pull request #99 from kingecg/v4
修复:按需拉流支持回放流
2023-07-04 18:42:09 +08:00
程广
585d5949d3 fix query record 2023-07-04 18:17:11 +08:00
kingecg
0285236cce 修复:按需拉流支持回放流 2023-06-30 22:31:51 +08:00
dexter
12895fa2cc Merge pull request #98 from kingecg/patch-1
Update channel.go to support device not use tcp
2023-06-30 21:24:07 +08:00
kingecg
c66303e7e8 Update channel.go to support device not use tcp
when media set to tcp and device not support, fallback to udp
2023-06-30 19:13:47 +08:00
ogofly
5435c2ef1c Merge pull request #95 from rufftio/v4
设备状态变更处理
2023-06-20 14:38:59 +08:00
ogofly
d8c6ad30dd 合并三处定时任务到一个协程 2023-06-19 15:50:31 +08:00
ogofly
86fa7cc7e6 Merge branch 'Monibuca:v4' into v4 2023-06-19 14:33:50 +08:00
liuyancong
692ec21877 定时删除注册超时设备,定时设置心跳超时设备为离线, 规范设备和通道状态为枚举量 2023-06-19 14:33:18 +08:00
liuyancong
71f2b36d2d update: 默认注册有效期配置为 3600s 2023-06-19 13:20:02 +08:00
dexter
b068bd9e5b Merge pull request #94 from rufftio/v4
add:处理 Register 消息的注销情况,将设备从列表中清除
2023-06-19 12:53:10 +08:00
liuyancong
78ac89e7af add:处理 Register 消息的注销情况,将设备从列表中清除 2023-06-19 11:56:17 +08:00
dexter
1cec5301c3 Merge pull request #92 from rufftio/v4
使用 engine 的 stream pasue 和 resume 替代 neverTimeout
2023-06-07 11:09:54 +08:00
ogofly
319f7fc636 Merge branch 'Monibuca:v4' into v4 2023-06-07 11:08:22 +08:00
liuyancong
7a75810203 使用 engine 的 stream pasue 和 resume 替代 neverTimeout 2023-06-07 10:46:42 +08:00
dexter
bbc7b09835 Merge pull request #91 from rufftio/v4
fix: ptz api 参数违规返回问题
2023-06-06 19:03:00 +08:00
liuyancong
1dbdff1fe5 add: 录像播放的暂停、恢复、快进、跳转到制定时间接口 2023-06-06 18:54:27 +08:00
ogofly
952c8f0ff8 Merge branch 'Monibuca:v4' into v4 2023-06-05 22:55:29 +08:00
liuyancong
730f3014f8 fix: ptz api 参数违规返回问题 2023-06-05 19:02:14 +08:00
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
langhuihui
37fd121d11 feat: change to use ps plugin 2023-05-14 11:12:25 +08:00
langhuihui
05fd8c38f7 feat: add a new way to config port 2023-05-04 09:57:20 +08:00
dexter
2d85e46a8b ChannelEx 2023-04-09 11:08:23 +08:00
charlestamz
4a90d7bf91 修复Recover导致断流的问题 2023-04-05 22:25:53 +08:00
dexter
a020f3ea81 适配引擎修改 2023-04-04 20:20:27 +08:00
dexter
c68862160f sip监听网卡地址可配,dump 可同时多路,修复 tcp 方式,媒体默认使用 tcp 2023-03-28 19:42:37 +08:00
dexter
9db29a9280 rtp引用采用v1版 2023-03-13 13:12:17 +08:00
charlestamz
5d5dae8939 增加了是否排序的功能 2023-03-09 05:57:40 +08:00
charlestamz
a0f16d1184 修复id小于9位时报错
增加一些更易排错的信息
2023-03-04 23:03:03 +08:00
charlestamz
53ddc0cb63 修复catalog报文parentid的处理 2023-03-01 17:15:04 +08:00
charlestamz
d156974f73 国标向上级联的一些改动 2023-03-01 17:06:34 +08:00
dexter
f3046bcde3 将ps发布逻辑移入引擎中 2023-02-28 20:13:19 +08:00
charlestamz
9c970ad282 注释暂不处理级联目录信息 2023-02-27 14:25:41 +08:00
charlestamz
c4de92e9f6 修复MediaPortMin无法使用的问题 2023-02-22 23:44:44 +08:00
charlestamz
cf5a803971 优化Invite,代码优化 2023-02-22 23:13:08 +08:00
dexter
f487be5fdb invite恢复成channelID 2023-02-21 21:28:23 +08:00
dexter
bd70d24a16 Merge pull request #81 from WXC9102/v4
修复对平台等非摄像头通道进行invite的问题
2023-02-21 14:02:02 +08:00
weixuechao
708cd042df 增加配置项inviteids,修复对平台等非摄像头通道进行invite的问题 2023-02-21 13:42:32 +08:00
charlestamz
a69b739e5e 修复Invite报文 2023-02-20 16:41:55 +08:00
dexter
4e96efa9ff fix: 丢包逻辑判断第一个包会丢掉 2023-02-20 16:20:28 +08:00
dexter
3a704b68cc 采用引擎处理ps 2023-02-20 00:20:20 +08:00
19 changed files with 1370 additions and 1330 deletions

View File

@@ -18,59 +18,29 @@ _ "m7s.live/plugin/gb28181/v4"
```yaml
gb28181:
autoinvite: true
invitemode: 1 #0、手动invite 1、表示自动发起invite当ServerSIP接收到设备信息时立即向设备发送invite命令获取流,2、按需拉流既等待订阅者触发
position:
autosubposition: false #是否自动订阅定位
expires: 3600s #订阅周期(单位:秒)默认3600
interval: 6s #订阅间隔单位默认6
prefetchrecord: false
udpcachesize: 0
sipnetwork: udp
sipip: ""
sipport: 5060
udpcachesize: 0 #表示UDP缓存大小默认为0不开启。仅当TCP关闭切缓存大于0时才开启
sipip: "" #sip服务器地址 默认 自动适配设备网段
serial: "34020000002000000001"
realm: "3402000000"
username: ""
password: ""
registervalidity: 60s
registervalidity: 60s #注册有效期
mediaip: ""
mediaport: 58200
mediaidletimeout: 30
medianetwork: udp
mediaportmin: 0
meidaportmax: 0
mediaip: "" #媒体服务器地址 默认 自动适配设备网段
port:
sip: udp:5060 #sip服务器端口
media: tcp:58200 #媒体服务器端口,用于接收设备的流,范围端口表示法udp:50000-60000
removebaninterval: 10m
removebaninterval: 10m #定时移除注册失败的设备黑名单单位秒默认10分钟600秒
loglevel: info
```
- `AutoInvite` bool 表示自动发起invite当ServerSIP接收到设备信息时立即向设备发送invite命令获取流
- `PreFetchRecord` bool
* sip服务器的配置
- `SipNetwork` string 传输协议默认UDP可选TCP
- `SipIP` string sip 服务器公网IP 默认 自动适配设备网段
- `SipPort` uint16 sip 服务器端口,默认 5060
- `Serial` string sip 服务器 id, 默认 34020000002000000001
- `Realm` string sip 服务器域,默认 3402000000
- `Username` string sip 服务器账号
- `Password` string sip 服务器密码
- `RegisterValidity` time.Duration 注册有效期,单位秒,默认 60
* 媒体服务器配置
- `MediaIP` string 媒体服务器地址 默认 自动适配设备网段
- `MediaPort` uint16 媒体服务器端口
- `MediaNetwork` string 媒体传输协议默认UDP可选TCP
- `MediaIdleTimeout` uint16 推流超时时间,超过则断开链接,让设备重连
- `MediaPortMin` uint16 媒体服务器端口范围最小值
- `MediaPortMax` uint16 媒体服务器端口范围最大值
- `LogLevel` string 日志级别,默认 infotracedebuginfowarnerrorfatal, panic
- `RemoveBanInterval` time.Duration 定时移除注册失败的设备黑名单单位秒默认10分钟600秒
- `UdpCacheSize` int 表示UDP缓存大小默认为0不开启。仅当TCP关闭切缓存大于0时才开启会最多缓存最多N个包并排序修复乱序造成的无法播放问题注意开启后会有一定的性能损耗并丢失部分包。
**如果配置了端口范围,将采用范围端口机制,每一个流对应一个端口
**注意某些摄像机没有设置用户名的地方摄像机会以自身的国标id作为用户名这个时候m7s会忽略使用摄像机的用户名忽略配置的用户名**
@@ -132,7 +102,7 @@ type Device struct {
| startTime | 否 | 开始时间纯数字Unix时间戳 |
| endTime | 否 | 结束时间纯数字Unix时间戳 |
返回200代表成功
返回200代表成功, 304代表已经在拉取中不能重复拉仅仅针对直播流
### 停止从设备拉流
@@ -143,6 +113,8 @@ type Device struct {
| id | 是 | 设备ID |
| channel | 是 | 通道编号 |
http 200 表示成功404流不存在
### 发送控制命令
`/gb28181/api/control`
@@ -161,8 +133,8 @@ type Device struct {
| --------- | ---- | -------------------------------------------- |
| id | 是 | 设备ID |
| channel | 是 | 通道编号 |
| startTime | 否 | 开始时间(字符串格式2021-7-23T12:00:00 |
| endTime | 否 | 结束时间(字符串格式同上 |
| startTime | 否 | 开始时间(Unix时间戳 |
| endTime | 否 | 结束时间(Unix时间戳 |
### 移动位置订阅

View File

@@ -1,41 +1,127 @@
package gb28181
import (
"errors"
"fmt"
"math/rand"
"net/http"
"strconv"
"strings"
"sync"
"time"
"sync/atomic"
"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"
)
type ChannelEx struct {
device *Device
RecordPublisher *GBPublisher `json:"-"`
LivePublisher *GBPublisher
LiveSubSP string //实时子码流
Records []*Record
RecordStartTime string
RecordEndTime string
recordStartTime time.Time
recordEndTime time.Time
liveInviteLock *sync.Mutex
tcpPortIndex uint16
GpsTime time.Time //gps时间
Longitude string //经度
Latitude string //纬度
var QUERY_RECORD_TIMEOUT = time.Second * 5
type PullStream struct {
opt *InviteOptions
channel *Channel
inviteRes sip.Response
}
func (p *PullStream) CreateRequest(method sip.RequestMethod) (req sip.Request) {
res := p.inviteRes
req = p.channel.CreateRequst(method)
from, _ := res.From()
to, _ := res.To()
callId, _ := res.CallID()
req.ReplaceHeaders(from.Name(), []sip.Header{from})
req.ReplaceHeaders(to.Name(), []sip.Header{to})
req.ReplaceHeaders(callId.Name(), []sip.Header{callId})
return
}
func (p *PullStream) Bye() int {
req := p.CreateRequest(sip.BYE)
resp, err := p.channel.device.SipRequestForResponse(req)
if p.opt.IsLive() {
p.channel.status.Store(0)
}
if p.opt.recyclePort != nil {
p.opt.recyclePort(p.opt.MediaPort)
}
if err != nil {
return http.StatusInternalServerError
}
return int(resp.StatusCode())
}
func (p *PullStream) info(body string) int {
d := p.channel.device
req := p.CreateRequest(sip.INFO)
contentType := sip.ContentType("Application/MANSRTSP")
req.AppendHeader(&contentType)
req.SetBody(body, true)
resp, err := d.SipRequestForResponse(req)
if err != nil {
log.Warnf("Send info to stream error: %v, stream=%s, body=%s", err, p.opt.StreamPath, body)
return getSipRespErrorCode(err)
}
return int(resp.StatusCode())
}
// 暂停播放
func (p *PullStream) Pause() int {
body := fmt.Sprintf(`PAUSE RTSP/1.0
CSeq: %d
PauseTime: now
`, p.channel.device.sn)
return p.info(body)
}
// 恢复播放
func (p *PullStream) Resume() int {
d := p.channel.device
body := fmt.Sprintf(`PLAY RTSP/1.0
CSeq: %d
Range: npt=now-
`, d.sn)
return p.info(body)
}
// 跳转到播放时间
// second: 相对于起始点调整到第 sec 秒播放
func (p *PullStream) PlayAt(second uint) int {
d := p.channel.device
body := fmt.Sprintf(`PLAY RTSP/1.0
CSeq: %d
Range: npt=%d-
`, d.sn, second)
return p.info(body)
}
// 快进/快退播放
// speed 取值: 0.25 0.5 1 2 4 或者其对应的负数表示倒放
func (p *PullStream) PlayForward(speed float32) int {
d := p.channel.device
body := fmt.Sprintf(`PLAY RTSP/1.0
CSeq: %d
Scale: %0.6f
`, d.sn, speed)
return p.info(body)
}
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
@@ -48,11 +134,16 @@ type Channel struct {
SafetyWay int
RegisterWay int
Secrecy int
Status string
Children []*Channel `json:"-"`
ChannelEx //自定义属性
Status ChannelStatus
}
type ChannelStatus string
const (
ChannelOnStatus = "ON"
ChannelOffStatus = "OFF"
)
func (channel *Channel) CreateRequst(Method sip.RequestMethod) (req sip.Request) {
d := channel.device
d.sn++
@@ -77,9 +168,13 @@ func (channel *Channel) CreateRequst(Method sip.RequestMethod) (req sip.Request)
//非同一域的目标地址需要使用@host
host := conf.Realm
if channel.DeviceID[0:9] != host {
deviceIp := d.NetAddr
deviceIp = deviceIp[0:strings.LastIndex(deviceIp, ":")]
host = fmt.Sprintf("%s:%d", deviceIp, channel.Port)
if channel.Port != 0 {
deviceIp := d.NetAddr
deviceIp = deviceIp[0:strings.LastIndex(deviceIp, ":")]
host = fmt.Sprintf("%s:%d", deviceIp, channel.Port)
} else {
host = d.NetAddr
}
}
channelAddr := sip.Address{
@@ -108,33 +203,41 @@ 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)
body := fmt.Sprintf(`<?xml version="1.0"?>
<Query>
<CmdType>RecordInfo</CmdType>
<SN>%d</SN>
<DeviceID>%s</DeviceID>
<StartTime>%s</StartTime>
<EndTime>%s</EndTime>
<Secrecy>0</Secrecy>
<Type>all</Type>
</Query>`, d.sn, channel.DeviceID, startTime, endTime)
// body := fmt.Sprintf(`<?xml version="1.0"?>
// <Query>
// <CmdType>RecordInfo</CmdType>
// <SN>%d</SN>
// <DeviceID>%s</DeviceID>
// <StartTime>%s</StartTime>
// <EndTime>%s</EndTime>
// <Secrecy>0</Secrecy>
// <Type>all</Type>
// </Query>`, d.sn, channel.DeviceID, startTime, endTime)
start, _ := strconv.ParseInt(startTime, 10, 0)
end, _ := strconv.ParseInt(endTime, 10, 0)
body := BuildRecordInfoXML(d.sn, channel.DeviceID, start, end)
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)
@@ -155,64 +258,8 @@ func (channel *Channel) Control(PTZCmd string) int {
return int(resp.StatusCode())
}
type InviteOptions struct {
Start int
End int
dump string
ssrc string
SSRC uint32
MediaPort uint16
}
func (o InviteOptions) IsLive() bool {
return o.Start == 0 || o.End == 0
}
func (o InviteOptions) Record() bool {
return !o.IsLive()
}
func (o *InviteOptions) Validate(start, end string) error {
if start != "" {
sint, err1 := strconv.ParseInt(start, 10, 0)
if err1 != nil {
return err1
}
o.Start = int(sint)
}
if end != "" {
eint, err2 := strconv.ParseInt(end, 10, 0)
if err2 != nil {
return err2
}
o.End = int(eint)
}
if o.Start >= o.End {
return errors.New("start < end")
}
return nil
}
func (o InviteOptions) String() string {
return fmt.Sprintf("t=%d %d", o.Start, o.End)
}
func (o *InviteOptions) CreateSSRC() {
ssrc := make([]byte, 10)
if o.IsLive() {
ssrc[0] = '0'
} else {
ssrc[0] = '1'
}
copy(ssrc[1:6], conf.Serial[3:8])
randNum := 1000 + rand.Intn(8999)
copy(ssrc[6:], strconv.Itoa(randNum))
o.ssrc = string(ssrc)
_ssrc, _ := strconv.ParseInt(o.ssrc, 10, 0)
o.SSRC = uint32(_ssrc)
}
//Invite 发送Invite报文注意里面的锁保证不同时发送invite报文该锁由channel持有
// Invite 发送Invite报文 invites a channel to play
// 注意里面的锁保证不同时发送invite报文该锁由channel持有
/***
f字段 f = v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率
各项具体含义:
@@ -257,18 +304,27 @@ f = v/a/编码格式/码率大小/采样率
f字段中视、音频参数段之间不需空格分割。
可使用f字段中的分辨率参数标识同一设备不同分辨率的码流。
*/
func (channel *Channel) Invite(opt InviteOptions) (code int, err error) {
func (channel *Channel) Invite(opt *InviteOptions) (code int, err error) {
if opt.IsLive() {
if !channel.liveInviteLock.TryLock() {
if !channel.status.CompareAndSwap(0, 1) {
return 304, nil
}
defer func() {
if code != 200 {
channel.liveInviteLock.Unlock()
if err != nil {
GB28181Plugin.Error("Invite", zap.Error(err))
channel.status.Store(0)
if conf.InviteMode == 1 {
// 5秒后重试
time.AfterFunc(time.Second*5, func() {
channel.Invite(opt)
})
}
} else {
channel.status.Store(2)
}
}()
}
channel.Bye(opt.IsLive())
d := channel.device
streamPath := fmt.Sprintf("%s/%s", d.ID, channel.DeviceID)
s := "Play"
@@ -277,49 +333,39 @@ func (channel *Channel) Invite(opt InviteOptions) (code int, err error) {
s = "Playback"
streamPath = fmt.Sprintf("%s/%s/%d-%d", d.ID, channel.DeviceID, opt.Start, opt.End)
}
if opt.StreamPath != "" {
streamPath = opt.StreamPath
} else {
opt.StreamPath = streamPath
}
if opt.dump == "" {
opt.dump = conf.DumpPath
}
// size := 1
// fps := 15
// bitrate := 200
// fmt.Sprintf("f=v/2/%d/%d/1/%da///", size, fps, bitrate)
publisher := &GBPublisher{
InviteOptions: opt,
channel: channel,
}
protocol := ""
networkType := "udp"
reusePort := true
if conf.IsMediaNetworkTCP() {
networkType = "tcp"
protocol = "TCP/"
if conf.tcpPorts.Valid {
opt.MediaPort, err = publisher.ListenTCP()
if err != nil {
return 500, err
}
} else if opt.MediaPort == 0 {
opt.MediaPort = conf.MediaPort
opt.MediaPort, err = conf.tcpPorts.GetPort()
opt.recyclePort = conf.tcpPorts.Recycle
reusePort = false
}
} else {
if conf.udpPorts.Valid {
opt.MediaPort, err = publisher.ListenUDP()
if err != nil {
code = 500
return
}
} else if opt.MediaPort == 0 {
opt.MediaPort = conf.MediaPort
opt.MediaPort, err = conf.udpPorts.GetPort()
opt.recyclePort = conf.udpPorts.Recycle
reusePort = false
}
}
// if opt.MediaPort == 0 {
// opt.MediaPort = conf.MediaPort
// if conf.IsMediaNetworkTCP() {
// protocol = "TCP/"
// opt.MediaPort = conf.MediaPort + channel.tcpPortIndex
// if channel.tcpPortIndex++; channel.tcpPortIndex >= conf.MediaPortMax {
// channel.tcpPortIndex = 0
// }
// }
// }
if err != nil {
return http.StatusInternalServerError, err
}
if opt.MediaPort == 0 {
opt.MediaPort = conf.MediaPort
}
sdpInfo := []string{
"v=0",
fmt.Sprintf("o=%s 0 0 IN IP4 %s", channel.DeviceID, d.mediaIP),
@@ -331,7 +377,6 @@ func (channel *Channel) Invite(opt InviteOptions) (code int, err error) {
"a=recvonly",
"a=rtpmap:96 PS/90000",
"y=" + opt.ssrc,
"",
}
if conf.IsMediaNetworkTCP() {
sdpInfo = append(sdpInfo, "a=setup:passive", "a=connection:new")
@@ -340,61 +385,153 @@ func (channel *Channel) Invite(opt InviteOptions) (code int, err error) {
contentType := sip.ContentType("application/sdp")
invite.AppendHeader(&contentType)
invite.SetBody(strings.Join(sdpInfo, "\r\n"), true)
invite.SetBody(strings.Join(sdpInfo, "\r\n")+"\r\n", true)
subject := sip.GenericHeader{
HeaderName: "Subject", Contents: fmt.Sprintf("%s:%s,%s:0", channel.DeviceID, opt.ssrc, conf.Serial),
}
invite.AppendHeader(&subject)
publisher.inviteRes, err = d.SipRequestForResponse(invite)
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(publisher.inviteRes.StatusCode())
plugin.Info(fmt.Sprintf("Channel :%s invite response status code: %d", channel.DeviceID, code))
if code == 200 {
ds := strings.Split(publisher.inviteRes.Body(), "\r\n")
code = int(inviteRes.StatusCode())
channel.Info("invite response", zap.Int("status code", code))
if code == http.StatusOK {
ds := strings.Split(inviteRes.Body(), "\r\n")
for _, l := range ds {
if ls := strings.Split(l, "="); len(ls) > 1 {
if ls[0] == "y" && len(ls[1]) > 0 {
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
}
if ls[0] == "m" && len(ls[1]) > 0 {
netinfo := strings.Split(ls[1], " ")
if strings.ToUpper(netinfo[2]) == "TCP/RTP/AVP" {
channel.Debug("Device support tcp")
} else {
channel.Debug("Device not support tcp")
networkType = "udp"
}
break
}
}
}
if conf.UdpCacheSize > 0 && !conf.IsMediaNetworkTCP() {
publisher.udpCache = utils.NewPqRtp()
err = ps.Receive(streamPath, opt.dump, fmt.Sprintf("%s:%d", networkType, opt.MediaPort), opt.SSRC, reusePort)
if err == nil {
PullStreams.Store(streamPath, &PullStream{
opt: opt,
channel: channel,
inviteRes: inviteRes,
})
err = srv.Send(sip.NewAckRequest("", invite, inviteRes, "", nil))
}
if err = plugin.Publish(streamPath, publisher); err != nil {
code = 403
return
}
ack := sip.NewAckRequest("", invite, publisher.inviteRes, "", nil)
srv.Send(ack)
} else if opt.IsLive() && conf.AutoInvite {
time.AfterFunc(time.Second*5, func() {
channel.Invite(InviteOptions{})
})
}
return
}
func (channel *Channel) Bye(live bool) int {
func (channel *Channel) Bye(streamPath string) int {
d := channel.device
streamPath := fmt.Sprintf("%s/%s", d.ID, channel.DeviceID)
if s := Streams.Get(streamPath); s != nil {
s.Close()
if streamPath == "" {
streamPath = fmt.Sprintf("%s/%s", d.ID, channel.DeviceID)
}
if live && channel.LivePublisher != nil {
return channel.LivePublisher.Bye()
if s, loaded := PullStreams.LoadAndDelete(streamPath); loaded {
s.(*PullStream).Bye()
if s := Streams.Get(streamPath); s != nil {
s.Close()
}
return http.StatusOK
}
return http.StatusNotFound
}
func (channel *Channel) Pause(streamPath string) int {
if s, loaded := PullStreams.Load(streamPath); loaded {
r := s.(*PullStream).Pause()
if s := Streams.Get(streamPath); s != nil {
s.Pause()
}
return r
}
return http.StatusNotFound
}
func (channel *Channel) Resume(streamPath string) int {
if s, loaded := PullStreams.Load(streamPath); loaded {
r := s.(*PullStream).Resume()
if s := Streams.Get(streamPath); s != nil {
s.Resume()
}
return r
}
return http.StatusNotFound
}
func (channel *Channel) PlayAt(streamPath string, second uint) int {
if s, loaded := PullStreams.Load(streamPath); loaded {
r := s.(*PullStream).PlayAt(second)
if s := Streams.Get(streamPath); s != nil {
s.Resume()
}
return r
}
return http.StatusNotFound
}
func (channel *Channel) PlayForward(streamPath string, speed float32) int {
if s, loaded := PullStreams.Load(streamPath); loaded {
return s.(*PullStream).PlayForward(speed)
}
if s := Streams.Get(streamPath); s != nil {
s.Resume()
}
return http.StatusNotFound
}
func (channel *Channel) TryAutoInvite(opt *InviteOptions) {
if channel.CanInvite() {
go channel.Invite(opt)
}
}
func (channel *Channel) CanInvite() bool {
if channel.status.Load() != 0 || len(channel.DeviceID) != 20 || channel.Status == ChannelOffStatus {
return false
}
if conf.InviteIDs == "" {
return true
}
// 1113位是设备类型编码
typeID := channel.DeviceID[10:13]
// format: start-end,type1,type2
tokens := strings.Split(conf.InviteIDs, ",")
for _, tok := range tokens {
if first, second, ok := strings.Cut(tok, "-"); ok {
if typeID >= first && typeID <= second {
return true
}
} else {
if typeID == first {
return true
}
}
}
return false
}
func getSipRespErrorCode(err error) int {
if re, ok := err.(*sip.RequestError); ok {
return int(re.Code)
} else {
return http.StatusInternalServerError
}
if !live && channel.RecordPublisher != nil {
return channel.RecordPublisher.Bye()
}
return 404
}

66
const.go Normal file
View File

@@ -0,0 +1,66 @@
package gb28181
var reasons = map[int]string{
100: "Trying",
180: "Ringing",
181: "Call Is Being Forwarded",
182: "Queued",
183: "Session Progress",
200: "OK",
202: "Accepted",
300: "Multiple Choices",
301: "Moved Permanently",
302: "Moved Temporarily",
305: "Use Proxy",
380: "Alternative Service",
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Timeout",
410: "Gone",
413: "Request Entity Too Large",
414: "Request-URI Too Long",
415: "Unsupported Media Type",
416: "Unsupported URI Scheme",
420: "Bad Extension",
421: "Extension Required",
423: "Interval Too Brief",
480: "Temporarily Unavailable",
481: "Call transaction Does Not Exist",
482: "Loop Detected",
483: "Too Many Hops",
484: "Address Incomplete",
485: "Ambiguous",
486: "Busy Here",
487: "Request Terminated",
488: "Not Acceptable Here",
489: "Bad Event",
491: "Request Pending",
493: "Undecipherable",
500: "Server Internal Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Server Tim",
505: "Version Not Supported",
513: "message Too Large",
600: "Busy Everywhere",
603: "Decline",
604: "Does Not Exist Anywhere",
606: "SESSION NOT ACCEPTABLE",
}
func Explain(statusCode int) string {
return reasons[statusCode]
}
const (
INVIDE_MODE_MANUAL = iota
INVIDE_MODE_AUTO
INVIDE_MODE_ONSUBSCRIBE
)

197
device.go
View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"golang.org/x/exp/maps"
"net/http"
"os"
"strings"
@@ -13,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"
@@ -24,7 +24,6 @@ const TIME_LAYOUT = "2006-01-02T15:04:05"
// Record 录像
type Record struct {
//channel *Channel
DeviceID string
Name string
FilePath string
@@ -45,8 +44,18 @@ var (
DeviceRegisterCount sync.Map //设备注册次数
)
type DeviceStatus string
const (
DeviceRegisterStatus = "REGISTER"
DeviceRecoverStatus = "RECOVER"
DeviceOnlineStatus = "ONLINE"
DeviceOfflineStatus = "OFFLINE"
DeviceAlarmedStatus = "ALARMED"
)
type Device struct {
//*transaction.Core `json:"-"`
//*transaction.Core `json:"-" yaml:"-"`
ID string
Name string
Manufacturer string
@@ -55,14 +64,13 @@ type Device struct {
RegisterTime time.Time
UpdateTime time.Time
LastKeepaliveAt time.Time
Status string
Status DeviceStatus
sn int
addr sip.Address
sipIP string //设备对应网卡的服务器ip
mediaIP string //设备对应网卡的服务器ip
NetAddr string
channelMap map[string]*Channel
channelMutex sync.RWMutex
channelMap sync.Map
subscriber struct {
CallID string
Timeout time.Time
@@ -71,17 +79,23 @@ type Device struct {
GpsTime time.Time //gps时间
Longitude string //经度
Latitude string //纬度
*log.Logger `json:"-" yaml:"-"`
}
func (d *Device) MarshalJSON() ([]byte, error) {
type Alias Device
return json.Marshal(&struct {
Channels []*Channel
data := &struct {
Channels []*ChannelInfo
*Alias
}{
Channels: maps.Values(d.channelMap),
Alias: (*Alias)(d),
Alias: (*Alias)(d),
}
d.channelMap.Range(func(key, value interface{}) bool {
c := value.(*Channel)
data.Channels = append(data.Channels, &c.ChannelInfo)
return true
})
return json.Marshal(data)
}
func (c *GB28181Config) RecoverDevice(d *Device, req sip.Request) {
from, _ := req.From()
@@ -109,13 +123,12 @@ 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.Status = string(sip.REGISTER)
d.Info("RecoverDevice", zap.String("deviceIp", deviceIp), zap.String("servIp", servIp), zap.String("sipIP", sipIP), zap.String("mediaIp", mediaIp))
d.Status = DeviceRegisterStatus
d.sipIP = sipIP
d.mediaIP = mediaIp
d.NetAddr = deviceIp
d.UpdateTime = time.Now()
d.channelMap = make(map[string]*Channel)
}
func (c *GB28181Config) StoreDevice(id string, req sip.Request) (d *Device) {
@@ -130,7 +143,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
@@ -151,18 +164,18 @@ 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(),
UpdateTime: time.Now(),
Status: string(sip.REGISTER),
Status: DeviceRegisterStatus,
addr: deviceAddr,
sipIP: sipIP,
mediaIP: mediaIp,
NetAddr: deviceIp,
channelMap: make(map[string]*Channel),
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()
}
@@ -176,6 +189,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)
}
}
@@ -196,42 +210,31 @@ func (c *GB28181Config) SaveDevices() {
}
}
func (d *Device) addOrUpdateChannel(channel *Channel) {
d.channelMutex.Lock()
defer d.channelMutex.Unlock()
channel.device = d
var oldLock *sync.Mutex
if old, ok := d.channelMap[channel.DeviceID]; ok {
//复制锁指针
oldLock = old.liveInviteLock
}
if oldLock == nil {
channel.liveInviteLock = &sync.Mutex{}
func (d *Device) addOrUpdateChannel(info ChannelInfo) (c *Channel) {
if old, ok := d.channelMap.Load(info.DeviceID); ok {
c = old.(*Channel)
c.ChannelInfo = info
} else {
channel.liveInviteLock = oldLock
}
d.channelMap[channel.DeviceID] = channel
}
func (d *Device) deleteChannel(DeviceID string) {
d.channelMutex.Lock()
defer d.channelMutex.Unlock()
delete(d.channelMap, DeviceID)
}
func (d *Device) CheckSubStream() {
d.channelMutex.Lock()
defer d.channelMutex.Unlock()
for _, c := range d.channelMap {
if s := engine.Streams.Get("sub/" + c.DeviceID); s != nil {
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)
}
return
}
func (d *Device) UpdateChannels(list []*Channel) {
func (d *Device) deleteChannel(DeviceID string) {
d.channelMap.Delete(DeviceID)
}
func (d *Device) UpdateChannels(list ...ChannelInfo) {
for _, c := range list {
if _, ok := conf.Ignores[c.DeviceID]; ok {
continue
@@ -240,43 +243,32 @@ func (d *Device) UpdateChannels(list []*Channel) {
if c.ParentID != "" {
path := strings.Split(c.ParentID, "/")
parentId := path[len(path)-1]
if c.DeviceID != parentId {
//如果父ID并非本身所属设备一般情况下这是因为下级设备上传了目录信息该信息通常不需要处理。
// 暂时不考虑级联目录的实现
if d.ID != parentId {
if v, ok := Devices.Load(parentId); ok {
parent := v.(*Device)
parent.addOrUpdateChannel(c)
continue
} else {
c.Model = "Directory " + c.Model
c.Status = "NoParent"
}
}
}
//本设备增加通道
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.AutoInvite && (c.LivePublisher == nil) {
go c.Invite(InviteOptions{})
if conf.InviteMode == INVIDE_MODE_AUTO {
channel.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.channelMutex.RLock()
if c, ok := d.channelMap[channelId]; ok {
c.Records = append(c.Records, list...)
}
d.channelMutex.RUnlock()
}
func (d *Device) CreateRequest(Method sip.RequestMethod) (req sip.Request) {
d.sn++
@@ -360,7 +352,7 @@ func (d *Device) Subscribe() int {
response, err := d.SipRequestForResponse(request)
if err == nil && response != nil {
if response.StatusCode() == 200 {
if response.StatusCode() == http.StatusOK {
callId, _ := request.CallID()
d.subscriber.CallID = string(*callId)
} else {
@@ -382,10 +374,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 {
GB28181Plugin.Sugar().Debugf("SIP<-Catalog Response: %s", resp.String())
return int(resp.StatusCode())
} else if err != nil {
GB28181Plugin.Error("SIP<-Catalog error:", zap.Error(err))
}
return http.StatusRequestTimeout
}
@@ -407,8 +402,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() == 200 {
d.Info("QueryDeviceInfo", zap.Uint16("status code", uint16(response.StatusCode())))
if response.StatusCode() == http.StatusOK {
break
}
}
@@ -436,7 +431,7 @@ func (d *Device) MobilePositionSubscribe(id string, expires time.Duration, inter
response, err := d.SipRequestForResponse(mobilePosition)
if err == nil && response != nil {
if response.StatusCode() == 200 {
if response.StatusCode() == http.StatusOK {
callId, _ := mobilePosition.CallID()
d.subscriber.CallID = callId.String()
} else {
@@ -449,17 +444,18 @@ func (d *Device) MobilePositionSubscribe(id string, expires time.Duration, inter
// UpdateChannelPosition 更新通道GPS坐标
func (d *Device) UpdateChannelPosition(channelId string, gpsTime string, lng string, lat string) {
if c, ok := d.channelMap[channelId]; ok {
c.ChannelEx.GpsTime = time.Now() //时间取系统收到的时间,避免设备时间和格式问题
c.ChannelEx.Longitude = lng
c.ChannelEx.Latitude = lat
plugin.Sugar().Debugf("更新通道[%s]坐标成功\n", c.Name)
if v, ok := d.channelMap.Load(channelId); ok {
c := v.(*Channel)
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))
}
}
@@ -468,20 +464,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,
@@ -495,17 +491,17 @@ func (d *Device) UpdateChannelStatus(deviceList []*notifyMessage) {
SafetyWay: v.SafetyWay,
RegisterWay: v.RegisterWay,
Secrecy: v.Secrecy,
Status: v.Status,
Status: ChannelStatus(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,
@@ -519,28 +515,29 @@ func (d *Device) UpdateChannelStatus(deviceList []*notifyMessage) {
SafetyWay: v.SafetyWay,
RegisterWay: v.RegisterWay,
Secrecy: v.Secrecy,
Status: v.Status,
Status: ChannelStatus(v.Status),
}
channels := []*Channel{channel}
d.UpdateChannels(channels)
d.UpdateChannels(channel)
}
}
}
func (d *Device) channelOnline(DeviceID string) {
if c, ok := d.channelMap[DeviceID]; ok {
c.Status = "ON"
plugin.Sugar().Debugf("通道[%s]在线\n", c.Name)
if v, ok := d.channelMap.Load(DeviceID); ok {
c := v.(*Channel)
c.Status = ChannelOnStatus
c.Debug("channel online", zap.String("channelId", DeviceID))
} else {
plugin.Sugar().Debugf("更新通道[%s]状态失败,未找到\n", DeviceID)
d.Debug("update channel status failed, not found", zap.String("channelId", DeviceID))
}
}
func (d *Device) channelOffline(DeviceID string) {
if c, ok := d.channelMap[DeviceID]; ok {
c.Status = "OFF"
plugin.Sugar().Debugf("通道[%s]离线\n", c.Name)
if v, ok := d.channelMap.Load(DeviceID); ok {
c := v.(*Channel)
c.Status = ChannelOffStatus
c.Debug("channel offline", zap.String("channelId", DeviceID))
} else {
plugin.Sugar().Debugf("更新通道[%s]状态失败,未找到\n", DeviceID)
d.Debug("update channel status failed, not found", zap.String("channelId", DeviceID))
}
}

39
go.mod
View File

@@ -1,20 +1,23 @@
module m7s.live/plugin/gb28181/v4
go 1.18
go 1.19
require (
github.com/ghettovoice/gosip v0.0.0-20221121090201-9a2ed2233b6d
github.com/husanpao/ip v0.0.0-20220711082147-73160bb611a8
github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/pion/rtp/v2 v2.0.0-20220302185659-b3d10fc096b0
github.com/pion/rtp v1.7.13
go.uber.org/zap v1.23.0
golang.org/x/net v0.2.0
golang.org/x/text v0.4.0
m7s.live/engine/v4 v4.8.5
golang.org/x/net v0.8.0
golang.org/x/text v0.8.0
m7s.live/engine/v4 v4.12.8
m7s.live/plugin/ps/v4 v4.0.1
)
require (
github.com/aler9/gortsplib/v2 v2.2.2 // indirect
github.com/cnotch/ipchub v1.1.0 // indirect
github.com/denisbrodbeck/machineid v1.0.1 // indirect
github.com/discoviking/fsm v0.0.0-20150126104936-f4a273feecca // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
@@ -25,21 +28,20 @@ require (
github.com/golang/mock v1.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lucas-clemente/quic-go v0.31.0 // indirect
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect
github.com/marten-seemann/qtls-go1-18 v0.1.3 // indirect
github.com/marten-seemann/qtls-go1-19 v0.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mcuadros/go-defaults v1.2.0 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/ginkgo/v2 v2.2.0 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtp v1.7.13 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pion/webrtc/v3 v3.1.49 // indirect
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
github.com/q191201771/naza v0.30.8 // indirect
github.com/quic-go/qtls-go1-18 v0.2.0 // indirect
github.com/quic-go/qtls-go1-19 v0.2.0 // indirect
github.com/quic-go/qtls-go1-20 v0.1.0 // indirect
github.com/quic-go/quic-go v0.32.0 // indirect
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect
github.com/shirou/gopsutil/v3 v3.22.10 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
@@ -47,15 +49,16 @@ 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-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.3.0 // indirect
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 // indirect
golang.org/x/mod v0.7.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.2.0 // indirect
golang.org/x/term v0.2.0 // indirect
golang.org/x/tools v0.3.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/tools v0.6.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

141
go.sum
View File

@@ -1,5 +1,10 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/aler9/gortsplib/v2 v2.2.2 h1:tTw8pdKSOEjlZjjE1S4ftXPHJkYOqjNNv3hjQ0Nto9M=
github.com/aler9/gortsplib/v2 v2.2.2/go.mod h1:k6uBVHGwsIc/0L5SLLqWwi6bSJUb4VR0HfvncyHlKQI=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cnotch/apirouter v0.0.0-20200731232942-89e243a791f3/go.mod h1:5deJPLON/x/s2dLOQfuKS0lenhOIT4xX0pvtN/OEIuY=
github.com/cnotch/ipchub v1.1.0 h1:hH0lh2mU3AZXPiqMwA0pdtqrwo7PFIMRGush9OobMUs=
github.com/cnotch/ipchub v1.1.0/go.mod h1:2PbeBs2q2VxxTVCn1eYCDwpAWuVXbq1+N0FU7GimOH4=
@@ -8,16 +13,18 @@ github.com/cnotch/queue v0.0.0-20200326024423-6e88bdbf2ad4/go.mod h1:zOssjAlNusO
github.com/cnotch/queue v0.0.0-20201224060551-4191569ce8f6/go.mod h1:zOssjAlNusOxvtaqT+EMA+Iyi8rrtKr4/XfzN1Fgoeg=
github.com/cnotch/scheduler v0.0.0-20200522024700-1d2da93eefc5/go.mod h1:F4GE3SZkJZ8an1Y0ZCqvSM3jeozNuKzoC67erG1PhIo=
github.com/cnotch/xlog v0.0.0-20201208005456-cfda439cd3a0/go.mod h1:RW9oHsR79ffl3sR3yMGgxYupMn2btzdtJUwoxFPUE5E=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
github.com/discoviking/fsm v0.0.0-20150126104936-f4a273feecca h1:cTTdXpkQ1aVbOOmHwdwtYuwUZcQtcMrleD1UXLWhAq8=
github.com/discoviking/fsm v0.0.0-20150126104936-f4a273feecca/go.mod h1:W+3LQaEkN8qAwwcw0KC546sUEnX86GIT8CcMLZC4mG0=
github.com/emitter-io/address v1.0.0/go.mod h1:GfZb5+S/o8694B1GMGK2imUYQyn2skszMvGNA5D84Ug=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/ghettovoice/gosip v0.0.0-20221121090201-9a2ed2233b6d h1:f1JRfm0MwkluwtUsbYxuVReDMajlc9Wn6zc2orX4sRE=
github.com/ghettovoice/gosip v0.0.0-20221121090201-9a2ed2233b6d/go.mod h1:yTr3BEYSFe9As6XM7ldyrVgqsPwlnw8Ahc4N28VFM2g=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
@@ -40,45 +47,52 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/husanpao/ip v0.0.0-20220711082147-73160bb611a8 h1:4Jk58quTZmzJcTrLlbB5L1Q6qXu49EIjCReWxcBFWKo=
github.com/husanpao/ip v0.0.0-20220711082147-73160bb611a8/go.mod h1:medl9/CfYoQlqAXtAARmMW5dAX2UOdwwkhaszYPk0AM=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/kelindar/process v0.0.0-20170730150328-69a29e249ec3/go.mod h1:+lTCLnZFXOkqwD8sLPl6u4erAc0cP8wFegQHfipz7KE=
github.com/kelindar/rate v1.0.0/go.mod h1:AjT4G+hTItNwt30lucEGZIz8y7Uk5zPho6vurIZ+1Es=
github.com/kelindar/tcp v1.0.0/go.mod h1:JB5hj1cshLU60XrLij2BBxW3JQ4hOye8vqbyvuKb52k=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/lucas-clemente/quic-go v0.31.0 h1:MfNp3fk0wjWRajw6quMFA3ap1AVtlU+2mtwmbVogB2M=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c h1:VtwQ41oftZwlMnOEbMWQtSEUgU64U4s+GHk7hZK+jtY=
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
github.com/marten-seemann/qtls-go1-18 v0.1.3 h1:R4H2Ks8P6pAtUagjFty2p7BVHn3XiwDAl7TTQf5h7TI=
github.com/marten-seemann/qtls-go1-18 v0.1.3/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
github.com/marten-seemann/qtls-go1-19 v0.1.1 h1:mnbxeq3oEyQxQXwI4ReCgW9DPoPR94sNlqWoDZnjRIE=
github.com/marten-seemann/qtls-go1-19 v0.1.1/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mcuadros/go-defaults v1.2.0 h1:FODb8WSf0uGaY8elWJAkoLL0Ri6AlZ1bFlenk56oZtc=
github.com/mcuadros/go-defaults v1.2.0/go.mod h1:WEZtHEVIGYVDqkKSWBdWKUVdRyKlMfulPaGDWIVeCWY=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.5/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
@@ -87,24 +101,45 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI=
github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
github.com/pion/ice/v2 v2.2.12/go.mod h1:z2KXVFyRkmjetRlaVRgjO9U3ShKwzhlUylvxKfHfd5A=
github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/rtp/v2 v2.0.0-20220302185659-b3d10fc096b0 h1:zyOGxHutZ6IhksQSMtwf3OFXB29W5R18yFQWOQJYWjU=
github.com/pion/rtp/v2 v2.0.0-20220302185659-b3d10fc096b0/go.mod h1:Vj+rrFbJCT3yxqE/VSwaOo9DQ2pMKGPxuE7hplGOlOs=
github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
github.com/pion/sctp v1.8.3/go.mod h1:OHbDjdk7kg+L+7TJim9q/qGVefdEJohuA2SZyihccgI=
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
github.com/pion/srtp/v2 v2.0.10/go.mod h1:XEeSWaK9PfuMs7zxXyiN252AHPbH12NX5q/CFDWtUuA=
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg=
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
github.com/pion/webrtc/v3 v3.1.49 h1:rbsNGxK9jMYts+xE6zYAJMUQHnGwmk/JYze8yttW+to=
github.com/pion/webrtc/v3 v3.1.49/go.mod h1:kHf/o47QW4No1rgpsFux/h7lUhtUnwFnSFDZOXeLapw=
github.com/pixelbender/go-sdp v1.1.0/go.mod h1:6IBlz9+BrUHoFTea7gcp4S54khtOhjCW/nVDLhmZBAs=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
@@ -112,9 +147,17 @@ github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkB
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/q191201771/naza v0.30.8 h1:Lhh29o65C4PmTDj2l+eKfsw9dddpgWZk4bFICtcnSaA=
github.com/q191201771/naza v0.30.8/go.mod h1:n+dpJjQSh90PxBwxBNuifOwQttywvSIN5TkWSSYCeBk=
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
github.com/quic-go/qtls-go1-18 v0.2.0 h1:5ViXqBZ90wpUcZS0ge79rf029yx0dYB0McyPJwqqj7U=
github.com/quic-go/qtls-go1-18 v0.2.0/go.mod h1:moGulGHK7o6O8lSPSZNoOwcLvJKJ85vVNc7oJFD65bc=
github.com/quic-go/qtls-go1-19 v0.2.0 h1:Cvn2WdhyViFUHoOqK52i51k4nDX8EwIh5VJiVM4nttk=
github.com/quic-go/qtls-go1-19 v0.2.0/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
github.com/quic-go/qtls-go1-20 v0.1.0 h1:d1PK3ErFy9t7zxKsG3NXBJXZjp/kMLoIb3y/kV54oAI=
github.com/quic-go/qtls-go1-20 v0.1.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
github.com/quic-go/quic-go v0.32.0 h1:lY02md31s1JgPiiyfqJijpu/UX/Iun304FI3yUqX7tA=
github.com/quic-go/quic-go v0.32.0/go.mod h1:/fCsKANhQIeD5l76c2JFU+07gVE3KaA0FP+0zMWwfwo=
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM=
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/shirou/gopsutil/v3 v3.22.10 h1:4KMHdfBRYXGF9skjDWiL4RA2N+E8dRdodU/bOZpPoVg=
github.com/shirou/gopsutil/v3 v3.22.10/go.mod h1:QNza6r4YQoydyCfo6rH0blGfKahgibh4dQmV5xdFkQk=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
@@ -132,8 +175,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5/go.mod h1:f1SCnEOt6sc3fOJfPQDRDzHOtSXuTtnz0ImG9kPRDV0=
github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA=
github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
@@ -145,8 +188,11 @@ 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-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=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
@@ -161,24 +207,42 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 h1:yZNXmy+j/JpX19vZkVktWqAo7Gny4PBWYYK3zskGpx4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -191,6 +255,7 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -200,26 +265,43 @@ golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201214095126-aec9a390925b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -230,9 +312,13 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
@@ -241,9 +327,12 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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.8.5 h1:bUH71X7Ravj4gxINy/UrA67Nxycz26saiLeL3hBzuMU=
m7s.live/engine/v4 v4.8.5/go.mod h1:Knz1H4ZhJDooORkHOuHGNquSyA4txJFgVCng5rTEAm8=
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=

124
handle.go
View File

@@ -5,8 +5,8 @@ import (
"crypto/md5"
"encoding/xml"
"fmt"
"strconv"
"github.com/logrusorgru/aurora"
"go.uber.org/zap"
"m7s.live/plugin/gb28181/v4/utils"
@@ -32,7 +32,7 @@ func (a *Authorization) Verify(username, passwd, realm, nonce string) bool {
r2 := a.getDigest(s2)
if r1 == "" || r2 == "" {
fmt.Println("Authorization algorithm wrong")
GB28181Plugin.Error("Authorization algorithm wrong")
return false
}
//3、将密文 1nonce 和密文 2 依次组合获取 1 个字符串,并对这个字符串使用算法加密,获得密文 r3即Response
@@ -53,10 +53,49 @@ 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().Debugf("OnRegister: %s, %s from %s ", req.Destination(), id, req.Source())
GB28181Plugin.Debug("SIP<-OnMessage", zap.String("id", id), zap.String("source", req.Source()), zap.String("req", req.String()))
isUnregister := false
if exps := req.GetHeaders("Expires"); len(exps) > 0 {
exp := exps[0]
expSec, err := strconv.ParseInt(exp.Value(), 10, 32)
if err != nil {
GB28181Plugin.Info("OnRegister",
zap.String("error", fmt.Sprintf("wrong expire header value %q", exp)),
zap.String("id", id),
zap.String("source", req.Source()),
zap.String("destination", req.Destination()))
return
}
if expSec == 0 {
isUnregister = true
}
} else {
GB28181Plugin.Info("OnRegister",
zap.String("error", "has no expire header"),
zap.String("id", id),
zap.String("source", req.Source()),
zap.String("destination", req.Destination()))
return
}
GB28181Plugin.Info("OnRegister",
zap.Bool("isUnregister", isUnregister),
zap.String("id", id),
zap.String("source", req.Source()),
zap.String("destination", req.Destination()))
if len(id) != 20 {
GB28181Plugin.Info("Wrong GB-28181", zap.String("id", id))
return
}
passAuth := false
// 不需要密码情况
if c.Username == "" && c.Password == "" {
@@ -92,11 +131,21 @@ func (c *GB28181Config) OnRegister(req sip.Request, tx sip.ServerTransaction) {
}
if passAuth {
var d *Device
if v, ok := Devices.Load(id); ok {
d = v.(*Device)
c.RecoverDevice(d, req)
if isUnregister {
tmpd, ok := Devices.LoadAndDelete(id)
if ok {
GB28181Plugin.Info("Unregister Device", zap.String("id", id))
d = tmpd.(*Device)
} else {
return
}
} else {
d = c.StoreDevice(id, req)
if v, ok := Devices.Load(id); ok {
d = v.(*Device)
c.RecoverDevice(d, req)
} else {
d = c.StoreDevice(id, req)
}
}
DeviceNonce.Delete(id)
DeviceRegisterCount.Delete(id)
@@ -111,9 +160,14 @@ func (c *GB28181Config) OnRegister(req sip.Request, tx sip.ServerTransaction) {
Contents: time.Now().Format(TIME_LAYOUT),
})
_ = tx.Respond(resp)
//订阅设备更新
go d.syncChannels()
if !isUnregister {
//订阅设备更新
go d.syncChannels()
}
} else {
GB28181Plugin.Info("OnRegister unauthorized", zap.String("id", id), zap.String("source", req.Source()),
zap.String("destination", req.Destination()))
response := sip.NewResponseFromRequest("", req, http.StatusUnauthorized, "Unauthorized", "")
_nonce, _ := DeviceNonce.LoadOrStore(id, utils.RandNumString(32))
auth := fmt.Sprintf(
@@ -135,37 +189,38 @@ func (c *GB28181Config) OnRegister(req sip.Request, tx sip.ServerTransaction) {
func (d *Device) syncChannels() {
if time.Since(d.lastSyncTime) > 2*conf.HeartbeatInterval {
d.lastSyncTime = time.Now()
d.QueryDeviceInfo()
d.Catalog()
d.Subscribe()
d.QueryDeviceInfo()
}
}
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 {
case "RECOVER":
case DeviceOfflineStatus, DeviceRecoverStatus:
c.RecoverDevice(d, req)
go d.syncChannels()
//return
case string(sip.REGISTER):
d.Status = "ONLINE"
case DeviceRegisterStatus:
d.Status = DeviceOnlineStatus
}
d.UpdateTime = time.Now()
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
@@ -173,7 +228,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
@@ -181,42 +236,43 @@ func (c *GB28181Config) OnMessage(req sip.Request, tx sip.ServerTransaction) {
case "Keepalive":
d.LastKeepaliveAt = time.Now()
//callID !="" 说明是订阅的事件类型信息
if d.channelMap == nil || len(d.channelMap) == 0 {
if d.lastSyncTime.IsZero() {
go d.syncChannels()
} else {
for _, ch := range d.channelMap {
if c.AutoInvite && (ch.LivePublisher == nil) {
ch.Invite(InviteOptions{})
d.channelMap.Range(func(key, value interface{}) bool {
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
d.Manufacturer = temp.Manufacturer
d.Model = temp.Model
case "Alarm":
d.Status = "Alarmed"
d.Status = DeviceAlarmedStatus
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
}
tx.Respond(sip.NewResponseFromRequest("", req, http.StatusOK, "OK", body))
} else {
GB28181Plugin.Debug("Unauthorized message, device not found", zap.String("id", id))
}
}
func (c *GB28181Config) OnBye(req sip.Request, tx sip.ServerTransaction) {
@@ -248,7 +304,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
@@ -262,7 +318,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

67
inviteoption.go Normal file
View File

@@ -0,0 +1,67 @@
package gb28181
import (
"errors"
"fmt"
"math/rand"
"strconv"
)
type InviteOptions struct {
Start int
End int
dump string
ssrc string
SSRC uint32
MediaPort uint16
StreamPath string
recyclePort func(p uint16) (err error)
}
func (o InviteOptions) IsLive() bool {
return o.Start == 0 || o.End == 0
}
func (o InviteOptions) Record() bool {
return !o.IsLive()
}
func (o *InviteOptions) Validate(start, end string) error {
if start != "" {
sint, err1 := strconv.ParseInt(start, 10, 0)
if err1 != nil {
return err1
}
o.Start = int(sint)
}
if end != "" {
eint, err2 := strconv.ParseInt(end, 10, 0)
if err2 != nil {
return err2
}
o.End = int(eint)
}
if o.Start >= o.End {
return errors.New("start < end")
}
return nil
}
func (o InviteOptions) String() string {
return fmt.Sprintf("t=%d %d", o.Start, o.End)
}
func (o *InviteOptions) CreateSSRC() {
ssrc := make([]byte, 10)
if o.IsLive() {
ssrc[0] = '0'
} else {
ssrc[0] = '1'
}
copy(ssrc[1:6], conf.Serial[3:8])
randNum := 1000 + rand.Intn(8999)
copy(ssrc[6:], strconv.Itoa(randNum))
o.ssrc = string(ssrc)
_ssrc, _ := strconv.ParseInt(o.ssrc, 10, 0)
o.SSRC = uint32(_ssrc)
}

119
link.go Normal file
View File

@@ -0,0 +1,119 @@
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),
}
return c
}
// 唯一区分一次录像查询
func recordQueryKey(deviceId, channelId string, sn int) string {
return fmt.Sprintf("%s-%s-%d", deviceId, channelId, sn)
}
// 定期清理过期的查询结果和请求
func (c *recordQueryLink) cleanTimeout() {
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))
}

139
main.go
View File

@@ -1,59 +1,65 @@
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/config"
"m7s.live/engine/v4/util"
)
type GB28181PositionConfig struct {
AutosubPosition bool //是否自动订阅定位
Expires time.Duration //订阅周期(单位:秒)
Interval time.Duration //订阅间隔(单位:秒)
Expires time.Duration `default:"3600s"` //订阅周期(单位:秒)
Interval time.Duration `default:"6s"` //订阅间隔(单位:秒)
}
type GB28181Config struct {
AutoInvite bool
PreFetchRecord bool
InviteMode int `default:"1"` //邀请模式0:手动拉流1:预拉流2:按需拉流
InviteIDs string //按照国标gb28181协议允许邀请的设备类型:132 摄像机 NVR
ListenAddr string `default:"0.0.0.0"`
//sip服务器的配置
SipNetwork string //传输协议默认UDP可选TCP
SipIP string //sip 服务器公网IP
SipPort uint16 //sip 服务器端口,默认 5060
Serial string //sip 服务器 id, 默认 34020000002000000001
Realm string //sip 服务器域,默认 3402000000
Username string //sip 服务器账号
Password string //sip 服务器密码
SipNetwork string `default:"udp"` //传输协议默认UDP可选TCP
SipIP string //sip 服务器公网IP
SipPort uint16 `default:"5060"` //sip 服务器端口,默认 5060
Serial string `default:"34020000002000000001"` //sip 服务器 id, 默认 34020000002000000001
Realm string `default:"3402000000"` //sip 服务器域,默认 3402000000
Username string //sip 服务器账号
Password string //sip 服务器密码
Port struct { // 新配置方式
Sip string `default:"udp:5060"`
Media string `default:"tcp:58200"`
}
// AckTimeout uint16 //sip 服务应答超时,单位秒
RegisterValidity time.Duration //注册有效期,单位秒,默认 3600
RegisterValidity time.Duration `default:"3600s"` //注册有效期,单位秒,默认 3600
// RegisterInterval int //注册间隔,单位秒,默认 60
HeartbeatInterval time.Duration //心跳间隔,单位秒,默认 60
HeartbeatInterval time.Duration `default:"60s"` //心跳间隔,单位秒,默认 60
// HeartbeatRetry int //心跳超时次数,默认 3
//媒体服务器配置
MediaIP string //媒体服务器地址
MediaPort uint16 //媒体服务器端口
MediaNetwork string //媒体传输协议默认UDP可选TCP
MediaPort uint16 `default:"58200"` //媒体服务器端口
MediaNetwork string `default:"tcp"` //媒体传输协议默认UDP可选TCP
MediaPortMin uint16
MediaPortMax uint16
// MediaIdleTimeout uint16 //推流超时时间,超过则断开链接,让设备重连
// WaitKeyFrame bool //是否等待关键帧,如果等待,则在收到第一个关键帧之前,忽略所有媒体流
RemoveBanInterval time.Duration //移除禁止设备间隔
UdpCacheSize int //udp缓存大小
config.Publish
Server
LogLevel string //trace, debug, info, warn, error, fatal, panic
RemoveBanInterval time.Duration `default:"600s"` //移除禁止设备间隔
// UdpCacheSize int //udp缓存大小
LogLevel string `default:"info"` //trace, debug, info, warn, error, fatal, panic
routes map[string]string
DumpPath string //dump PS流本地文件路径
Ignores map[string]struct{}
tcpPorts PortManager
udpPorts PortManager
Position GB28181PositionConfig //关于定位的配置参数
}
func (c *GB28181Config) initRoutes() {
@@ -65,14 +71,61 @@ 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) {
switch event.(type) {
switch e := event.(type) {
case FirstConfig:
if c.Port.Sip != "udp:5060" {
protocol, ports := util.Conf2Listener(c.Port.Sip)
c.SipNetwork = protocol
c.SipPort = ports[0]
}
if c.Port.Media != "tcp:58200" {
protocol, ports := util.Conf2Listener(c.Port.Media)
c.MediaNetwork = protocol
if len(ports) > 1 {
c.MediaPortMin = ports[0]
c.MediaPortMax = ports[1]
} else {
c.MediaPort = ports[0]
}
}
os.MkdirAll(c.DumpPath, 0766)
c.ReadDevices()
go c.initRoutes()
c.startServer()
case *Stream:
if c.InviteMode == INVIDE_MODE_ONSUBSCRIBE {
//流可能是回放流stream path是device/channel/start-end形式
streamNames := strings.Split(e.StreamName, "/")
if channel := FindChannel(e.AppName, streamNames[0]); channel != nil {
opt := InviteOptions{}
if len(streamNames) > 1 {
last := len(streamNames) - 1
timestr := streamNames[last]
trange := strings.Split(timestr, "-")
if len(trange) == 2 {
startTime := trange[0]
endTime := trange[1]
opt.Validate(startTime, endTime)
}
}
channel.TryAutoInvite(&opt)
}
}
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()
}
}
}
@@ -80,33 +133,7 @@ func (c *GB28181Config) IsMediaNetworkTCP() bool {
return strings.ToLower(c.MediaNetwork) == "tcp"
}
var conf = &GB28181Config{
AutoInvite: true,
PreFetchRecord: false,
UdpCacheSize: 0,
SipNetwork: "udp",
SipIP: "",
SipPort: 5060,
Serial: "34020000002000000001",
Realm: "3402000000",
Username: "",
Password: "",
var conf GB28181Config
// AckTimeout: 10,
RegisterValidity: 60 * time.Second,
// RegisterInterval: 60,
HeartbeatInterval: 60 * time.Second,
// HeartbeatRetry: 3,
MediaIP: "",
MediaPort: 58200,
// MediaIdleTimeout: 30,
MediaNetwork: "udp",
RemoveBanInterval: 600 * time.Second,
LogLevel: "info",
// WaitKeyFrame: true,
Position: GB28181PositionConfig{AutosubPosition: false, Expires: 3600 * time.Second, Interval: 6 * time.Second},
}
var plugin = InstallPlugin(conf)
var GB28181Plugin = InstallPlugin(&conf)
var PullStreams sync.Map //拉流

View File

@@ -2,6 +2,7 @@ package gb28181
import (
"fmt"
"strconv"
"time"
)
@@ -43,6 +44,17 @@ var (
</Query>`
)
func intTotime(t int64) time.Time {
tstr := strconv.FormatInt(t, 10)
if len(tstr) == 10 {
return time.Unix(t, 0)
}
if len(tstr) == 13 {
return time.UnixMilli(t)
}
return time.Now()
}
// BuildDeviceInfoXML 获取设备详情指令
func BuildDeviceInfoXML(sn int, id string) string {
return fmt.Sprintf(DeviceInfoXML, sn, id)
@@ -55,7 +67,7 @@ func BuildCatalogXML(sn int, id string) string {
// BuildRecordInfoXML 获取录像文件列表指令
func BuildRecordInfoXML(sn int, id string, start, end int64) string {
return fmt.Sprintf(RecordInfoXML, sn, id, time.Unix(start, 0).Format("2006-01-02T15:04:05"), time.Unix(end, 0).Format("2006-01-02T15:04:05"))
return fmt.Sprintf(RecordInfoXML, sn, id, intTotime(start).Format("2006-01-02T15:04:05"), intTotime(end).Format("2006-01-02T15:04:05"))
}
// BuildDevicePositionXML 订阅设备位置

47
portmanager.go Normal file
View File

@@ -0,0 +1,47 @@
package gb28181
import "io"
type PortManager struct {
recycle chan uint16
max uint16
pos uint16
Valid bool
}
func (pm *PortManager) Init(start, end uint16) {
pm.pos = start - 1
pm.max = end
if pm.pos > 0 && pm.max > pm.pos {
pm.Valid = true
pm.recycle = make(chan uint16, pm.Range())
}
}
func (pm *PortManager) Range() uint16 {
return pm.max - pm.pos
}
func (pm *PortManager) Recycle(p uint16) (err error) {
select {
case pm.recycle <- p:
return nil
default:
return io.EOF //TODO: 换一个Error
}
}
func (pm *PortManager) GetPort() (p uint16, err error) {
select {
case p = <-pm.recycle:
return
default:
if pm.Range() > 0 {
pm.pos++
p = pm.pos
return
} else {
return 0, io.EOF //TODO: 换一个Error
}
}
}

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,319 +0,0 @@
package gb28181
import (
"encoding/binary"
"fmt"
"io"
"net"
"os"
"time"
"github.com/ghettovoice/gosip/sip"
"github.com/pion/rtp/v2"
"go.uber.org/zap"
. "m7s.live/engine/v4"
. "m7s.live/engine/v4/codec"
"m7s.live/engine/v4/codec/mpegts"
. "m7s.live/engine/v4/track"
"m7s.live/engine/v4/util"
"m7s.live/plugin/gb28181/v4/utils"
)
type GBPublisher struct {
Publisher
InviteOptions
channel *Channel
inviteRes sip.Response
parser *utils.DecPSPackage
lastSeq uint16
udpCache *utils.PriorityQueueRtp
dumpFile *os.File
dumpPrint io.Writer
lastReceive time.Time
reorder util.RTPReorder[*rtp.Packet]
}
func (p *GBPublisher) PrintDump(s string) {
if p.dumpPrint != nil {
p.dumpPrint.Write([]byte(s))
}
}
func (p *GBPublisher) OnEvent(event any) {
if p.channel == nil {
p.IO.OnEvent(event)
return
}
switch event.(type) {
case IPublisher:
if p.IsLive() {
p.Type = "GB28181 Live"
p.channel.LivePublisher = p
} else {
p.Type = "GB28181 Playback"
p.channel.RecordPublisher = p
}
conf.publishers.Add(p.SSRC, p)
if err := error(nil); p.dump != "" {
if p.dumpFile, err = os.OpenFile(p.dump, os.O_WRONLY|os.O_CREATE, 0644); err != nil {
p.Error("open dump file failed", zap.Error(err))
}
}
case SEwaitPublish:
//掉线自动重新拉流
if p.IsLive() {
if p.channel.LivePublisher != nil {
p.channel.LivePublisher = nil
p.channel.liveInviteLock.Unlock()
}
go p.channel.Invite(InviteOptions{})
}
case SEclose, SEKick:
if p.IsLive() {
if p.channel.LivePublisher != nil {
p.channel.LivePublisher = nil
p.channel.liveInviteLock.Unlock()
}
} else {
p.channel.RecordPublisher = nil
}
conf.publishers.Delete(p.SSRC)
if p.dumpFile != nil {
p.dumpFile.Close()
}
p.Bye()
}
p.Publisher.OnEvent(event)
}
func (p *GBPublisher) Bye() int {
res := p.inviteRes
if res == nil {
return 404
}
defer p.Stop()
p.inviteRes = nil
bye := p.channel.CreateRequst(sip.BYE)
from, _ := res.From()
to, _ := res.To()
callId, _ := res.CallID()
bye.ReplaceHeaders(from.Name(), []sip.Header{from})
bye.ReplaceHeaders(to.Name(), []sip.Header{to})
bye.ReplaceHeaders(callId.Name(), []sip.Header{callId})
resp, err := p.channel.device.SipRequestForResponse(bye)
if err != nil {
p.Error("Bye", zap.Error(err))
return 500
}
return int(resp.StatusCode())
}
func (p *GBPublisher) PushVideo(pts uint32, dts uint32, payload []byte) {
if p.VideoTrack == nil {
switch p.parser.VideoStreamType {
case mpegts.STREAM_TYPE_H264:
p.VideoTrack = NewH264(p.Publisher.Stream)
case mpegts.STREAM_TYPE_H265:
p.VideoTrack = NewH265(p.Publisher.Stream)
default:
//推测编码类型
var maybe264 H264NALUType
maybe264 = maybe264.Parse(payload[4])
switch maybe264 {
case NALU_Non_IDR_Picture,
NALU_IDR_Picture,
NALU_SEI,
NALU_SPS,
NALU_PPS,
NALU_Access_Unit_Delimiter:
p.VideoTrack = NewH264(p.Publisher.Stream)
default:
p.Info("maybe h265", zap.Uint8("type", maybe264.Byte()))
p.VideoTrack = NewH265(p.Publisher.Stream)
}
}
}
if len(payload) > 10 {
p.PrintDump(fmt.Sprintf("<td>pts:%d dts:%d data: % 2X</td>", pts, dts, payload[:10]))
} else {
p.PrintDump(fmt.Sprintf("<td>pts:%d dts:%d data: % 2X</td>", pts, dts, payload))
}
if dts == 0 {
dts = pts
}
// if binary.BigEndian.Uint32(payload) != 1 {
// panic("not annexb")
// }
p.WriteAnnexB(pts, dts, payload)
}
func (p *GBPublisher) PushAudio(ts uint32, payload []byte) {
if p.AudioTrack == nil {
switch p.parser.AudioStreamType {
case mpegts.STREAM_TYPE_G711A:
p.AudioTrack = NewG711(p.Publisher.Stream, true, uint32(90000))
case mpegts.STREAM_TYPE_G711U:
p.AudioTrack = NewG711(p.Publisher.Stream, false, uint32(90000))
case mpegts.STREAM_TYPE_AAC:
p.AudioTrack = NewAAC(p.Publisher.Stream, uint32(90000))
p.WriteADTS(ts, payload)
case 0: //推测编码类型
if payload[0] == 0xff && payload[1]>>4 == 0xf {
p.AudioTrack = NewAAC(p.Publisher.Stream, uint32(90000))
p.WriteADTS(ts, payload)
}
default:
p.Error("audio type not supported yet", zap.Uint32("type", p.parser.AudioStreamType))
}
} else if p.parser.AudioStreamType == mpegts.STREAM_TYPE_AAC {
p.WriteADTS(ts, payload)
} else {
p.WriteRaw(ts, payload)
}
}
// 解析rtp封装 https://www.ietf.org/rfc/rfc2250.txt
func (p *GBPublisher) PushPS(rtp *rtp.Packet) {
if p.parser == nil {
p.parser = utils.NewDecPSPackage(p)
}
if conf.IsMediaNetworkTCP() {
p.parser.Feed(rtp)
p.lastSeq = rtp.SequenceNumber
} else {
for rtp = p.reorder.Push(rtp.SequenceNumber, rtp); rtp != nil; rtp = p.reorder.Pop() {
if rtp.SequenceNumber != p.lastSeq+1 {
fmt.Println("drop", rtp.SequenceNumber, p.lastSeq)
p.parser.Drop()
if p.VideoTrack != nil {
p.SetLostFlag()
}
}
p.parser.Feed(rtp)
p.lastSeq = rtp.SequenceNumber
}
}
}
func (p *GBPublisher) Replay(f *os.File) (err error) {
var rtpPacket rtp.Packet
defer f.Close()
if p.dumpPrint != nil {
p.PrintDump(`<style type="text/css">
.gray {
color: gray;
}
</style>
`)
p.PrintDump("<table>")
defer p.PrintDump("</table>")
}
var t uint16
for l := make([]byte, 6); !p.IsClosed(); time.Sleep(time.Millisecond * time.Duration(t)) {
_, err = f.Read(l)
if err != nil {
return
}
payload := make([]byte, util.ReadBE[int](l[:4]))
t = util.ReadBE[uint16](l[4:])
p.PrintDump(fmt.Sprintf("[<b>%d</b> %d]", t, len(payload)))
_, err = f.Read(payload)
if err != nil {
return
}
rtpPacket.Unmarshal(payload)
p.PushPS(&rtpPacket)
}
return
}
func (p *GBPublisher) ListenUDP() (port uint16, err error) {
var rtpPacket rtp.Packet
networkBuffer := 1048576
port, err = conf.udpPorts.GetPort()
if err != nil {
return
}
addr := fmt.Sprintf(":%d", port)
mediaAddr, _ := net.ResolveUDPAddr("udp", addr)
conn, err := net.ListenUDP("udp", mediaAddr)
if err != nil {
conf.udpPorts.Recycle(port)
plugin.Error("listen media server udp err", zap.String("addr", addr), zap.Error(err))
return 0, err
}
p.SetIO(conn)
go func() {
defer conn.Close()
bufUDP := make([]byte, networkBuffer)
plugin.Info("Media udp server start.", zap.Uint16("port", port))
defer plugin.Info("Media udp server stop", zap.Uint16("port", port))
defer conf.udpPorts.Recycle(port)
dumpLen := make([]byte, 6)
conn.SetReadDeadline(time.Now().Add(time.Second * 10))
for n, _, err := conn.ReadFromUDP(bufUDP); err == nil; n, _, err = conn.ReadFromUDP(bufUDP) {
ps := bufUDP[:n]
if err := rtpPacket.Unmarshal(ps); err != nil {
plugin.Error("Decode rtp error:", zap.Error(err))
}
if p.dumpFile != nil {
util.PutBE(dumpLen[:4], n)
if p.lastReceive.IsZero() {
util.PutBE(dumpLen[4:], 0)
} else {
util.PutBE(dumpLen[4:], uint16(time.Since(p.lastReceive).Milliseconds()))
}
p.lastReceive = time.Now()
p.dumpFile.Write(dumpLen)
p.dumpFile.Write(ps)
}
p.PushPS(&rtpPacket)
conn.SetReadDeadline(time.Now().Add(time.Second * 10))
}
}()
return
}
func (p *GBPublisher) ListenTCP() (port uint16, err error) {
port, err = conf.tcpPorts.GetPort()
if err != nil {
return
}
addr := fmt.Sprintf(":%d", port)
mediaAddr, _ := net.ResolveTCPAddr("tcp", addr)
listen, err := net.ListenTCP("tcp", mediaAddr)
if err != nil {
defer conf.tcpPorts.Recycle(port)
plugin.Error("listen media server tcp err", zap.String("addr", addr), zap.Error(err))
return 0, err
}
go func() {
plugin.Info("Media tcp server start.", zap.Uint16("port", port))
defer conf.tcpPorts.Recycle(port)
defer plugin.Info("Media tcp server stop", zap.Uint16("port", port))
conn, err := listen.Accept()
listen.Close()
p.SetIO(conn)
if err != nil {
plugin.Error("Accept err=", zap.Error(err))
return
}
var rtpPacket rtp.Packet
lenBuf := make([]byte, 2)
defer conn.Close()
for err == nil {
if _, err = io.ReadFull(conn, lenBuf); err != nil {
return
}
ps := make([]byte, binary.BigEndian.Uint16(lenBuf))
if _, err = io.ReadFull(conn, ps); err != nil {
return
}
if err := rtpPacket.Unmarshal(ps); err != nil {
plugin.Error("gb28181 decode rtp error:", zap.Error(err))
} else if !p.IsClosed() {
p.PushPS(&rtpPacket)
}
}
}()
return
}

View File

@@ -1,8 +1,9 @@
package gb28181
import (
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"time"
@@ -10,15 +11,15 @@ import (
"m7s.live/engine/v4/util"
)
var (
playScaleValues = map[float32]bool{0.25: true, 0.5: true, 1: true, 2: true, 4: true}
)
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 {
Devices.Delete(key)
} else {
list = append(list, device)
}
list = append(list, value.(*Device))
return true
})
return
@@ -26,12 +27,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)
}
@@ -48,67 +60,137 @@ 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)
return
}
vsN, err := strconv.ParseUint(vs, 10, 8)
if err != nil {
WriteJSON(w, "vSpeed parameter is invalid", 400)
return
}
zsN, err := strconv.ParseUint(zs, 10, 8)
if err != nil {
WriteJSON(w, "zSpeed parameter is invalid", 400)
return
}
ptzcmd, err := toPtzStrByCmdName(cmd, uint8(hsN), uint8(vsN), uint8(zsN))
if err != nil {
WriteJSON(w, err.Error(), 400)
return
}
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")
channel := query.Get("channel")
streamPath := query.Get("streamPath")
port, _ := strconv.Atoi(query.Get("mediaPort"))
opt := InviteOptions{
dump: query.Get("dump"),
MediaPort: uint16(port),
dump: query.Get("dump"),
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.LivePublisher != nil {
w.WriteHeader(304) //直播流已存在
} else if code, err := c.Invite(opt); err == nil {
} else if opt.IsLive() && c.status.Load() > 0 {
http.Error(w, "live stream already exists", http.StatusNotModified)
} else if code, err := c.Invite(&opt); err == nil {
w.WriteHeader(code)
} else {
http.Error(w, err.Error(), code)
}
}
func (c *GB28181Config) API_replay(w http.ResponseWriter, r *http.Request) {
dump := r.URL.Query().Get("dump")
printOut := r.URL.Query().Get("print")
if dump == "" {
dump = c.DumpPath
}
f, err := os.OpenFile(dump, os.O_RDONLY, 0644)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
func (c *GB28181Config) API_bye(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
channel := r.URL.Query().Get("channel")
streamPath := r.URL.Query().Get("streamPath")
if c := FindChannel(id, channel); c != nil {
w.WriteHeader(c.Bye(streamPath))
} else {
streamPath := dump
if strings.HasPrefix(dump, "/") {
streamPath = "replay" + dump
} else {
streamPath = "replay/" + dump
}
var pub GBPublisher
pub.SetIO(f)
if err = plugin.Publish(streamPath, &pub); err == nil {
if printOut != "" {
pub.dumpPrint = w
pub.SetParentCtx(r.Context())
err = pub.Replay(f)
} else {
go pub.Replay(f)
w.Write([]byte("ok"))
}
} else {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
http.NotFound(w, r)
}
}
func (c *GB28181Config) API_bye(w http.ResponseWriter, r *http.Request) {
// CORS(w, r)
func (c *GB28181Config) API_play_pause(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
channel := r.URL.Query().Get("channel")
live := r.URL.Query().Get("live")
streamPath := r.URL.Query().Get("streamPath")
if c := FindChannel(id, channel); c != nil {
w.WriteHeader(c.Bye(live != "false"))
w.WriteHeader(c.Pause(streamPath))
} else {
http.NotFound(w, r)
}
}
func (c *GB28181Config) API_play_resume(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
channel := r.URL.Query().Get("channel")
streamPath := r.URL.Query().Get("streamPath")
if c := FindChannel(id, channel); c != nil {
w.WriteHeader(c.Resume(streamPath))
} else {
http.NotFound(w, r)
}
}
func (c *GB28181Config) API_play_seek(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
channel := r.URL.Query().Get("channel")
streamPath := r.URL.Query().Get("streamPath")
secStr := r.URL.Query().Get("second")
sec, err := strconv.ParseUint(secStr, 10, 32)
if err != nil {
WriteJSON(w, "second parameter is invalid: "+err.Error(), 400)
return
}
if c := FindChannel(id, channel); c != nil {
w.WriteHeader(c.PlayAt(streamPath, uint(sec)))
} else {
http.NotFound(w, r)
}
}
func (c *GB28181Config) API_play_forward(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
channel := r.URL.Query().Get("channel")
streamPath := r.URL.Query().Get("streamPath")
speedStr := r.URL.Query().Get("speed")
speed, err := strconv.ParseFloat(speedStr, 32)
secondErrMsg := "speed parameter is invalid, should be one of 0.25,0.5,1,2,4"
if err != nil || !playScaleValues[float32(speed)] {
WriteJSON(w, secondErrMsg, 400)
return
}
if c := FindChannel(id, channel); c != nil {
w.WriteHeader(c.PlayForward(streamPath, float32(speed)))
} else {
http.NotFound(w, r)
}
@@ -169,3 +251,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)
}

307
server.go
View File

@@ -1,19 +1,14 @@
package gb28181
import (
"bufio"
"encoding/binary"
"context"
"fmt"
"io"
"net"
"strconv"
"strings"
"time"
"github.com/logrusorgru/aurora"
"github.com/pion/rtp/v2"
"go.uber.org/zap"
"m7s.live/engine/v4/util"
"m7s.live/plugin/gb28181/v4/utils"
"github.com/ghettovoice/gosip"
@@ -23,65 +18,14 @@ import (
var srv gosip.Server
type PortManager struct {
recycle chan uint16
max uint16
pos uint16
Valid bool
}
func (pm *PortManager) Init(start, end uint16) {
pm.pos = start
pm.max = end
if pm.pos > 0 && pm.max > pm.pos {
pm.Valid = true
pm.recycle = make(chan uint16, pm.Range())
}
}
func (pm *PortManager) Range() uint16 {
return pm.max - pm.pos
}
func (pm *PortManager) Recycle(p uint16) (err error) {
select {
case pm.recycle <- p:
return nil
default:
return io.EOF //TODO: 换一个Error
}
}
func (pm *PortManager) GetPort() (p uint16, err error) {
select {
case p = <-pm.recycle:
return
default:
if pm.Range() > 0 {
pm.pos++
p = pm.pos
return
} else {
return 0, io.EOF //TODO: 换一个Error
}
}
}
type Server struct {
Ignores map[string]struct{}
publishers util.Map[uint32, *GBPublisher]
tcpPorts PortManager
udpPorts PortManager
}
const MaxRegisterCount = 3
func FindChannel(deviceId string, channelId string) (c *Channel) {
if v, ok := Devices.Load(deviceId); ok {
d := v.(*Device)
d.channelMutex.RLock()
c = d.channelMap[channelId]
d.channelMutex.RUnlock()
if v, ok := d.channelMap.Load(channelId); ok {
return v.(*Channel)
}
}
return
}
@@ -96,11 +40,85 @@ var levelMap = map[string]log.Level{
"panic": log.PanicLevel,
}
func (c *GB28181Config) startServer() {
c.publishers.Init()
addr := "0.0.0.0:" + strconv.Itoa(int(c.SipPort))
func GetSipServer(transport string) gosip.Server {
return srv
}
logger := utils.NewZapLogger(plugin.Logger, "GB SIP Server", nil)
var sn = 0
func CreateRequest(exposedId string, Method sip.RequestMethod, recipient *sip.Address, netAddr string) (req sip.Request) {
sn++
callId := sip.CallID(utils.RandNumString(10))
userAgent := sip.UserAgentHeader("Monibuca")
cseq := sip.CSeq{
SeqNo: uint32(sn),
MethodName: Method,
}
port := sip.Port(conf.SipPort)
serverAddr := sip.Address{
//DisplayName: sip.String{Str: d.config.Serial},
Uri: &sip.SipUri{
FUser: sip.String{Str: exposedId},
FHost: conf.SipIP,
FPort: &port,
},
Params: sip.NewParams().Add("tag", sip.String{Str: utils.RandNumString(9)}),
}
req = sip.NewRequest(
"",
Method,
recipient.Uri,
"SIP/2.0",
[]sip.Header{
serverAddr.AsFromHeader(),
recipient.AsToHeader(),
&callId,
&userAgent,
&cseq,
serverAddr.AsContactHeader(),
},
"",
nil,
)
req.SetTransport(conf.SipNetwork)
req.SetDestination(netAddr)
//fmt.Printf("构建请求参数:%s", *&req)
// requestMsg.DestAdd, err2 = d.ResolveAddress(requestMsg)
// if err2 != nil {
// return nil
// }
//intranet ip , let's resolve it with public ip
// var deviceIp, deviceSourceIP net.IP
// switch addr := requestMsg.DestAdd.(type) {
// case *net.UDPAddr:
// deviceIp = addr.IP
// case *net.TCPAddr:
// deviceIp = addr.IP
// }
// switch addr2 := d.SourceAddr.(type) {
// case *net.UDPAddr:
// deviceSourceIP = addr2.IP
// case *net.TCPAddr:
// deviceSourceIP = addr2.IP
// }
// if deviceIp.IsPrivate() && !deviceSourceIP.IsPrivate() {
// requestMsg.DestAdd = d.SourceAddr
// }
return
}
func RequestForResponse(transport string, request sip.Request,
options ...gosip.RequestWithContextOption) (sip.Response, error) {
return (GetSipServer(transport)).RequestWithContext(context.Background(), request, options...)
}
func (c *GB28181Config) startServer() {
addr := c.ListenAddr + ":" + strconv.Itoa(int(c.SipPort))
logger := utils.NewZapLogger(GB28181Plugin.Logger, "GB SIP Server", nil)
logger.SetLevel(levelMap[c.LogLevel])
// logger := log.NewDefaultLogrusLogger().WithPrefix("GB SIP Server")
srvConf := gosip.ServerConfig{}
@@ -114,112 +132,17 @@ 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)))
}
go c.startMediaServer()
if c.Username != "" || c.Password != "" {
go c.removeBanDevice()
}
}
func (c *GB28181Config) startMediaServer() {
if c.MediaNetwork == "tcp" {
c.tcpPorts.Init(c.MediaPortMin, c.MediaPortMax)
if !c.tcpPorts.Valid {
c.listenMediaTCP()
}
} else {
c.udpPorts.Init(c.MediaPortMin, c.MediaPortMax)
if !c.udpPorts.Valid {
c.listenMediaUDP()
}
}
}
func (c *GB28181Config) processTcpMediaConn(conn net.Conn) {
var rtpPacket rtp.Packet
reader := bufio.NewReader(conn)
lenBuf := make([]byte, 2)
defer conn.Close()
var err error
for err == nil {
if _, err = io.ReadFull(reader, lenBuf); err != nil {
return
}
ps := make([]byte, binary.BigEndian.Uint16(lenBuf))
if _, err = io.ReadFull(reader, ps); err != nil {
return
}
if err := rtpPacket.Unmarshal(ps); err != nil {
plugin.Error("gb28181 decode rtp error:", zap.Error(err))
} else if publisher := c.publishers.Get(rtpPacket.SSRC); publisher != nil && publisher.Publisher.Err() == nil {
publisher.PushPS(&rtpPacket)
}
}
}
func (c *GB28181Config) listenMediaTCP() {
addr := ":" + strconv.Itoa(int(c.MediaPort))
mediaAddr, _ := net.ResolveTCPAddr("tcp", addr)
listen, err := net.ListenTCP("tcp", mediaAddr)
if err != nil {
plugin.Error("listen media server tcp err", zap.String("addr", addr), zap.Error(err))
return
}
plugin.Info("Media tcp server start.", zap.Uint16("port", c.MediaPort))
defer listen.Close()
defer plugin.Info("Media tcp server stop", zap.Uint16("port", c.MediaPort))
for {
conn, err := listen.Accept()
if err != nil {
plugin.Error("Accept err=", zap.Error(err))
}
go c.processTcpMediaConn(conn)
}
}
func (c *GB28181Config) listenMediaUDP() {
var rtpPacket rtp.Packet
networkBuffer := 1048576
addr := ":" + strconv.Itoa(int(c.MediaPort))
mediaAddr, _ := net.ResolveUDPAddr("udp", addr)
conn, err := net.ListenUDP("udp", mediaAddr)
if err != nil {
plugin.Error("listen media server udp err", zap.String("addr", addr), zap.Error(err))
return
}
bufUDP := make([]byte, networkBuffer)
plugin.Info("Media udp server start.", zap.Uint16("port", c.MediaPort))
defer plugin.Info("Media udp server stop", zap.Uint16("port", c.MediaPort))
dumpLen := make([]byte, 6)
for n, _, err := conn.ReadFromUDP(bufUDP); err == nil; n, _, err = conn.ReadFromUDP(bufUDP) {
ps := bufUDP[:n]
if err := rtpPacket.Unmarshal(ps); err != nil {
plugin.Error("Decode rtp error:", zap.Error(err))
}
if publisher := c.publishers.Get(rtpPacket.SSRC); publisher != nil && publisher.Publisher.Err() == nil {
if publisher.dumpFile != nil {
util.PutBE(dumpLen[:4], n)
if publisher.lastReceive.IsZero() {
util.PutBE(dumpLen[4:], 0)
} else {
util.PutBE(dumpLen[4:], uint16(time.Since(publisher.lastReceive).Milliseconds()))
}
publisher.lastReceive = time.Now()
publisher.dumpFile.Write(dumpLen)
publisher.dumpFile.Write(ps)
}
publisher.PushPS(&rtpPacket)
}
}
go c.startJob()
}
// func queryCatalog(config *transaction.Config) {
@@ -237,14 +160,58 @@ func (c *GB28181Config) listenMediaUDP() {
// }
// }
func (c *GB28181Config) removeBanDevice() {
t := time.NewTicker(c.RemoveBanInterval)
for range t.C {
DeviceRegisterCount.Range(func(key, value interface{}) bool {
if value.(int) > MaxRegisterCount {
DeviceRegisterCount.Delete(key)
// 定时任务
func (c *GB28181Config) startJob() {
statusTick := time.NewTicker(c.HeartbeatInterval / 2)
banTick := time.NewTicker(c.RemoveBanInterval)
linkTick := time.NewTicker(time.Millisecond * 100)
GB28181Plugin.Debug("start job")
for {
select {
case <-banTick.C:
if c.Username != "" || c.Password != "" {
c.removeBanDevice()
}
return true
})
case <-statusTick.C:
c.statusCheck()
case <-linkTick.C:
RecordQueryLink.cleanTimeout()
}
}
}
func (c *GB28181Config) removeBanDevice() {
DeviceRegisterCount.Range(func(key, value interface{}) bool {
if value.(int) > MaxRegisterCount {
DeviceRegisterCount.Delete(key)
}
return true
})
}
// statusCheck
// - 当设备超过 3 倍心跳时间未发送过心跳(通过 UpdateTime 判断), 视为离线
// - 当设备超过注册有效期内为发送过消息,则从设备列表中删除
// UpdateTime 在设备发送心跳之外的消息也会被更新,相对于 LastKeepaliveAt 更能体现出设备最会一次活跃的时间
func (c *GB28181Config) statusCheck() {
Devices.Range(func(key, value any) bool {
d := value.(*Device)
if time.Since(d.UpdateTime) > c.RegisterValidity {
Devices.Delete(key)
GB28181Plugin.Info("Device register timeout",
zap.String("id", d.ID),
zap.Time("registerTime", d.RegisterTime),
zap.Time("updateTime", d.UpdateTime),
)
} else if time.Since(d.UpdateTime) > c.HeartbeatInterval*3 {
d.Status = DeviceOfflineStatus
d.channelMap.Range(func(key, value any) bool {
ch := value.(*Channel)
ch.Status = ChannelOffStatus
return true
})
GB28181Plugin.Info("Device offline", zap.String("id", d.ID), zap.Time("updateTime", d.UpdateTime))
}
return true
})
}

View File

@@ -6,17 +6,18 @@ import (
"github.com/ghettovoice/gosip/log"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
m7slog "m7s.live/engine/v4/log"
)
type ZapLogger struct {
log *zap.Logger
log *m7slog.Logger
prefix string
fields log.Fields
sugared *zap.SugaredLogger
level log.Level
}
func NewZapLogger(log *zap.Logger, prefix string, fields log.Fields) (z *ZapLogger) {
func NewZapLogger(log *m7slog.Logger, prefix string, fields log.Fields) (z *ZapLogger) {
z = &ZapLogger{
log: log,
prefix: prefix,

View File

@@ -1,340 +0,0 @@
package utils
import (
"encoding/binary"
"errors"
"fmt"
"io"
"github.com/pion/rtp/v2"
)
const (
UDPTransfer int = 0
TCPTransferActive int = 1
TCPTransferPassive int = 2
LocalCache int = 3
StartCodePS = 0x000001ba
StartCodeSYS = 0x000001bb
StartCodeMAP = 0x000001bc
StartCodeVideo = 0x000001e0
StartCodeAudio = 0x000001c0
PrivateStreamCode = 0x000001bd
MEPGProgramEndCode = 0x000001b9
RTPHeaderLength int = 12
PSHeaderLength int = 14
SystemHeaderLength int = 18
MAPHeaderLength int = 24
PESHeaderLength int = 19
RtpLoadLength int = 1460
PESLoadLength int = 0xFFFF
MAXFrameLen int = 1024 * 1024 * 2
)
var (
ErrNotFoundStartCode = errors.New("not found the need start code flag")
ErrMarkerBit = errors.New("marker bit value error")
ErrFormatPack = errors.New("not package standard")
ErrParsePakcet = errors.New("parse ps packet error")
)
type Pusher interface {
PushVideo(uint32, uint32, []byte)
PushAudio(uint32, []byte)
PrintDump(string)
}
/*
This implement from VLC source code
notes: https://github.com/videolan/vlc/blob/master/modules/mux/mpeg/bits.h
*/
//bitsBuffer bits buffer
// type bitsBuffer struct {
// iSize int
// iData int
// iMask uint8
// pData []byte
// }
// func bitsInit(isize int, buffer []byte) *bitsBuffer {
// bits := &bitsBuffer{
// iSize: isize,
// iData: 0,
// iMask: 0x80,
// pData: buffer,
// }
// if bits.pData == nil {
// bits.pData = make([]byte, isize)
// }
// return bits
// }
// func bitsAlign(bits *bitsBuffer) {
// if bits.iMask != 0x80 && bits.iData < bits.iSize {
// bits.iMask = 0x80
// bits.iData++
// bits.pData[bits.iData] = 0x00
// }
// }
// func bitsWrite(bits *bitsBuffer, count int, src uint64) *bitsBuffer {
// for count > 0 {
// count--
// if ((src >> uint(count)) & 0x01) != 0 {
// bits.pData[bits.iData] |= bits.iMask
// } else {
// bits.pData[bits.iData] &= ^bits.iMask
// }
// bits.iMask >>= 1
// if bits.iMask == 0 {
// bits.iData++
// bits.iMask = 0x80
// }
// }
// return bits
// }
/*
https://github.com/videolan/vlc/blob/master/modules/demux/mpeg
*/
type DecPSPackage struct {
// systemClockReferenceBase uint64
// systemClockReferenceExtension uint64
// programMuxRate uint32
VideoStreamType uint32
AudioStreamType uint32
IOBuffer
Payload []byte
videoBuffer []byte
audioBuffer []byte
aPTS uint32
aDTS uint32
vPTS uint32
vDTS uint32
Pusher
}
func NewDecPSPackage(p Pusher) *DecPSPackage {
p.PrintDump("<tr><td>")
return &DecPSPackage{
Pusher: p,
}
}
// func (dec *DecPSPackage) clean() {
// dec.systemClockReferenceBase = 0
// dec.systemClockReferenceExtension = 0
// dec.programMuxRate = 0
// dec.Payload = nil
// dec.PTS = 0
// dec.DTS = 0
// }
func (dec *DecPSPackage) ReadPayload() (payload []byte, err error) {
payloadlen, err := dec.Uint16()
if err != nil {
return
}
if l := int(payloadlen); dec.Len() >= l {
return dec.Next(l), nil
}
return dec.Next(dec.Len()), io.EOF
}
// Drop 由于丢包引起的必须丢弃的数据
func (dec *DecPSPackage) Drop() {
dec.Reset()
dec.videoBuffer = nil
dec.audioBuffer = nil
dec.Payload = nil
}
func (dec *DecPSPackage) Feed(rtp *rtp.Packet) (err error) {
ps := rtp.Payload
if len(ps) < 4 {
return nil
}
// println(binary.BigEndian.Uint32(ps))
switch binary.BigEndian.Uint32(ps) {
case StartCodePS, StartCodeSYS, StartCodeMAP, StartCodeVideo, StartCodeAudio, PrivateStreamCode, MEPGProgramEndCode:
defer dec.Write(ps)
if dec.Len() >= 4 {
//说明需要处理PS包处理完后清空缓存
defer dec.Reset()
} else {
return
}
default:
// 说明是中间数据,直接写入缓存,否则数据不合法需要丢弃
if dec.Len() > 0 {
dec.Write(ps)
}
return nil
}
for dec.Len() >= 4 {
code, _ := dec.Uint32()
// println("code:", code)
switch code {
case StartCodePS:
dec.PrintDump("</td></tr><tr><td>")
if len(dec.audioBuffer) > 0 {
dec.PushAudio(dec.aPTS, dec.audioBuffer)
dec.audioBuffer = nil
}
if err := dec.Skip(9); err != nil {
return err
}
psl, err := dec.ReadByte()
if err == nil {
psl &= 0x07
dec.Skip(int(psl))
}
if len(dec.videoBuffer) > 0 {
dec.PushVideo(dec.vPTS, dec.vDTS, dec.videoBuffer)
dec.videoBuffer = nil
}
case StartCodeSYS:
dec.PrintDump("</td><td>[sys]")
dec.ReadPayload()
case StartCodeMAP:
dec.decProgramStreamMap()
dec.PrintDump("</td><td>[map]")
case StartCodeVideo:
if dec.videoBuffer == nil {
dec.PrintDump("</td><td>")
}
dec.decPESPacket(&dec.vPTS, &dec.vDTS)
dec.videoBuffer = append(dec.videoBuffer, dec.Payload...)
// if err != nil {
//说明还有后续数据,需要继续处理
// println(rtp.SequenceNumber)
// }
dec.PrintDump("[video]")
case StartCodeAudio:
if dec.audioBuffer == nil {
dec.PrintDump("</td><td>")
}
if err = dec.decPESPacket(&dec.aPTS, &dec.aDTS); err == nil {
dec.audioBuffer = append(dec.audioBuffer, dec.Payload...)
dec.PrintDump("[audio]")
} else {
fmt.Println("audio", err)
}
case PrivateStreamCode:
dec.ReadPayload()
dec.PrintDump("</td></tr><tr><td>[ac3]")
case MEPGProgramEndCode:
dec.PrintDump("</td></tr>")
return io.EOF
default:
fmt.Println("unknow code", code)
return ErrParsePakcet
}
}
return nil
}
/*
func (dec *DecPSPackage) decSystemHeader() error {
syslens, err := dec.Uint16()
if err != nil {
return err
}
// drop rate video audio bound and lock flag
syslens -= 6
if err = dec.Skip(6); err != nil {
return err
}
// ONE WAY: do not to parse the stream and skip the buffer
//br.Skip(syslen * 8)
// TWO WAY: parse every stream info
for syslens > 0 {
if nextbits, err := dec.Uint8(); err != nil {
return err
} else if (nextbits&0x80)>>7 != 1 {
break
}
if err = dec.Skip(2); err != nil {
return err
}
syslens -= 3
}
return nil
}
*/
func (dec *DecPSPackage) decProgramStreamMap() error {
psm, err := dec.ReadPayload()
if err != nil {
return err
}
l := len(psm)
index := 2
programStreamInfoLen := binary.BigEndian.Uint16(psm[index:])
index += 2
index += int(programStreamInfoLen)
programStreamMapLen := binary.BigEndian.Uint16(psm[index:])
index += 2
for programStreamMapLen > 0 {
if l <= index+1 {
break
}
streamType := psm[index]
index++
elementaryStreamID := psm[index]
index++
if elementaryStreamID >= 0xe0 && elementaryStreamID <= 0xef {
dec.VideoStreamType = uint32(streamType)
} else if elementaryStreamID >= 0xc0 && elementaryStreamID <= 0xdf {
dec.AudioStreamType = uint32(streamType)
}
if l <= index+1 {
break
}
elementaryStreamInfoLength := binary.BigEndian.Uint16(psm[index:])
index += 2
index += int(elementaryStreamInfoLength)
programStreamMapLen -= 4 + elementaryStreamInfoLength
}
return nil
}
func (dec *DecPSPackage) decPESPacket(pts *uint32, dts *uint32) error {
payload, err := dec.ReadPayload()
if len(payload) < 4 {
return errors.New("not enough data")
}
//data_alignment_indicator := (payload[0]&0b0001_0000)>>4 == 1
flag := payload[1]
ptsFlag := flag>>7 == 1
dtsFlag := (flag&0b0100_0000)>>6 == 1
pesHeaderDataLen := payload[2]
payload = payload[3:]
extraData := payload[:pesHeaderDataLen]
if ptsFlag && len(extraData) > 4 {
*pts = uint32(extraData[0]&0b0000_1110) << 29
*pts += uint32(extraData[1]) << 22
*pts += uint32(extraData[2]&0b1111_1110) << 14
*pts += uint32(extraData[3]) << 7
*pts += uint32(extraData[4]) >> 1
if dtsFlag && len(extraData) > 9 {
*dts = uint32(extraData[5]&0b0000_1110) << 29
*dts += uint32(extraData[6]) << 22
*dts += uint32(extraData[7]&0b1111_1110) << 14
*dts += uint32(extraData[8]) << 7
*dts += uint32(extraData[9]) >> 1
}
}
dec.Payload = payload[pesHeaderDataLen:]
return err
}

View File

@@ -4,7 +4,7 @@ import (
"container/heap"
"errors"
"github.com/pion/rtp/v2"
"github.com/pion/rtp"
)
const MaxRtpDiff = 65000 //相邻两个包之间的最大差值