Compare commits

...

41 Commits

Author SHA1 Message Date
dexter
2fac74846a Merge pull request #19 from dwdcth/patch-1
Update message.go
2021-01-24 12:27:43 +08:00
dwdcth
fed0b22513 Update message.go 2021-01-24 12:20:52 +08:00
dwdcth
2326500086 Update message.go
宇视平台设备 xml body 换行修复
2021-01-24 11:10:01 +08:00
langhuihui
c31d10c349 Merge branch 'master' of https://github.com/Monibuca/plugin-gb28181 2021-01-18 23:11:55 +08:00
langhuihui
0d1a15f511 设置AutoUnPublish 2021-01-18 23:11:47 +08:00
dexter
0f0b36dc3d 多路播放invite机制调整 2021-01-18 22:33:14 +08:00
langhuihui
f224a96033 多画面采用低画质流 2021-01-17 21:30:25 +08:00
langhuihui
3b70a3ee69 修复超时判断逻辑 2021-01-13 21:20:34 +08:00
langhuihui
b1b0bf06f2 修改invte参数 2021-01-05 12:56:52 +08:00
dexter
7c48ad044c 实现Bye操作 2021-01-02 22:12:20 +08:00
dexter
bcd59cfc0f 录像回放播出画面 2020-12-31 15:11:30 +08:00
dexter
89f133e50e 大量修复 2020-12-28 23:12:39 +08:00
langhuihui
b885173222 重大修改,尝试增加对录像的回放功能 2020-12-28 18:50:51 +08:00
wancheng1990
e7e85466bf Merge pull request #17 from bosscheng/master
fix bug
2020-12-27 21:10:42 +08:00
万成
98cc8824f0 fix bug 2020-12-27 21:11:10 +08:00
wancheng1990
dbdf66cdef Merge pull request #12 from Monibuca/master
merge
2020-12-27 20:46:14 +08:00
langhuihui
66c1182a4d 修正一个小错误 2020-12-27 09:41:44 +08:00
langhuihui
07498fbe58 Merge remote-tracking branch 'origin/master' 2020-12-26 22:24:00 +08:00
wancheng1990
ed3cea25ef Merge pull request #16 from bosscheng/master
fix bug
2020-12-26 22:20:49 +08:00
wancheng1990
49b465be1b Merge pull request #11 from Monibuca/master
merge
2020-12-26 22:19:40 +08:00
万成
8faeab6728 fix bug 2020-12-26 22:19:45 +08:00
langhuihui
5ccebf2479 records遗漏 2020-12-26 22:09:56 +08:00
langhuihui
67c37b56a8 RecordList和DeviceList使用指针 2020-12-26 20:28:17 +08:00
万成
78b163384f fix bug 2020-12-26 20:04:43 +08:00
wancheng1990
a1534f72f8 Merge pull request #15 from bosscheng/master
fix bug
2020-12-26 20:04:33 +08:00
wancheng1990
beed7cba2a Merge pull request #14 from bosscheng/master
fix bug
2020-12-26 19:58:28 +08:00
wancheng1990
5799281628 Merge pull request #10 from Monibuca/master
Merge pull request #13 from bosscheng/master
2020-12-26 19:57:35 +08:00
万成
3ae1805543 fix bug 2020-12-26 19:57:59 +08:00
wancheng1990
c5d328da16 Merge pull request #13 from bosscheng/master
fix bug
2020-12-25 22:47:32 +08:00
wancheng1990
9ceeb2d511 Merge pull request #9 from Monibuca/master
Merge pull request #12 from bosscheng/master
2020-12-25 22:46:42 +08:00
万成
f3ffbb7f3d fix bugs 2020-12-25 22:47:07 +08:00
wancheng1990
af8829baa2 Merge pull request #12 from bosscheng/master
add record list
2020-12-24 15:17:56 +08:00
bosscheng1210
5b8f63a13b add record 2020-12-24 15:14:38 +08:00
万成
822f75d36b Update App.vue 2020-12-23 23:30:30 +08:00
wancheng1990
05b8d75155 Merge pull request #8 from Monibuca/master
merge
2020-12-23 21:58:02 +08:00
李宇翔
9669085328 增加查询录像接口 2020-12-23 09:00:01 +08:00
langhuihui
22a56b02fb 之前一次提交改错地方了 2020-12-19 20:41:30 +08:00
langhuihui
0f58d9dde6 使用外部暴露的IP作为接受推流的IP 2020-12-18 22:10:21 +08:00
wancheng1990
7f9fb67230 Merge pull request #11 from bosscheng/master
fix bug
2020-12-18 22:03:24 +08:00
万成
d25bb3854a fix bug 2020-12-18 22:02:15 +08:00
李宇翔
261bc00de0 兼容sip头部中参数没有值对情况 2020-12-18 09:17:47 +08:00
33 changed files with 3773 additions and 2098 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
node_modules
.vscode
.vscode
.idea

339
device.go Normal file
View File

