Compare commits

...

121 Commits

Author SHA1 Message Date
banshan
ab1ef3a560 修复通道更新 2021-06-28 16:00:19 +08:00
langhuihui
8c248fc0c0 g711默认单通道 2021-06-26 22:55:01 +08:00
langhuihui
51d8a8fa91 更新通道逻辑优化 2021-06-26 13:50:56 +08:00
langhuihui
1105d732ae 增加对注册包的处理逻辑 2021-06-26 09:57:18 +08:00
langhuihui
72be771799 修复并发写入map的问题 2021-06-26 08:17:24 +08:00
李宇翔
5cae3bd5c4 防止音频PTS覆盖 2021-06-25 18:12:01 +08:00
李宇翔
686ce753a3 PS中增加PTS、DTS解析 2021-06-25 16:51:12 +08:00
dexter
0f3fda9159 Merge pull request #27 from dwdcth/v3
还原invite时间格式和前端保持一致
2021-06-25 11:18:49 +08:00
banshan
0fcd23cee0 Merge branch 'v3' of https://github.com/dwdcth/plugin-gb28181 into v3 2021-06-25 10:40:08 +08:00
banshan
28f5774073 修复拼写错误 2021-06-25 10:39:53 +08:00
banshan
e813086907 Merge branch 'Monibuca:v3' into v3 2021-06-25 10:37:20 +08:00
banshan
8a8d97c676 改回时间戳和前端保持一致 2021-06-25 10:36:56 +08:00
dexter
356142a2db Merge pull request #26 from dwdcth/v3
进一步优化catalog,修复录像播放
2021-06-25 09:34:34 +08:00
banshan
0c9712e1e5 sdp时间戳 2021-06-25 09:31:29 +08:00
banshan
f22a6303c6 录像时间问题 2021-06-25 09:23:09 +08:00
banshan
53bb479588 优化目录查询,第一次notify也发送catalog 2021-06-25 09:16:25 +08:00
dexter
aea30f751a Merge pull request #24 from dwdcth/v3
定时catalog,并在注册时发送catalog
2021-06-22 19:28:02 +08:00
banshan
4b33a4c8a3 设备注册时不存在才发送catalog 2021-06-22 16:36:10 +08:00
banshan
0a0f58b1db 定时catalog 2021-06-22 11:16:34 +08:00
langhuihui
4528a67e92 修复bug 2021-06-16 07:19:04 +08:00
langhuihui
87ac220de1 修复bug 2021-06-15 08:55:12 +08:00
langhuihui
640c310a77 修复一个升级带来的bug 2021-06-15 08:40:04 +08:00
langhuihui
53bb4244db 适配引擎版本升级 2021-06-15 08:06:52 +08:00
langhuihui
6b193a4208 录像文件获取优化 2021-06-06 20:47:46 +08:00
李宇翔
bf54332376 加入反馈SDP中的y解析 2021-06-04 19:22:49 +08:00
langhuihui
a527903825 兼容channelID和device的Id相同的情况 2021-06-03 08:11:23 +08:00
langhuihui
bb0209fed9 防止重复添加Channel 2021-06-01 22:50:33 +08:00
langhuihui
23e01d4ccc 修改parentID问题 2021-06-01 22:34:16 +08:00
langhuihui
c52549b0c2 channel也可能有parentID 2021-06-01 08:20:07 +08:00
langhuihui
3a00e5dccc 修改树形逻辑 2021-05-31 22:21:42 +08:00
langhuihui
65d758ed0b 对于已存在的直播流不进行重复invite 2021-05-30 18:08:46 +08:00
langhuihui
5cf4b49033 加入autoInvite 2021-05-30 16:07:19 +08:00
langhuihui
04c93225a1 支持树形结构 2021-05-29 10:54:31 +08:00
langhuihui
709b394af6 去掉无效import 2021-05-29 09:55:39 +08:00
李宇翔
57e7977eae 树形结构 2021-05-28 19:17:41 +08:00
pg
ed0826a35d 限制通道功能修改,采用once 2021-05-26 22:08:54 +08:00
zhouyun
4495bd2d4d 修改自动invite拉流逻辑 2021-05-26 20:12:35 +08:00
pg
7d235cdd04 修改为只会自动拉取第一个设备的第一个通道里的视频流 2021-05-25 23:54:52 +08:00
zhouyun
6188118ef6 修改start和end默认为空时t=传值错误 2021-05-25 16:25:42 +08:00
pg
0481b71d52 增加读取autopublish配置 2021-05-25 10:27:58 +08:00
pg
6ab0f9d1c8 仅自动invite一个通道 2021-05-25 00:18:56 +08:00
pg
fd3564afe0 增加自动拉流功能 2021-05-24 08:34:38 +08:00
langhuihui
0d0208f0f7 修正ssrc 2021-05-09 23:46:12 +08:00
langhuihui
cc47907517 调整streamPath 2021-05-09 22:56:56 +08:00
langhuihui
8729b32cb2 修改发布路径 2021-05-09 22:26:55 +08:00
langhuihui
43fa8b3c37 修正错误 2021-05-09 20:55:10 +08:00
langhuihui
c33b93431a 调整流的关闭机制 2021-05-05 23:03:12 +08:00
langhuihui
1c8618076d 加入判断 2021-05-05 09:19:49 +08:00
langhuihui
12d069d949 改成单端口收流 2021-04-12 08:42:00 +08:00
langhuihui
779a868821 添加SetOriginVT 2021-04-07 21:39:04 +08:00
langhuihui
5841faa3cb 修改路由 2021-02-26 22:06:09 +08:00
langhuihui
e5f9aa54c1 适配3.0 2021-02-23 13:23:13 +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
langhuihui
f36ddce527 增加超时机制 2020-11-20 12:34:25 +08:00
wancheng1990
70f2c64a5c Merge pull request #8 from bosscheng/master
fix bug
2020-11-20 10:54:22 +08:00
bosscheng1210
2d61fc6308 Merge branch 'master' of https://github.com/bosscheng/plugin-gb28181 2020-11-20 10:47:20 +08:00
bosscheng1210
dd23d81a40 fix bug 2020-11-20 10:47:06 +08:00
wancheng1990
a758a770f1 Merge pull request #7 from bosscheng/master
add  N channels play
2020-11-19 17:03:31 +08:00
wancheng1990
1b9711ea2f Merge pull request #5 from Monibuca/master
merge
2020-11-19 17:02:19 +08:00
langhuihui
f6eec6d6b7 修改超时时间 2020-11-19 12:16:26 +08:00
bosscheng1210
801ccb98ca add files 2020-11-17 16:26:41 +08:00
bosscheng1210
c8323822e8 add n play 2020-11-16 16:53:59 +08:00
wancheng1990
7dfb19d9c3 Merge pull request #6 from bosscheng/master
update cycle operation
2020-11-16 11:02:28 +08:00
wancheng1990
934926e596 Merge pull request #4 from Monibuca/master
merge
2020-11-16 10:56:46 +08:00
bosscheng1210
f06b9146be fix bug 2020-11-16 10:53:47 +08:00
langhuihui
b4b04ec0f9 如果流存在则认为在线 2020-11-15 19:34:30 +08:00
39 changed files with 1215 additions and 16330 deletions

3
.gitignore vendored
View File

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

287
channel.go Normal file
View File

