Compare commits

...

61 Commits

Author SHA1 Message Date
李宇翔
893afd2e37 增加对注册用户ID的长度判断 2021-03-23 10:00:31 +08:00
dexter
6f4910eebb Merge pull request #20 from dwdcth/patch-2
embed
2021-03-20 12:51:23 +08:00
dwdcth
5706d9751e Update main.go 2021-03-19 14:32:25 +08:00
dwdcth
2c5bbc2ee6 Update go.mod 2021-03-19 14:30:37 +08:00
wancheng1990
d007411c38 fix bug 2021-02-24 10:42:05 +08:00
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
wancheng1990
7cfd4fccbd Merge pull request #10 from bosscheng/master
add N line page list
2020-12-17 11:24:33 +08:00
wancheng1990
211c8bd32c Merge pull request #7 from Monibuca/master
Merge pull request #9 from bosscheng/master
2020-12-17 11:22:32 +08:00
bosscheng1210
f3b0595863 fix bugs 2020-12-17 11:17:42 +08:00
wancheng1990
de07b41647 Merge pull request #9 from bosscheng/master
merge fix bugs
2020-12-16 23:43:45 +08:00
万成
38220d62e3 fix bug 2020-12-16 23:43:34 +08:00
wancheng1990
818fd6bd33 Merge pull request #6 from Monibuca/master
merge
2020-12-16 23:41:16 +08:00
wancheng1990
8154d852f4 Merge branch 'master' into master 2020-12-16 23:41:06 +08:00
万成
c4a54d7eae fix bugs 2020-12-16 23:35:13 +08:00
langhuihui
3ffb58606a 增加随机端口范围 2020-12-07 23:05:08 +08:00
langhuihui
c284e4e28e 修正发送地址 2020-12-06 13:11:53 +08:00
李宇翔
7269ec50de 对transactions加入读写锁 2020-12-05 08:47:45 +08:00
langhuihui
e33079e36b 增加连接退出关闭 2020-12-05 08:02:26 +08:00
langhuihui
d1de189dcf 修改逻辑 2020-11-24 22:48:15 +08:00
langhuihui
e45b266de9 修改返回response的位置 2020-11-24 22:42:27 +08:00
langhuihui
8663a9ecef 调整response阻塞逻辑 2020-11-24 22:18:51 +08:00
35 changed files with 10863 additions and 2297 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)
}

9
go.mod
View File

@@ -1,10 +1,15 @@
module github.com/Monibuca/plugin-gb28181
go 1.13
go 1.16
require (
github.com/Monibuca/engine/v2 v2.2.2
github.com/Monibuca/engine/v2 v2.4.0
github.com/Monibuca/plugin-rtp v1.0.0
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/mattn/go-colorable v0.1.7 // indirect
github.com/pion/rtp v1.6.0 // indirect
github.com/shirou/gopsutil v2.20.8+incompatible // indirect
github.com/stretchr/testify v1.6.1 // indirect
golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d
)

26
go.sum
View File

@@ -1,17 +1,18 @@
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.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.4.0 h1:aa647MW5ToMdVQlB4nehfc3Vcos6752aLYRAkRnMLqo=
github.com/Monibuca/engine/v2 v2.4.0/go.mod h1:LBuAJFcTtUjVsGKWcUKQpIftRECf7Ii9DfuGWHL3Ngg=
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/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/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/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=
@@ -20,19 +21,27 @@ github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczG
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
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/pion/rtp v1.5.4 h1:PuNg6xqV3brIUihatcKZj1YDUs+M45L0ZbrZWYtkDxY=
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/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/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/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.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.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=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -41,7 +50,6 @@ golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/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-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -51,3 +59,5 @@ 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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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=

226
main.go
View File