@@ -0,0 +1,339 @@
package gb28181
import (
"fmt"
"strings"
"time"
"github.com/Monibuca/plugin-gb28181/transaction"
"github.com/Monibuca/plugin-gb28181/sip"
"github.com/Monibuca/plugin-gb28181/utils"
)
type ChannelEx struct {
device *Device
inviteRes *sip.Message
recordInviteRes *sip.Message
RecordSP string //正在播放录像的StreamPath
LiveSP string //实时StreamPath
Connected bool
Records []*Record
}
// Channel 通道
type Channel struct {
DeviceID string
Name string
Manufacturer string
Model string
Owner string
CivilCode string
Address string
Parental int
SafetyWay int
RegisterWay int
Secrecy int
Status string
ChannelEx //自定义属性
}
// func (c *Channel) MarshalJSON() ([]byte, error) {
// var data = map[string]interface{}{
// "DeviceID": c.DeviceID,
// "Name": c.Name,
// "Manufacturer": c.Manufacturer,
// "Address": c.Address,
// "Status": c.Status,
// "RecordSP": c.RecordSP,
// "LiveSP": c.LiveSP,
// "Records": c.Records,
// "Connected": c.Connected,
// }
// return json.Marshal(data)
// }
// Record 录像
type Record struct {
//channel *Channel
DeviceID string
Name string
FilePath string
Address string
StartTime string
EndTime string
Secrecy int
Type string
}
func (r *Record) GetPublishStreamPath() string {
return fmt.Sprintf("%s/%s", r.DeviceID, r.StartTime)
}
type Device struct {
*transaction.Core `json:"-"`
ID string
RegisterTime time.Time
UpdateTime time.Time
Status string
Channels []*Channel
sn int
from *sip.Contact
to *sip.Contact
Addr string
SipIP string //暴露的IP
}
func (d *Device) UpdateChannels(list []*Channel) {
for _, c := range list {
c.device = d
have := false
for i, o := range d.Channels {
if o.DeviceID == c.DeviceID {
c.ChannelEx = o.ChannelEx
d.Channels[i] = c
have = true
break
}
}
if !have {
d.Channels = append(d.Channels, c)
}
}
}
func (d *Device) UpdateRecord(channelId string, list []*Record) {
for _, c := range d.Channels {
if c.DeviceID == channelId {
c.Records = list
//for _, o := range list {
// o.channel = c
//}
break
}
}
}
func (c *Channel) CreateMessage(Method sip.Method) (requestMsg *sip.Message) {
requestMsg = c.device.CreateMessage(Method)
requestMsg.StartLine.Uri = sip.NewURI(c.DeviceID + "@" + c.device.to.Uri.Domain())
requestMsg.To = &sip.Contact{
Uri: requestMsg.StartLine.Uri,
}
requestMsg.From = &sip.Contact{
Uri: sip.NewURI(config.Serial + "@" + config.Realm),
Params: map[string]string{"tag": utils.RandNumString(9)},
}
return
}
func (c *Channel) GetPublishStreamPath(start string) string {
if start == "0" {
return fmt.Sprintf("%s/%s", c.device.ID, c.DeviceID)
}
return fmt.Sprintf("%s/%s", c.DeviceID, start)
}
func (d *Device) CreateMessage(Method sip.Method) (requestMsg *sip.Message) {
d.sn++
requestMsg = &sip.Message{
Mode: sip.SIP_MESSAGE_REQUEST,
MaxForwards: 70,
UserAgent: "Monibuca",
StartLine: &sip.StartLine{
Method: Method,
Uri: d.to.Uri,
}, Via: &sip.Via{
Transport: "UDP",
Host: d.Core.SipIP,
Port: fmt.Sprintf("%d", d.SipPort),
Params: map[string]string{
"branch": fmt.Sprintf("z9hG4bK%s", utils.RandNumString(8)),
"rport": "-1", //only key,no-value
},
}, From: d.from,
To: d.to, CSeq: &sip.CSeq{
ID: uint32(d.sn),
Method: Method,
}, CallID: utils.RandNumString(10),
Addr: d.Addr,
}
requestMsg.From.Params["tag"] = utils.RandNumString(9)
return
}
func (d *Device) Query() int {
requestMsg := d.CreateMessage(sip.MESSAGE)
requestMsg.ContentType = "Application/MANSCDP+xml"
requestMsg.Body = fmt.Sprintf(`<?xml version="1.0"?>
<Query>
<CmdType>Catalog</CmdType>
<SN>%d</SN>
<DeviceID>%s</DeviceID>
</Query>`, d.sn, requestMsg.To.Uri.UserInfo())
requestMsg.ContentLength = len(requestMsg.Body)
response := d.SendMessage(requestMsg)
if response.Data != nil && response.Data.Via.Params["received"] != "" {
d.SipIP = response.Data.Via.Params["received"]
}
return response.Code
}
func (d *Device) QueryRecord(channelIndex int, startTime, endTime string) int {
channel := d.Channels[channelIndex]
requestMsg := channel.CreateMessage(sip.MESSAGE)
requestMsg.ContentType = "Application/MANSCDP+xml"
requestMsg.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>time</Type>
</Query>`, d.sn, requestMsg.To.Uri.UserInfo(), startTime, endTime)
requestMsg.ContentLength = len(requestMsg.Body)
return d.SendMessage(requestMsg).Code
}
func (d *Device) Control(channelIndex int, PTZCmd string) int {
channel := d.Channels[channelIndex]
requestMsg := channel.CreateMessage(sip.MESSAGE)
requestMsg.ContentType = "Application/MANSCDP+xml"
requestMsg.Body = fmt.Sprintf(`<?xml version="1.0"?>
<Control>
<CmdType>DeviceControl</CmdType>
<SN>%d</SN>
<DeviceID>%s</DeviceID>
<PTZCmd>%s</PTZCmd>
</Control>`, d.sn, requestMsg.To.Uri.UserInfo(), PTZCmd)
requestMsg.ContentLength = len(requestMsg.Body)
return d.SendMessage(requestMsg).Code
}
/*
f字段 f = v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率
各项具体含义:
v后续参数为视频的参数各参数间以 “/”分割;
编码格式:十进制整数字符串表示
1 MPEG-4 2 H.264 3 SVAC 4 3GP
分辨率:十进制整数字符串表示
1 QCIF 2 CIF 3 4CIF 4 D1 5 720P 6 1080P/I
帧率:十进制整数字符串表示 099
码率类型:十进制整数字符串表示
1 固定码率CBR 2 可变码率VBR
码率大小:十进制整数字符串表示 0100000如 1表示1kbps
a后续参数为音频的参数各参数间以 “/”分割;
编码格式:十进制整数字符串表示
1 G.711 2 G.723.1 3 G.729 4 G.722.1
码率大小:十进制整数字符串
音频编码码率: 1 — 5.3 kbps G.723.1中使用)
2 — 6.3 kbps G.723.1中使用)
3 — 8 kbps G.729中使用)
4 — 16 kbps G.722.1中使用)
5 — 24 kbps G.722.1中使用)
6 — 32 kbps G.722.1中使用)
7 — 48 kbps G.722.1中使用)
8 — 64 kbpsG.711中使用)
采样率:十进制整数字符串表示
1 — 8 kHzG.711/ G.723.1/ G.729中使用)
2—14 kHzG.722.1中使用)
3—16 kHzG.722.1中使用)
4—32 kHzG.722.1中使用)
注1字符串说明
本节中使用的“十进制整数字符串”的含义为“0”“4294967296” 之间的十进制数字字符串。
注2参数分割标识
各参数间以“/”分割,参数间的分割符“/”不能省略;
若两个分割符 “/”间的某参数为空时(即两个分割符 “/”直接将相连时)表示无该参数值;
注3f字段说明
使用f字段时应保证视频和音频参数的结构完整性即在任何时候f字段的结构都应是完整的结构
f = v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率
若只有视频时,音频中的各参数项可以不填写,但应保持 “a///”的结构:
f = v/编码格式/分辨率/帧率/码率类型/码率大小a///
若只有音频时也类似处理,视频中的各参数项可以不填写,但应保持 “v/”的结构:
f = v/a/编码格式/码率大小/采样率
f字段中视、音频参数段之间不需空格分割。
可使用f字段中的分辨率参数标识同一设备不同分辨率的码流。
*/
func (d *Device) Invite(channelIndex int, start, end string, f string) int {
channel := d.Channels[channelIndex]
port, publisher := d.publish(channel.GetPublishStreamPath(start))
if port == 0 {
channel.Connected = true
return 304
}
ssrc := "0200000001"
// size := 1
// fps := 15
// bitrate := 200
// fmt.Sprintf("f=v/2/%d/%d/1/%da///", size, fps, bitrate)
s := "Play"
if start != "0" {
s = "Playback"
publisher.AutoUnPublish = true
channel.RecordSP = publisher.StreamPath
} else {
channel.LiveSP = publisher.StreamPath
}
sdpInfo := []string{
"v=0",
fmt.Sprintf("o=%s 0 0 IN IP4 %s", d.Serial, d.SipIP),
"s=" + s,
"u=" + channel.DeviceID + ":0",
"c=IN IP4 " + d.SipIP,
fmt.Sprintf("t=%s %s", start, end),
fmt.Sprintf("m=video %d RTP/AVP 96 97 98", port),
"a=recvonly",
"a=rtpmap:96 PS/90000",
"a=rtpmap:97 MPEG4/90000",
"a=rtpmap:98 H264/90000",
"y=" + ssrc,
"f=" + f,
}
invite := channel.CreateMessage(sip.INVITE)
invite.ContentType = "application/sdp"
invite.Contact = &sip.Contact{
Uri: sip.NewURI(fmt.Sprintf("%s@%s:%d", d.Serial, d.SipIP, d.SipPort)),
}
invite.Body = strings.Join(sdpInfo, "\r\n") + "\r\n"
invite.ContentLength = len(invite.Body)
invite.Subject = fmt.Sprintf("%s:%s,%s:0", channel.DeviceID, ssrc, config.Serial)
response := d.SendMessage(invite)
fmt.Printf("invite response statuscode: %d\n", response.Code)
if response.Code == 200 {
if start == "0" {
channel.inviteRes = response.Data
channel.Connected = true
} else {
channel.recordInviteRes = response.Data
}
ack := d.CreateMessage(sip.ACK)
ack.StartLine = &sip.StartLine{
Uri: sip.NewURI(channel.DeviceID + "@" + d.to.Uri.Domain()),
Method: sip.ACK,
}
ack.From = response.Data.From
ack.To = response.Data.To
ack.CallID = response.Data.CallID
ack.CSeq.ID = invite.CSeq.ID
go d.Send(ack)
}
return response.Code
}
func (d *Device) Bye(channelIndex int) int {
channel := d.Channels[channelIndex]
defer func() {
channel.inviteRes = nil
channel.Connected = false
}()
return channel.Bye(channel.inviteRes).Code
}
func (c *Channel) Bye(res *sip.Message) *transaction.Response {
if res == nil {
return nil
}
bye := c.device.CreateMessage(sip.BYE)
bye.StartLine = &sip.StartLine{
Uri: sip.NewURI(c.DeviceID + "@" + c.device.to.Uri.Domain()),
Method: sip.BYE,
}
bye.From = res.From
bye.To = res.To
bye.CallID = res.CallID
return c.device.SendMessage(bye)
}

11
go.mod
View File

@@ -3,8 +3,17 @@ module github.com/Monibuca/plugin-gb28181
go 1.13
require (
github.com/Monibuca/engine/v2 v2.2.2
github.com/Monibuca/engine/v2 v2.2.5
github.com/Monibuca/plugin-gateway v1.2.6 // indirect
github.com/Monibuca/plugin-hdl v1.2.5 // indirect
github.com/Monibuca/plugin-hls v1.2.1 // indirect
github.com/Monibuca/plugin-jessica v1.3.0 // indirect
github.com/Monibuca/plugin-logrotate v1.2.3 // indirect
github.com/Monibuca/plugin-record v1.0.4 // indirect
github.com/Monibuca/plugin-rtmp v1.2.5 // indirect
github.com/Monibuca/plugin-rtp v1.0.0
github.com/Monibuca/plugin-rtsp v1.3.1 // indirect
github.com/Monibuca/plugin-webrtc v1.2.2 // indirect
github.com/logrusorgru/aurora v2.0.3+incompatible
golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d
)

133
go.sum
View File

@@ -1,53 +1,180 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Monibuca/engine/v2 v2.0.0/go.mod h1:34EYjjV15G6myuHOKaJkO7y5tJ1Arq/NfC9Weacr2mc=
github.com/Monibuca/engine/v2 v2.1.9/go.mod h1:34EYjjV15G6myuHOKaJkO7y5tJ1Arq/NfC9Weacr2mc=
github.com/Monibuca/engine/v2 v2.2.0/go.mod h1:34EYjjV15G6myuHOKaJkO7y5tJ1Arq/NfC9Weacr2mc=
github.com/Monibuca/engine/v2 v2.2.2 h1:ho5M3aFW9Mlj9Lb56Qvk0m+9L8yWc7RhwPh8dRWAeBk=
github.com/Monibuca/engine/v2 v2.2.2/go.mod h1:34EYjjV15G6myuHOKaJkO7y5tJ1Arq/NfC9Weacr2mc=
github.com/Monibuca/plugin-gb28181 v1.0.0-alpha3 h1:1oOSVIhkHxWZ5ALCVJG7P5MKxywNVm2zEwaHj+UqU1g=
github.com/Monibuca/plugin-gb28181 v1.0.0-alpha3/go.mod h1:fyzQG2o13Df9VdCd4QrjbY0AFtcoKeIfnTMErVhWpLA=
github.com/Monibuca/engine/v2 v2.2.5 h1:/w0BrvdTy4cqLD2uaIRaqBwdnu+/VDk+r3sjFbpbc1E=
github.com/Monibuca/engine/v2 v2.2.5/go.mod h1:34EYjjV15G6myuHOKaJkO7y5tJ1Arq/NfC9Weacr2mc=
github.com/Monibuca/plugin-gateway v1.2.6 h1:ZHwHMmjBs3Itxxd/Z/OR7c5QdED1sqSmBJ/6U+Y/Hc0=
github.com/Monibuca/plugin-gateway v1.2.6/go.mod h1:l+BaE5QZY0Pr92iiIXS67gt2VxcBuOxYyqIGOEGbuFk=
github.com/Monibuca/plugin-hdl v1.2.5 h1:zIo4ptkIzn/L1KhutsZ2Bk17OoyRbundXG+8c1ki6cQ=
github.com/Monibuca/plugin-hdl v1.2.5/go.mod h1:+Qm9B5tQfxQdZ1hJQZbW4BZLOQWbPwN9qkB0vPGFo1U=
github.com/Monibuca/plugin-hls v1.2.1 h1:vD8LFB1t87fKuEycuQYa1nayCcEkRpIo0xosDJX3yGM=
github.com/Monibuca/plugin-hls v1.2.1/go.mod h1:8mIEvD09qDxp2Lso7fOQnfNFcxPcMxa6P9dGBbUOZD0=
github.com/Monibuca/plugin-jessica v1.3.0 h1:VPLPl76ykEHvrtQMy4+UZQBJd2gjYfWQbrg47qnOIT0=
github.com/Monibuca/plugin-jessica v1.3.0/go.mod h1:9TOsOzeVNbTCbgZ6xMANNMA0h5O/GxdlCLhavo6yVHM=
github.com/Monibuca/plugin-logrotate v1.2.3 h1:Q3V4VrN8fVUQnN+RIP+wn/+ZrZ/cu8xM5rTeIg74QVM=
github.com/Monibuca/plugin-logrotate v1.2.3/go.mod h1:bo2zR3H2CuyN2+dbsnZ6IvM+NT87r9H2RptVyyxkvTY=
github.com/Monibuca/plugin-record v1.0.4 h1:dPXJKlkljyhNUrpVP3586QO1L2DFvvl8m+ZXmQlEul8=
github.com/Monibuca/plugin-record v1.0.4/go.mod h1:POh+M09c+U1YAt6ratoKXahjt8GopEL0k6M8DUXbmAc=
github.com/Monibuca/plugin-rtmp v1.2.5 h1:y9tQ7ayg8y3PE8jpBmb3/EHNgBL5cppptKvulFdvkzE=
github.com/Monibuca/plugin-rtmp v1.2.5/go.mod h1:RSbufAndiyYfeLz4OiZwcGS/UTHCyTDtqvHzvzRQtWE=
github.com/Monibuca/plugin-rtp v1.0.0 h1:yksNsIIGxoKX8UZirkAUK+mGZ/XoEeS2vqbIqtqXyCg=
github.com/Monibuca/plugin-rtp v1.0.0/go.mod h1:0xkNm23a/BjVnEMz1zXyOqfEjoVmGe3PJqPNF1KyFGc=
github.com/Monibuca/plugin-rtsp v1.3.1 h1:b7xtRIBVcoa6WDseErVDzt+8dZGaUwhDLHxGuutROag=
github.com/Monibuca/plugin-rtsp v1.3.1/go.mod h1:N5nEu2SZTf4NMJn7DLlw39wQ4kXZAuMrfbbNtYES894=
github.com/Monibuca/plugin-ts v1.2.1 h1:Y2QgEal9/ot43QEvtzRkmA8yk+N2uzieAzKuujZJFMs=
github.com/Monibuca/plugin-ts v1.2.1/go.mod h1:MZsdv34Od1Kh1WOkuqIAO/t2irjzW39RMO9WbpFks4g=
github.com/Monibuca/plugin-webrtc v1.2.2 h1:XTc3kUyzpoxXC0L9qVUhrhGFBy9YagdRzJb6lDU9HTc=
github.com/Monibuca/plugin-webrtc v1.2.2/go.mod h1:ZPcGimoI8Yl/IzQJhSp4jgVOpmMeqPDcN7H8xv1Xj9g=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/funny/slab v0.0.0-20180511031532-b1fad5e5d478 h1:Db9StoJ6RZN3YttC0Pm0I4Y5izITRYch3RMbT59BYN0=
github.com/funny/slab v0.0.0-20180511031532-b1fad5e5d478/go.mod h1:0j1+svBH8ABEIPdUP0AIg4qedsybnXGJBakCEw8cfoo=
github.com/funny/utest v0.0.0-20161029064919-43870a374500 h1:Z0r1CZnoIWFB/Uiwh1BU5FYmuFe6L5NPi6XWQEmsTRg=
github.com/funny/utest v0.0.0-20161029064919-43870a374500/go.mod h1:mUn39tBov9jKnTWV1RlOYoNzxdBFHiSzXWdY1FoNGGg=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
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/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
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.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
github.com/mask-pp/rtp-ps v1.0.0 h1:JFxuJL9N+gD1ldgJlAy3b7rYfY8wAVHi9ODNmdP4+EE=
github.com/mask-pp/rtp-ps v1.0.0/go.mod h1:jCxsZ2G7z/jX+aqFypEWMePnhNrfnUiXUEKm6Xp0vgU=
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pion/datachannel v1.4.17 h1:8CChK5VrJoGrwKCysoTscoWvshCAFpUkgY11Tqgz5hE=
github.com/pion/datachannel v1.4.17/go.mod h1:+vPQfypU9vSsyPXogYj1hBThWQ6MNXEQoQAzxoPvjYM=
github.com/pion/dtls/v2 v2.0.0 h1:Fk+MBhLZ/U1bImzAhmzwbO/pP2rKhtTw8iA934H3ybE=
github.com/pion/dtls/v2 v2.0.0/go.mod h1:VkY5VL2wtsQQOG60xQ4lkV5pdn0wwBBTzCfRJqXhp3A=
github.com/pion/ice v0.7.15 h1:s1In+gnuyVq7WKWGVQL+1p+OcrMsbfL+VfSe2isH8Ag=
github.com/pion/ice v0.7.15/go.mod h1:Z6zybEQgky5mZkKcLfmvc266JukK2srz3VZBBD1iXBw=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY=
github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0=
github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k=
github.com/pion/randutil v0.0.0 h1:aLWLVhTG2jzoD25F0OlW6nXvXrjoGwiXq2Sz7j7NzL0=
github.com/pion/randutil v0.0.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.1 h1:S3yG4KpYAiSmBVqKAfgRa5JdwBNj4zK3RLUa8JYdhak=
github.com/pion/rtcp v1.2.1/go.mod h1:a5dj2d6BKIKHl43EnAOIrCczcjESrtPuMgfmL6/K6QM=
github.com/pion/rtp v1.5.4 h1:PuNg6xqV3brIUihatcKZj1YDUs+M45L0ZbrZWYtkDxY=
github.com/pion/rtp v1.5.4/go.mod h1:bg60AL5GotNOlYZsqycbhDtEV3TkfbpXG0KBiUq29Mg=
github.com/pion/rtp v1.6.0 h1:4Ssnl/T5W2LzxHj9ssYpGVEQh3YYhQFNVmSWO88MMwk=
github.com/pion/rtp v1.6.0/go.mod h1:QgfogHsMBVE/RFNno467U/KBqfUywEH+HK+0rtnwsdI=
github.com/pion/sctp v1.7.6 h1:8qZTdJtbKfAns/Hv5L0PAj8FyXcsKhMH1pKUCGisQg4=
github.com/pion/sctp v1.7.6/go.mod h1:ichkYQ5tlgCQwEwvgfdcAolqx1nHbYCxo4D7zK/K0X8=
github.com/pion/sdp/v2 v2.3.7 h1:WUZHI3pfiYCaE8UGUYcabk863LCK+Bq3AklV5O0oInQ=
github.com/pion/sdp/v2 v2.3.7/go.mod h1:+ZZf35r1+zbaWYiZLfPutWfx58DAWcGb2QsS3D/s9M8=
github.com/pion/srtp v1.3.3 h1:8bjs9YaSNvSrbH0OfKxzPX+PTrCyAC2LoT9Qesugi+U=
github.com/pion/srtp v1.3.3/go.mod h1:jNe0jmIOqksuurR9S/7yoKDalfPeluUFrNPCBqI4FOI=
github.com/pion/stun v0.3.3/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M=
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE=
github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8=
github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE=
github.com/pion/transport v0.10.1 h1:2W+yJT+0mOQ160ThZYUx5Zp2skzshiNgxrNE9GUfhJM=
github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
github.com/pion/turn/v2 v2.0.3/go.mod h1:kl1hmT3NxcLynpXVnwJgObL8C9NaCyPTeqI2DcCpSZs=
github.com/pion/turn/v2 v2.0.4 h1:oDguhEv2L/4rxwbL9clGLgtzQPjtuZwCdoM7Te8vQVk=
github.com/pion/turn/v2 v2.0.4/go.mod h1:1812p4DcGVbYVBTiraUmP50XoKye++AMkbfp+N27mog=
github.com/pion/webrtc/v2 v2.2.14 h1:bRjnXTqMDJ3VERPF45z439Sv6QfDfjdYvdQk1QcIx8M=
github.com/pion/webrtc/v2 v2.2.14/go.mod h1:G+8lShCMbHhjpMF1ZJBkyuvrxXrvW4bxs3nOt+mJ2UI=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/quangngotan95/go-m3u8 v0.1.0 h1:8oseBjJn5IKHQKdRZwSNskkua3NLrRtlvXXtoVgBzMk=
github.com/quangngotan95/go-m3u8 v0.1.0/go.mod h1:smzfWHlYpBATVNu1GapKLYiCtEo5JxridIgvvudZ+Wc=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/shirou/gopsutil v2.20.1+incompatible h1:oIq9Cq4i84Hk8uQAUOG3eNdI/29hBawGrD5YRl6JRDY=
github.com/shirou/gopsutil v2.20.1+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/gopsutil v2.20.7+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/gopsutil v2.20.8+incompatible h1:8c7Atn0FAUZJo+f4wYbN0iVpdWniCQk7IYwGtgdh1mY=
github.com/shirou/gopsutil v2.20.8+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA=
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0=
github.com/zhangpeihao/goamf v0.0.0-20140409082417-3ff2c19514a8 h1:r1JUI0wuHlgRb8jNd3zPBBkjUdrjpVKr8SdJWc8ntg8=
github.com/zhangpeihao/goamf v0.0.0-20140409082417-3ff2c19514a8/go.mod h1:RZd/IqzNpFANwOB9rVmsnAYpo/6KesK4PqrN1a5cRgg=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
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-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d h1:dOiJ2n2cMwGLce/74I/QHMbnpk5GfY7InR8rczoMqRM=
golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200828161417-c663848e9a16/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

156
main.go
View File

@@ -1,6 +1,8 @@
package gb28181
import (
"bytes"
"encoding/xml"
"log"
"math/rand"
"net"
@@ -10,6 +12,9 @@ import (
"sync"
"time"
"github.com/Monibuca/plugin-gb28181/sip"
"golang.org/x/net/html/charset"
. "github.com/Monibuca/engine/v2"
"github.com/Monibuca/engine/v2/util"
"github.com/Monibuca/plugin-gb28181/transaction"
@@ -62,14 +67,34 @@ func run() {
MediaPortMax: config.MediaPortMax,
MediaIdleTimeout: 30,
}
s := transaction.NewCore(config)
s.OnInvite = onPublish
http.HandleFunc("/gb28181/query/records", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
id := r.URL.Query().Get("id")
channel, err := strconv.Atoi(r.URL.Query().Get("channel"))
if err != nil {
w.WriteHeader(404)
}
startTime := r.URL.Query().Get("startTime")
endTime := r.URL.Query().Get("endTime")
if v, ok := Devices.Load(id); ok {
w.WriteHeader(v.(*Device).QueryRecord(channel, startTime, endTime))
} else {
w.WriteHeader(404)
}
})
http.HandleFunc("/gb28181/list", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
sse := util.NewSSE(w, r.Context())
for {
var list []*transaction.Device
s.Devices.Range(func(key, value interface{}) bool {
list = append(list, value.(*transaction.Device))
var list []*Device
Devices.Range(func(key, value interface{}) bool {
device := value.(*Device)
if time.Since(device.UpdateTime) > time.Duration(config.RegisterValidity)*time.Second {
Devices.Delete(key)
} else {
list = append(list, device)
}
return true
})
sse.WriteJSON(list)
@@ -88,21 +113,31 @@ func run() {
w.WriteHeader(404)
}
ptzcmd := r.URL.Query().Get("ptzcmd")
if v, ok := s.Devices.Load(id); ok {
w.WriteHeader(v.(*transaction.Device).Control(channel, ptzcmd))
if v, ok := Devices.Load(id); ok {
w.WriteHeader(v.(*Device).Control(channel, ptzcmd))
} else {
w.WriteHeader(404)
}
})
http.HandleFunc("/gb28181/invite", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
id := r.URL.Query().Get("id")
channel, err := strconv.Atoi(r.URL.Query().Get("channel"))
query := r.URL.Query()
id := query.Get("id")
channel, err := strconv.Atoi(query.Get("channel"))
startTime := query.Get("startTime")
endTime := query.Get("endTime")
f := query.Get("f")
if startTime == "" {
startTime = "0"
}
if endTime == "" {
endTime = "0"
}
if err != nil {
w.WriteHeader(404)
}
if v, ok := s.Devices.Load(id); ok {
w.WriteHeader(v.(*transaction.Device).Invite(channel))
if v, ok := Devices.Load(id); ok {
w.WriteHeader(v.(*Device).Invite(channel, startTime, endTime, f))
} else {
w.WriteHeader(404)
}
@@ -114,25 +149,83 @@ func run() {
if err != nil {
w.WriteHeader(404)
}
if v, ok := s.Devices.Load(id); ok {
w.WriteHeader(v.(*transaction.Device).Bye(channel))
if v, ok := Devices.Load(id); ok {
w.WriteHeader(v.(*Device).Bye(channel))
} else {
w.WriteHeader(404)
}
})
s := transaction.NewCore(config)
s.OnRegister = func(msg *sip.Message) {
Devices.Store(msg.From.Uri.UserInfo(), &Device{
ID: msg.From.Uri.UserInfo(),
RegisterTime: time.Now(),
UpdateTime: time.Now(),
Status: string(sip.REGISTER),
Core: s,
from: &sip.Contact{Uri: msg.StartLine.Uri, Params: make(map[string]string)},
to: msg.To,
Addr: msg.Via.GetSendBy(),
SipIP: config.MediaIP,
})
}
s.OnMessage = func(msg *sip.Message) bool {
if v, ok := Devices.Load(msg.From.Uri.UserInfo()); ok {
d := v.(*Device)
if d.Status == string(sip.REGISTER) {
d.Status = "ONLINE"
}
d.UpdateTime = time.Now()
temp := &struct {
XMLName xml.Name
CmdType string
DeviceID string
DeviceList []*Channel `xml:"DeviceList>Item"`
RecordList []*Record `xml:"RecordList>Item"`
}{}
decoder := xml.NewDecoder(bytes.NewReader([]byte(msg.Body)))
decoder.CharsetReader = charset.NewReaderLabel
decoder.Decode(temp)
switch temp.XMLName.Local {
case "Notify":
go d.Query()
case "Response":
switch temp.CmdType {
case "Catalog":
d.UpdateChannels(temp.DeviceList)
case "RecordInfo":
d.UpdateRecord(temp.DeviceID, temp.RecordList)
}
}
return true
}
return false
}
//OnStreamClosedHooks.AddHook(func(stream *Stream) {
// Devices.Range(func(key, value interface{}) bool {
// device:=value.(*Device)
// for _,channel := range device.Channels {
// if stream.StreamPath == channel.RecordSP {
//
// }
// }
// })
//})
s.Start()
}
func onPublish(channel *transaction.Channel) (port int) {
rtpPublisher := new(rtp.RTP_PS)
if !rtpPublisher.Publish("gb28181/" + channel.DeviceID) {
func (d *Device) publish(name string) (port int, publisher *rtp.RTP_PS) {
publisher = new(rtp.RTP_PS)
if !publisher.Publish(name) {
return
}
defer func() {
if port == 0 {
rtpPublisher.Close()
publisher.Close()
}
}()
rtpPublisher.Type = "GB28181"
publisher.Type = "GB28181"
publisher.AutoUnPublish = true
var conn *net.UDPConn
var err error
rang := int(config.MediaPortMax - config.MediaPortMin)
@@ -166,16 +259,35 @@ func onPublish(channel *transaction.Channel) (port int) {
bufUDP := make([]byte, 1048576)
Printf("udp server start listen video port[%d]", port)
defer Printf("udp server stop listen video port[%d]", port)
defer conn.Close()
for rtpPublisher.Err() == nil {
for publisher.Err() == nil {
if err = conn.SetReadDeadline(time.Now().Add(time.Second * 30)); err != nil {
return
}
if n, _, err := conn.ReadFromUDP(bufUDP); err == nil {
rtpPublisher.PushPS(bufUDP[:n])
publisher.PushPS(bufUDP[:n])
} else {
Println("udp server read video pack error", err)
rtpPublisher.Close()
publisher.Close()
if !publisher.AutoUnPublish {
for _, channel := range d.Channels {
if channel.LiveSP == name {
channel.LiveSP = ""
channel.Connected = false
channel.Bye(channel.inviteRes)
break
}
}
}
}
}
conn.Close()
if publisher.AutoUnPublish {
for _, channel := range d.Channels {
if channel.RecordSP == name {
channel.RecordSP = ""
channel.Bye(channel.recordInviteRes)
break
}
}
}
}()

View File

@@ -12,7 +12,7 @@ import (
//windows : \n
//Mac OS : \r
const (
VERSION = "SIP/2.0" // sip version
VERSION = "SIP/2.0" // sip version
CRLF = "\r\n" // 0x0D0A
CRLFCRLF = "\r\n\r\n" // 0x0D0A0D0A
@@ -440,28 +440,29 @@ type URI struct {
params map[string]string // include branch/maddr/received/ttl/rport
headers map[string]string // include branch/maddr/received/ttl/rport
}
func (u *URI) Host() string {
return u.host
}
func (u *URI) UserInfo() string {
return strings.Split(u.host,"@")[0]
return strings.Split(u.host, "@")[0]
}
func (u *URI) Domain() string {
return strings.Split(u.host,"@")[1]
return strings.Split(u.host, "@")[1]
}
func (u *URI) IP() string {
t:=strings.Split(u.host,"@")
t := strings.Split(u.host, "@")
if len(t) == 1 {
return strings.Split(t[0],":")[0]
return strings.Split(t[0], ":")[0]
}
return strings.Split(t[1],":")[0]
return strings.Split(t[1], ":")[0]
}
func (u *URI) Port() string {
t:=strings.Split(u.host,"@")
t := strings.Split(u.host, "@")
if len(t) == 1 {
return strings.Split(t[0],":")[1]
return strings.Split(t[0], ":")[1]
}
return strings.Split(t[1],":")[1]
return strings.Split(t[1], ":")[1]
}
func (u *URI) String() string {
if u.scheme == "" {
@@ -546,8 +547,12 @@ func parseURI(str string) (ret URI, err error) {
arr1 := strings.Split(paramStr, ";")
for _, one := range arr1 {
tmp := strings.Split(one, "=")
k, v := tmp[0], tmp[1]
ret.params[k] = v
if len(tmp) == 2 {
k, v := tmp[0], tmp[1]
ret.params[k] = v
} else {
ret.params[tmp[0]] = ""
}
}
}

View File

@@ -6,7 +6,7 @@ import (
)
func TestContact(t *testing.T) {
str1 := "\"Mr.Watson\" <sip:watson@worcester.bell-telephone.com>;q=0.7; expires=3600,\"Mr.Watson\" <mailto:watson@bell-telephone.com>";
str1 := "\"Mr.Watson\" <sip:watson@worcester.bell-telephone.com>;q=0.7; expires=3600,\"Mr.Watson\" <mailto:watson@bell-telephone.com>"
//str1 := `"Mr.Watson" <sip:watson@worcester.bell-telephone.com>;q=0.7;`
c := &Contact{}
err := c.Parse(str1)

View File

@@ -219,8 +219,10 @@ func Decode(data []byte) (msg *Message, err error) {
}
headStr := strings.TrimSpace(msgArr[0])
if len(msgArr) > 1 {
msg.Body = strings.TrimSpace(msgArr[1])
if msgArrLen := len(msgArr); msgArrLen > 1 {
for i := 1; i < msgArrLen; i++ {
msg.Body += strings.TrimSpace(msgArr[i])
}
}
headStr = strings.Trim(headStr, CRLF)
@@ -426,6 +428,12 @@ func Encode(msg *Message) ([]byte, error) {
sb.WriteString(CRLF)
}
if msg.Subject != "" {
sb.WriteString("Subject: ")
sb.WriteString(msg.Subject)
sb.WriteString(CRLF)
}
if msg.IsRequest() {
//request only

View File

@@ -88,9 +88,8 @@ var errorMap = map[int]string{
}
func DumpError(code int) string {
if code == 0{
if code == 0 {
return "invalid status reason for request"
}
return fmt.Sprintf("%d %s", code, errorMap[code])
}

View File

@@ -1,11 +1,8 @@
package transaction
import (
"bytes"
"context"
"encoding/xml"
"fmt"
"io"
"net"
"os"
"sync"
@@ -14,7 +11,6 @@ import (
"github.com/Monibuca/plugin-gb28181/sip"
"github.com/Monibuca/plugin-gb28181/transport"
"github.com/Monibuca/plugin-gb28181/utils"
"golang.org/x/net/html/charset"
)
//Core: transactions manager
@@ -26,9 +22,9 @@ type Core struct {
mutex sync.RWMutex //transactions的锁
removeTa chan string //要删除transaction的时候通过chan传递tid
tp transport.ITransport //transport
config *Config //sip server配置信息
Devices sync.Map
OnInvite func(*Channel) int
*Config //sip server配置信息
OnRegister func(*sip.Message)
OnMessage func(*sip.Message) bool
}
//初始化一个 Core需要能响应请求也要能发起请求
@@ -42,7 +38,7 @@ func NewCore(config *Config) *Core {
handlers: make(map[State]map[Event]Handler),
transactions: make(map[string]*Transaction),
removeTa: make(chan string, 10),
config: config,
Config: config,
ctx: context.Background(),
}
if config.SipNetwork == "TCP" {
@@ -221,9 +217,7 @@ func (c *Core) Handler() {
os.Exit(1)
}
}()
ch := c.tp.ReadPacketChan()
timer := time.Tick(time.Second * 5)
//阻塞读取消息
for {
//fmt.Println("PacketHandler ========== SIP Client")
@@ -236,8 +230,6 @@ func (c *Core) Handler() {
fmt.Println("handler sip response message failed:", err.Error())
continue
}
case <-timer:
c.RemoveDead()
}
}
}
@@ -251,8 +243,9 @@ func (c *Core) Handler() {
//发送之后就开启timer超时重传还要记录和修改每次超时时间。不超时的话记得删掉timer
//发送 register 消息
func (c *Core) SendMessage(msg *sip.Message) *Response {
methond := msg.GetMethod()
fmt.Println("send message:", methond)
method := msg.GetMethod()
// data, _ := sip.Encode(msg)
fmt.Println("send message:", method)
e := c.NewOutGoingMessageEvent(msg)
@@ -267,7 +260,7 @@ func (c *Core) SendMessage(msg *sip.Message) *Response {
//如果是sip 消息事件则将消息缓存填充typo和state
if msg.IsRequest() {
//as uac
if msg.GetMethod() == sip.INVITE || msg.GetMethod() == sip.ACK {
if method == sip.INVITE || method == sip.ACK {
ta.typo = FSM_ICT
ta.state = ICT_PRE_CALLING
} else {
@@ -304,7 +297,7 @@ func (c *Core) SendMessage(msg *sip.Message) *Response {
//响应消息则需要匹配到请求让请求的transaction来处理。
//TODO参考srs和osip的流程以及文档做最终处理。需要将逻辑分成两层TU 层和 transaction 层
func (c *Core) HandleReceiveMessage(p *transport.Packet) (err error) {
//fmt.Println("packet content:", string(p.Data))
// fmt.Println("packet content:", string(p.Data))
var msg *sip.Message
msg, err = sip.Decode(p.Data)
if err != nil {
@@ -345,34 +338,8 @@ func (c *Core) HandleReceiveMessage(p *transport.Packet) (err error) {
c.Send(msg.BuildResponse(200))
return
case sip.MESSAGE:
if v, ok := c.Devices.Load(msg.From.Uri.UserInfo()); ok {
d := v.(*Device)
if d.Status == string(sip.REGISTER) {
d.Status = "ONLINE"
}
d.UpdateTime = time.Now()
temp := &struct {
XMLName xml.Name
CmdType string
DeviceList []Channel `xml:"DeviceList>Item"`
}{}
decoder := xml.NewDecoder(bytes.NewReader([]byte(msg.Body)))
decoder.CharsetReader = func(c string, i io.Reader) (io.Reader, error) {
return charset.NewReaderLabel(c, i)
}
decoder.Decode(temp)
switch temp.XMLName.Local {
case "Notify":
go d.Query()
case "Response":
switch temp.CmdType {
case "Catalog":
d.UpdateChannels(temp.DeviceList)
}
}
if ta == nil {
c.Send(msg.BuildResponse(200))
}
if c.OnMessage(msg) && ta == nil {
c.Send(msg.BuildResponse(200))
}
if ta != nil {
ta.event <- c.NewOutGoingMessageEvent(msg.BuildResponse(200))
@@ -384,7 +351,7 @@ func (c *Core) HandleReceiveMessage(p *transport.Packet) (err error) {
ta.state = NIST_PROCEEDING
c.AddTransaction(ta)
}
c.AddDevice(msg)
c.OnRegister(msg)
ta.event <- c.NewOutGoingMessageEvent(msg.BuildResponse(200))
//case sip.INVITE:
// ta.typo = FSM_IST
@@ -451,17 +418,3 @@ func (c *Core) Send(msg *sip.Message) error {
c.tp.WritePacket(pkt)
return nil
}
func (c *Core) AddDevice(msg *sip.Message) *Device {
v := &Device{
ID: msg.From.Uri.UserInfo(),
RegisterTime: time.Now(),
UpdateTime: time.Now(),
Status: string(sip.REGISTER),
core: c,
from: &sip.Contact{Uri: msg.StartLine.Uri, Params: make(map[string]string)},
to: msg.To,
Addr: msg.Via.GetSendBy(),
}
c.Devices.Store(msg.From.Uri.UserInfo(), v)
return v
}

View File

@@ -1,192 +0,0 @@
package transaction
import (
"fmt"
"strings"
"time"
"github.com/Monibuca/plugin-gb28181/sip"
"github.com/Monibuca/plugin-gb28181/utils"
)
type Channel struct {
DeviceID string
Name string
Manufacturer string
Model string
Owner string
CivilCode string
Address string
Parental int
SafetyWay int
RegisterWay int
Secrecy int
Status string
device *Device
inviteRes *sip.Message
Connected bool
}
type Device struct {
ID string
RegisterTime time.Time
UpdateTime time.Time
Status string
Channels []Channel
core *Core
sn int
from *sip.Contact
to *sip.Contact
Addr string
}
func (c *Core) RemoveDead() {
c.Devices.Range(func(k, v interface{}) bool {
device := v.(*Device)
if device.UpdateTime.Sub(device.RegisterTime) > time.Duration(c.config.RegisterValidity)*time.Second {
c.Devices.Delete(k)
}
return true
})
}
func (d *Device) UpdateChannels(list []Channel) {
for _, c := range list {
c.device = d
have := false
for i, o := range d.Channels {
if o.DeviceID == c.DeviceID {
c.inviteRes = o.inviteRes
c.Connected = o.inviteRes != nil
d.Channels[i] = c
have = true
break
}
}
if !have {
d.Channels = append(d.Channels, c)
}
}
}
func (c *Channel) CreateMessage(Method sip.Method) (requestMsg *sip.Message) {
requestMsg = c.device.CreateMessage(Method)
requestMsg.StartLine.Uri = sip.NewURI(c.DeviceID + "@" + c.device.to.Uri.Domain())
requestMsg.To = &sip.Contact{
Uri: requestMsg.StartLine.Uri,
}
requestMsg.From = &sip.Contact{
Uri: sip.NewURI(c.device.core.config.Serial + "@" + c.device.core.config.Realm),
Params: map[string]string{"tag": utils.RandNumString(9)},
}
return
}
func (d *Device) CreateMessage(Method sip.Method) (requestMsg *sip.Message) {
d.sn++
requestMsg = &sip.Message{
Mode: sip.SIP_MESSAGE_REQUEST,
MaxForwards: 70,
UserAgent: "Monibuca",
StartLine: &sip.StartLine{
Method: Method,
Uri: d.to.Uri,
}, Via: &sip.Via{
Transport: "UDP",
Host: d.core.config.SipIP,
Port: fmt.Sprintf("%d", d.core.config.SipPort),
Params: map[string]string{
"branch": fmt.Sprintf("z9hG4bK%s", utils.RandNumString(8)),
"rport": "-1", //only key,no-value
},
}, From: d.from,
To: d.to, CSeq: &sip.CSeq{
ID: 1,
Method: Method,
}, CallID: utils.RandNumString(10),
Addr: d.Addr,
}
requestMsg.From.Params["tag"] = utils.RandNumString(9)
return
}
func (d *Device) Query() int {
requestMsg := d.CreateMessage(sip.MESSAGE)
requestMsg.ContentType = "Application/MANSCDP+xml"
requestMsg.Body = fmt.Sprintf(`<?xml version="1.0"?>
<Query>
<CmdType>Catalog</CmdType>
<SN>%d</SN>
<DeviceID>%s</DeviceID>
</Query>`, d.sn, requestMsg.To.Uri.UserInfo())
requestMsg.ContentLength = len(requestMsg.Body)
return d.core.SendMessage(requestMsg).Code
}
func (d *Device) Control(channelIndex int, PTZCmd string) int {
channel := &d.Channels[channelIndex]
requestMsg := channel.CreateMessage(sip.MESSAGE)
requestMsg.ContentType = "Application/MANSCDP+xml"
requestMsg.Body = fmt.Sprintf(`<?xml version="1.0"?>
<Control>
<CmdType>DeviceControl</CmdType>
<SN>%d</SN>
<DeviceID>%s</DeviceID>
<PTZCmd>%s</PTZCmd>
</Control>`, d.sn, requestMsg.To.Uri.UserInfo(), PTZCmd)
requestMsg.ContentLength = len(requestMsg.Body)
return d.core.SendMessage(requestMsg).Code
}
func (d *Device) Invite(channelIndex int) int {
channel := &d.Channels[channelIndex]
port := d.core.OnInvite(channel)
if port == 0 {
channel.Connected = true
return 304
}
sdp := fmt.Sprintf(`v=0
o=%s 0 0 IN IP4 %s
s=Play
c=IN IP4 %s
t=0 0
m=video %d RTP/AVP 96 98 97
a=recvonly
a=rtpmap:96 PS/90000
a=rtpmap:97 MPEG4/90000
a=rtpmap:98 H264/90000
y=0200000001
`, d.core.config.Serial, d.core.config.MediaIP, d.core.config.MediaIP, port)
sdp = strings.ReplaceAll(sdp, "\n", "\r\n")
invite := channel.CreateMessage(sip.INVITE)
invite.ContentType = "application/sdp"
invite.Contact = &sip.Contact{
Uri: sip.NewURI(fmt.Sprintf("%s@%s:%d", d.core.config.Serial, d.core.config.SipIP, d.core.config.SipPort)),
}
invite.Body = sdp
invite.ContentLength = len(sdp)
invite.Subject = fmt.Sprintf("%s:0200000001,34020000002020000001:0", channel.DeviceID)
response := d.core.SendMessage(invite)
fmt.Printf("invite response statuscode: %d\n", response.Code)
if response.Code == 200 {
channel.inviteRes = response.Data
channel.Connected = true
channel.Ack()
}
return response.Code
}
func (d *Device) Bye(channelIndex int) int {
channel := &d.Channels[channelIndex]
defer func() {
channel.inviteRes = nil
channel.Connected = false
}()
return channel.Bye().Code
}
func (c *Channel) Ack() {
ack := c.CreateMessage(sip.ACK)
ack.From = c.inviteRes.From
ack.To = c.inviteRes.To
ack.CallID = c.inviteRes.CallID
go c.device.core.Send(ack)
}
func (c *Channel) Bye() *Response {
bye := c.CreateMessage(sip.BYE)
bye.From = c.inviteRes.From
bye.To = c.inviteRes.To
bye.CallID = c.inviteRes.CallID
return c.device.core.SendMessage(bye)
}

View File

@@ -6,8 +6,8 @@ import (
//transaction 的错误定义
var (
ErrorSyntax = errors.New("message syntax error")
ErrorCheck = errors.New("message check failed")
ErrorParse = errors.New("message parse failed")
ErrorUnknown = errors.New("message unknown")
ErrorSyntax = errors.New("message syntax error")
ErrorCheck = errors.New("message check failed")
ErrorParse = errors.New("message parse failed")
ErrorUnknown = errors.New("message unknown")
)

View File

@@ -135,8 +135,25 @@ func ict_rcv_3456xx(t *Transaction, e *EventObj) error {
}
func ict_create_ack(t *Transaction, resp *sip.Message) *sip.Message {
return nil
return &sip.Message{
Mode: t.origRequest.Mode,
Addr: t.origRequest.Addr,
StartLine: &sip.StartLine{
Method: sip.ACK,
Uri: t.origRequest.StartLine.Uri,
},
MaxForwards: t.origRequest.MaxForwards,
CallID: t.callID,
Contact: t.origRequest.Contact,
UserAgent: t.origRequest.UserAgent,
Via: t.via,
From: t.from,
To: t.to,
CSeq: &sip.CSeq{
ID: 1,
Method: sip.ACK,
},
}
}
func ict_retransmit_ack(t *Transaction, e *EventObj) error {

View File

@@ -128,7 +128,7 @@ func nict_rcv_1xx(t *Transaction, e *EventObj) error {
func nict_rcv_23456xx(t *Transaction, e *EventObj) error {
t.lastResponse = e.msg
t.state = NICT_COMPLETED
if e.msg.IsReliable() {
//不设置timerK
} else {

View File

@@ -1,8 +1,8 @@
package transaction
import (
"github.com/Monibuca/plugin-gb28181/sip"
"fmt"
"github.com/Monibuca/plugin-gb28181/sip"
"net"
"strings"
)

View File

@@ -48,7 +48,7 @@ func (c *TCPClient) Start() error {
if err != nil {
fmt.Println("dial tcp server failed :", err.Error())
return err
}else{
} else {
fmt.Println("start tcp client")
}

View File

@@ -45,7 +45,6 @@ func NewClient(config *transaction.Config, static *ClientStatic) *Client {
}
}
//TODO对于一个TU开启之后
//运行一个sip client
func RunClient() {
@@ -82,7 +81,7 @@ func RunClient() {
//TODO先发起注册
//TODO:build sip message
msg := BuildMessageRequest("", "", "", "", "", "",
0, 0, 0,"")
0, 0, 0, "")
resp := c.SendMessage(msg)
if resp.Code != 0 {
fmt.Println("request failed")

View File

@@ -20,7 +20,7 @@ expires: 过期时间
cseq消息序列号当前对话递增
*/
//构建消息以客户端可能是IPC也可能是SIP Server的角度
func BuildMessageRequest(method sip.Method, transport, sipSerial, sipRealm, username , srcIP string, srcPort uint16, expires, cseq int,body string) *sip.Message {
func BuildMessageRequest(method sip.Method, transport, sipSerial, sipRealm, username, srcIP string, srcPort uint16, expires, cseq int, body string) *sip.Message {
server := fmt.Sprintf("%s@%s", sipSerial, sipRealm)
client := fmt.Sprintf("%s@%s", username, sipRealm)
@@ -62,7 +62,7 @@ func BuildMessageRequest(method sip.Method, transport, sipSerial, sipRealm, user
msg.Contact = &sip.Contact{
Uri: sip.NewURI(fmt.Sprintf("%s@%s:%d", username, srcIP, srcPort)),
}
if len(body)>0{
if len(body) > 0 {
msg.ContentLength = len(body)
msg.Body = body
}

View File

@@ -15,7 +15,7 @@ type Server struct {
//提供config参数
func NewServer(config *transaction.Config) *Server {
return &Server{
Core: transaction.NewCore(config),
Core: transaction.NewCore(config),
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
.arrow1[data-v-76558468]{grid-column:2;grid-row:1}.arrow2[data-v-76558468]{transform:rotate(90deg);grid-column:3;grid-row:2}.arrow3[data-v-76558468]{transform:rotate(180deg);grid-column:2;grid-row:3}.arrow4[data-v-76558468]{transform:rotate(270deg);grid-column:1;grid-row:2}.arrow5[data-v-76558468]{transform:rotate(-45deg);grid-column:1;grid-row:1}.arrow6[data-v-76558468]{transform:rotate(45deg);grid-column:3;grid-row:1}.arrow7[data-v-76558468]{transform:rotate(-135deg);grid-column:1;grid-row:3}.arrow8[data-v-76558468]{transform:rotate(135deg);grid-column:3;grid-row:3}.arrow9[data-v-76558468]{grid-column:2;grid-row:2}.container[data-v-76558468]{position:relative;height:350px}.control[data-v-76558468]{position:absolute;top:20px;right:0;display:grid;grid-template-columns:repeat(3,33.33%);grid-template-rows:repeat(3,33.33%);width:192px;height:192px}.control2[data-v-76558468]{top:210px}.control3[data-v-76558468]{top:260px}.control4[data-v-76558468]{top:310px}.control5[data-v-76558468]{top:360px}.control>[data-v-76558468]{cursor:pointer;fill:grey;width:50px;height:50px}.control5>[data-v-76558468]{margin-right:10px}.control2>[data-v-76558468],.control3>[data-v-76558468],.control4>[data-v-76558468]{width:40px;height:40px}.control>[data-v-76558468]:hover,.cycling[data-v-76558468]{fill:#0ff}.player-wrap[data-v-683c37a8]{width:100%;height:100%;border-radius:4px;box-shadow:0 0 5px #40d3fc,inset 0 0 5px #40d3fc,0 0 0 1px #40d3fc}.player-wrap video[data-v-683c37a8]{width:100%;height:100%}.flex-box[data-v-31458ad7]{display:flex;flex-flow:row wrap;align-content:flex-start}.flex-item[data-v-31458ad7]{flex:0 0 33.3333%;height:275px;box-sizing:border-box;padding:10px}
.arrow1[data-v-43b2c727]{grid-column:2;grid-row:1}.arrow2[data-v-43b2c727]{transform:rotate(90deg);grid-column:3;grid-row:2}.arrow3[data-v-43b2c727]{transform:rotate(180deg);grid-column:2;grid-row:3}.arrow4[data-v-43b2c727]{transform:rotate(270deg);grid-column:1;grid-row:2}.arrow5[data-v-43b2c727]{transform:rotate(-45deg);grid-column:1;grid-row:1}.arrow6[data-v-43b2c727]{transform:rotate(45deg);grid-column:3;grid-row:1}.arrow7[data-v-43b2c727]{transform:rotate(-135deg);grid-column:1;grid-row:3}.arrow8[data-v-43b2c727]{transform:rotate(135deg);grid-column:3;grid-row:3}.arrow9[data-v-43b2c727]{grid-column:2;grid-row:2}.container[data-v-43b2c727]{position:relative;height:350px}.control[data-v-43b2c727]{position:absolute;top:20px;right:0;display:grid;grid-template-columns:repeat(3,33.33%);grid-template-rows:repeat(3,33.33%);width:192px;height:192px}.control2[data-v-43b2c727]{top:210px}.control3[data-v-43b2c727]{top:260px}.control4[data-v-43b2c727]{top:310px}.control5[data-v-43b2c727]{top:360px}.control>[data-v-43b2c727]{cursor:pointer;fill:grey;width:50px;height:50px}.control5>[data-v-43b2c727]{margin-right:10px}.control2>[data-v-43b2c727],.control3>[data-v-43b2c727],.control4>[data-v-43b2c727]{width:40px;height:40px}.control>[data-v-43b2c727]:hover,.cycling[data-v-43b2c727]{fill:#0ff}.player-wrap[data-v-3d23233a]{width:100%;height:100%;border-radius:4px;box-shadow:0 0 5px #40d3fc,inset 0 0 5px #40d3fc,0 0 0 1px #40d3fc}.player-wrap video[data-v-3d23233a]{width:100%;height:100%}.container[data-v-1496ea1d]{position:relative;height:500px;background-image:radial-gradient(rgba(197,45,208,.48),rgba(74,23,152,.48),rgba(3,0,19,.48));color:#fff;background-color:#000;overflow:auto}.search[data-v-1496ea1d]{padding:10px 0}.flex-box[data-v-6660cc55]{display:flex;flex-flow:row wrap;align-content:flex-start}.flex-item[data-v-6660cc55]{flex:0 0 33.3333%;height:275px;box-sizing:border-box;padding:10px}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3298
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,5 +11,8 @@
"devDependencies": {
"@vue/cli-service": "^4.5.4",
"vue-template-compiler": "^2.6.12"
},
"dependencies": {
"vue": "^2.6.12"
}
}

View File

@@ -16,22 +16,28 @@
<td>
<mu-button
flat
v-if="item.Connected"
@click="ptz(prop.row.ID, $index, item)"
>云台
</mu-button>
<mu-button
<!-- <mu-button
flat
v-if="item.Connected"
@click="bye(prop.row.ID, $index)"
>断开</mu-button
>
@click="bye(prop.row.ID, $index, item)"
>断开
</mu-button>
<mu-button
v-else
flat
@click="invite(prop.row.ID, $index, item)"
>连接
</mu-button>
</mu-button> -->
<mu-button
flat
@click="
getRecords(prop.row.ID, $index, item)
"
>录像</mu-button
>
</td>
</template>
</mu-data-table>
@@ -52,33 +58,58 @@
</div>
<div class="tabpanel" v-if="$parent.titleTabActive === 1">
<div class="flex-box">
<template v-for="(channel,index) in channelShowList">
<div class="flex-item" :key="index" v-if="channel.DeviceID">
<webrtc-player2 :stream-path="'gb28181/'+channel.DeviceID"></webrtc-player2>
<template v-for="(channel, index) in channelShowList">
<div class="flex-item" :key="index">
<webrtc-player2
@hook:mounted="
invite(
channel.device.ID,
channel.device.Channels.indexOf(channel),
channel,
'v/2/1/15/1/200a///'
)
"
:stream-path="
channel.device.ID + '/' + channel.DeviceID
"
></webrtc-player2>
</div>
</template>
</div>
<template v-if="channelList.length > 0">
<Page :total="channelList.length" :page-size="pageInfo.onePageSize" @on-change="handlePageChange"></Page>
<Page
:total="channelList.length"
:page-size="pageInfo.onePageSize"
@on-change="handlePageChange"
></Page>
</template>
</div>
<webrtc-player
ref="player"
@ptz="sendPtz"
v-model="previewStreamPath"
:PublicIP="PublicIP"
></webrtc-player>
<records
ref="records"
v-model="recordModal"
:search="recordSearch"
:channel="currentChannel"
@close="initRecordSearch"
></records>
</div>
</template>
<script>
import WebrtcPlayer from "./components/Player";
import WebrtcPlayer2 from "./components/Player2";
import Records from "./components/Records";
import { getPTZCmd, PTZ_TYPE } from "./utils/ptz-cmd";
import { getOneTimeRange } from "./utils";
export default {
components: {
WebrtcPlayer,
WebrtcPlayer2,
Records,
},
props: {
ListenAddr: String,
@@ -97,7 +128,14 @@ export default {
pageInfo: {
onePageSize: 9,
totalPage: 0,
currentPage: 0
currentPage: 0,
},
currentChannel: null,
recordModal: false,
recordSearch: {
id: null,
channel: null,
deviceId: null,
},
context: {
id: null,
@@ -134,7 +172,7 @@ export default {
},
methods: {
fetchlist() {
const listES = new EventSource(this.apiHost + "/gb28181/list");
const listES = new EventSource("/gb28181/list");
listES.onmessage = (evt) => {
if (!evt.data) return;
this.Devices = JSON.parse(evt.data) || [];
@@ -143,7 +181,18 @@ export default {
this.Devices.forEach((device) => {
const channels = device.Channels || [];
if (channels.length > 0) {
channelList = channelList.concat(channels);
channelList = channelList.concat(
channels.map((x) => ((x.device = device), x))
);
}
if (this.recordSearch.id && this.recordSearch.deviceId) {
const channel = channels.find((i) => {
return (
i.DeviceID === this.recordSearch.deviceId &&
this.recordSearch.id === device.ID
);
});
if (channel) this.currentChannel = channel;
}
});
if (channelList.length > 0) {
@@ -153,15 +202,23 @@ export default {
};
this.$once("hook:destroyed", () => listES.close());
},
ptz(id, channel, item) {
async ptz(id, channel, item) {
await this.invite(id, channel, item);
this.context = {
id,
channel,
item,
};
this.previewStreamPath = true;
const unwatch = this.$watch(
"previewStreamPath",
(newValue, oldValue) => {
this.bye(id, channel, item);
unwatch();
}
);
this.$nextTick(() =>
this.$refs.player.play("gb28181/" + item.DeviceID)
this.$refs.player.play(id + "/" + item.DeviceID)
);
},
sendPtz(options) {
@@ -190,21 +247,24 @@ export default {
});
},
handlePageChange(page){
sendQueryRecords(options) {},
handlePageChange(page) {
let showList = [];
const onePageSize = this.pageInfo.onePageSize;
const firstIndex = page * onePageSize - onePageSize;
const lastIndex = page * onePageSize - 1 ;
const lastIndex = page * onePageSize - 1;
showList = this.channelList.filter((item, index) => {
return index >= firstIndex && index <= lastIndex;
});
this.channelShowList = showList;
if(showList.length > 0){
if (showList.length > 0) {
this.pageInfo.currentPage = page;
}
},
updatePageInfo(totalSize){
updatePageInfo(totalSize) {
const onePageSize = this.pageInfo.onePageSize;
let totalPage = totalSize / onePageSize;
@@ -212,20 +272,36 @@ export default {
totalPage = totalPage + 1;
}
this.pageInfo.totalPage = totalPage;
if(this.pageInfo.currentPage === 0){
this.handlePageChange(1)
if (this.pageInfo.currentPage === 0) {
this.handlePageChange(1);
}
},
invite(id, channel, item) {
this.ajax.get("/gb28181/invite", { id, channel }).then((x) => {
item.Connected = true;
});
invite(id, channel, item, f = "") {
return this.ajax
.get("/gb28181/invite", { id, channel, f })
.then((x) => {
item.Connected = true;
});
},
bye(id, channel, item) {
this.ajax.get("/gb28181/bye", { id, channel }).then((x) => {
return this.ajax.get("/gb28181/bye", { id, channel }).then((x) => {
item.Connected = false;
});
},
getRecords(id, channel, item) {
this.recordSearch.id = id;
this.recordSearch.channel = channel;
this.recordSearch.deviceId = item.DeviceID;
this.recordModal = true;
},
initRecordSearch() {
this.recordModal = false;
this.recordSearch.id = null;
this.recordSearch.channel = null;
this.recordSearch.deviceId = null;
},
},
};
</script>

View File

@@ -65,9 +65,6 @@ export default {
ptzPositionIndex:1,
ptzType:PTZ_TYPE,
};
},
props:{
PublicIP:String
},
methods: {
async play(streamPath) {

View File

@@ -71,11 +71,12 @@
const result = await this.ajax({
type: "POST",
processData: false,
data: localDescriptionData,
data: JSON.stringify(localDescriptionData),
url: "/webrtc/play?streamPath=" + streamPath,
dataType: "json"
});
if (result.error) {
if (result.errmsg) {
console.error(result.errmsg);
return;
}
//

View File

@@ -0,0 +1,172 @@
<template>
<Modal
v-bind="$attrs"
draggable
width="900"
v-on="$listeners"
title="录像列表"
@on-ok="$emit('close')"
>
<webrtc-player2
v-if="channel && channel.RecordSP && player"
:streamPath="channel.RecordSP"
></webrtc-player2>
<div class="container" v-else-if="!player">
<div class="search">
<DatePicker
type="date"
:options="timeOptions"
:value="search.time"
placeholder="请选择时间"
style="width: 200px"
:clearable="false"
@on-change="handleTimeChange"
></DatePicker>
</div>
<div>
<mu-data-table :columns="columns" :data="recordList">
<template #default="scope">
<td>{{ scope.row.DeviceID }}</td>
<td>{{ scope.row.Name }}</td>
<td>{{ scope.row.startTime }}</td>
<td>{{ scope.row.endTime }}</td>
<td>{{ scope.row.length }}</td>
<td>
<m-button @click="play(scope.row)">播放</m-button>
</td>
</template>
</mu-data-table>
</div>
</div>
<div v-else>正在连接请稍后</div>
<div slot="footer" v-if="player">
<mu-button @click="back">返回</mu-button>
</div>
</Modal>
</template>
<script>
import { getOneTimeRange, formatTimeTips, parseTime, isDef } from "../utils";
import WebrtcPlayer2 from "./Player2";
const _now = new Date();
export default {
name: "Records",
components: {
WebrtcPlayer2,
},
props: ["search", "channel"],
data() {
return {
player: false,
timeOptions: {
disabledDate(date) {
return date && date.valueOf() > Date.now();
},
},
columns: Object.freeze(
["设备ID", "名称", "开始时间", "结束时间", "时长", "操作"].map(
(title) => ({
title,
})
)
),
};
},
computed: {
records() {
return (this.channel && this.channel.Records) || [];
},
startTime() {
if (!this.search.time) {
return "";
}
const start = getOneTimeRange(this.search.time).start;
const isoString = new Date(start).toISOString();
return isoString.replace(".000Z", "");
},
endTime() {
if (!this.search.time) {
return "";
}
const end = getOneTimeRange(this.search.time).end;
const isoString = new Date(end).toISOString();
return isoString.replace(".000Z", "");
},
recordList() {
const list = this.records.map((record) => {
const startTime = new Date(record.StartTime).getTime();
const endTime = new Date(record.EndTime).getTime();
const timestamp = endTime - startTime;
const timeLength = formatTimeTips(timestamp / 1000);
const _startTime = parseTime(startTime);
const _endTime = parseTime(endTime);
record._startTime = (startTime / 1000) >> 0;
record._endTime = (endTime / 1000) >> 0;
record.length = timeLength;
record.startTime = _startTime;
record.endTime = _endTime;
return record;
});
return list;
},
},
mounted() {
this._fetchList();
},
methods: {
_fetchList() {
if (
isDef(this.search.id) &&
isDef(this.search.channel) &&
this.startTime &&
this.endTime
) {
const query = {
id: this.search.id,
channel: this.search.channel,
startTime: this.startTime,
endTime: this.endTime,
};
this.ajax.get("/gb28181/query/records", query).then((x) => {});
}
},
handleTimeChange(date) {
this.search.time = new Date(date);
this._fetchList();
},
play(record) {
const query = {
id: this.search.id,
channel: this.search.channel,
startTime: record._startTime,
endTime: record._endTime,
};
this.ajax.get("/gb28181/invite", query).then((x) => {});
this.player = true;
},
back() {
fetch("/api/stop?stream=" + this.streamPath);
this.player = false;
},
},
};
</script>
<style scoped>
.container {
position: relative;
height: 500px;
background-image: radial-gradient(#c52dd07a, #4a17987a, #0300137a);
color: #ffffff;
background-color: black;
overflow: auto;
}
.search {
padding: 10px 0;
}
</style>

125
ui/src/utils/index.js Normal file
View File

@@ -0,0 +1,125 @@
/**
* Date:2020/12/24
* Desc:
*/
export function getOneTimeRange(time, options) {
let date;
// 都为空的时候
if (!time && !options) {
date = new Date();
} else if (Object.prototype.toString.call(time) !== '[object Date]' && time !== null && typeof time === 'object') {
// time 为 options 参数。
options = time;
date = new Date();
} else if (Object.prototype.toString.call(time) === '[object Date]') {
// time 是时间格式
date = time;
} else {
// time 是 int 格式。
if (('' + time).length === 10) time = parseInt(time) * 1000;
time = +time; // 转成int 型
date = new Date(time);
}
options = options || {};
let result = {
start: 0,
end: 0
};
let _startTime = new Date(date).setHours(options.startHour || 0, options.startMin || 0, 0, 0);
let _endTime = new Date(date).setHours(options.endHour || 23, options.endMin || 59, 59, 0);
result.start = new Date(_startTime).getTime();
result.end = new Date(_endTime).getTime();
return result;
};
export function formatTimestamp(t) {
var d = 0,
h = 0,
m = 0,
s = 0;
if (t > 0) {
d = Math.floor(t / 1000 / 3600 / 24)
h = Math.floor(t / 1000 / 60 / 60 % 24)
m = Math.floor(t / 1000 / 60 % 60)
s = Math.floor(t / 1000 % 60)
}
return `${d}${h}${m}${s}`
}
// 单位秒
export function formatTimeTips(timestamp) {
let result;
//
if (timestamp > -1) {
let hour = Math.floor(timestamp / 3600);
let min = Math.floor(timestamp / 60) % 60;
let sec = timestamp % 60;
sec = Math.round(sec);
if (hour < 10) {
result = '0' + hour + ":";
} else {
result = hour + ":";
}
if (min < 10) {
result += "0";
}
result += min + ":";
if (sec < 10) {
result += "0";
}
result += sec.toFixed(0);
}
return result;
}
export function parseTime(time, cFormat) {
if (arguments.length === 0) {
return null
}
var format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
var date;
if (typeof time === 'object') {
date = time
} else {
if (('' + time).length === 10) time = parseInt(time) * 1000;
time = +time; // 转成int 型
date = new Date(time)
}
var formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay()
};
var time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
var value = formatObj[key]
if (key === 'a') return ['一', '二', '三', '四', '五', '六', '日'][value - 1]
if (result.length > 0 && value < 10) {
value = '0' + value
}
return value || 0
});
return time_str
}
export function isDef(v) {
return v !== undefined && v !== null;
}