mirror of
https://github.com/Monibuca/plugin-gb28181.git
synced 2025-12-24 13:27:57 +08:00
Compare commits
54 Commits
v1.0.0-alp
...
v1.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66c1182a4d | ||
|
|
07498fbe58 | ||
|
|
ed3cea25ef | ||
|
|
49b465be1b | ||
|
|
8faeab6728 | ||
|
|
5ccebf2479 | ||
|
|
67c37b56a8 | ||
|
|
78b163384f | ||
|
|
a1534f72f8 | ||
|
|
beed7cba2a | ||
|
|
5799281628 | ||
|
|
3ae1805543 | ||
|
|
c5d328da16 | ||
|
|
9ceeb2d511 | ||
|
|
f3ffbb7f3d | ||
|
|
af8829baa2 | ||
|
|
5b8f63a13b | ||
|
|
822f75d36b | ||
|
|
05b8d75155 | ||
|
|
9669085328 | ||
|
|
22a56b02fb | ||
|
|
0f58d9dde6 | ||
|
|
7f9fb67230 | ||
|
|
d25bb3854a | ||
|
|
261bc00de0 | ||
|
|
7cfd4fccbd | ||
|
|
211c8bd32c | ||
|
|
f3b0595863 | ||
|
|
de07b41647 | ||
|
|
38220d62e3 | ||
|
|
818fd6bd33 | ||
|
|
8154d852f4 | ||
|
|
c4a54d7eae | ||
|
|
3ffb58606a | ||
|
|
c284e4e28e | ||
|
|
7269ec50de | ||
|
|
e33079e36b | ||
|
|
d1de189dcf | ||
|
|
e45b266de9 | ||
|
|
8663a9ecef | ||
|
|
f36ddce527 | ||
|
|
70f2c64a5c | ||
|
|
2d61fc6308 | ||
|
|
dd23d81a40 | ||
|
|
a758a770f1 | ||
|
|
1b9711ea2f | ||
|
|
f6eec6d6b7 | ||
|
|
801ccb98ca | ||
|
|
c8323822e8 | ||
|
|
7dfb19d9c3 | ||
|
|
934926e596 | ||
|
|
f06b9146be | ||
|
|
b4b04ec0f9 | ||
|
|
e507e46ced |
85
main.go
85
main.go
@@ -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 {
|
||||
|
||||
27
sip/head.go
27
sip/head.go
@@ -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]] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
//TODO:CANCEL、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(),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
//TODO:TU层处理:根据需要,创建,或者匹配 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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! */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package transaction
|
||||
|
||||
import (
|
||||
"github.com/Monibuca/plugin-gb28181/sip"
|
||||
"fmt"
|
||||
"github.com/Monibuca/plugin-gb28181/sip"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ type Server struct {
|
||||
//提供config参数
|
||||
func NewServer(config *transaction.Config) *Server {
|
||||
return &Server{
|
||||
Core: transaction.NewCore(config),
|
||||
Core: transaction.NewCore(config),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1630
ui/dist/plugin-gb28181.common.js
vendored
1630
ui/dist/plugin-gb28181.common.js
vendored
File diff suppressed because one or more lines are too long
2
ui/dist/plugin-gb28181.common.js.map
vendored
2
ui/dist/plugin-gb28181.common.js.map
vendored
File diff suppressed because one or more lines are too long
2
ui/dist/plugin-gb28181.css
vendored
2
ui/dist/plugin-gb28181.css
vendored
@@ -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}
|
||||
1630
ui/dist/plugin-gb28181.umd.js
vendored
1630
ui/dist/plugin-gb28181.umd.js
vendored
File diff suppressed because one or more lines are too long
2
ui/dist/plugin-gb28181.umd.js.map
vendored
2
ui/dist/plugin-gb28181.umd.js.map
vendored
File diff suppressed because one or more lines are too long
6
ui/dist/plugin-gb28181.umd.min.js
vendored
6
ui/dist/plugin-gb28181.umd.min.js
vendored
File diff suppressed because one or more lines are too long
2
ui/dist/plugin-gb28181.umd.min.js.map
vendored
2
ui/dist/plugin-gb28181.umd.min.js.map
vendored
File diff suppressed because one or more lines are too long
413
ui/src/App.vue
413
ui/src/App.vue
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
111
ui/src/components/Player2.vue
Normal file
111
ui/src/components/Player2.vue
Normal 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>
|
||||
162
ui/src/components/Records.vue
Normal file
162
ui/src/components/Records.vue
Normal 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
124
ui/src/utils/index.js
Normal 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
6793
ui/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user