@@ -1,28 +1,42 @@
package gb28181
import (
. "github.com/Monibuca/engine/v2"
"github.com/Monibuca/engine/v2/util"
"github.com/Monibuca/plugin-gb28181/transaction"
rtp "github.com/Monibuca/plugin-rtp"
. "github.com/logrusorgru/aurora"
"bytes"
"embed"
"encoding/xml"
"log"
"math/rand"
"net"
"net/http"
"strconv"
"strings"
"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"
rtp "github.com/Monibuca/plugin-rtp"
. "github.com/logrusorgru/aurora"
)
var Devices sync.Map
var config = struct {
Serial string
Realm string
ListenAddr string
Expires int
AutoInvite bool
}{"34020000002000000001", "3402000000", "127.0.0.1:5060", 3600, true}
Serial string
Realm string
ListenAddr string
Expires int
AutoInvite bool
MediaPortMin uint16
MediaPortMax uint16
}{"34020000002000000001", "3402000000", "127.0.0.1:5060", 3600, true, 58200, 58300}
//go:embed ui/*
//go:embed README.md
var ui embed.FS
func init() {
InstallPlugin(&PluginConfig{
@@ -30,6 +44,7 @@ func init() {
Config: &config,
Type: PLUGIN_PUBLISHER,
Run: run,
UIFile: &ui,
})
}
@@ -54,18 +69,38 @@ func run() {
AudioEnable: true,
WaitKeyFrame: true,
MediaPortMin: 58200,
MediaPortMax: 58300,
MediaPortMin: config.MediaPortMin,
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)
@@ -79,26 +114,36 @@ func run() {
http.HandleFunc("/gb28181/control", 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{
channel, err := strconv.Atoi(r.URL.Query().Get("channel"))
if err != nil {
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)
}
@@ -106,38 +151,113 @@ func run() {
http.HandleFunc("/gb28181/bye", 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"))
channel, err := strconv.Atoi(r.URL.Query().Get("channel"))
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) {
id := msg.From.Uri.UserInfo()
if len(id) != len(config.Serial) {
return
}
Devices.Store(id, &Device{
ID: id,
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
}
rtpPublisher.Type = "GB28181"
addr, err := net.ResolveUDPAddr("udp", ":0")
if err != nil {
return
defer func() {
if port == 0 {
publisher.Close()
}
}()
publisher.Type = "GB28181"
publisher.AutoUnPublish = true
var conn *net.UDPConn
var err error
rang := int(config.MediaPortMax - config.MediaPortMin)
for count := rang; count > 0; count-- {
randNum := rand.Intn(rang)
port = int(config.MediaPortMin) + randNum
addr, _ := net.ResolveUDPAddr("udp", ":"+strconv.Itoa(port))
conn, err = net.ListenUDP("udp", addr)
if err != nil {
continue
} else {
break
}
}
conn, err := net.ListenUDP("udp", addr)
if err != nil {
return
}
networkBuffer := 1048576
if err := conn.SetReadBuffer(networkBuffer); err != nil {
if err = conn.SetReadBuffer(networkBuffer); err != nil {
Printf("udp server video conn set read buffer error, %v", err)
}
if err := conn.SetWriteBuffer(networkBuffer); err != nil {
if err = conn.SetWriteBuffer(networkBuffer); err != nil {
Printf("udp server video conn set write buffer error, %v", err)
}
la := conn.LocalAddr().String()
@@ -149,15 +269,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)
for rtpPublisher.Err() == nil {
if err = conn.SetReadDeadline(time.Now().Add(time.Second*30));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
@@ -23,12 +19,12 @@ type Core struct {
ctx context.Context //上下文
handlers map[State]map[Event]Handler //每个状态都可以处理有限个事件。不必加锁。
transactions map[string]*Transaction //管理所有 transactions,key:tid,value:transaction
mutex sync.Mutex //transactions的锁
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,13 +243,16 @@ 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)
//匹配事物
c.mutex.RLock()
ta, ok := c.transactions[e.tid]
c.mutex.RUnlock()
if !ok {
//新的请求
ta = c.initTransaction(c.ctx, e)
@@ -265,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 {
@@ -282,10 +277,14 @@ func (c *Core) SendMessage(msg *sip.Message) *Response {
//把event推到transaction
ta.event <- e
select {
case res := <-ta.response:
return res
case <-time.After(time.Second * 10):
<-ta.done
if ta.lastResponse != nil {
return &Response{
Code: ta.lastResponse.GetStatusCode(),
Data: ta.lastResponse,
Message: ta.lastResponse.GetReason(),
}
} else {
return &Response{
Code: 504,
}
@@ -298,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 {
@@ -326,8 +325,9 @@ func (c *Core) HandleReceiveMessage(p *transport.Packet) (err error) {
}
//TODOCANCEL、BYE 和 ACK 需要特殊处理使用事物或者直接由TU层处理
//查找transaction
c.mutex.RLock()
ta, ok := c.transactions[e.tid]
c.mutex.RUnlock()
method := msg.GetMethod()
if msg.IsRequest() {
switch method {
@@ -338,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))
@@ -377,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
@@ -391,13 +365,7 @@ func (c *Core) HandleReceiveMessage(p *transport.Packet) (err error) {
}
} else if ok {
ta.event <- e
if msg.GetStatusCode() >= 200 {
ta.response <- &Response{
Code: msg.GetStatusCode(),
Data: msg,
Message: msg.GetReason(),
}
}
}
//TODOTU层处理根据需要创建或者匹配 Dialog
//通过tag匹配到call和dialog
@@ -450,18 +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,
host: msg.Via.Host,
port: msg.Via.Port,
}
c.Devices.Store(msg.From.Uri.UserInfo(), v)
return v
}

View File

@@ -1,193 +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
host string
port 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.host + ":" + d.port,
}
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

@@ -1,9 +1,10 @@
package transaction
import (
"github.com/Monibuca/plugin-gb28181/sip"
"fmt"
"time"
"github.com/Monibuca/plugin-gb28181/sip"
)
/*
@@ -105,14 +106,12 @@ func ict_rcv_1xx(t *Transaction, e *EventObj) error {
}
func ict_rcv_2xx(t *Transaction, e *EventObj) error {
t.lastResponse = e.msg
t.Terminate()
return nil
}
func ict_rcv_3456xx(t *Transaction, e *EventObj) error {
t.lastResponse = e.msg
if t.state != ICT_COMPLETED {
/* not a retransmission */
/* automatic handling of ack! */
@@ -136,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

@@ -2,11 +2,12 @@ package transaction
import (
"context"
"github.com/Monibuca/plugin-gb28181/sip"
"github.com/Monibuca/plugin-gb28181/transport"
"fmt"
"net"
"time"
"github.com/Monibuca/plugin-gb28181/sip"
"github.com/Monibuca/plugin-gb28181/transport"
)
//状态机之状态
@@ -355,7 +356,6 @@ func (ta *Transaction) Run() {
if err != nil {
fmt.Printf("transaction run failed, state:%s, event:%s\n", state.String(), e.evt.String())
}
case <-ta.done:
fmt.Println("fsm exit")
return
@@ -399,8 +399,8 @@ func (ta *Transaction) SipSend(msg *sip.Message) error {
if err != nil {
return err
}
addr := msg.Addr
if addr==""{
addr := msg.Addr
if addr == "" {
viaParams := msg.Via.Params
//host
var host, port string

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-379ce7fe],.player-wrap video[data-v-379ce7fe]{width:100%;height:100%}.flex-box[data-v-77f8f0e9]{display:flex;flex-flow:row wrap;align-content:flex-start}.flex-item[data-v-77f8f0e9]{flex:0 0 33.3333%;height:275px;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

@@ -3,7 +3,10 @@
<div class="tabpanel" v-if="$parent.titleTabActive === 0">
<mu-data-table :data="Devices" :columns="columns">
<template #expand="prop">
<mu-data-table :data="prop.row.Channels" :columns="columns2">
<mu-data-table
:data="prop.row.Channels"
:columns="columns2"
>
<template #default="{ row: item, $index }">
<td>{{ item.DeviceID }}</td>
<td>{{ item.Name }}</td>
@@ -11,12 +14,29 @@
<td>{{ item.Address }}</td>
<td>{{ item.Status }}</td>
<td>
<mu-button flat v-if="item.Connected" @click="ptz(prop.row.ID, $index,item)">云台
<mu-button
flat
@click="ptz(prop.row.ID, $index, item)"
>云台
</mu-button>
<mu-button flat v-if="item.Connected" @click="bye(prop.row.ID, $index)">断开</mu-button>
<mu-button v-else flat @click="invite(prop.row.ID, $index,item)"
>连接
</mu-button
<!-- <mu-button
flat
v-if="item.Connected"
@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
flat
@click="
getRecords(prop.row.ID, $index, item)
"
>录像</mu-button
>
</td>
</template>
@@ -24,6 +44,7 @@
</template>
<template #default="{ row: item }">
<td>{{ item.ID }}</td>
<td>{{ item.Addr }}</td>
<td>{{ item.Channels ? item.Channels.length : 0 }}</td>
<td>
<StartTime :value="item.RegisterTime"></StartTime>
@@ -36,113 +57,184 @@
</mu-data-table>
</div>
<div class="tabpanel" v-if="$parent.titleTabActive === 1">
<div class="search">
<i-select v-model="channelSelectedList" multiple>
<Option v-for="(channel,index) in channelList"
:value="channel"
:label="channel.DeviceID + '(' + channel.Name + ')'"
:key="index">
</Option>
</i-select>
</div>
<div class="flex-box">
<template v-for="(channel,index) in channelSelectedList">
<div class="flex-item" :key="index" v-if="channel.ID">
<webrtc-player2 :stream-path="'gb28181/'+channel.ID"></webrtc-player2>
<template v-for="(channel, index) in channelShowList">
<div class="flex-item" :key="channel.DeviceID">
<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>
</template>
</div>
<webrtc-player ref="player" @ptz="sendPtz" v-model="previewStreamPath" :PublicIP="PublicIP"></webrtc-player>
<webrtc-player
ref="player"
@ptz="sendPtz"
v-model="previewStreamPath"
></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 {getPTZCmd, PTZ_TYPE} from "./utils/ptz-cmd";
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
export default {
components: {
WebrtcPlayer,
WebrtcPlayer2,
Records,
},
props: {
ListenAddr: String,
},
computed: {
PublicIP() {
return this.ListenAddr.split(":")[0];
},
props: {
ListenAddr: String
},
computed: {
PublicIP() {
return this.ListenAddr.split(":")[0]
}
},
data() {
return {
Devices: [], previewStreamPath: false,
channelList: [],
channelSelectedList:[],
context: {
id: null,
channel: 0,
item: null
},
columns: Object.freeze(
["设备号", "通道数", "注册时间", "更新时间", "状态"].map(
(title) => ({
title,
})
)
),
columns2: Object.freeze([
"通道编号",
"名称",
"厂商",
},
data() {
return {
Devices: [],
previewStreamPath: false,
channelList: [],
channelShowList: [],
pageInfo: {
onePageSize: 9,
totalPage: 0,
currentPage: 0,
},
currentChannel: null,
recordModal: false,
recordSearch: {
id: null,
channel: null,
deviceId: null,
},
context: {
id: null,
channel: 0,
item: null,
},
columns: Object.freeze(
[
"设备号",
"地址",
"通道数",
"注册时间",
"更新时间",
"状态",
"操作",
]).map((title) => ({title})),
};
},
created() {
this.fetchlist();
},
mounted() {
this.$parent.titleTabs = ["列表", "N路播放"];
},
methods: {
fetchlist() {
const listES = new EventSource(this.apiHost + "/gb28181/list");
listES.onmessage = (evt) => {
if (!evt.data) return;
this.Devices = JSON.parse(evt.data) || [];
this.Devices.sort((a, b) => (a.ID > b.ID ? 1 : -1));
let channelList =[]
this.Devices.forEach((device)=>{
const channels = device.Channels || [];
if(channels.length > 0){
channelList = channelList.concat(channels);
}
});
if(channelList.length > 0){
this.channelList = channelList;
].map((title) => ({
title,
}))
),
columns2: Object.freeze([
"通道编号",
"名称",
"厂商",
"地址",
"状态",
"操作",
]).map((title) => ({ title })),
};
},
created() {
this.fetchlist();
},
mounted() {
this.$parent.titleTabs = ["列表", "N路播放"];
},
methods: {
fetchlist() {
const listES = new EventSource("/gb28181/list");
listES.onmessage = (evt) => {
if (!evt.data) return;
this.Devices = JSON.parse(evt.data) || [];
this.Devices.sort((a, b) => (a.ID > b.ID ? 1 : -1));
let channelList = [];
this.Devices.forEach((device) => {
const channels = device.Channels || [];
if (channels.length > 0) {
channelList = channelList.concat(
channels.map((x) => ((x.device = device), x))
);
}
};
this.$once("hook:destroyed", () => listES.close());
},
ptz(id, channel, item) {
this.context = {
id, channel, item
};
this.previewStreamPath = true
this.$nextTick(() => this.$refs.player.play("gb28181/" + item.DeviceID));
},
sendPtz(options) {
const ptzCmd = getPTZCmd(options);
const ptzCmdStop = getPTZCmd({type: PTZ_TYPE.stop});
this.ajax.get("/gb28181/control", {
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) {
this.channelList = channelList.filter((channel)=> !!channel.DeviceID);
this.updatePageInfo(this.channelList.length);
}
};
this.$once("hook:destroyed", () => listES.close());
},
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(id + "/" + item.DeviceID)
);
},
sendPtz(options) {
const ptzCmd = getPTZCmd(options);
const ptzCmdStop = getPTZCmd({ type: PTZ_TYPE.stop });
this.ajax
.get("/gb28181/control", {
id: this.context.id,
channel: this.context.channel,
ptzcmd: ptzCmd,
}).then(x => {
if (options.type === PTZ_TYPE.stop || options.cycle === true) {
})
.then((x) => {
if (
options.type === PTZ_TYPE.stop ||
options.cycle === true
) {
return;
}
setTimeout(() => {
@@ -151,32 +243,79 @@
channel: this.context.channel,
ptzcmd: ptzCmdStop,
});
}, 500)
});
},
invite(id, channel, item) {
this.ajax.get("/gb28181/invite", {id, channel}).then(x => {
item.Connected = true
});
},
bye(id, channel, item) {
this.ajax.get("/gb28181/bye", {id, channel}).then(x => {
item.Connected = false
}, 500);
});
},
sendQueryRecords(options) {},
handlePageChange(page) {
let showList = [];
const onePageSize = this.pageInfo.onePageSize;
const firstIndex = page * onePageSize - onePageSize;
const lastIndex = page * onePageSize - 1;
showList = this.channelList.filter((item, index) => {
return index >= firstIndex && index <= lastIndex;
});
this.channelShowList = showList;
if (showList.length > 0) {
this.pageInfo.currentPage = page;
}
},
};
updatePageInfo(totalSize) {
const onePageSize = this.pageInfo.onePageSize;
let totalPage = totalSize / onePageSize;
if (totalSize % onePageSize > 0) {
totalPage = totalPage + 1;
}
this.pageInfo.totalPage = totalPage;
if (this.pageInfo.currentPage === 0) {
this.handlePageChange(1);
}
},
invite(id, channel, item, f = "") {
return this.ajax
.get("/gb28181/invite", { id, channel, f })
.then((x) => {
item.Connected = true;
});
},
bye(id, channel, item) {
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>
<style scoped>
.flex-box {
display: flex;
flex-flow: row wrap;
align-content: flex-start;
}
.flex-box {
display: flex;
flex-flow: row wrap;
align-content: flex-start;
}
.flex-item {
flex: 0 0 33.3333%;
height: 275px;
padding: 10px;
}
.flex-item {
flex: 0 0 33.3333%;
height: 275px;
box-sizing: border-box;
padding: 10px;
}
</style>

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;
}
//
@@ -99,6 +100,8 @@
.player-wrap {
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 {

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;
}

6793
ui/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

4
yarn.lock Normal file
View File

@@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1