Compare commits

...

54 Commits

Author SHA1 Message Date
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
langhuihui
e507e46ced 增加36秒超时机制 2020-11-11 22:06:50 +08:00
28 changed files with 9951 additions and 1242 deletions

85
main.go
View File

@@ -1,28 +1,32 @@
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"
"log"
"math/rand"
"net"
"net/http"
"strconv"
"strings"
"sync"
"time"
. "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}
func init() {
InstallPlugin(&PluginConfig{
@@ -54,12 +58,27 @@ 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 := s.Devices.Load(id); ok {
w.WriteHeader(v.(*transaction.Device).QueryRecord(channel, startTime, endTime))
} else {
w.WriteHeader(404)
}
})
http.HandleFunc("/gb28181/list", func(w http.ResponseWriter, r *http.Request) {
sse := util.NewSSE(w, r.Context())
for {
@@ -79,13 +98,13 @@ 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))
w.WriteHeader(v.(*transaction.Device).Control(channel, ptzcmd))
} else {
w.WriteHeader(404)
}
@@ -93,7 +112,7 @@ func run() {
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"))
channel, err := strconv.Atoi(r.URL.Query().Get("channel"))
if err != nil {
w.WriteHeader(404)
}
@@ -106,7 +125,7 @@ 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)
}
@@ -118,26 +137,39 @@ func run() {
})
s.Start()
}
func onPublish(channel *transaction.Channel) (port int) {
rtpPublisher := new(rtp.RTP_PS)
if !rtpPublisher.Publish("gb28181/" + channel.DeviceID) {
return
}
defer func() {
if port == 0 {
rtpPublisher.Close()
}
}()
rtpPublisher.Type = "GB28181"
addr, err := net.ResolveUDPAddr("udp", ":0")
if err != nil {
return
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,8 +181,9 @@ func onPublish(channel *transaction.Channel) (port int) {
bufUDP := make([]byte, 1048576)
Printf("udp server start listen video port[%d]", port)
defer Printf("udp server stop listen video port[%d]", port)
defer conn.Close()
for rtpPublisher.Err() == nil {
if err = conn.SetReadDeadline(time.Now().Add(time.Second*30));err!=nil{
if err = conn.SetReadDeadline(time.Now().Add(time.Second * 30)); err != nil {
return
}
if n, _, err := conn.ReadFromUDP(bufUDP); err == nil {

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

@@ -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

@@ -23,7 +23,7 @@ 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配置信息
@@ -223,6 +223,7 @@ func (c *Core) Handler() {
}()
ch := c.tp.ReadPacketChan()
timer := time.Tick(time.Second * 5)
//阻塞读取消息
for {
//fmt.Println("PacketHandler ========== SIP Client")
@@ -235,6 +236,8 @@ func (c *Core) Handler() {
fmt.Println("handler sip response message failed:", err.Error())
continue
}
case <-timer:
c.RemoveDead()
}
}
}
@@ -254,7 +257,9 @@ func (c *Core) SendMessage(msg *sip.Message) *Response {
e := c.NewOutGoingMessageEvent(msg)
//匹配事物
c.mutex.RLock()
ta, ok := c.transactions[e.tid]
c.mutex.RUnlock()
if !ok {
//新的请求
ta = c.initTransaction(c.ctx, e)
@@ -279,9 +284,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,
}
}
}
//接收到的消息处理
@@ -318,8 +332,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 {
@@ -339,7 +354,9 @@ func (c *Core) HandleReceiveMessage(p *transport.Packet) (err error) {
temp := &struct {
XMLName xml.Name
CmdType string
DeviceList []Channel `xml:"DeviceList>Item"`
DeviceID string
DeviceList []*Channel `xml:"DeviceList>Item"`
RecordList []*Record `xml:"RecordList>Item"`
}{}
decoder := xml.NewDecoder(bytes.NewReader([]byte(msg.Body)))
decoder.CharsetReader = func(c string, i io.Reader) (io.Reader, error) {
@@ -353,6 +370,8 @@ func (c *Core) HandleReceiveMessage(p *transport.Packet) (err error) {
switch temp.CmdType {
case "Catalog":
d.UpdateChannels(temp.DeviceList)
case "RecordInfo":
d.UpdateRecord(temp.DeviceID, temp.RecordList)
}
}
if ta == nil {
@@ -383,13 +402,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
@@ -451,8 +464,7 @@ func (c *Core) AddDevice(msg *sip.Message) *Device {
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,
Addr: msg.Via.GetSendBy(),
}
c.Devices.Store(msg.From.Uri.UserInfo(), v)
return v

View File

@@ -2,12 +2,14 @@ package transaction
import (
"fmt"
"github.com/Monibuca/plugin-gb28181/sip"
"github.com/Monibuca/plugin-gb28181/utils"
"strings"
"time"
"github.com/Monibuca/plugin-gb28181/sip"
"github.com/Monibuca/plugin-gb28181/utils"
)
// Channel 通道
type Channel struct {
DeviceID string
Name string
@@ -24,29 +26,52 @@ type Channel struct {
device *Device
inviteRes *sip.Message
Connected bool
Records []*Record
}
// Record 录像
type Record struct {
DeviceID string
Name string
FilePath string
Address string
StartTime string
EndTime string
Secrecy int
Type string
}
type Device struct {
ID string
RegisterTime time.Time
UpdateTime time.Time
Status string
Channels []Channel
Channels []*Channel
core *Core
sn int
from *sip.Contact
to *sip.Contact
host string
port string
Addr string
SipIP string //暴露的IP
}
func (d *Device) UpdateChannels(list []Channel) {
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
c.Connected = o.inviteRes != nil
c.Records = o.Records
d.Channels[i] = c
have = true
break
@@ -57,6 +82,14 @@ func (d *Device) UpdateChannels(list []Channel) {
}
}
}
func (d *Device) UpdateRecord(channelId string, list []*Record) {
for _, c := range d.Channels {
if c.DeviceID == channelId {
c.Records = list
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())
@@ -91,7 +124,7 @@ func (d *Device) CreateMessage(Method sip.Method) (requestMsg *sip.Message) {
ID: 1,
Method: Method,
}, CallID: utils.RandNumString(10),
Addr: d.host + ":" + d.port,
Addr: d.Addr,
}
requestMsg.From.Params["tag"] = utils.RandNumString(9)
return
@@ -105,11 +138,32 @@ func (d *Device) Query() int {
<SN>%d</SN>
<DeviceID>%s</DeviceID>
</Query>`, d.sn, requestMsg.To.Uri.UserInfo())
requestMsg.ContentLength = len(requestMsg.Body)
response := d.core.SendMessage(requestMsg)
if response.Data != nil {
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.core.SendMessage(requestMsg).Code
}
func (d *Device) Control(channelIndex int, PTZCmd string) int {
channel := &d.Channels[channelIndex]
channel := d.Channels[channelIndex]
requestMsg := channel.CreateMessage(sip.MESSAGE)
requestMsg.ContentType = "Application/MANSCDP+xml"
requestMsg.Body = fmt.Sprintf(`<?xml version="1.0"?>
@@ -123,11 +177,16 @@ func (d *Device) Control(channelIndex int, PTZCmd string) int {
return d.core.SendMessage(requestMsg).Code
}
func (d *Device) Invite(channelIndex int) int {
channel := &d.Channels[channelIndex]
channel := d.Channels[channelIndex]
port := d.core.OnInvite(channel)
if port == 0 {
channel.Connected = true
return 304
}
ip := d.core.config.MediaIP
if d.SipIP != "" {
ip = d.SipIP
}
sdp := fmt.Sprintf(`v=0
o=%s 0 0 IN IP4 %s
s=Play
@@ -139,7 +198,7 @@ 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)
`, d.core.config.Serial, ip, ip, port)
sdp = strings.ReplaceAll(sdp, "\n", "\r\n")
invite := channel.CreateMessage(sip.INVITE)
invite.ContentType = "application/sdp"
@@ -158,8 +217,8 @@ y=0200000001
}
return response.Code
}
func (d *Device) Bye(channelIndex int) int{
channel := &d.Channels[channelIndex]
func (d *Device) Bye(channelIndex int) int {
channel := d.Channels[channelIndex]
defer func() {
channel.inviteRes = nil
channel.Connected = false
@@ -173,7 +232,7 @@ func (c *Channel) Ack() {
ack.CallID = c.inviteRes.CallID
go c.device.core.Send(ack)
}
func (c *Channel) Bye() *Response{
func (c *Channel) Bye() *Response {
bye := c.CreateMessage(sip.BYE)
bye.From = c.inviteRes.From
bye.To = c.inviteRes.To

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! */

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-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}
.arrow1[data-v-a6774e8e]{grid-column:2;grid-row:1}.arrow2[data-v-a6774e8e]{transform:rotate(90deg);grid-column:3;grid-row:2}.arrow3[data-v-a6774e8e]{transform:rotate(180deg);grid-column:2;grid-row:3}.arrow4[data-v-a6774e8e]{transform:rotate(270deg);grid-column:1;grid-row:2}.arrow5[data-v-a6774e8e]{transform:rotate(-45deg);grid-column:1;grid-row:1}.arrow6[data-v-a6774e8e]{transform:rotate(45deg);grid-column:3;grid-row:1}.arrow7[data-v-a6774e8e]{transform:rotate(-135deg);grid-column:1;grid-row:3}.arrow8[data-v-a6774e8e]{transform:rotate(135deg);grid-column:3;grid-row:3}.arrow9[data-v-a6774e8e]{grid-column:2;grid-row:2}.container[data-v-a6774e8e]{position:relative;height:350px}.control[data-v-a6774e8e]{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-a6774e8e]{top:210px}.control3[data-v-a6774e8e]{top:260px}.control4[data-v-a6774e8e]{top:310px}.control5[data-v-a6774e8e]{top:360px}.control>[data-v-a6774e8e]{cursor:pointer;fill:grey;width:50px;height:50px}.control5>[data-v-a6774e8e]{margin-right:10px}.control2>[data-v-a6774e8e],.control3>[data-v-a6774e8e],.control4>[data-v-a6774e8e]{width:40px;height:40px}.control>[data-v-a6774e8e]:hover,.cycling[data-v-a6774e8e]{fill:#0ff}.player-wrap[data-v-2191453b]{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-2191453b]{width:100%;height:100%}.container[data-v-c7c34c76]{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-c7c34c76]{padding:10px 0}.flex-box[data-v-51504d30]{display:flex;flex-flow:row wrap;align-content:flex-start}.flex-item[data-v-51504d30]{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

View File

@@ -1,131 +1,294 @@
<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>
<div>
<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"
>
<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>
<mu-button flat @click="getRecords(prop.row.ID, $index,item)">录像</mu-button>
</td>
</template>
</mu-data-table>
</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>
</td>
<td>
<StartTime :value="item.UpdateTime"></StartTime>
</td>
<td>{{ item.Status }}</td>
</template>
</mu-data-table>
</div>
<div class="tabpanel" v-if="$parent.titleTabActive === 1">
<div class="flex-box">
<template v-for="(channel,index) in channelShowList">
<div class="flex-item" :key="index" v-if="channel.DeviceID">
<webrtc-player2 :stream-path="'gb28181/'+channel.DeviceID"></webrtc-player2>
</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>
<records ref="records" v-model="recordModal" :records.sync="recordList" @close="initRecordSearch"></records>
</div>
</template>
<script>
import WebrtcPlayer from "./components/Player"
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
},
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})),
export default {
components: {
WebrtcPlayer,
WebrtcPlayer2,
Records
},
props: {
ListenAddr: String,
},
computed: {
PublicIP() {
return this.ListenAddr.split(":")[0];
},
},
data() {
return {
Devices: [],
previewStreamPath: false,
channelList: [],
channelShowList: [],
pageInfo: {
onePageSize: 9,
totalPage: 0,
currentPage: 0
},
recordList: [],
recordModal: false,
recordSearch: {
id: null,
channel: null,
deviceId: null
},
context: {
id: null,
channel: 0,
item: null,
},
columns: Object.freeze(
[
"设备号",
"地址",
"通道数",
"注册时间",
"更新时间",
"状态",
].map((title) => ({
title,
}))
),
columns2: 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 = [];
let recordList = [];
this.Devices.forEach((device) => {
const channels = device.Channels || [];
if (channels.length > 0) {
channelList = channelList.concat(channels);
}
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.recordList = channel.Records || [];
}
}
});
if (channelList.length > 0) {
this.channelList = channelList;
this.updatePageInfo(channelList.length);
}
};
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);
});
},
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) {
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;
});
},
getRecords(id, channel, item) {
this.recordSearch.id = id;
this.recordSearch.channel = channel;
this.recordSearch.deviceId = item.DeviceID;
this.recordModal = true;
this.$nextTick(() =>
this.$refs.records.getList(this.recordSearch)
);
},
initRecordSearch() {
this.recordModal = false;
this.recordSearch.id = null;
this.recordSearch.channel = null;
this.recordSearch.deviceId = null;
this.recordList = [];
}
},
};
},
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>
<style scoped>
.flex-box {
display: flex;
flex-flow: row wrap;
align-content: flex-start;
}
.flex-item {
flex: 0 0 33.3333%;
height: 275px;
box-sizing: border-box;
padding: 10px;
}
</style>