@@ -0,0 +1,287 @@
package gb28181
import (
"fmt"
"math/rand"
"strconv"
"strings"
"time"
"github.com/Monibuca/engine/v3"
"github.com/Monibuca/plugin-gb28181/v3/sip"
"github.com/Monibuca/plugin-gb28181/v3/transaction"
"github.com/Monibuca/plugin-gb28181/v3/utils"
)
type ChannelEx struct {
device *Device
inviteRes *sip.Message
recordInviteRes *sip.Message
RecordSP string //正在播放录像的StreamPath
LiveSP string //实时StreamPath
Records []*Record
RecordStartTime string
RecordEndTime string
recordStartTime time.Time
recordEndTime time.Time
}
// Channel 通道
type Channel struct {
DeviceID string
ParentID string
Name string
Manufacturer string
Model string
Owner string
CivilCode string
Address string
Parental int
SafetyWay int
RegisterWay int
Secrecy int
Status string
Children []*Channel
ChannelEx //自定义属性
}
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 (channel *Channel) QueryRecord(startTime, endTime string) int {
d := channel.device
channel.RecordStartTime = startTime
channel.RecordEndTime = endTime
channel.recordStartTime, _ = time.Parse(TIME_LAYOUT, startTime)
channel.recordEndTime, _ = time.Parse(TIME_LAYOUT, endTime)
channel.Records = nil
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 (channel *Channel) Control(PTZCmd string) int {
d := channel.device
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 (channel *Channel) Invite(start, end string) int {
sint, err1 := strconv.ParseInt(start,10,0)
eint, err2 := strconv.ParseInt(end,10,0)
d := channel.device
streamPath := fmt.Sprintf("%s/%s", d.ID, channel.DeviceID)
s := "Play"
ssrc := make([]byte, 10)
if start != "" {
if err1 != nil || err2 != nil {
return 400
}
s = "Playback"
ssrc[0] = '1'
streamPath = fmt.Sprintf("%s/%s/%s-%s", d.ID, channel.DeviceID, start, end)
} else {
ssrc[0] = '0'
}
var publisher Publisher
// size := 1
// fps := 15
// bitrate := 200
// fmt.Sprintf("f=v/2/%d/%d/1/%da///", size, fps, bitrate)
copy(ssrc[1:6], []byte(config.Serial[3:8]))
randNum := rand.Intn(10000)
copy(ssrc[6:], []byte(strconv.Itoa(randNum)))
_ssrc := string(ssrc)
_SSRC, _ := strconv.Atoi(_ssrc)
SSRC := uint32(_SSRC)
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=%d %d", sint, eint),
fmt.Sprintf("m=video %d RTP/AVP 96 97 98", config.MediaPort),
"a=recvonly",
"a=rtpmap:96 PS/90000",
"a=rtpmap:97 MPEG4/90000",
"a=rtpmap:98 H264/90000",
"y=" + _ssrc,
}
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 {
ds := strings.Split(response.Data.Body, "\r\n")
for _, l := range ds {
if ls := strings.Split(l, "="); len(ls) > 1 {
if ls[0] == "y" {
_ssrc = ls[1]
_SSRC, _ = strconv.Atoi(_ssrc)
SSRC = uint32(_SSRC)
}
}
}
publisher.Stream = &engine.Stream{
StreamPath: streamPath,
Type: "GB18181",
AutoUnPublish: config.AutoUnPublish,
}
if start == "" {
publisher.Close = func() {
publishers.Remove(SSRC)
channel.LiveSP = ""
if channel.inviteRes != nil {
channel.ByeBye(channel.inviteRes)
}
}
} else {
publisher.Close = func() {
publishers.Remove(SSRC)
channel.RecordSP = ""
if channel.recordInviteRes != nil {
channel.ByeBye(channel.recordInviteRes)
}
}
}
if !publisher.Publish() {
return 403
}
publishers.Add(SSRC, &publisher)
if start == "" {
channel.inviteRes = response.Data
channel.LiveSP = _ssrc
} else {
channel.RecordSP = _ssrc
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 (channel *Channel) Bye() int {
if channel.inviteRes != nil {
defer func() {
channel.inviteRes = nil
SSRC, _ := strconv.Atoi(channel.LiveSP)
if p := publishers.Get(uint32(SSRC)); p != nil {
p.Close()
}
}()
return channel.ByeBye(channel.inviteRes).Code
}
if channel.recordInviteRes != nil {
defer func() {
channel.recordInviteRes = nil
SSRC, _ := strconv.Atoi(channel.RecordSP)
if p := publishers.Get(uint32(SSRC)); p != nil {
p.Close()
}
}()
return channel.ByeBye(channel.recordInviteRes).Code
}
return 404
}
func (c *Channel) ByeBye(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)
}

140
device.go Normal file
View File

@@ -0,0 +1,140 @@
package gb28181
import (
"fmt"
"strings"
"sync"
"time"
"github.com/Monibuca/plugin-gb28181/v3/sip"
"github.com/Monibuca/plugin-gb28181/v3/transaction"
"github.com/Monibuca/plugin-gb28181/v3/utils"
// . "github.com/Monibuca/utils/v3"
// . "github.com/logrusorgru/aurora"
)
const TIME_LAYOUT = "2006-01-02T15:04:05"
// 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
channelMap map[string]*Channel
channelMutex sync.RWMutex
}
func (d *Device) addChannel(channel *Channel) {
for _, c := range d.Channels {
if c.DeviceID == channel.DeviceID {
return
}
}
d.Channels = append(d.Channels, channel)
}
func (d *Device) UpdateChannels(list []*Channel) {
d.channelMutex.Lock()
defer d.channelMutex.Unlock()
for _, c := range list {
c.device = d
if c.ParentID != "" {
path := strings.Split(c.ParentID, "/")
parentId := path[len(path)-1]
if parent, ok := d.channelMap[parentId]; ok {
parent.Children = append(parent.Children, c)
} else {
d.addChannel(c)
}
} else {
d.addChannel(c)
}
if old, ok := d.channelMap[c.DeviceID]; ok {
c.ChannelEx = old.ChannelEx
if len(old.Children) == 0 {
n := time.Now()
n = time.Date(n.Year(), n.Month(), n.Day(), 0, 0, 0, 0, time.Local)
if len(c.Records) == 0 || (n.Format(TIME_LAYOUT) == c.RecordStartTime && n.Add(time.Hour*24-time.Second).Format(TIME_LAYOUT) == c.RecordEndTime) {
go c.QueryRecord(n.Format(TIME_LAYOUT), n.Add(time.Hour*24-time.Second).Format(TIME_LAYOUT))
}
if config.AutoInvite && c.LiveSP == "" {
go c.Invite("", "")
}
}
}
d.channelMap[c.DeviceID] = c
}
}
func (d *Device) UpdateRecord(channelId string, list []*Record) {
d.channelMutex.RLock()
if c, ok := d.channelMap[channelId]; ok {
c.Records = append(c.Records, list...)
}
d.channelMutex.RUnlock()
}
func (d *Device) 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: &sip.Contact{Uri: d.from.Uri, Params: map[string]string{"tag": utils.RandNumString(9)}},
To: d.to, CSeq: &sip.CSeq{
ID: uint32(d.sn),
Method: Method,
}, CallID: utils.RandNumString(10),
Addr: d.Addr,
}
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
}

10
go.mod
View File

@@ -1,10 +1,12 @@
module github.com/Monibuca/plugin-gb28181
module github.com/Monibuca/plugin-gb28181/v3
go 1.13
require (
github.com/Monibuca/engine/v2 v2.2.2
github.com/Monibuca/plugin-rtp v1.0.0
github.com/Monibuca/engine/v3 v3.0.0-beta8
github.com/Monibuca/utils/v3 v3.0.0-beta1
github.com/logrusorgru/aurora v2.0.3+incompatible
golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d
github.com/mask-pp/rtp-ps v1.0.0
github.com/pion/rtp v1.6.5
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
)

60
go.sum
View File

@@ -1,53 +1,39 @@
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/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/Monibuca/engine/v3 v3.0.0-beta8 h1:bJ3VHKAd8eJO7qOrSQp8Byve2xfCN7/fBDhz4Nz+AM8=
github.com/Monibuca/engine/v3 v3.0.0-beta8/go.mod h1:ckcxVangFrP8uC7UlhuLJabN4A4NMLYCKEmYHud1Tbk=
github.com/Monibuca/utils/v3 v3.0.0-beta1 h1:M+miUm9+ojr6AahACOvVaFs+jc5jHmcUi38Dpe1QGgQ=
github.com/Monibuca/utils/v3 v3.0.0-beta1/go.mod h1:mQYP/OMox1tkWP6Qut7pBfARr1TXSRkK662dexQl6kI=
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/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/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.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/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/rtp v1.5.4/go.mod h1:bg60AL5GotNOlYZsqycbhDtEV3TkfbpXG0KBiUq29Mg=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtp v1.6.5 h1:o2cZf8OascA5HF/b0PAbTxRKvOWxTQxWYt7SlToxFGI=
github.com/pion/rtp v1.6.5/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
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/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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
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=
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/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=
github.com/q191201771/naza v0.19.1 h1:4KLcxT2CHztO+7miPRtBG3FFgadSQYQw1gPPPKN7rnY=
github.com/q191201771/naza v0.19.1/go.mod h1:5LeGupZZFtYP1g/S203n9vXoUNVdlRnPIfM6rExjqt0=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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=
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/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/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=

306
main.go
View File

