diff --git a/api/c/commanders.go b/api/c/commanders.go new file mode 100644 index 0000000..7b1cf8b --- /dev/null +++ b/api/c/commanders.go @@ -0,0 +1,90 @@ +package api + +import ( + "strconv" + + "github.com/gin-gonic/gin" + "github.com/panjjo/gosip/m" + sipapi "github.com/panjjo/gosip/sip" +) + +// | 名称 | 形式 | 类型 | 必选 | 描述 | +// | ------- | -------- | ------- | --- | -------------------------------------------------------------------------------------------- | +// | id | path | string | 是 | 设备id | +// | command | formData | string | 是 | 控制指令 允许值: left, right, up, down, upleft, upright, downleft, downright, zoomin, zoomout, stop | +// | channel | formData | string | 否 | 通道id,默认会选择第一个通道 | +// | speed | formData | Integer | 否 | 速度(0~255) 默认值: 129 | + +func PTZCtrl(c *gin.Context) { + id := c.Param("id") + if id == "" { + m.JsonResponse(c, m.StatusParamsERR, "缺少设备ID") + return + } + device := sipapi.GetActiveDevice(id) + if device == nil { + m.JsonResponse(c, m.StatusDBERR, "该设备不在线") + return + } + command := c.Query("command") + if command == "" { + m.JsonResponse(c, m.StatusParamsERR, "缺少控制指令") + return + } + channelId := c.Query("channel") + if channelId != "" { + device.DeviceID = channelId + } + speedStr := c.Query("speed") + speed := 50 + if speedStr != "" { + speed1, err := strconv.Atoi(speedStr) + if err != nil { + m.JsonResponse(c, m.StatusParamsERR, "速度参数转换数据类型有误") + return + } + if speed1 < 0 && speed1 > 255 { + m.JsonResponse(c, m.StatusParamsERR, "速度参数不在合理范围内0~255") + return + } + speed = speed1 + } + + cmdCode := 0 + switch command { + case "left": + cmdCode = 2 + case "right": + cmdCode = 1 + case "up": + cmdCode = 8 + case "down": + cmdCode = 4 + case "upleft": + cmdCode = 10 + case "upright": + cmdCode = 9 + case "downleft": + cmdCode = 6 + case "downright": + cmdCode = 5 + case "zoomin": + cmdCode = 16 + case "zoomout": + cmdCode = 32 + case "stop": + cmdCode = 0 + speed = 0 + default: + m.JsonResponse(c, m.StatusParamsERR, "未知控制指令") + return + } + cmdStr := sipapi.CmdString(cmdCode, speed, speed, speed) + + err := sipapi.DevicePTZ(*device, cmdStr) + if err != nil { + m.JsonResponse(c, m.StatusParamsERR, err) + return + } + m.JsonResponse(c, m.StatusSucc, "") +} diff --git a/api/c/zlm.go b/api/c/zlm.go index 46c27e3..62f65b3 100644 --- a/api/c/zlm.go +++ b/api/c/zlm.go @@ -43,6 +43,10 @@ func ZLMWebHook(c *gin.Context) { "enableRtxp": m.MConfig.Stream.RTMP, "msg": "success", }) + case "on_server_keepalive": + c.JSON(http.StatusOK, map[string]any{ + "code": 0, + "msg": "success"}) case "on_stream_none_reader": // 无人阅读通知 关闭流 zlmStreamNoneReader(c) diff --git a/api/main.go b/api/main.go index ae91d8c..b7435a1 100644 --- a/api/main.go +++ b/api/main.go @@ -36,6 +36,10 @@ func Init(r *gin.Engine) { { r.GET("/channels/:id/records", api.RecordsList) } + // PTZ 控制 + { + r.POST("/devices/:id/ptz", api.PTZCtrl) + } // zlm webhook { r.POST("/zlm/webhook/:method", api.ZLMWebHook) diff --git a/config.yml b/config.yml index 68fb844..87599d7 100644 --- a/config.yml +++ b/config.yml @@ -1,6 +1,6 @@ mod: debug database: - dialect: mysql # mysql postgresql sqllite + dialect: mysql # mysql postgresql sqlite3 url: root:123456@tcp(localhost:3307)/gosip?charset=utf8&parseTime=True&loc=Local # 数据库地址 udp: 0.0.0.0:5060 # sip服务器udp端口 api: 0.0.0.0:8090 # sip服务 restfulapi 端口 diff --git a/db/gorm.go b/db/gorm.go index 72fb70f..03edbe2 100644 --- a/db/gorm.go +++ b/db/gorm.go @@ -5,6 +5,8 @@ import ( "strings" "time" + _ "github.com/jinzhu/gorm/dialects/sqlite" + _ "github.com/jinzhu/gorm/dialects/mysql" "github.com/panjjo/gorm" // _ "github.com/jinzhu/gorm/dialects/postgres" diff --git a/go.mod b/go.mod index 1c1068d..da00109 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,10 @@ require ( github.com/gin-gonic/gin v1.8.1 github.com/gofrs/uuid v4.3.0+incompatible github.com/jinzhu/gorm v1.9.16 + github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible github.com/panjjo/gorm v1.0.1 github.com/panjjo/gosdp v0.0.0-20201029020038-56e3a0ec56ef + github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 github.com/robfig/cron v1.2.0 github.com/sirupsen/logrus v1.9.0 github.com/spf13/viper v1.13.0 @@ -36,12 +38,15 @@ require ( github.com/goccy/go-json v0.9.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jonboulle/clockwork v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/leodido/go-urn v1.2.1 // indirect + github.com/lestrrat-go/strftime v1.0.6 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-sqlite3 v1.14.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect diff --git a/go.sum b/go.sum index cd257ac..2828007 100644 --- a/go.sum +++ b/go.sum @@ -205,6 +205,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= +github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -227,6 +229,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= +github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4= +github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA= +github.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205AhTIGQQ= +github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw= github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= @@ -285,6 +293,8 @@ github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo= +github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..70346ae --- /dev/null +++ b/logger.go @@ -0,0 +1,63 @@ +package main + +import ( + "bytes" + "fmt" + "path/filepath" + "time" + + rotatelogs "github.com/lestrrat-go/file-rotatelogs" + "github.com/rifflock/lfshook" + "github.com/sirupsen/logrus" +) + +type LoggerFormatter struct{} + +func (m *LoggerFormatter) Format(entry *logrus.Entry) ([]byte, error) { + var b *bytes.Buffer + if entry.Buffer != nil { + b = entry.Buffer + } else { + b = &bytes.Buffer{} + } + + timestamp := entry.Time.Format("2006-01-02 15:04:05") + var newLog string + + //HasCaller()为true才会有调用信息 + if entry.HasCaller() { + fName := filepath.Base(entry.Caller.File) + newLog = fmt.Sprintf("[%s] [%s] [%s:%d %s] %s\n", + timestamp, entry.Level, fName, entry.Caller.Line, entry.Caller.Function, entry.Message) + } else { + newLog = fmt.Sprintf("[%s] [%s] %s\n", timestamp, entry.Level, entry.Message) + } + + b.WriteString(newLog) + return b.Bytes(), nil +} + +func initLog() { + + filepaths := "./log/gosip.log" + writer, _ := rotatelogs.New( + filepaths+".%Y%m%d%H%M", + rotatelogs.WithLinkName(filepaths), + rotatelogs.WithMaxAge(time.Duration(604800)*time.Second), + rotatelogs.WithRotationTime(time.Duration(86400)*time.Second), + ) + writeMap := lfshook.WriterMap{ + logrus.InfoLevel: writer, + logrus.FatalLevel: writer, + logrus.DebugLevel: writer, + logrus.WarnLevel: writer, + logrus.ErrorLevel: writer, + logrus.PanicLevel: writer, + logrus.TraceLevel: writer, + } + + logrus.SetReportCaller(true) + lfHook := lfshook.NewHook(writeMap, &LoggerFormatter{}) + logrus.AddHook(lfHook) + +} diff --git a/m/config.go b/m/config.go index 831ccff..840a14d 100644 --- a/m/config.go +++ b/m/config.go @@ -76,10 +76,11 @@ func DefaultInfo() *SysInfo { var MConfig *Config -func LoadConfig() { - viper.SetConfigType("yml") - viper.SetConfigName("config") - viper.AddConfigPath("./") +func LoadConfig(fpath string) { + // viper.SetConfigType("yml") + // viper.SetConfigName("gosip") + // viper.AddConfigPath(filepath.Dir(fpath)) + viper.SetConfigFile(fpath) viper.SetDefault("logger", "debug") viper.SetDefault("udp", "0.0.0.0:5060") viper.SetDefault("api", "0.0.0.0:8090") diff --git a/main.go b/main.go index e671004..d767c1d 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "flag" "net/http" _ "net/http/pprof" @@ -36,6 +37,9 @@ import ( // @securityDefinitions.basic BasicAuth func main() { + var config_fpath = flag.String("c", "./conf.yml", "set a config file") + flag.Parse() + m.LoadConfig(*config_fpath) //pprof go func() { http.ListenAndServe("0.0.0.0:6060", nil) @@ -54,7 +58,7 @@ func main() { } func init() { - m.LoadConfig() + initLog() _cron() } diff --git a/sip/devices.go b/sip/devices.go index f73793f..beabcd7 100644 --- a/sip/devices.go +++ b/sip/devices.go @@ -4,6 +4,7 @@ import ( "encoding/xml" "fmt" "net" + "strconv" "time" "github.com/panjjo/gosip/db" @@ -222,6 +223,40 @@ func sipCatalog(to Devices) { } } +func GetActiveDevice(deviceID string) *Devices { + device, ok := _activeDevices.Get(deviceID) + if !ok { + return nil + } + return &device +} + +func DevicePTZ(to Devices, cmd string) error { + to.addr.URI.SetHost(to.Host) + iport, _ := strconv.Atoi(to.Port) + to.addr.URI.FPort = sip.NewPort(iport) + logrus.Infoln("to addr URI:", to.addr.URI.String()) + logrus.Infoln("from addr URI:", _serverDevices.addr.URI.String()) + + hb := sip.NewHeaderBuilder().SetTo(to.addr).SetFrom(_serverDevices.addr).AddVia(&sip.ViaHop{ + Params: sip.NewParams().Add("branch", sip.String{Str: sip.GenerateBranch()}), + }).SetContentType(&sip.ContentTypeXML).SetMethod(sip.MESSAGE) + + req := sip.NewRequest("", sip.MESSAGE, to.addr.URI, sip.DefaultSipVersion, hb.Build(), sip.GetDevicePTZXML(to.DeviceID, cmd)) + req.SetDestination(to.source) + tx, err := srv.Request(req) + if err != nil { + logrus.Warnln("sipDevicePTZ error,", err) + return err + } + _, err = sipResponse(tx) + if err != nil { + logrus.Warnln("sipDevicePTZ response error,", err) + return err + } + return nil +} + // MessageDeviceInfoResponse 主设备明细返回结构 type MessageDeviceInfoResponse struct { CmdType string `xml:"CmdType"` diff --git a/sip/handler.go b/sip/handler.go index f6503c3..acbeea3 100644 --- a/sip/handler.go +++ b/sip/handler.go @@ -116,6 +116,6 @@ func handlerRegister(req *sip.Request, tx *sip.Transaction) { } } resp := sip.NewResponseFromRequest("", req, http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized), nil) - resp.AppendHeader(&sip.GenericHeader{HeaderName: "WWW-Authenticate", Contents: fmt.Sprintf("Digest nonce=\"%s\", algorithm=MD5, realm=\"%s\",qop=\"auth\"", utils.RandString(32), _sysinfo.Region)}) + resp.AppendHeader(&sip.GenericHeader{HeaderName: "WWW-Authenticate", Contents: fmt.Sprintf("Digest nonce=\"%s\",algorithm=MD5,realm=\"%s\",qop=\"auth\"", utils.RandString(32), _sysinfo.Region)}) tx.Respond(resp) } diff --git a/sip/record.go b/sip/record.go index c06a740..5053ff6 100644 --- a/sip/record.go +++ b/sip/record.go @@ -20,7 +20,7 @@ func SipRecordList(to *Channels, start, end int64) (*Records, error) { defer close(resp) device, ok := _activeDevices.Get(to.DeviceID) if !ok { - return nil, errors.New("设备不在线") + return nil, errors.New(fmt.Sprintf("设备[%s]不在线", to.DeviceID)) } channelURI, _ := sip.ParseURI(to.URIStr) to.addr = &sip.Address{URI: channelURI} diff --git a/sip/s/models.go b/sip/s/models.go index 9aa6569..20226b5 100644 --- a/sip/s/models.go +++ b/sip/s/models.go @@ -106,6 +106,18 @@ var ( %d %s +` + + DevicePTZXML = ` + +DeviceControl +%d +%s +%s + +5 + + ` ) @@ -114,6 +126,11 @@ func GetDeviceInfoXML(id string) []byte { return []byte(fmt.Sprintf(DeviceInfoXML, utils.RandInt(100000, 999999), id)) } +// GetDevicePTZXML 获取设备ptz指令 +func GetDevicePTZXML(id string, cmd string) []byte { + return []byte(fmt.Sprintf(DevicePTZXML, utils.RandInt(100000, 999999), id, cmd)) +} + // GetCatalogXML 获取NVR下设备列表指令 func GetCatalogXML(id string) []byte { return []byte(fmt.Sprintf(CatalogXML, utils.RandInt(100000, 999999), id)) diff --git a/sip/utils.go b/sip/utils.go new file mode 100644 index 0000000..0593800 --- /dev/null +++ b/sip/utils.go @@ -0,0 +1,61 @@ +package sipapi + +import ( + "fmt" + "math/rand" + "time" +) + +/** + * 云台指令码计算 包括PTZ指令、FI指令、预置位指令、巡航指令、扫描指令和辅助开关指令 + * + * @param cmdCode 指令码 + * @param parameter1 数据1 + * @param parameter2 数据2 + * @param combineCode2 组合码2 + */ +func CmdString(cmdCode, parameter1, parameter2, combineCode2 int) string { + builder := "A50F01" + strTmp := fmt.Sprintf("%02X", cmdCode) + builder += strTmp[0:2] + strTmp = fmt.Sprintf("%02X", parameter1) + builder += strTmp[0:2] + strTmp = fmt.Sprintf("%02X", parameter2) + builder += strTmp[0:2] + strTmp = fmt.Sprintf("%X", combineCode2) + builder += strTmp[0:1] + builder += "0" + + //计算校验码 + checkCode := (0xA5 + 0x0F + 0x01 + cmdCode + parameter1 + parameter2 + (combineCode2 & 0xF0)) % 0x100 + strTmp = fmt.Sprintf("%02X", checkCode) + builder += strTmp[0:2] + return builder +} + +func RandNumString(n int) string { + numbers := "0123456789" + return randStringBySoure(numbers, n) +} + +func randStringBySoure(src string, n int) string { + randomness := make([]byte, n) + + rand.Seed(time.Now().UnixNano()) + _, err := rand.Read(randomness) + if err != nil { + panic(err) + } + + l := len(src) + + // fill output + output := make([]byte, n) + for pos := range output { + random := randomness[pos] + randomPos := random % uint8(l) + output[pos] = src[randomPos] + } + + return string(output) +}