diff --git a/README.md b/README.md index d71a67e..d7bc4d4 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,6 @@ services: ## 功能特性 - [x] 开箱即用,支持 web -- [ ] 支持移动端 app - [x] 支持 rtmp 流分发 - [x] 支持 rtsp 流分发 - [x] 支持输出 HTTP_FLV,Websocket_FLV,HLS,WebRTC,RTSP、RTMP 等多种协议流地址 @@ -207,14 +206,15 @@ services: - [x] 设备注册,支持 7 种接入方式 - [x] 支持 UDP 和 TCP 两种国标信令传输模式 - [x] 设备校时 - - [x] 设备目录查询 - - [x] 设备信息同步 + - [x] 支持信息查询 + - [x] 设备目录查询 + - [x] 设备信息查询 + - [x] 设备基础配置查询(例如设备侧填写超时 3 秒,次数 3 次,则 9+x 秒左右收不到心跳认为离线,x 是检测间隔周期) - [x] 设备实时直播 - [x] 支持 UDP 和 TCP 被动两种国标流传输模式 - [x] 按需拉流,节省流量 - [x] 视频支持播放 H264 和 H265 - [x] 音频支持 g711a/g711u/aac - - [x] 与设备侧超时同步,例如设备侧填写超时 3 秒,次数 3 次,则 9+x 秒左右收不到心跳认为离线,x 是检测间隔周期 - [ ] 设备云台控制 - [ ] 录像回放 - [ ] 报警事件订阅 diff --git a/configs/config.toml b/configs/config.toml index 5714994..edf7174 100644 --- a/configs/config.toml +++ b/configs/config.toml @@ -55,10 +55,10 @@ # 媒体服务器 HTTP 端口 HTTPPort = 8080 # 媒体服务器密钥 - Secret = '' + Secret = 'jvRqCAzEg7AszBi4gm1cfhwXpmnVmJMG' # 用于流媒体 webhook 回调 - WebHookIP = '192.168.1.10' + WebHookIP = '192.168.10.31' # 媒体服务器 RTP 端口范围 - RTPPortRange = '20000-20500' + RTPPortRange = '20000-20300' # 媒体服务器 SDP IP - SDPIP = '192.168.1.10' \ No newline at end of file + SDPIP = '192.168.10.31' \ No newline at end of file diff --git a/internal/conf/default.go b/internal/conf/default.go index c3bef7e..e444e39 100644 --- a/internal/conf/default.go +++ b/internal/conf/default.go @@ -41,7 +41,7 @@ func DefaultConfig() Bootstrap { Secret: "", WebHookIP: "127.0.0.1", SDPIP: "127.0.0.1", - RTPPortRange: "20000-20500", + RTPPortRange: "20000-20300", }, Log: Log{ Dir: "./logs", diff --git a/internal/web/api/gb28181.go b/internal/web/api/gb28181.go index 70040f9..a349491 100755 --- a/internal/web/api/gb28181.go +++ b/internal/web/api/gb28181.go @@ -3,6 +3,8 @@ package api import ( "fmt" + "io" + "os" "strings" "github.com/gin-gonic/gin" @@ -13,6 +15,7 @@ import ( "github.com/gowvp/gb28181/internal/core/uniqueid" "github.com/gowvp/gb28181/pkg/gbs" "github.com/gowvp/gb28181/pkg/zlm" + "github.com/ixugo/goweb/pkg/orm" "github.com/ixugo/goweb/pkg/web" ) @@ -30,6 +33,15 @@ func NewGB28181Core(store gb28181.Storer, uni uniqueid.Core) gb28181.Core { } func registerGB28181(g gin.IRouter, api GB28181API, handler ...gin.HandlerFunc) { + g.Any("/gb28181/snapshot", func(c *gin.Context) { + fmt.Println(">>>>>>>>>>>>>>>") + b, err := io.ReadAll(c.Request.Body) + if err != nil { + panic(err) + } + os.WriteFile(orm.GenerateRandomString(10)+".jpg", b, 0o644) + c.JSON(200, gin.H{"msg": "ok"}) + }) { group := g.Group("/devices", handler...) group.GET("", web.WarpH(api.findDevice)) @@ -46,6 +58,9 @@ func registerGB28181(g gin.IRouter, api GB28181API, handler ...gin.HandlerFunc) group.GET("", web.WarpH(api.findChannel)) group.PUT("/:id", web.WarpH(api.editChannel)) group.POST("/:id/play", web.WarpH(api.play)) + + group.POST("/:id/snapshot", web.WarpH(api.querySnapshot)) // 图像抓拍 + group.GET("/:id/snapshot", web.WarpH(api.getSnapshot)) // 获取图像 // group.GET("/:id", web.WarpH(api.getChannel)) // group.POST("", web.WarpH(api.addChannel)) // group.DELETE("/:id", web.WarpH(api.delChannel)) @@ -236,3 +251,24 @@ func (a GB28181API) play(c *gin.Context, _ *struct{}) (*playOutput, error) { func (uc *Usecase) play(channelID string) { } + +func (a GB28181API) querySnapshot(c *gin.Context, _ *struct{}) (any, error) { + channelID := c.Param("id") + ch, err := a.gb28181Core.GetChannel(c.Request.Context(), channelID) + if err != nil { + return nil, err + } + + if err := a.uc.SipServer.QuerySnapshot(ch.DeviceID, ch.ChannelID); err != nil { + return nil, web.ErrDevice.Msg(err.Error()) + } + return gin.H{"msg": "ok"}, nil +} + +func (a GB28181API) getSnapshot(c *gin.Context, _ *struct{}) (any, error) { + // did := c.Param("id") + // if err := a.uc.SipServer.GetSnapshot(did); err != nil { + // return nil, web.ErrDevice.Msg(err.Error()) + // } + return gin.H{"msg": "ok"}, nil +} diff --git a/pkg/gbs/config.go b/pkg/gbs/config.go index f5586aa..c8c7ff3 100644 --- a/pkg/gbs/config.go +++ b/pkg/gbs/config.go @@ -3,6 +3,7 @@ package gbs import ( "encoding/hex" "encoding/xml" + "fmt" "log/slog" "math" @@ -33,16 +34,15 @@ const ( // AlarmReport = "AlarmReport" // // OSDConfig 前端OSD设置 // OSDConfig = "OSDConfig" - // // SnapShotConfig 图像抓拍配置 - // SnapShotConfig = "SnapShotConfig" ) type ConfigDownloadRequest struct { - XMLName xml.Name `xml:"Query"` - CmdType string `xml:"CmdType"` // 命令类型:设备配置查询(必选) - SN int32 `xml:"SN"` // 命令序列号(必选) - DeviceID string `xml:"DeviceID"` // 目标设备编码(必选) - ConfigType string `xml:"ConfigType"` // 查询配置参数类型(必选) + XMLName xml.Name `xml:"Query"` + CmdType string `xml:"CmdType"` // 命令类型:设备配置查询(必选) + SN int32 `xml:"SN"` // 命令序列号(必选) + DeviceID string `xml:"DeviceID"` // 目标设备编码(必选) + ConfigType string `xml:"ConfigType"` // 查询配置参数类型(必选) + SnapShotConfig *SnapShot `xml:"SnapShotConfig"` } type ConfigDownloadResponse struct { @@ -62,7 +62,14 @@ type ConfigDownloadResponse struct { // FrameMirror *FrameMirror `xml:"FrameMirror"` // AlarmReport *AlarmReport `xml:"AlarmReport"` // OSDConfig *OSDConfig `xml:"OSDConfig"` - // SnapShot *SnapShot `xml:"SnapShot"` + SnapShot *SnapShot `xml:"SnapShot"` +} + +type SnapShot struct { + SnapNum int `xml:"SnapNum"` // 连拍张数(必选),最多10张,当手动抓拍时,取值为1 + Interval int `xml:"Interval"` // 单张抓拍间隔时间,单位:秒(必选),取值范围:最短1秒 + UploadURL string `xml:"UploadURL"` // 抓拍图像上传路径(必选) + SessionID string `xml:"SessionID"` // 会话ID,由平台生成,用于关联抓拍的图像与平台请求(必选) } // BasicParam 设备基本参数配置 @@ -73,25 +80,27 @@ type BasicParam struct { HeartBeatCount int `xml:"HeartBeatCount"` // 心跳超时次数 } -func NewConfigDownloadRequest(sn int32, deviceID string, configType string) []byte { +const CMDTypeConfigDownload = "ConfigDownload" + +func NewBasicParamRequest(sn int32, deviceID string) []byte { c := ConfigDownloadRequest{ - CmdType: "ConfigDownload", + CmdType: CMDTypeConfigDownload, SN: sn, DeviceID: deviceID, - ConfigType: configType, + ConfigType: basicParam, } xmlData, _ := sip.XMLEncode(c) return xmlData } -func (g *GB28181API) QueryConfigDownloadBasic(deviceID, configType string) error { +func (g *GB28181API) QueryConfigDownloadBasic(deviceID string) error { slog.Debug("QueryConfigDownloadBasic", "deviceID", deviceID) ipc, ok := g.svr.memoryStorer.Load(deviceID) if !ok { return ErrDeviceOffline } - tx, err := g.svr.wrapRequest(ipc, sip.MethodMessage, &sip.ContentTypeXML, NewConfigDownloadRequest(1, deviceID, configType)) + tx, err := g.svr.wrapRequest(ipc, sip.MethodMessage, &sip.ContentTypeXML, NewBasicParamRequest(1, deviceID)) if err != nil { return err } @@ -99,6 +108,25 @@ func (g *GB28181API) QueryConfigDownloadBasic(deviceID, configType string) error return err } +func (g *GB28181API) handleDeviceConfig(ctx *sip.Context) { + slog.Debug("handleDeviceConfig", "deviceID", ctx.DeviceID) + + b := ctx.Request.Body() + fmt.Println(">>>", string(b)) + // var msg DeviceConfigResponse + // if err := sip.XMLDecode(ctx.Request.Body(), &msg); err != nil { + // ctx.Log.Error("handleDeviceConfig", "err", err, "body", hex.EncodeToString(ctx.Request.Body())) + // ctx.String(400, ErrXMLDecode.Error()) + // return + // } + + // if msg.SnapShotConfig != nil { + // slog.Debug("handleDeviceConfig", "snapShotConfig", msg.SnapShotConfig) + // } + + ctx.String(200, "OK") +} + func (g *GB28181API) sipMessageConfigDownload(ctx *sip.Context) { slog.Debug("sipMessageConfigDownload", "deviceID", ctx.DeviceID) diff --git a/pkg/gbs/img.go b/pkg/gbs/img.go new file mode 100644 index 0000000..0be341f --- /dev/null +++ b/pkg/gbs/img.go @@ -0,0 +1,29 @@ +package gbs + +import ( + "log/slog" + + "github.com/gowvp/gb28181/pkg/gbs/sip" +) + +func (g *GB28181API) QuerySnapshot(deviceID, channelID string) error { + slog.Debug("QuerySnapshot", "deviceID", deviceID) + ipc, ok := g.svr.memoryStorer.Load(deviceID) + if !ok { + return ErrDeviceOffline + } + + body := NewDeviceConfig(channelID).SetSnapShotConfig(&SnapShot{ + SnapNum: 1, + Interval: 1, + UploadURL: "http://192.168.10.31:15123/gb28181/snapshot", + SessionID: "1234567890", + }).Marshal() + + tx, err := g.svr.wrapRequest(ipc, sip.MethodMessage, &sip.ContentTypeXML, body) + if err != nil { + return err + } + _, err = sipResponse(tx) + return err +} diff --git a/pkg/gbs/model.go b/pkg/gbs/model.go new file mode 100644 index 0000000..ebb90fa --- /dev/null +++ b/pkg/gbs/model.go @@ -0,0 +1,37 @@ +package gbs + +import "encoding/xml" + +const snapShotConfig = "SnapShotConfig" // 图像抓拍配置 + +// 设备配置 A.2.3.2.1 +type DeviceConfigRequest struct { + XMLName xml.Name `xml:"Control"` + CmdType string `xml:"CmdType"` // 命令类型:设备配置查询(必选) + SN int32 `xml:"SN"` // 命令序列号(必选) + DeviceID string `xml:"DeviceID"` // 目标设备编码(必选) + SnapShotConfig *SnapShot `xml:"SnapShotConfig"` +} + +func NewDeviceConfig(deviceID string) *DeviceConfigRequest { + return &DeviceConfigRequest{ + CmdType: "DeviceConfig", + SN: 1, + DeviceID: deviceID, + } +} + +func (d *DeviceConfigRequest) SetSN(sn int32) *DeviceConfigRequest { + d.SN = sn + return d +} + +func (d *DeviceConfigRequest) SetSnapShotConfig(snapShot *SnapShot) *DeviceConfigRequest { + d.SnapShotConfig = snapShot + return d +} + +func (d *DeviceConfigRequest) Marshal() []byte { + b, _ := xml.Marshal(d) + return b +} diff --git a/pkg/gbs/register.go b/pkg/gbs/register.go index fe524c3..2c87220 100644 --- a/pkg/gbs/register.go +++ b/pkg/gbs/register.go @@ -151,7 +151,7 @@ func (g *GB28181API) handlerRegister(ctx *sip.Context) { g.QueryDeviceInfo(ctx) _ = g.QueryCatalog(dev.DeviceID) - _ = g.QueryConfigDownloadBasic(dev.DeviceID, basicParam) + _ = g.QueryConfigDownloadBasic(dev.DeviceID) } func (g GB28181API) login(ctx *sip.Context, expire string) { diff --git a/pkg/gbs/server.go b/pkg/gbs/server.go index 72916d3..2942f58 100644 --- a/pkg/gbs/server.go +++ b/pkg/gbs/server.go @@ -59,6 +59,7 @@ func NewServer(cfg *conf.Bootstrap, store gb28181.GB28181, sc sms.Core) (*Server msg.Handle("Catalog", api.sipMessageCatalog) msg.Handle("DeviceInfo", api.sipMessageDeviceInfo) msg.Handle("ConfigDownload", api.sipMessageConfigDownload) + msg.Handle("DeviceConfig", api.handleDeviceConfig) // msg.Handle("RecordInfo", api.handlerMessage) @@ -214,3 +215,8 @@ func (s *Server) Play(in *PlayInput) error { func (s *Server) StopPlay(in *StopPlayInput) error { return s.gb.StopPlay(in) } + +// QuerySnapshot 厂商实现抓图的少,sip 层已实现,先搁置 +func (s *Server) QuerySnapshot(deviceID, channelID string) error { + return s.gb.QuerySnapshot(deviceID, channelID) +}