@@ -1,36 +1,78 @@
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"
"encoding/xml"
"log"
"net"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/Monibuca/engine/v3"
"github.com/Monibuca/plugin-gb28181/v3/sip"
"github.com/Monibuca/plugin-gb28181/v3/transaction"
. "github.com/Monibuca/utils/v3"
. "github.com/logrusorgru/aurora"
"github.com/pion/rtp"
"golang.org/x/net/html/charset"
)
var Devices sync.Map
func FindChannel(deviceId string, channelId string) (c *Channel) {
if v, ok := Devices.Load(deviceId); ok {
d := v.(*Device)
d.channelMutex.RLock()
c = d.channelMap[channelId]
d.channelMutex.RUnlock()
}
return
}
type Publishers struct {
data map[uint32]*Publisher
sync.RWMutex
}
var publishers Publishers
func (p *Publishers) Add(key uint32, pp *Publisher) {
p.Lock()
p.data[key] = pp
p.Unlock()
}
func (p *Publishers) Remove(key uint32) {
p.Lock()
delete(p.data, key)
p.Unlock()
}
func (p *Publishers) Get(key uint32) *Publisher {
p.RLock()
defer p.RUnlock()
return p.data[key]
}
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
MediaPort uint16
AutoInvite bool
AutoUnPublish bool
Debug bool
CatalogInterval int
}{"34020000002000000001", "3402000000", "127.0.0.1:5060", 3600, 58200, false, true, false, 30}
func init() {
InstallPlugin(&PluginConfig{
engine.InstallPlugin(&engine.PluginConfig{
Name: "GB28181",
Config: &config,
Type: PLUGIN_PUBLISHER,
Run: run,
})
publishers.data = make(map[uint32]*Publisher)
}
func run() {
@@ -51,21 +93,37 @@ func run() {
RegisterInterval: 60,
HeartbeatInterval: 60,
HeartbeatRetry: 3,
AudioEnable: true,
WaitKeyFrame: true,
MediaPortMin: 58200,
MediaPortMax: 58300,
MediaIdleTimeout: 30,
Debug: config.Debug,
AudioEnable: true,
WaitKeyFrame: true,
MediaIdleTimeout: 30,
CatalogInterval: config.CatalogInterval,
}
s := transaction.NewCore(config)
s.OnInvite = onPublish
http.HandleFunc("/gb28181/list", func(w http.ResponseWriter, r *http.Request) {
sse := util.NewSSE(w, r.Context())
http.HandleFunc("/api/gb28181/query/records", func(w http.ResponseWriter, r *http.Request) {
CORS(w, r)
id := r.URL.Query().Get("id")
channel := r.URL.Query().Get("channel")
startTime := r.URL.Query().Get("startTime")
endTime := r.URL.Query().Get("endTime")
if c := FindChannel(id, channel); c != nil {
w.WriteHeader(c.QueryRecord(startTime, endTime))
} else {
w.WriteHeader(404)
}
})
http.HandleFunc("/api/gb28181/list", func(w http.ResponseWriter, r *http.Request) {
CORS(w, r)
sse := 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)
@@ -76,90 +134,164 @@ func run() {
}
}
})
http.HandleFunc("/gb28181/control", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
http.HandleFunc("/api/gb28181/control", func(w http.ResponseWriter, r *http.Request) {
CORS(w, r)
id := r.URL.Query().Get("id")
channel ,err:= strconv.Atoi(r.URL.Query().Get("channel"))
if err!=nil{
w.WriteHeader(404)
}
channel := r.URL.Query().Get("channel")
ptzcmd := r.URL.Query().Get("ptzcmd")
if v, ok := s.Devices.Load(id); ok {
w.WriteHeader(v.(*transaction.Device).Control(channel,ptzcmd))
if c := FindChannel(id, channel); c != nil {
w.WriteHeader(c.Control(ptzcmd))
} else {
w.WriteHeader(404)
}
})
http.HandleFunc("/gb28181/invite", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
http.HandleFunc("/api/gb28181/invite", func(w http.ResponseWriter, r *http.Request) {
CORS(w, r)
query := r.URL.Query()
id := query.Get("id")
channel := r.URL.Query().Get("channel")
startTime := query.Get("startTime")
endTime := query.Get("endTime")
if c := FindChannel(id, channel); c != nil {
if startTime == "" && c.LiveSP != "" {
w.WriteHeader(304) //直播流已存在
} else {
w.WriteHeader(c.Invite(startTime, endTime))
}
} else {
w.WriteHeader(404)
}
})
http.HandleFunc("/api/gb28181/bye", func(w http.ResponseWriter, r *http.Request) {
CORS(w, r)
id := r.URL.Query().Get("id")
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).Invite(channel))
channel := r.URL.Query().Get("channel")
if c := FindChannel(id, channel); c != nil {
w.WriteHeader(c.Bye())
} else {
w.WriteHeader(404)
}
})
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"))
if err != nil {
w.WriteHeader(404)
s := transaction.NewCore(config)
s.OnRegister = func(msg *sip.Message) {
id := msg.From.Uri.UserInfo()
d := &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,
channelMap: make(map[string]*Channel),
}
if v, ok := s.Devices.Load(id); ok {
w.WriteHeader(v.(*transaction.Device).Bye(channel))
if old, ok := Devices.Load(id); !ok {
go d.Query()
} else {
w.WriteHeader(404)
oldD := old.(*Device)
d.RegisterTime = oldD.RegisterTime
d.channelMap = oldD.channelMap
d.Status = oldD.Status
}
})
Devices.Store(id, d)
}
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":
if d.Channels == nil {
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 {
//
// }
// }
// })
//})
go listenMedia()
go queryCatalog(config)
s.Start()
}
func onPublish(channel *transaction.Channel) (port int) {
rtpPublisher := new(rtp.RTP_PS)
if !rtpPublisher.Publish("gb28181/" + channel.DeviceID) {
return
}
rtpPublisher.Type = "GB28181"
addr, err := net.ResolveUDPAddr("udp", ":0")
func listenMedia() {
networkBuffer := 1048576
var rtpPacket rtp.Packet
addr, err := net.ResolveUDPAddr("udp", ":"+strconv.Itoa(int(config.MediaPort)))
if err != nil {
return
log.Fatalf("udp server ResolveUDPAddr MediaPort:%d error, %v", config.MediaPort, err)
}
conn, err := net.ListenUDP("udp", addr)
if err != nil {
return
log.Fatalf("udp server ListenUDP MediaPort:%d error, %v", config.MediaPort, err)
}
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()
strPort := la[strings.LastIndex(la, ":")+1:]
if port, err = strconv.Atoi(strPort); err != nil {
return
}
go func() {
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{
return
}
if n, _, err := conn.ReadFromUDP(bufUDP); err == nil {
rtpPublisher.PushPS(bufUDP[:n])
} else {
Println("udp server read video pack error", err)
rtpPublisher.Close()
}
bufUDP := make([]byte, 1048576)
Printf("udp server start listen video port[%d]", config.MediaPort)
defer Printf("udp server stop listen video port[%d]", config.MediaPort)
for n, _, err := conn.ReadFromUDP(bufUDP); err == nil; n, _, err = conn.ReadFromUDP(bufUDP) {
ps := bufUDP[:n]
if err := rtpPacket.Unmarshal(ps); err != nil {
Println(err)
}
}()
return
if publisher := publishers.Get(rtpPacket.SSRC); publisher != nil && publisher.Err() == nil {
publisher.PushPS(rtpPacket.Payload, rtpPacket.Timestamp)
}
}
}
func queryCatalog(config *transaction.Config) {
t := time.NewTicker(time.Duration(config.CatalogInterval) * time.Second)
for {
select {
case <-t.C:
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 {
go device.Query()
}
return true
})
default:
}
}
}

78
publisher.go Normal file
View File

@@ -0,0 +1,78 @@
package gb28181
import (
"github.com/Monibuca/engine/v3"
"github.com/Monibuca/plugin-gb28181/v3/utils"
. "github.com/Monibuca/utils/v3"
)
type Publisher struct {
*engine.Stream
psPacket []byte
parser utils.DecPSPackage
pushVideo func(engine.VideoPack)
pushAudio func(engine.AudioPack)
}
func (p *Publisher) Publish() (result bool) {
if result = p.Stream.Publish(); result {
p.pushVideo = func(pack engine.VideoPack) {
var vt *engine.VideoTrack
switch p.parser.VideoStreamType {
case utils.StreamTypeH264:
vt = p.Stream.NewVideoTrack(7)
case utils.StreamTypeH265:
vt = p.Stream.NewVideoTrack(12)
default:
return
}
vt.PushAnnexB(pack)
p.pushVideo = vt.PushAnnexB
}
p.pushAudio = func(pack engine.AudioPack) {
switch p.parser.AudioStreamType {
case utils.G711A:
at := p.Stream.NewAudioTrack(7)
at.SoundRate = 8000
at.SoundSize = 16
at.Channels = 1
at.ExtraData = []byte{(at.CodecID << 4) | (1 << 1)}
at.PushRaw(pack)
p.pushAudio = at.PushRaw
// case utils.G711U:
// at := p.Stream.NewAudioTrack(8)
// at.SoundRate = 8000
// at.SoundSize = 16
// asc := at.CodecID << 4
// asc = asc + 1<<1
// at.ExtraData = []byte{asc}
// at.PushRaw(pack)
// p.pushAudio = at.PushRaw
}
}
}
return
}
func (p *Publisher) PushPS(ps []byte, ts uint32) {
if len(ps) >= 4 && BigEndian.Uint32(ps) == utils.StartCodePS {
if p.psPacket != nil {
if err := p.parser.Read(p.psPacket); err == nil {
if p.parser.DTS != 0 {
ts = p.parser.DTS
p.pushVideo(engine.VideoPack{Timestamp: ts / 90, CompositionTime: (p.parser.PTS/90 - p.parser.DTS/90), Payload: p.parser.VideoPayload})
} else {
p.pushVideo(engine.VideoPack{Timestamp: ts / 90, Payload: p.parser.VideoPayload})
}
if p.parser.AudioPayload != nil {
p.pushAudio(engine.AudioPack{Timestamp: ts / 8, Raw: p.parser.AudioPayload})
}
} else {
Print(err)
}
p.psPacket = nil
}
p.psPacket = append(p.psPacket, ps...)
} else if p.psPacket != nil {
p.psPacket = append(p.psPacket, ps...)
}
}

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

@@ -6,7 +6,7 @@ import (
"strconv"
"strings"
"github.com/Monibuca/plugin-gb28181/utils"
"github.com/Monibuca/plugin-gb28181/v3/utils"
)
//Content-Type: Application/MANSCDP+xml
@@ -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

@@ -57,4 +57,6 @@ type Config struct {
MediaIdleTimeout uint16 //推流超时时间,超过则断开链接,让设备重连
AudioEnable bool //是否开启音频
WaitKeyFrame bool //是否等待关键帧,如果等待,则在收到第一个关键帧之前,忽略所有媒体流
Debug bool //是否打印调试信息
CatalogInterval int //目录查询间隔
}

View File

@@ -1,20 +1,16 @@
package transaction
import (
"bytes"
"context"
"encoding/xml"
"fmt"
"io"
"net"
"os"
"sync"
"time"
"github.com/Monibuca/plugin-gb28181/sip"
"github.com/Monibuca/plugin-gb28181/transport"
"github.com/Monibuca/plugin-gb28181/utils"
"golang.org/x/net/html/charset"
"github.com/Monibuca/plugin-gb28181/v3/sip"
"github.com/Monibuca/plugin-gb28181/v3/transport"
"github.com/Monibuca/plugin-gb28181/v3/utils"
)
//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,9 +277,18 @@ func (c *Core) SendMessage(msg *sip.Message) *Response {
//把event推到transaction
ta.event <- e
//等待事件结束,并返回
return <-ta.response
<-ta.done
if ta.lastResponse != nil {
return &Response{
Code: ta.lastResponse.GetStatusCode(),
Data: ta.lastResponse,
Message: ta.lastResponse.GetReason(),
}
} else {
return &Response{
Code: 504,
}
}
}
//接收到的消息处理
@@ -293,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 {
@@ -321,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 {
@@ -333,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))
@@ -372,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
@@ -386,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
@@ -423,8 +396,8 @@ func (c *Core) Send(msg *sip.Message) error {
addr = fmt.Sprintf("%s:%s", host, port)
}
fmt.Println("dest addr:", addr)
// fmt.Println("dest addr:", addr)
var err1, err2 error
pkt := &transport.Packet{}
pkt.Data, err1 = sip.Encode(msg)
@@ -445,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,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
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.Second*36 {
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 {
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"
// "fmt"
"time"
"github.com/Monibuca/plugin-gb28181/v3/sip"
)
/*
@@ -58,9 +59,9 @@ func ict_snd_invite(t *Transaction, e *EventObj) error {
//发送出去之后,开启 timer
if msg.IsReliable() {
//stop timer E in reliable transport
fmt.Println("Reliabel")
//fmt.Println("Reliabel")
} else {
fmt.Println("Not Reliable")
//fmt.Println("Not Reliable")
//发送定时器,每次加倍,没有上限?
t.timerA = NewSipTimer(T1, 0, func() {
t.event <- &EventObj{
@@ -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

@@ -1,7 +1,7 @@
package transaction
import (
"fmt"
// "fmt"
"time"
)
@@ -53,7 +53,7 @@ import (
*/
func nict_snd_request(t *Transaction, e *EventObj) error {
msg := e.msg
fmt.Println("nict request:", msg.GetMethod())
//fmt.Println("nict request:", msg.GetMethod())
t.origRequest = msg
t.state = NICT_TRYING
@@ -67,9 +67,9 @@ func nict_snd_request(t *Transaction, e *EventObj) error {
//发送出去之后,开启 timer
if msg.IsReliable() {
//stop timer E in reliable transport
fmt.Println("Reliabel")
//fmt.Println("Reliabel")
} else {
fmt.Println("Not Reliable")
//fmt.Println("Not Reliable")
//发送定时器
t.timerE = NewSipTimer(T1, T2, func() {
t.event <- &EventObj{

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/v3/sip"
"github.com/Monibuca/plugin-gb28181/v3/transport"
)
//状态机之状态
@@ -337,31 +338,30 @@ func (ta *Transaction) Run() {
select {
case e := <-ta.event:
//根据event调用对应的handler
fmt.Println("fsm run event:", e.evt.String())
//fmt.Println("fsm run event:", e.evt.String())
core := ta.core
state := ta.state
evtHandlers, ok1 := core.handlers[state]
if !ok1 {
fmt.Println("invalid state:", ta.state.String())
//fmt.Println("invalid state:", ta.state.String())
break
}
f, ok2 := evtHandlers[e.evt]
if !ok2 {
fmt.Println("invalid handler for this event:", e.evt.String())
//fmt.Println("invalid handler for this event:", e.evt.String())
break
}
fmt.Printf("state:%s, event:%s\n", state.String(), e.evt.String())
//fmt.Printf("state:%s, event:%s\n", state.String(), e.evt.String())
err := f(ta, e)
if err != nil {
fmt.Printf("transaction run failed, state:%s, event:%s\n", state.String(), e.evt.String())
//fmt.Printf("transaction run failed, state:%s, event:%s\n", state.String(), e.evt.String())
}
case <-ta.done:
fmt.Println("fsm exit")
//fmt.Println("fsm exit")
return
case <-ta.ctx.Done():
fmt.Println("fsm killed")
//fmt.Println("fsm killed")
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
@@ -424,7 +424,7 @@ func (ta *Transaction) SipSend(msg *sip.Message) error {
addr = fmt.Sprintf("%s:%s", host, port)
}
fmt.Println("dest addr:", addr)
//fmt.Println("dest addr:", addr)
var err1, err2 error
pkt := &transport.Packet{}

View File

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

View File

@@ -2,7 +2,7 @@ package transport
import (
"fmt"
"github.com/Monibuca/plugin-gb28181/utils"
"github.com/Monibuca/plugin-gb28181/v3/utils"
"os"
"time"
)

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

@@ -2,7 +2,7 @@ package tu
import (
"fmt"
"github.com/Monibuca/plugin-gb28181/transaction"
"github.com/Monibuca/plugin-gb28181/v3/transaction"
)
//sip server和client的配置可以得到sip URIsip
@@ -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

@@ -2,8 +2,8 @@ package tu
import (
"fmt"
"github.com/Monibuca/plugin-gb28181/sip"
"github.com/Monibuca/plugin-gb28181/utils"
"github.com/Monibuca/plugin-gb28181/v3/sip"
"github.com/Monibuca/plugin-gb28181/v3/utils"
)
//根据参数构建各种消息
@@ -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

@@ -1,8 +1,9 @@
package tu
import (
"github.com/Monibuca/plugin-gb28181/transaction"
"sync"
"github.com/Monibuca/plugin-gb28181/v3/transaction"
)
//TODO:参考http服务使用者仅需要根据需要实现某些handler替换某些header fileds or body信息。其他的处理都由库来实现。
@@ -15,7 +16,7 @@ type Server struct {
//提供config参数
func NewServer(config *transaction.Config) *Server {
return &Server{
Core: transaction.NewCore(config),
Core: transaction.NewCore(config),
}
}

19
ui/dist/demo.html vendored
View File

@@ -1,19 +0,0 @@
<meta charset="utf-8">
<title>plugin-gb28181 demo</title>
<script src="https://unpkg.com/vue"></script>
<script src="./plugin-gb28181.umd.js"></script>
<link rel="stylesheet" href="./plugin-gb28181.css">
<div id="app">
<demo></demo>
</div>
<script>
new Vue({
components: {
demo: plugin-gb28181
}
}).$mount('#app')
</script>

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 +0,0 @@
.arrow1[data-v-3ceca3db]{grid-column:2;grid-row:1}.arrow2[data-v-3ceca3db]{transform:rotate(90deg);grid-column:3;grid-row:2}.arrow3[data-v-3ceca3db]{transform:rotate(180deg);grid-column:2;grid-row:3}.arrow4[data-v-3ceca3db]{transform:rotate(270deg);grid-column:1;grid-row:2}.arrow5[data-v-3ceca3db]{transform:rotate(-45deg);grid-column:1;grid-row:1}.arrow6[data-v-3ceca3db]{transform:rotate(45deg);grid-column:3;grid-row:1}.arrow7[data-v-3ceca3db]{transform:rotate(-135deg);grid-column:1;grid-row:3}.arrow8[data-v-3ceca3db]{transform:rotate(135deg);grid-column:3;grid-row:3}.arrow9[data-v-3ceca3db]{grid-column:2;grid-row:2}.container[data-v-3ceca3db]{position:relative;height:350px}.control[data-v-3ceca3db]{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-3ceca3db]{top:210px}.control3[data-v-3ceca3db]{top:260px}.control4[data-v-3ceca3db]{top:310px}.control5[data-v-3ceca3db]{top:360px}.control>[data-v-3ceca3db]{cursor:pointer;fill:grey;width:50px;height:50px}.control5>[data-v-3ceca3db]{margin-right:10px}.control2>[data-v-3ceca3db],.control3>[data-v-3ceca3db],.control4>[data-v-3ceca3db]{width:40px;height:40px}.control>[data-v-3ceca3db]:hover,.cycling[data-v-3ceca3db]{fill:#0ff}

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

9373
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +0,0 @@
{
"name": "dashboard",
"version": "1.0.0",
"description": "dashboard of gb28181 plugin for monibuca",
"main": "index.js",
"scripts": {
"build": "vue-cli-service build --target lib --name plugin-gb28181"
},
"author": "dexter",
"license": "ISC",
"devDependencies": {
"@vue/cli-service": "^4.5.4",
"vue-template-compiler": "^2.6.12"
}
}

View File

@@ -1,131 +0,0 @@
<template>
<div>
<mu-data-table :data="Devices" :columns="columns">
<template #expand="prop">
<mu-data-table :data="prop.row.Channels" :columns="columns2">
<template #default="{ row: item, $index }">
<td>{{ item.DeviceID }}</td>
<td>{{ item.Name }}</td>
<td>{{ item.Manufacturer }}</td>
<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>
<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
>
</td>
</template>
</mu-data-table>
</template>
<template #default="{ row: item }">
<td>{{ item.ID }}</td>
<td>{{ item.Channels ? item.Channels.length : 0 }}</td>
<td>
<StartTime :value="item.RegisterTime"></StartTime>
</td>
<td>
<StartTime :value="item.UpdateTime"></StartTime>
</td>
<td>{{ item.Status }}</td>
</template>
</mu-data-table>
<webrtc-player ref="player" @ptz="sendPtz" v-model="previewStreamPath" :PublicIP="PublicIP"></webrtc-player>
</div>
</template>
<script>
import WebrtcPlayer from "./components/Player"
import {getPTZCmd,PTZ_TYPE} from "./utils/ptz-cmd";
export default {
components:{
WebrtcPlayer
},
props:{
ListenAddr:String
},
computed:{
PublicIP(){
return this.ListenAddr.split(":")[0]
}
},
data() {
return {
Devices: [], previewStreamPath:false,
context:{
id:null,
channel:0,
item:null
},
columns: Object.freeze(
["设备号", "通道数", "注册时间", "更新时间", "状态"].map(
(title) => ({
title,
})
)
),
columns2: Object.freeze([
"通道编号",
"名称",
"厂商",
"地址",
"状态",
"操作",
]).map((title) => ({title})),
};
},
created() {
this.fetchlist();
},
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));
};
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", {
id:this.context.id,
channel:this.context.channel,
ptzcmd: ptzCmd,
}).then(x=>{
if(options.type === PTZ_TYPE.stop || options.cycle === true){
return;
}
setTimeout(()=>{
this.ajax.get("/gb28181/control", {
id:this.context.id,
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
});
}
},
};
</script>

View File

@@ -1,245 +0,0 @@
<template>
<Modal
v-bind="$attrs"
draggable
width="750"
v-on="$listeners"
:title="streamPath"
@on-ok="onClosePreview"
@on-cancel="onClosePreview"
>
<div class="container">
<video ref="webrtc" :srcObject.prop="stream" width="488" height="275" autoplay muted controls></video>
<div class="control">
<svg v-for="n in 8" @click="ptzCmdDirection(n)" :class="'arrow'+n" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><defs><style type="text/css"></style></defs><path d="M682.666667 955.733333H341.333333a17.066667 17.066667 0 0 1-17.066666-17.066666V529.066667H85.333333a17.066667 17.066667 0 0 1-12.066133-29.1328l426.666667-426.666667a17.0496 17.0496 0 0 1 24.132266 0l426.666667 426.666667A17.066667 17.066667 0 0 1 938.666667 529.066667H699.733333v409.6a17.066667 17.066667 0 0 1-17.066666 17.066666z m-324.266667-34.133333h307.2V512a17.066667 17.066667 0 0 1 17.066667-17.066667h214.801066L512 109.4656 126.532267 494.933333H341.333333a17.066667 17.066667 0 0 1 17.066667 17.066667v409.6z" p-id="6849"></path></svg>
<svg @click="ptzCmdCycle" class="arrow9" :class="{'cycling':isCycling}" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M512 960c-210.96 0-395.36-149.68-438.47-355.91-2.98-14.24 6.16-28.21 20.4-31.19 14.22-2.93 28.21 6.15 31.18 20.41C163.15 775.25 325.86 907.29 512 907.29s348.85-132.05 386.89-313.98c2.99-14.26 16.97-23.35 31.19-20.41 14.24 2.99 23.38 16.95 20.41 31.19C907.36 810.32 722.95 960 512 960zM927.48 466.94c-12.61 0-23.75-9.07-25.95-21.91C869.06 254.78 705.24 116.71 512 116.71c-193.23 0-357.05 138.07-389.52 328.32-2.45 14.35-16.08 24.01-30.41 21.54-14.35-2.46-23.99-16.07-21.55-30.42C107.33 220.51 293 64 512 64c219.01 0 404.68 156.51 441.48 372.15 2.44 14.35-7.21 27.97-21.54 30.42-1.5 0.25-3 0.37-4.46 0.37z" /><path d="M96.52 466.94c-9.11 0-17.97-4.72-22.85-13.18-7.28-12.61-2.96-28.72 9.64-36l131.76-76.07c12.6-7.26 28.73-2.96 36 9.65 7.28 12.61 2.96 28.72-9.64 36l-131.76 76.07a26.18 26.18 0 0 1-13.15 3.53zM792.95 701.14c-9.11 0-17.96-4.72-22.85-13.18-7.28-12.6-2.96-28.72 9.64-36l131.76-76.09c12.58-7.28 28.72-2.95 36 9.65 7.27 12.6 2.96 28.72-9.65 36l-131.75 76.1a26.271 26.271 0 0 1-13.15 3.52z" /></svg>
</div>
<div class="control control2">
<svg @click="ptzCmd(ptzType.zoomFar)" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M994.990643 859.352971L713.884166 578.246494A381.208198 381.208198 0 0 0 767.307984 383.653992C767.307984 171.765089 595.542895 0 383.653992 0S0 171.765089 0 383.653992s171.765089 383.653992 383.653992 383.653992c71.119859 0 137.507985-19.694238 194.592502-53.423818l281.106477 281.090491a95.913498 95.913498 0 1 0 135.637672-135.621686zM383.653992 671.394486c-158.912681 0-287.740494-128.827813-287.740494-287.740494S224.741311 95.913498 383.653992 95.913498s287.740494 128.827813 287.740494 287.740494-128.827813 287.740494-287.740494 287.740494z m159.85583-335.697243h-111.899081v-111.899081a47.956749 47.956749 0 1 0-95.913498 0v111.899081h-111.899081a47.956749 47.956749 0 1 0 0 95.913498h111.899081v111.899081a47.956749 47.956749 0 1 0 95.913498 0v-111.899081h111.899081a47.956749 47.956749 0 1 0 0-95.913498z" /></svg>
<svg @click="ptzCmd(ptzType.zoomNear)" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M994.990643 859.352971L713.884166 578.246494A381.208198 381.208198 0 0 0 767.307984 383.653992C767.307984 171.765089 595.542895 0 383.653992 0S0 171.765089 0 383.653992s171.765089 383.653992 383.653992 383.653992c71.119859 0 137.507985-19.694238 194.592502-53.423818l281.106477 281.090491a95.913498 95.913498 0 1 0 135.637672-135.621686zM383.653992 671.394486c-158.912681 0-287.740494-128.827813-287.740494-287.740494S224.741311 95.913498 383.653992 95.913498s287.740494 128.827813 287.740494 287.740494-128.827813 287.740494-287.740494 287.740494z m159.85583-335.697243H223.798162a47.956749 47.956749 0 1 0 0 95.913498h319.71166a47.956749 47.956749 0 1 0 0-95.913498z" /></svg>
</div>
<div class="control control3">
<svg @click="ptzCmd(ptzType.apertureFar)" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M956.39 400.827C922.164 266.675 828.186 155.703 701.502 99.874l94.522 443.782L956.39 400.827zM206.208 189.167C106.183 286.191 56.845 424.181 72.696 562.659l351.347-309.096-217.835-64.396zM643.118 78.847a446.363 446.363 0 0 0-138.947-16.775 448.047 448.047 0 0 0-250.583 86.934l437.868 146.949-48.338-217.108zM83.786 623.979c34.443 133.772 128.248 244.407 254.583 300.291l-95.915-426.55L83.786 623.979zM969.893 496.089a372.746 372.746 0 0 0-2.37-34.138l-329.972 303.78 196.157 69.256c91.522-88.456 141.056-211.704 136.185-338.898zM396.862 945.166a447.857 447.857 0 0 0 139.077 16.766 447.784 447.784 0 0 0 250.322-86.718L349.286 733.05l47.576 212.116z" /><path fill="#333333" d="M397.253 471.171h245.668c22.593 0 40.923 18.32 40.923 40.913 0 22.592-18.33 40.922-40.923 40.922H397.253c-22.592 0-40.922-18.33-40.922-40.922 0-22.593 18.33-40.913 40.922-40.913z" /><path fill="#333333" d="M479.17 634.879V389.21c0-22.593 18.32-40.923 40.913-40.923s40.923 18.33 40.923 40.923v245.668c0 22.592-18.33 40.922-40.923 40.922s-40.913-18.329-40.913-40.921z" /></svg>
<svg @click="ptzCmd(ptzType.apertureNear)" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M956.39 400.827C922.164 266.675 828.186 155.703 701.502 99.874l94.522 443.782L956.39 400.827z m-750.182-211.66C106.183 286.191 56.845 424.181 72.696 562.659l351.347-309.096-217.835-64.396z m436.91-110.32a446.363 446.363 0 0 0-138.947-16.775 448.047 448.047 0 0 0-250.583 86.934l437.868 146.949-48.338-217.108zM83.786 623.979c34.443 133.772 128.248 244.407 254.583 300.291l-95.915-426.55L83.786 623.979z m886.107-127.89a372.746 372.746 0 0 0-2.37-34.138l-329.972 303.78 196.157 69.256c91.522-88.456 141.056-211.704 136.185-338.898zM396.862 945.166a447.857 447.857 0 0 0 139.077 16.766 447.784 447.784 0 0 0 250.322-86.718L349.286 733.05l47.576 212.116z m0.391-474.039h245.668c22.593 0 40.923 18.32 40.923 40.912 0 22.593-18.33 40.923-40.923 40.923H397.253c-22.592 0-40.922-18.33-40.922-40.923 0-22.592 18.33-40.912 40.922-40.912z" /></svg>
</div>
<div class="control control4">
<svg @click="ptzCmd(ptzType.focusFar)" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M849.07153297 646.81872559c9.30432153 0 17.26391602 3.30249 23.82934617 9.88769507 6.60992408 6.59509253 9.88769508 14.52502465 9.88769508 23.79473901v101.14617896c0 27.90801978-9.87780761 51.70275879-29.61364722 71.47814965-19.75067115 19.77539086-43.56518578 29.66308594-71.48803711 29.66308594h-101.1165166c-9.32409644 0-17.25402856-3.29754663-23.83428954-9.9865725-6.59509253-6.49127173-9.90252662-14.52502465-9.90252662-23.7947383 0-9.26971435 3.30743408-17.20458984 9.90252662-23.79473901 6.58026099-6.59014916 14.51019311-9.88769508 23.83428954-9.88769507h101.1165166c9.29937744 0 17.26391602-3.29754663 23.82440137-9.88769579 6.61486816-6.59014916 9.88769508-14.52008057 9.88769579-23.89361573v-101.04235815c0-9.36859107 3.28765845-17.30346656 9.89758254-23.78979493 6.57531762-6.69396997 14.52502465-9.99151587 23.83923363-9.99151587l-0.06427025 0.09887671zM242.38726782 141.3103025h101.10168506c9.30432153 0 17.2688601 3.29754663 23.819458 9.88769578 6.62475562 6.59509253 9.89758325 14.52502465 9.89758254 23.7947383 0 9.37353516-3.27282691 17.30346656-9.89758254 23.79473901-6.5505979 6.69396997-14.51513648 9.9865725-23.81451463 9.9865725h-101.10168433c-9.31915307 0-17.2688601 3.19372583-23.82934547 9.88769508-6.62475562 6.49127173-9.91241479 14.52502465-9.91241479 23.794739v101.04235816c0 9.36859107-3.28271508 17.30346656-9.89758324 23.89361573-6.57531762 6.59014916-14.51513648 9.88769508-23.81451392 9.88769507-9.31420898 0-17.25402856-3.29754663-23.82934547-9.88769507C144.49908423 360.80230689 141.21142578 352.86743141 141.21142578 343.49884033V242.45648217c0-27.91296386 9.86792016-51.70275879 29.62353539-71.47814965 19.75067115-19.77539086 43.57507324-29.66308594 71.48803711-29.66308594h0.06426954zM174.9877932 646.81872559c9.30432153 0 17.24414039 3.30249 23.81451393 9.88769507 6.62475562 6.59509253 9.90252662 14.52502465 9.90252662 23.79473901v101.14617896c0 9.26971435 3.27282691 17.19964576 9.89758324 23.78979492 6.57531762 6.59014916 14.51513648 9.88769508 23.81451393 9.88769579h101.12640404c9.29937744 0 17.25402856 3.29754663 23.82934547 9.88769507 6.60992408 6.59014916 9.88769508 14.52502465 9.88769579 23.89361572 0 9.26971435-3.27777099 17.20458984-9.88769579 23.79473901-6.57531762 6.59014916-14.52996803 9.88769508-23.82934547 9.88769508H242.41693092c-27.91296386 0-51.71264625-9.88769508-71.47814895-29.66308594-19.75561523-19.67651344-29.62353539-43.57012915-29.62353539-71.47814965v-101.04235816c0-9.26971435 3.27282691-17.30346656 9.88769507-23.89361573 6.58026099-6.59509253 14.52502465-9.88769508 23.81451464-9.88769507h-0.02966309zM680.57037329 141.3103025h101.1165166c27.92285133 0 51.73736596 9.88769508 71.48803711 29.56420922 19.73583961 19.77539086 29.61364722 43.57012915 29.61364722 71.47814965v101.14617896c0 9.26971435-3.27777099 17.30346656-9.88769508 23.78979493-6.56542945 6.69396997-14.52502465 9.88769508-23.82934617 9.88769506-9.29937744 0-17.26391602-3.19372583-23.82440139-9.88769506-6.61486816-6.48632836-9.88769508-14.52008057-9.88769579-23.78979493V242.35266137c0-9.26971435-3.28765845-17.19964576-9.90252661-23.78979492-6.57037354-6.59509253-14.52008057-9.88769508-23.83428955-9.88769579h-101.10168433c-9.31420898 0-17.2688601-3.29754663-23.82934618-9.88769507-6.60992408-6.59509253-9.89758325-14.52502465-9.89758254-23.79473902 0-9.37353516 3.28765845-17.30346656 9.89758254-23.89361571 6.56048608-6.59014916 14.51513648-9.88769508 23.82934618-9.88769508l0.04943799 0.09887672z" /></svg>
<svg @click="ptzCmd(ptzType.focusNear)" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M512 170.666667A341.333333 341.333333 0 1 1 170.666667 512 341.333333 341.333333 0 0 1 512 170.666667m0-42.666667a384 384 0 1 0 384 384A384 384 0 0 0 512 128z" /><path fill="#333333" d="M298.666667 533.333333H170.666667a21.333333 21.333333 0 0 1 0-42.666666h128a21.333333 21.333333 0 0 1 0 42.666666zM853.333333 533.333333h-128a21.333333 21.333333 0 0 1 0-42.666666h128a21.333333 21.333333 0 0 1 0 42.666666zM512 320a21.333333 21.333333 0 0 1-21.333333-21.333333V170.666667a21.333333 21.333333 0 0 1 42.666666 0v128a21.333333 21.333333 0 0 1-21.333333 21.333333zM512 874.666667a21.333333 21.333333 0 0 1-21.333333-21.333334v-128a21.333333 21.333333 0 0 1 42.666666 0v128a21.333333 21.333333 0 0 1-21.333333 21.333334z" /></svg>
</div>
<div class="control5">
<i-select v-model="ptzPositionIndex" style="width: 100px">
<Option v-for="n in 10" :value="n" :key="n">预置点{{n}}</Option>
</i-select>
<i-button type="primary" @click="ptzCmd(ptzType.setPos)">设置</i-button>
<i-button type="success" @click="ptzCmd(ptzType.calPos)">调用</i-button>
<i-button type="error" @click="ptzCmd(ptzType.delPos)">删除</i-button>
速度
<InputNumber :max="10" :min="1" v-model="ptzSpeed"></InputNumber>
</div>
</div>
<div slot="footer">
<mu-badge v-if="remoteSDP">
<a slot="content" :href="remoteSDPURL" download="remoteSDP.txt">remoteSDP</a>
</mu-badge>
<mu-badge v-if="localSDP">
<a slot="content" :href="localSDPURL" download="localSDP.txt">localSDP</a>
</mu-badge>
</div>
</Modal>
</template>
<script>
import {PTZ_TYPE} from "../utils/ptz-cmd";
const PTZ_DIRECTION_ARRAY = [PTZ_TYPE.up,PTZ_TYPE.right,PTZ_TYPE.down,PTZ_TYPE.left,PTZ_TYPE.leftUp,PTZ_TYPE.rightUp,PTZ_TYPE.leftDown,PTZ_TYPE.rightDown];
let pc = null;
export default {
data() {
return {
iceConnectionState: pc && pc.iceConnectionState,
stream: null,
localSDP: "",
remoteSDP: "",
remoteSDPURL: "",
localSDPURL: "",
streamPath: "",
ptzSpeed:5,
ptzPositionIndex:1,
ptzType:PTZ_TYPE,
isCycling:false
};
},
props:{
PublicIP:String
},
methods: {
async play(streamPath) {
pc = new RTCPeerConnection();
pc.addTransceiver('video',{
direction:'recvonly'
})
this.streamPath = streamPath;
pc.onsignalingstatechange = e => {
//console.log(e);
};
pc.oniceconnectionstatechange = e => {
this.$toast.info(pc.iceConnectionState);
this.iceConnectionState = pc.iceConnectionState;
};
pc.onicecandidate = event => {
console.log(event)
};
pc.ontrack = event => {
// console.log(event);
if (event.track.kind == "video")
this.stream = event.streams[0];
};
await pc.setLocalDescription(await pc.createOffer());
this.localSDP = pc.localDescription.sdp;
this.localSDPURL = URL.createObjectURL(
new Blob([this.localSDP], { type: "text/plain" })
);
const result = await this.ajax({
type: "POST",
processData: false,
data: JSON.stringify(pc.localDescription.toJSON()),
url: "/webrtc/play?streamPath=" + this.streamPath,
dataType: "json"
});
if (result.errmsg) {
this.$toast.error(result.errmsg);
return;
} else {
this.remoteSDP = result.sdp;
this.remoteSDPURL = URL.createObjectURL(new Blob([this.remoteSDP], { type: "text/plain" }));
}
await pc.setRemoteDescription(new RTCSessionDescription(result));
},
ptzCmdDirection(direction){
const type = PTZ_DIRECTION_ARRAY[direction-1];
if(this.isCycling){
this.ptzCmdCycle();
}
this.ptzCmd(type);
},
ptzCmdCycle(){
if(this.isCycling){
this.isCycling = false;
this.ptzCmd(PTZ_TYPE.stop);
}
else {
this.isCycling = true;
this.ptzCmd(PTZ_TYPE.right);
}
},
ptzCmd(type){
this.$emit('ptz',{type:type,speed:this.ptzSpeed,index:this.ptzPositionIndex,cycle:this.isCycling})
},
onClosePreview() {
pc.close();
}
}
};
</script>
<style scoped>
.arrow1{
grid-column: 2;
grid-row: 1;
}
.arrow2{
transform: rotate(90deg);
grid-column: 3;
grid-row: 2;
}
.arrow3{
transform: rotate(180deg);
grid-column: 2;
grid-row: 3;
}
.arrow4{
transform: rotate(270deg);
grid-column: 1;
grid-row: 2;
}
.arrow5{
transform: rotate(-45deg);
grid-column: 1;
grid-row: 1;
}
.arrow6{
transform: rotate(45deg);
grid-column: 3;
grid-row: 1;
}
.arrow7{
transform: rotate(-135deg);
grid-column: 1;
grid-row: 3;
}
.arrow8{
transform: rotate(135deg);
grid-column: 3;
grid-row: 3;
}
.arrow9{
grid-column: 2;
grid-row: 2;
}
.container {
position: relative;
height: 350px;
}
.control {
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{
top: 210px;
}
.control3{
top: 260px;
}
.control4{
top: 310px;
}
.control5{
top: 360px;
}
.control >* {
cursor: pointer;
fill: gray;
width: 50px;
height: 50px;
}
.control5 >*{
margin-right: 10px;
}
.control2 >*,.control3 >*,.control4 >*{
width: 40px;
height: 40px;
}
.control >*:hover,.cycling{
fill: cyan
}
</style>

View File

@@ -1,172 +0,0 @@
/**
* Date:2020/11/9
* Desc:
*/
/**
* Date:2020/11/2
* Desc: ptz cmd 封装
* cmd[0] //首字节以05H开头
* cmd[1] //组合码高4位为版本信息v1.0,版本信息0H低四位为校验码
* // 校验码 = (cmd[0]的高4位+cmd[0]的低4位+cmd[1]的高4位)%16
* cmd[2] //地址的低8位什么地址地址范围000h ~ FFFh(0~4095),其中000h为广播地址
* cmd[3] //指令码
* cmd[4] //数据1,水平控制速度、聚焦速度
* cmd[5] //数据2垂直控制速度、光圈速度
* cmd[6] // 高4位为数据3=变倍控制速度低4位为地址高4位
*/
const PTZ_TYPE = {
stop: 'stop',
right: 'right',
left: 'left',
up: 'up',
down: 'down',
leftUp: 'leftUp',
leftDown: 'leftDown',
rightUp: 'rightUp',
rightDown: 'rightDown',
zoomFar: 'zoomFar',
zoomNear: 'zoomNear',
apertureFar: 'apertureFar',
apertureNear: 'apertureNear',
focusFar: 'focusFar',
focusNear: 'focusNear',
setPos: 'setPos',
calPos: 'calPos',
delPos: 'delPos'
};
const PTZ_CMD_TYPE = {
stop: 0x00,
right: 0x01,
left: 0x02,
up: 0x08,
down: 0x04,
leftUp: 0x0A,
leftDown: 0x06,
rightUp: 0x09,
rightDown: 0x05,
zoomFar: 0x10, // 镜头 放大
zoomNear: 0x20, // 镜头 缩小
apertureFar: 0x48, // 光圈 缩小
apertureNear: 0x44, // 光圈 放大
focusFar: 0x42, // 聚焦 近
focusNear: 0x41, // 聚焦 远
setPos: 0x81,
calPos: 0x82,
delPos: 0x83
};
const SPEED_ARRAY = [0x19, 0x32, 0x4b, 0x64, 0x7d, 0x96, 0xAF, 0xC8, 0xE1, 0xFA];
const POSITION_ARRAY = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10];
// 获取 direction 方向型
/**
*
* @param options
* type:
* speed:default 5
* index:
* @returns {string}
*/
function getPTZCmd(options) {
const {type, speed, index} = options;
const ptzSpeed = getPTZSpeed(speed);
let indexValue3, indexValue4, indexValue5, indexValue6;
indexValue3 = PTZ_CMD_TYPE[type];
switch (type) {
case PTZ_TYPE.up:
case PTZ_TYPE.down:
case PTZ_TYPE.apertureFar:
case PTZ_TYPE.apertureNear:
indexValue5 = ptzSpeed;
break;
case PTZ_TYPE.right:
case PTZ_TYPE.left:
case PTZ_TYPE.focusFar:
case PTZ_TYPE.focusNear:
indexValue4 = ptzSpeed;
break;
case PTZ_TYPE.leftUp:
case PTZ_TYPE.leftDown:
case PTZ_TYPE.rightUp:
case PTZ_TYPE.rightDown:
indexValue4 = ptzSpeed;
indexValue5 = ptzSpeed;
break;
case PTZ_TYPE.zoomFar:
case PTZ_TYPE.zoomNear:
indexValue6 = 0x10;
break;
case PTZ_TYPE.calPos:
case PTZ_TYPE.delPos:
case PTZ_TYPE.setPos:
indexValue5 = getPTZPositionIndex(index);
break;
default:
break;
}
return ptzCmdToString(indexValue3, indexValue4, indexValue5, indexValue6);
}
function getPTZSpeed(speed) {
speed = speed || 5;
const speedIndex = speed - 1;
const ptzSpeed = SPEED_ARRAY[speedIndex] || SPEED_ARRAY[4];
return ptzSpeed;
}
function getPTZPositionIndex(index) {
return POSITION_ARRAY[index - 1];
}
function ptzCmdToString(indexValue3, indexValue4, indexValue5, indexValue6) {
//
let cmd = Buffer.alloc(8);
// 首字节以05H开头
cmd[0] = 0xA5;
// 组合码高4位为版本信息v1.0,版本信息0H低四位为校验码
cmd[1] = 0x0F;
// 校验码 = (cmd[0]的高4位+cmd[0]的低4位+cmd[1]的高4位)%16
cmd[2] = 0x01;
//
if (indexValue3) {
cmd[3] = indexValue3;
}
if (indexValue4) {
cmd[4] = indexValue4;
}
if (indexValue5) {
cmd[5] = indexValue5;
}
if (indexValue6) {
cmd[6] = indexValue6;
}
cmd[7] = (cmd[0] + cmd[1] + cmd[2] + cmd[3] + cmd[4] + cmd[5] + cmd[6]) % 256;
return bytes2HexString(cmd);
}
function bytes2HexString(byte) {
let hexs = "";
for (let i = 0; i < byte.length; i++) {
let hex = (byte[i]).toString(16);
if (hex.length === 1) {
hex = '0' + hex;
}
hexs += hex.toUpperCase();
}
return hexs;
}
export {
getPTZCmd,
PTZ_TYPE
}

333
utils/ps.go Normal file
View File

@@ -0,0 +1,333 @@
package utils
import (
"errors"
"github.com/mask-pp/rtp-ps/buffer"
)
//
const (
UDPTransfer int = 0
TCPTransferActive int = 1
TCPTransferPassive int = 2
LocalCache int = 3
StreamTypeH264 = 0x1b
StreamTypeH265 = 0x24
G711A = 0x90 //PCMA
G7221AUDIOTYPE = 0x92
G7231AUDIOTYPE = 0x93
G729AUDIOTYPE = 0x99
StreamIDVideo = 0xe0
StreamIDAudio = 0xc0
StartCodePS = 0x000001ba
StartCodeSYS = 0x000001bb
StartCodeMAP = 0x000001bc
StartCodeVideo = 0x000001e0
StartCodeAudio = 0x000001c0
HaiKangCode = 0x000001bd
MEPGProgramEndCode = 0x000001b9
RTPHeaderLength int = 12
PSHeaderLength int = 14
SystemHeaderLength int = 18
MAPHeaderLength int = 24
PESHeaderLength int = 19
RtpLoadLength int = 1460
PESLoadLength int = 0xFFFF
MAXFrameLen int = 1024 * 1024 * 2
)
var (
ErrNotFoundStartCode = errors.New("not found the need start code flag")
ErrMarkerBit = errors.New("marker bit value error")
ErrFormatPack = errors.New("not package standard")
ErrParsePakcet = errors.New("parse ps packet error")
)
/*
This implement from VLC source code
notes: https://github.com/videolan/vlc/blob/master/modules/mux/mpeg/bits.h
*/
//bitsBuffer bits buffer
type bitsBuffer struct {
iSize int
iData int
iMask uint8
pData []byte
}
func bitsInit(isize int, buffer []byte) *bitsBuffer {
bits := &bitsBuffer{
iSize: isize,
iData: 0,
iMask: 0x80,
pData: buffer,
}
if bits.pData == nil {
bits.pData = make([]byte, isize)
}
return bits
}
func bitsAlign(bits *bitsBuffer) {
if bits.iMask != 0x80 && bits.iData < bits.iSize {
bits.iMask = 0x80
bits.iData++
bits.pData[bits.iData] = 0x00
}
}
func bitsWrite(bits *bitsBuffer, count int, src uint64) *bitsBuffer {
for count > 0 {
count--
if ((src >> uint(count)) & 0x01) != 0 {
bits.pData[bits.iData] |= bits.iMask
} else {
bits.pData[bits.iData] &= ^bits.iMask
}
bits.iMask >>= 1
if bits.iMask == 0 {
bits.iData++
bits.iMask = 0x80
}
}
return bits
}
/*
https://github.com/videolan/vlc/blob/master/modules/demux/mpeg
*/
type DecPSPackage struct {
systemClockReferenceBase uint64
systemClockReferenceExtension uint64
programMuxRate uint32
VideoStreamType uint32
AudioStreamType uint32
buffer.RawBuffer
VideoPayload []byte
AudioPayload []byte
PTS uint32
DTS uint32
}
// data包含 接受到完整一帧数据后所有的payload, 解析出去后是一阵完整的raw数据
func (dec *DecPSPackage) Read(data []byte) error {
return dec.decPackHeader(append(data, 0x00, 0x00, 0x01, 0xb9))
}
func (dec *DecPSPackage) clean() {
dec.systemClockReferenceBase = 0
dec.systemClockReferenceExtension = 0
dec.programMuxRate = 0
dec.VideoPayload = nil
dec.AudioPayload = nil
dec.PTS = 0
dec.DTS = 0
}
func (dec *DecPSPackage) decPackHeader(data []byte) error {
dec.clean()
// 加载数据
dec.LoadBuffer(data)
if startcode, err := dec.Uint32(); err != nil {
return err
} else if startcode != StartCodePS {
return ErrNotFoundStartCode
}
if err := dec.Skip(9); err != nil {
return err
}
psl, err := dec.Uint8()
if err != nil {
return err
}
psl &= 0x07
if err = dec.Skip(int(psl)); err != nil {
return err
}
for {
nextStartCode, err := dec.Uint32()
if err != nil {
return err
}
switch nextStartCode {
case StartCodeSYS:
if err := dec.decSystemHeader(); err != nil {
return err
}
case StartCodeMAP:
if err := dec.decProgramStreamMap(); err != nil {
return err
}
case StartCodeVideo, StartCodeAudio:
if err := dec.decPESPacket(nextStartCode); err != nil {
return err
}
case HaiKangCode, MEPGProgramEndCode:
return nil
}
}
}
func (dec *DecPSPackage) decSystemHeader() error {
syslens, err := dec.Uint16()
if err != nil {
return err
}
// drop rate video audio bound and lock flag
syslens -= 6
if err = dec.Skip(6); err != nil {
return err
}
// ONE WAY: do not to parse the stream and skip the buffer
//br.Skip(syslen * 8)
// TWO WAY: parse every stream info
for syslens > 0 {
if nextbits, err := dec.Uint8(); err != nil {
return err
} else if (nextbits&0x80)>>7 != 1 {
break
}
if err = dec.Skip(2); err != nil {
return err
}
syslens -= 3
}
return nil
}
func (dec *DecPSPackage) decProgramStreamMap() error {
psm, err := dec.Uint16()
if err != nil {
return err
}
//drop psm version infor
if err = dec.Skip(2); err != nil {
return err
}
psm -= 2
programStreamInfoLen, err := dec.Uint16()
if err != nil {
return err
}
if err = dec.Skip(int(programStreamInfoLen)); err != nil {
return err
}
psm -= programStreamInfoLen + 2
programStreamMapLen, err := dec.Uint16()
if err != nil {
return err
}
psm -= 2 + programStreamMapLen
for programStreamMapLen > 0 {
streamType, err := dec.Uint8()
if err != nil {
return err
}
elementaryStreamID, err := dec.Uint8()
if err != nil {
return err
}
if elementaryStreamID >= 0xe0 && elementaryStreamID <= 0xef {
dec.VideoStreamType = uint32(streamType)
} else if elementaryStreamID >= 0xc0 && elementaryStreamID <= 0xdf {
dec.AudioStreamType = uint32(streamType)
}
elementaryStreamInfoLength, err := dec.Uint16()
if err != nil {
return err
}
if err = dec.Skip(int(elementaryStreamInfoLength)); err != nil {
return err
}
programStreamMapLen -= 4 + elementaryStreamInfoLength
}
// crc 32
if psm != 4 {
return ErrFormatPack
}
if err = dec.Skip(4); err != nil {
return err
}
return nil
}
func (dec *DecPSPackage) decPESPacket(t uint32) error {
payloadlen, err := dec.Uint16()
if err != nil {
return err
}
if err = dec.Skip(1); err != nil {
return err
}
flag, err := dec.Uint8()
if err != nil {
return err
}
ptsFlag := flag >> 7
dtsFlag := (flag & 0b0100_000) >> 6
var pts ,dts uint32
payloadlen -= 2
pesHeaderDataLen, err := dec.Uint8()
if err != nil {
return err
}
payloadlen -= uint16(pesHeaderDataLen) + 1
if extraData, err := dec.Bytes(int(pesHeaderDataLen)); err != nil {
return err
} else {
if ptsFlag == 1 {
pts = uint32(extraData[0]&0b0000_1110) << 29
pts += uint32(extraData[1]) << 22
pts += uint32(extraData[2]&0b1111_1110) << 14
pts += uint32(extraData[3]) << 7
pts += uint32(extraData[4]) >> 1
if dtsFlag == 1 {
dts = uint32(extraData[5]&0b0000_1110) << 29
dts += uint32(extraData[6]) << 22
dts += uint32(extraData[7]&0b1111_1110) << 14
dts += uint32(extraData[8]) << 7
dts += uint32(extraData[9]) >> 1
}
}
}
if payload, err := dec.Bytes(int(payloadlen)); err != nil {
return err
} else {
if StartCodeVideo == t {
dec.PTS = pts
dec.DTS = dts
dec.VideoPayload = append(dec.VideoPayload, payload...)
} else {
dec.AudioPayload = append(dec.AudioPayload, payload...)
}
}
return nil
}

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