View File

@@ -12,7 +12,7 @@
<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>
<svg @mousedown="startPtzCmdCycle" @mouseup="stopPtzCmdCycle" class="arrow9" 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>
@@ -64,12 +64,11 @@ export default {
ptzSpeed:5,
ptzPositionIndex:1,
ptzType:PTZ_TYPE,
isCycling:false
};
},
props:{
PublicIP:String
},
props:{
PublicIP:String
},
methods: {
async play(streamPath) {
pc = new RTCPeerConnection();
@@ -115,23 +114,16 @@ props:{
},
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);
}
startPtzCmdCycle(){
this.ptzCmd(PTZ_TYPE.right,true);
},
ptzCmd(type){
this.$emit('ptz',{type:type,speed:this.ptzSpeed,index:this.ptzPositionIndex,cycle:this.isCycling})
stopPtzCmdCycle(){
this.ptzCmd(PTZ_TYPE.stop);
},
ptzCmd(type,isCycling){
this.$emit('ptz',{type:type,speed:this.ptzSpeed,index:this.ptzPositionIndex,cycle:isCycling})
},
onClosePreview() {
pc.close();

View File

@@ -0,0 +1,111 @@
<template>
<div class="player-wrap">
<template v-if="rtcStream">
<video :srcObject.prop="rtcStream" autoplay muted controls></video>
</template>
</div>
</template>
<script>
export default {
name: "WebrtcPlayer",
rtcPeerConnection: null,
data() {
return {
iceConnectionState: '',
rtcPeerConnectionInit: false,
rtcStream: null
}
},
props: {
streamPath: {
type: String,
default: ''
}
},
async created() {
await this.initRtcPeerConnection();
console.log('initRtcPeerConnectioned');
if (this.streamPath) {
await this.play(this.streamPath);
console.log('played');
}
},
methods: {
async initRtcPeerConnection() {
const rtcPeerConnection = new RTCPeerConnection();
rtcPeerConnection.addTransceiver('video', {
direction: "recvonly"
});
rtcPeerConnection.onsignalingstatechange = e => {
console.log('onsignalingstatechange', e);
};
rtcPeerConnection.oniceconnectionstatechange = e => {
console.log('oniceconnectionstatechange', rtcPeerConnection.iceConnectionState);
};
rtcPeerConnection.onicecandidate = event => {
console.log('onicecandidate', event);
};
rtcPeerConnection.ontrack = event => {
console.log('ontrack', event);
if (event.track.kind === "video") {
this.rtcStream = event.streams[0];
}
};
const rtcSessionDescriptionInit = await rtcPeerConnection.createOffer();
await rtcPeerConnection.setLocalDescription(rtcSessionDescriptionInit);
this.rtcPeerConnectionInit = true;
this.$options.rtcPeerConnection = rtcPeerConnection;
},
//
async play(streamPath) {
const rtcPeerConnection = this.$options.rtcPeerConnection;
const localDescriptionData = rtcPeerConnection.localDescription.toJSON();
const result = await this.ajax({
type: "POST",
processData: false,
data: JSON.stringify(localDescriptionData),
url: "/webrtc/play?streamPath=" + streamPath,
dataType: "json"
});
if (result.errmsg) {
console.error(result.errmsg);
return;
}
//
rtcPeerConnection.setRemoteDescription(new RTCSessionDescription({
type: result.type,
sdp: result.sdp
}));
},
close() {
const rtcPeerConnection = this.$options.rtcPeerConnection;
rtcPeerConnection && rtcPeerConnection.close();
}
},
destroyed() {
this.close();
}
}
</script>
<style scoped>
.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 {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,162 @@
<template>
<Modal v-bind="$attrs"
draggable
width="900"
v-on="$listeners"
title="Records"
@on-ok="onClosePreview"
@on-cancel="onClosePreview">
<div class="container">
<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 #expand="prop">
<div>
<m-button @click="play(prop.row)">播放</m-button>
</div>
</template>
<template #default="scope">
<td>{{scope.row.DeviceID}}</td>
<td>{{scope.row.Name}}</td>
<td>{{scope.row.time}}</td>
<td>{{scope.row.length}}</td>
<td>{{scope.row.FilePath}}</td>
<td>{{scope.row.Address}}</td>
<td>{{scope.row.Type}}</td>
</template>
</mu-data-table>
</div>
</div>
</Modal>
</template>
<script>
import {getOneTimeRange, formatTimeTips, parseTime, isDef} from "../utils";
const _now = new Date();
export default {
name: "Records",
props: {
records: Array
},
data() {
return {
timeOptions: {
disabledDate(date) {
return date && date.valueOf() > Date.now();
}
},
search: {
id: null,
channel: null,
deviceId: null,
time: _now
},
columns: Object.freeze(
[
'设备ID',
'名称',
'时间',
'时长',
'文件路径',
'地址',
'类型'
].map((title) => ({
title,
}))
)
}
},
computed: {
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);
const _startTime = parseTime(startTime);
record.length = timeLength;
record.time = _startTime;
return record;
});
return list;
}
},
methods: {
getList(options) {
this.search.id = options.id;
this.search.channel = options.channel;
this.search.deviceId = options.deviceId;
this._fetchList();
},
_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) => {
});
}
},
onClosePreview() {
this.search.channel = null;
this.search.deviceId = null;
this.search.id = null;
this.$emit('close');
},
handleTimeChange(date) {
this.search.time = new Date(date);
this._fetchList();
},
play() {
}
}
}
</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>

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

@@ -0,0 +1,124 @@
/**
* 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