gofmt
This commit is contained in:
@ -1,92 +1,92 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AuthInfo 存储解析后的认证信息
|
||||
type AuthInfo struct {
|
||||
Username string
|
||||
Realm string
|
||||
Nonce string
|
||||
URI string
|
||||
Response string
|
||||
Algorithm string
|
||||
Method string
|
||||
}
|
||||
|
||||
// GenerateNonce 生成随机 nonce 字符串
|
||||
func GenerateNonce() string {
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
return fmt.Sprintf("%x", b)
|
||||
}
|
||||
|
||||
// ParseAuthorization 解析 SIP Authorization 头
|
||||
// Authorization: Digest username="34020000001320000001",realm="3402000000",
|
||||
// nonce="44010b73623249f6916a6acf7c316b8e",uri="sip:34020000002000000001@3402000000",
|
||||
// response="e4ca3fdc5869fa1c544ea7af60014444",algorithm=MD5
|
||||
func ParseAuthorization(auth string) *AuthInfo {
|
||||
auth = strings.TrimPrefix(auth, "Digest ")
|
||||
parts := strings.Split(auth, ",")
|
||||
result := &AuthInfo{}
|
||||
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if !strings.Contains(part, "=") {
|
||||
continue
|
||||
}
|
||||
|
||||
kv := strings.SplitN(part, "=", 2)
|
||||
key := strings.TrimSpace(kv[0])
|
||||
value := strings.Trim(strings.TrimSpace(kv[1]), "\"")
|
||||
|
||||
switch key {
|
||||
case "username":
|
||||
result.Username = value
|
||||
case "realm":
|
||||
result.Realm = value
|
||||
case "nonce":
|
||||
result.Nonce = value
|
||||
case "uri":
|
||||
result.URI = value
|
||||
case "response":
|
||||
result.Response = value
|
||||
case "algorithm":
|
||||
result.Algorithm = value
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ValidateAuth 验证 SIP 认证信息
|
||||
func ValidateAuth(authInfo *AuthInfo, password string) bool {
|
||||
if authInfo == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 默认方法为 REGISTER
|
||||
method := "REGISTER"
|
||||
if authInfo.Method != "" {
|
||||
method = authInfo.Method
|
||||
}
|
||||
|
||||
// 计算 MD5 哈希
|
||||
ha1 := md5Hex(authInfo.Username + ":" + authInfo.Realm + ":" + password)
|
||||
ha2 := md5Hex(method + ":" + authInfo.URI)
|
||||
correctResponse := md5Hex(ha1 + ":" + authInfo.Nonce + ":" + ha2)
|
||||
|
||||
return authInfo.Response == correctResponse
|
||||
}
|
||||
|
||||
// md5Hex 计算字符串的 MD5 哈希值并返回十六进制字符串
|
||||
func md5Hex(s string) string {
|
||||
hash := md5.New()
|
||||
hash.Write([]byte(s))
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AuthInfo 存储解析后的认证信息
|
||||
type AuthInfo struct {
|
||||
Username string
|
||||
Realm string
|
||||
Nonce string
|
||||
URI string
|
||||
Response string
|
||||
Algorithm string
|
||||
Method string
|
||||
}
|
||||
|
||||
// GenerateNonce 生成随机 nonce 字符串
|
||||
func GenerateNonce() string {
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
return fmt.Sprintf("%x", b)
|
||||
}
|
||||
|
||||
// ParseAuthorization 解析 SIP Authorization 头
|
||||
// Authorization: Digest username="34020000001320000001",realm="3402000000",
|
||||
// nonce="44010b73623249f6916a6acf7c316b8e",uri="sip:34020000002000000001@3402000000",
|
||||
// response="e4ca3fdc5869fa1c544ea7af60014444",algorithm=MD5
|
||||
func ParseAuthorization(auth string) *AuthInfo {
|
||||
auth = strings.TrimPrefix(auth, "Digest ")
|
||||
parts := strings.Split(auth, ",")
|
||||
result := &AuthInfo{}
|
||||
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if !strings.Contains(part, "=") {
|
||||
continue
|
||||
}
|
||||
|
||||
kv := strings.SplitN(part, "=", 2)
|
||||
key := strings.TrimSpace(kv[0])
|
||||
value := strings.Trim(strings.TrimSpace(kv[1]), "\"")
|
||||
|
||||
switch key {
|
||||
case "username":
|
||||
result.Username = value
|
||||
case "realm":
|
||||
result.Realm = value
|
||||
case "nonce":
|
||||
result.Nonce = value
|
||||
case "uri":
|
||||
result.URI = value
|
||||
case "response":
|
||||
result.Response = value
|
||||
case "algorithm":
|
||||
result.Algorithm = value
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ValidateAuth 验证 SIP 认证信息
|
||||
func ValidateAuth(authInfo *AuthInfo, password string) bool {
|
||||
if authInfo == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 默认方法为 REGISTER
|
||||
method := "REGISTER"
|
||||
if authInfo.Method != "" {
|
||||
method = authInfo.Method
|
||||
}
|
||||
|
||||
// 计算 MD5 哈希
|
||||
ha1 := md5Hex(authInfo.Username + ":" + authInfo.Realm + ":" + password)
|
||||
ha2 := md5Hex(method + ":" + authInfo.URI)
|
||||
correctResponse := md5Hex(ha1 + ":" + authInfo.Nonce + ":" + ha2)
|
||||
|
||||
return authInfo.Response == correctResponse
|
||||
}
|
||||
|
||||
// md5Hex 计算字符串的 MD5 哈希值并返回十六进制字符串
|
||||
func md5Hex(s string) string {
|
||||
hash := md5.New()
|
||||
hash.Write([]byte(s))
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
@ -343,4 +343,3 @@ func TestParseAuthorizationQuotedValues(t *testing.T) {
|
||||
t.Logf("Realm value: '%s'", result.Realm)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/emiago/sipgo"
|
||||
"github.com/ossrs/srs-sip/pkg/config"
|
||||
)
|
||||
|
||||
type Cascade struct {
|
||||
ua *sipgo.UserAgent
|
||||
sipCli *sipgo.Client
|
||||
sipSvr *sipgo.Server
|
||||
|
||||
ctx context.Context
|
||||
conf *config.MainConfig
|
||||
}
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/emiago/sipgo"
|
||||
"github.com/ossrs/srs-sip/pkg/config"
|
||||
)
|
||||
|
||||
type Cascade struct {
|
||||
ua *sipgo.UserAgent
|
||||
sipCli *sipgo.Client
|
||||
sipSvr *sipgo.Server
|
||||
|
||||
ctx context.Context
|
||||
conf *config.MainConfig
|
||||
}
|
||||
|
||||
@ -1,171 +1,171 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/emiago/sipgo/sip"
|
||||
"github.com/ossrs/srs-sip/pkg/models"
|
||||
"github.com/ossrs/srs-sip/pkg/service/stack"
|
||||
"golang.org/x/net/html/charset"
|
||||
)
|
||||
|
||||
const GB28181_ID_LENGTH = 20
|
||||
|
||||
func (s *UAS) isSameIP(addr1, addr2 string) bool {
|
||||
ip1, _, err1 := net.SplitHostPort(addr1)
|
||||
ip2, _, err2 := net.SplitHostPort(addr2)
|
||||
|
||||
// 如果解析出错,回退到完整字符串比较
|
||||
if err1 != nil || err2 != nil {
|
||||
return addr1 == addr2
|
||||
}
|
||||
|
||||
return ip1 == ip2
|
||||
}
|
||||
|
||||
func (s *UAS) onRegister(req *sip.Request, tx sip.ServerTransaction) {
|
||||
id := req.From().Address.User
|
||||
if len(id) != GB28181_ID_LENGTH {
|
||||
slog.Error("invalid device ID")
|
||||
return
|
||||
}
|
||||
|
||||
slog.Debug(fmt.Sprintf("Received REGISTER %s", req.String()))
|
||||
|
||||
if s.conf.GB28181.Auth.Enable {
|
||||
// Check if Authorization header exists
|
||||
authHeader := req.GetHeaders("Authorization")
|
||||
|
||||
// If no Authorization header, send 401 response to request authentication
|
||||
if len(authHeader) == 0 {
|
||||
nonce := GenerateNonce()
|
||||
resp := stack.NewUnauthorizedResponse(req, http.StatusUnauthorized, "Unauthorized", nonce, s.conf.GB28181.Realm)
|
||||
_ = tx.Respond(resp)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate Authorization
|
||||
authInfo := ParseAuthorization(authHeader[0].Value())
|
||||
if !ValidateAuth(authInfo, s.conf.GB28181.Auth.Password) {
|
||||
slog.Error("auth failed", "device_id", id, "source", req.Source())
|
||||
s.respondRegister(req, http.StatusForbidden, "Auth Failed", tx)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
isUnregister := false
|
||||
if exps := req.GetHeaders("Expires"); len(exps) > 0 {
|
||||
exp := exps[0]
|
||||
expSec, err := strconv.ParseInt(exp.Value(), 10, 32)
|
||||
if err != nil {
|
||||
slog.Error("parse expires header error", "error", err.Error())
|
||||
return
|
||||
}
|
||||
if expSec == 0 {
|
||||
isUnregister = true
|
||||
}
|
||||
} else {
|
||||
slog.Error("empty expires header")
|
||||
return
|
||||
}
|
||||
|
||||
if isUnregister {
|
||||
DM.RemoveDevice(id)
|
||||
slog.Warn("Device unregistered", "device_id", id)
|
||||
return
|
||||
} else {
|
||||
if d, ok := DM.GetDevice(id); !ok {
|
||||
DM.AddDevice(id, &DeviceInfo{
|
||||
DeviceID: id,
|
||||
SourceAddr: req.Source(),
|
||||
NetworkType: req.Transport(),
|
||||
})
|
||||
s.respondRegister(req, http.StatusOK, "OK", tx)
|
||||
slog.Info(fmt.Sprintf("Register success %s %s", id, req.Source()))
|
||||
|
||||
go s.ConfigDownload(id)
|
||||
go s.Catalog(id)
|
||||
} else {
|
||||
if d.SourceAddr != "" && !s.isSameIP(d.SourceAddr, req.Source()) {
|
||||
slog.Error("Device already registered", "device_id", id, "old_source", d.SourceAddr, "new_source", req.Source())
|
||||
// TODO: 如果ID重复,应采用虚拟ID
|
||||
s.respondRegister(req, http.StatusBadRequest, "Conflict Device ID", tx)
|
||||
} else {
|
||||
d.SourceAddr = req.Source()
|
||||
d.NetworkType = req.Transport()
|
||||
DM.UpdateDevice(id, d)
|
||||
s.respondRegister(req, http.StatusOK, "OK", tx)
|
||||
|
||||
slog.Info(fmt.Sprintf("Re-register success %s %s", id, req.Source()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UAS) respondRegister(req *sip.Request, code sip.StatusCode, reason string, tx sip.ServerTransaction) {
|
||||
res := stack.NewRegisterResponse(req, code, reason)
|
||||
_ = tx.Respond(res)
|
||||
|
||||
}
|
||||
|
||||
func (s *UAS) onMessage(req *sip.Request, tx sip.ServerTransaction) {
|
||||
id := req.From().Address.User
|
||||
if len(id) != 20 {
|
||||
slog.Error("invalid device ID", "request", req.String())
|
||||
}
|
||||
|
||||
slog.Debug(fmt.Sprintf("Received MESSAGE %s", req.String()))
|
||||
|
||||
temp := &models.XmlMessageInfo{}
|
||||
decoder := xml.NewDecoder(bytes.NewReader([]byte(req.Body())))
|
||||
decoder.CharsetReader = charset.NewReaderLabel
|
||||
if err := decoder.Decode(temp); err != nil {
|
||||
slog.Error("decode message error", "error", err.Error(), "message", req.Body())
|
||||
}
|
||||
|
||||
slog.Info(fmt.Sprintf("Received MESSAGE %s %s %s", temp.CmdType, temp.DeviceID, req.Source()))
|
||||
|
||||
var body string
|
||||
switch temp.CmdType {
|
||||
case "Keepalive":
|
||||
if d, ok := DM.GetDevice(temp.DeviceID); ok && d.Online {
|
||||
// 更新设备心跳时间
|
||||
DM.UpdateDeviceHeartbeat(temp.DeviceID)
|
||||
} else {
|
||||
tx.Respond(sip.NewResponseFromRequest(req, http.StatusBadRequest, "", nil))
|
||||
return
|
||||
}
|
||||
case "SensorCatalog": // 兼容宇视,非国标
|
||||
case "Catalog":
|
||||
DM.UpdateChannels(temp.DeviceID, temp.DeviceList...)
|
||||
//go s.AutoInvite(temp.DeviceID, temp.DeviceList...)
|
||||
case "ConfigDownload":
|
||||
DM.UpdateDeviceConfig(temp.DeviceID, &temp.BasicParam)
|
||||
case "Alarm":
|
||||
slog.Info("Alarm")
|
||||
case "RecordInfo":
|
||||
// 从 recordQueryResults 中获取对应通道的结果通道
|
||||
if ch, ok := s.recordQueryResults.Load(temp.DeviceID); ok {
|
||||
// 发送查询结果
|
||||
resultChan := ch.(chan *models.XmlMessageInfo)
|
||||
resultChan <- temp
|
||||
}
|
||||
default:
|
||||
slog.Warn("Not supported CmdType", "cmd_type", temp.CmdType)
|
||||
response := sip.NewResponseFromRequest(req, http.StatusBadRequest, "", nil)
|
||||
tx.Respond(response)
|
||||
return
|
||||
}
|
||||
tx.Respond(sip.NewResponseFromRequest(req, http.StatusOK, "OK", []byte(body)))
|
||||
}
|
||||
|
||||
func (s *UAS) onNotify(req *sip.Request, tx sip.ServerTransaction) {
|
||||
slog.Debug(fmt.Sprintf("Received NOTIFY %s", req.String()))
|
||||
tx.Respond(sip.NewResponseFromRequest(req, http.StatusOK, "OK", nil))
|
||||
}
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/emiago/sipgo/sip"
|
||||
"github.com/ossrs/srs-sip/pkg/models"
|
||||
"github.com/ossrs/srs-sip/pkg/service/stack"
|
||||
"golang.org/x/net/html/charset"
|
||||
)
|
||||
|
||||
const GB28181_ID_LENGTH = 20
|
||||
|
||||
func (s *UAS) isSameIP(addr1, addr2 string) bool {
|
||||
ip1, _, err1 := net.SplitHostPort(addr1)
|
||||
ip2, _, err2 := net.SplitHostPort(addr2)
|
||||
|
||||
// 如果解析出错,回退到完整字符串比较
|
||||
if err1 != nil || err2 != nil {
|
||||
return addr1 == addr2
|
||||
}
|
||||
|
||||
return ip1 == ip2
|
||||
}
|
||||
|
||||
func (s *UAS) onRegister(req *sip.Request, tx sip.ServerTransaction) {
|
||||
id := req.From().Address.User
|
||||
if len(id) != GB28181_ID_LENGTH {
|
||||
slog.Error("invalid device ID")
|
||||
return
|
||||
}
|
||||
|
||||
slog.Debug(fmt.Sprintf("Received REGISTER %s", req.String()))
|
||||
|
||||
if s.conf.GB28181.Auth.Enable {
|
||||
// Check if Authorization header exists
|
||||
authHeader := req.GetHeaders("Authorization")
|
||||
|
||||
// If no Authorization header, send 401 response to request authentication
|
||||
if len(authHeader) == 0 {
|
||||
nonce := GenerateNonce()
|
||||
resp := stack.NewUnauthorizedResponse(req, http.StatusUnauthorized, "Unauthorized", nonce, s.conf.GB28181.Realm)
|
||||
_ = tx.Respond(resp)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate Authorization
|
||||
authInfo := ParseAuthorization(authHeader[0].Value())
|
||||
if !ValidateAuth(authInfo, s.conf.GB28181.Auth.Password) {
|
||||
slog.Error("auth failed", "device_id", id, "source", req.Source())
|
||||
s.respondRegister(req, http.StatusForbidden, "Auth Failed", tx)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
isUnregister := false
|
||||
if exps := req.GetHeaders("Expires"); len(exps) > 0 {
|
||||
exp := exps[0]
|
||||
expSec, err := strconv.ParseInt(exp.Value(), 10, 32)
|
||||
if err != nil {
|
||||
slog.Error("parse expires header error", "error", err.Error())
|
||||
return
|
||||
}
|
||||
if expSec == 0 {
|
||||
isUnregister = true
|
||||
}
|
||||
} else {
|
||||
slog.Error("empty expires header")
|
||||
return
|
||||
}
|
||||
|
||||
if isUnregister {
|
||||
DM.RemoveDevice(id)
|
||||
slog.Warn("Device unregistered", "device_id", id)
|
||||
return
|
||||
} else {
|
||||
if d, ok := DM.GetDevice(id); !ok {
|
||||
DM.AddDevice(id, &DeviceInfo{
|
||||
DeviceID: id,
|
||||
SourceAddr: req.Source(),
|
||||
NetworkType: req.Transport(),
|
||||
})
|
||||
s.respondRegister(req, http.StatusOK, "OK", tx)
|
||||
slog.Info(fmt.Sprintf("Register success %s %s", id, req.Source()))
|
||||
|
||||
go s.ConfigDownload(id)
|
||||
go s.Catalog(id)
|
||||
} else {
|
||||
if d.SourceAddr != "" && !s.isSameIP(d.SourceAddr, req.Source()) {
|
||||
slog.Error("Device already registered", "device_id", id, "old_source", d.SourceAddr, "new_source", req.Source())
|
||||
// TODO: 如果ID重复,应采用虚拟ID
|
||||
s.respondRegister(req, http.StatusBadRequest, "Conflict Device ID", tx)
|
||||
} else {
|
||||
d.SourceAddr = req.Source()
|
||||
d.NetworkType = req.Transport()
|
||||
DM.UpdateDevice(id, d)
|
||||
s.respondRegister(req, http.StatusOK, "OK", tx)
|
||||
|
||||
slog.Info(fmt.Sprintf("Re-register success %s %s", id, req.Source()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UAS) respondRegister(req *sip.Request, code sip.StatusCode, reason string, tx sip.ServerTransaction) {
|
||||
res := stack.NewRegisterResponse(req, code, reason)
|
||||
_ = tx.Respond(res)
|
||||
|
||||
}
|
||||
|
||||
func (s *UAS) onMessage(req *sip.Request, tx sip.ServerTransaction) {
|
||||
id := req.From().Address.User
|
||||
if len(id) != 20 {
|
||||
slog.Error("invalid device ID", "request", req.String())
|
||||
}
|
||||
|
||||
slog.Debug(fmt.Sprintf("Received MESSAGE %s", req.String()))
|
||||
|
||||
temp := &models.XmlMessageInfo{}
|
||||
decoder := xml.NewDecoder(bytes.NewReader([]byte(req.Body())))
|
||||
decoder.CharsetReader = charset.NewReaderLabel
|
||||
if err := decoder.Decode(temp); err != nil {
|
||||
slog.Error("decode message error", "error", err.Error(), "message", req.Body())
|
||||
}
|
||||
|
||||
slog.Info(fmt.Sprintf("Received MESSAGE %s %s %s", temp.CmdType, temp.DeviceID, req.Source()))
|
||||
|
||||
var body string
|
||||
switch temp.CmdType {
|
||||
case "Keepalive":
|
||||
if d, ok := DM.GetDevice(temp.DeviceID); ok && d.Online {
|
||||
// 更新设备心跳时间
|
||||
DM.UpdateDeviceHeartbeat(temp.DeviceID)
|
||||
} else {
|
||||
tx.Respond(sip.NewResponseFromRequest(req, http.StatusBadRequest, "", nil))
|
||||
return
|
||||
}
|
||||
case "SensorCatalog": // 兼容宇视,非国标
|
||||
case "Catalog":
|
||||
DM.UpdateChannels(temp.DeviceID, temp.DeviceList...)
|
||||
//go s.AutoInvite(temp.DeviceID, temp.DeviceList...)
|
||||
case "ConfigDownload":
|
||||
DM.UpdateDeviceConfig(temp.DeviceID, &temp.BasicParam)
|
||||
case "Alarm":
|
||||
slog.Info("Alarm")
|
||||
case "RecordInfo":
|
||||
// 从 recordQueryResults 中获取对应通道的结果通道
|
||||
if ch, ok := s.recordQueryResults.Load(temp.DeviceID); ok {
|
||||
// 发送查询结果
|
||||
resultChan := ch.(chan *models.XmlMessageInfo)
|
||||
resultChan <- temp
|
||||
}
|
||||
default:
|
||||
slog.Warn("Not supported CmdType", "cmd_type", temp.CmdType)
|
||||
response := sip.NewResponseFromRequest(req, http.StatusBadRequest, "", nil)
|
||||
tx.Respond(response)
|
||||
return
|
||||
}
|
||||
tx.Respond(sip.NewResponseFromRequest(req, http.StatusOK, "OK", []byte(body)))
|
||||
}
|
||||
|
||||
func (s *UAS) onNotify(req *sip.Request, tx sip.ServerTransaction) {
|
||||
slog.Debug(fmt.Sprintf("Received NOTIFY %s", req.String()))
|
||||
tx.Respond(sip.NewResponseFromRequest(req, http.StatusOK, "OK", nil))
|
||||
}
|
||||
|
||||
@ -1,81 +1,81 @@
|
||||
package service
|
||||
|
||||
import "fmt"
|
||||
|
||||
var (
|
||||
ptzCmdMap = map[string]uint8{
|
||||
"stop": 0,
|
||||
"right": 1,
|
||||
"left": 2,
|
||||
"down": 4,
|
||||
"downright": 5,
|
||||
"downleft": 6,
|
||||
"up": 8,
|
||||
"upright": 9,
|
||||
"upleft": 10,
|
||||
"zoomin": 16,
|
||||
"zoomout": 32,
|
||||
}
|
||||
|
||||
ptzSpeedMap = map[string]uint8{
|
||||
"1": 25,
|
||||
"2": 50,
|
||||
"3": 75,
|
||||
"4": 100,
|
||||
"5": 125,
|
||||
"6": 150,
|
||||
"7": 175,
|
||||
"8": 200,
|
||||
"9": 225,
|
||||
"10": 255,
|
||||
}
|
||||
|
||||
defaultSpeed uint8 = 125
|
||||
)
|
||||
|
||||
func getPTZSpeed(speed string) uint8 {
|
||||
if v, ok := ptzSpeedMap[speed]; ok {
|
||||
return v
|
||||
}
|
||||
return defaultSpeed
|
||||
}
|
||||
|
||||
func toPTZCmd(cmdName, speed string) (string, error) {
|
||||
cmdCode, ok := ptzCmdMap[cmdName]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("invalid ptz command: %q", cmdName)
|
||||
}
|
||||
|
||||
speedValue := getPTZSpeed(speed)
|
||||
|
||||
var horizontalSpeed, verticalSpeed, zSpeed uint8
|
||||
|
||||
switch cmdName {
|
||||
case "left", "right":
|
||||
horizontalSpeed = speedValue
|
||||
verticalSpeed = 0
|
||||
case "up", "down":
|
||||
verticalSpeed = speedValue
|
||||
horizontalSpeed = 0
|
||||
case "upleft", "upright", "downleft", "downright":
|
||||
verticalSpeed = speedValue
|
||||
horizontalSpeed = speedValue
|
||||
case "zoomin", "zoomout":
|
||||
zSpeed = speedValue << 4 // zoom速度在高4位
|
||||
default:
|
||||
horizontalSpeed = 0
|
||||
verticalSpeed = 0
|
||||
zSpeed = 0
|
||||
}
|
||||
|
||||
sum := uint16(0xA5) + uint16(0x0F) + uint16(0x01) + uint16(cmdCode) + uint16(horizontalSpeed) + uint16(verticalSpeed) + uint16(zSpeed)
|
||||
checksum := uint8(sum % 256)
|
||||
|
||||
return fmt.Sprintf("A50F01%02X%02X%02X%02X%02X",
|
||||
cmdCode,
|
||||
horizontalSpeed,
|
||||
verticalSpeed,
|
||||
zSpeed,
|
||||
checksum,
|
||||
), nil
|
||||
}
|
||||
package service
|
||||
|
||||
import "fmt"
|
||||
|
||||
var (
|
||||
ptzCmdMap = map[string]uint8{
|
||||
"stop": 0,
|
||||
"right": 1,
|
||||
"left": 2,
|
||||
"down": 4,
|
||||
"downright": 5,
|
||||
"downleft": 6,
|
||||
"up": 8,
|
||||
"upright": 9,
|
||||
"upleft": 10,
|
||||
"zoomin": 16,
|
||||
"zoomout": 32,
|
||||
}
|
||||
|
||||
ptzSpeedMap = map[string]uint8{
|
||||
"1": 25,
|
||||
"2": 50,
|
||||
"3": 75,
|
||||
"4": 100,
|
||||
"5": 125,
|
||||
"6": 150,
|
||||
"7": 175,
|
||||
"8": 200,
|
||||
"9": 225,
|
||||
"10": 255,
|
||||
}
|
||||
|
||||
defaultSpeed uint8 = 125
|
||||
)
|
||||
|
||||
func getPTZSpeed(speed string) uint8 {
|
||||
if v, ok := ptzSpeedMap[speed]; ok {
|
||||
return v
|
||||
}
|
||||
return defaultSpeed
|
||||
}
|
||||
|
||||
func toPTZCmd(cmdName, speed string) (string, error) {
|
||||
cmdCode, ok := ptzCmdMap[cmdName]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("invalid ptz command: %q", cmdName)
|
||||
}
|
||||
|
||||
speedValue := getPTZSpeed(speed)
|
||||
|
||||
var horizontalSpeed, verticalSpeed, zSpeed uint8
|
||||
|
||||
switch cmdName {
|
||||
case "left", "right":
|
||||
horizontalSpeed = speedValue
|
||||
verticalSpeed = 0
|
||||
case "up", "down":
|
||||
verticalSpeed = speedValue
|
||||
horizontalSpeed = 0
|
||||
case "upleft", "upright", "downleft", "downright":
|
||||
verticalSpeed = speedValue
|
||||
horizontalSpeed = speedValue
|
||||
case "zoomin", "zoomout":
|
||||
zSpeed = speedValue << 4 // zoom速度在高4位
|
||||
default:
|
||||
horizontalSpeed = 0
|
||||
verticalSpeed = 0
|
||||
zSpeed = 0
|
||||
}
|
||||
|
||||
sum := uint16(0xA5) + uint16(0x0F) + uint16(0x01) + uint16(cmdCode) + uint16(horizontalSpeed) + uint16(verticalSpeed) + uint16(zSpeed)
|
||||
checksum := uint8(sum % 256)
|
||||
|
||||
return fmt.Sprintf("A50F01%02X%02X%02X%02X%02X",
|
||||
cmdCode,
|
||||
horizontalSpeed,
|
||||
verticalSpeed,
|
||||
zSpeed,
|
||||
checksum,
|
||||
), nil
|
||||
}
|
||||
|
||||
@ -141,7 +141,7 @@ func TestToPTZCmdSpecificCases(t *testing.T) {
|
||||
|
||||
func TestToPTZCmdWithDifferentSpeeds(t *testing.T) {
|
||||
speeds := []string{"1", "5", "10"}
|
||||
|
||||
|
||||
for _, speed := range speeds {
|
||||
t.Run("Right with speed "+speed, func(t *testing.T) {
|
||||
result, err := toPTZCmd("right", speed)
|
||||
@ -196,4 +196,3 @@ func TestPTZSpeedMap(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,68 +1,68 @@
|
||||
package stack
|
||||
|
||||
import (
|
||||
"github.com/emiago/sipgo/sip"
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
)
|
||||
|
||||
type OutboundConfig struct {
|
||||
Transport string
|
||||
Via string
|
||||
From string
|
||||
To string
|
||||
}
|
||||
|
||||
func NewRequest(method sip.RequestMethod, body []byte, conf OutboundConfig) (*sip.Request, error) {
|
||||
if len(conf.From) != 20 || len(conf.To) != 20 {
|
||||
return nil, errors.Errorf("From or To length is not 20")
|
||||
}
|
||||
|
||||
dest := conf.Via
|
||||
to := sip.Uri{User: conf.To, Host: conf.To[:10]}
|
||||
from := &sip.Uri{User: conf.From, Host: conf.From[:10]}
|
||||
|
||||
fromHeader := &sip.FromHeader{Address: *from, Params: sip.NewParams()}
|
||||
fromHeader.Params.Add("tag", sip.GenerateTagN(16))
|
||||
|
||||
req := sip.NewRequest(method, to)
|
||||
req.AppendHeader(fromHeader)
|
||||
req.AppendHeader(&sip.ToHeader{Address: to})
|
||||
req.AppendHeader(&sip.ContactHeader{Address: *from})
|
||||
req.AppendHeader(sip.NewHeader("Max-Forwards", "70"))
|
||||
req.SetBody(body)
|
||||
req.SetDestination(dest)
|
||||
req.SetTransport(conf.Transport)
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func NewRegisterRequest(conf OutboundConfig) (*sip.Request, error) {
|
||||
req, err := NewRequest(sip.REGISTER, nil, conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.AppendHeader(sip.NewHeader("Expires", "3600"))
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func NewInviteRequest(body []byte, subject string, conf OutboundConfig) (*sip.Request, error) {
|
||||
req, err := NewRequest(sip.INVITE, body, conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.AppendHeader(sip.NewHeader("Content-Type", "application/sdp"))
|
||||
req.AppendHeader(sip.NewHeader("Subject", subject))
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func NewMessageRequest(body []byte, conf OutboundConfig) (*sip.Request, error) {
|
||||
req, err := NewRequest(sip.MESSAGE, body, conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.AppendHeader(sip.NewHeader("Content-Type", "Application/MANSCDP+xml"))
|
||||
|
||||
return req, nil
|
||||
}
|
||||
package stack
|
||||
|
||||
import (
|
||||
"github.com/emiago/sipgo/sip"
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
)
|
||||
|
||||
type OutboundConfig struct {
|
||||
Transport string
|
||||
Via string
|
||||
From string
|
||||
To string
|
||||
}
|
||||
|
||||
func NewRequest(method sip.RequestMethod, body []byte, conf OutboundConfig) (*sip.Request, error) {
|
||||
if len(conf.From) != 20 || len(conf.To) != 20 {
|
||||
return nil, errors.Errorf("From or To length is not 20")
|
||||
}
|
||||
|
||||
dest := conf.Via
|
||||
to := sip.Uri{User: conf.To, Host: conf.To[:10]}
|
||||
from := &sip.Uri{User: conf.From, Host: conf.From[:10]}
|
||||
|
||||
fromHeader := &sip.FromHeader{Address: *from, Params: sip.NewParams()}
|
||||
fromHeader.Params.Add("tag", sip.GenerateTagN(16))
|
||||
|
||||
req := sip.NewRequest(method, to)
|
||||
req.AppendHeader(fromHeader)
|
||||
req.AppendHeader(&sip.ToHeader{Address: to})
|
||||
req.AppendHeader(&sip.ContactHeader{Address: *from})
|
||||
req.AppendHeader(sip.NewHeader("Max-Forwards", "70"))
|
||||
req.SetBody(body)
|
||||
req.SetDestination(dest)
|
||||
req.SetTransport(conf.Transport)
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func NewRegisterRequest(conf OutboundConfig) (*sip.Request, error) {
|
||||
req, err := NewRequest(sip.REGISTER, nil, conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.AppendHeader(sip.NewHeader("Expires", "3600"))
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func NewInviteRequest(body []byte, subject string, conf OutboundConfig) (*sip.Request, error) {
|
||||
req, err := NewRequest(sip.INVITE, body, conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.AppendHeader(sip.NewHeader("Content-Type", "application/sdp"))
|
||||
req.AppendHeader(sip.NewHeader("Subject", subject))
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func NewMessageRequest(body []byte, conf OutboundConfig) (*sip.Request, error) {
|
||||
req, err := NewRequest(sip.MESSAGE, body, conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.AppendHeader(sip.NewHeader("Content-Type", "Application/MANSCDP+xml"))
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
@ -1,41 +1,41 @@
|
||||
package stack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/emiago/sipgo/sip"
|
||||
)
|
||||
|
||||
const TIME_LAYOUT = "2024-01-01T00:00:00"
|
||||
const EXPIRES_TIME = 3600
|
||||
|
||||
func newResponse(req *sip.Request, code sip.StatusCode, reason string) *sip.Response {
|
||||
resp := sip.NewResponseFromRequest(req, code, reason, nil)
|
||||
|
||||
newTo := &sip.ToHeader{Address: resp.To().Address, Params: sip.NewParams()}
|
||||
newTo.Params.Add("tag", sip.GenerateTagN(10))
|
||||
|
||||
resp.ReplaceHeader(newTo)
|
||||
resp.RemoveHeader("Allow")
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func NewRegisterResponse(req *sip.Request, code sip.StatusCode, reason string) *sip.Response {
|
||||
resp := newResponse(req, code, reason)
|
||||
|
||||
expires := sip.ExpiresHeader(EXPIRES_TIME)
|
||||
resp.AppendHeader(&expires)
|
||||
resp.AppendHeader(sip.NewHeader("Date", time.Now().Format(TIME_LAYOUT)))
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func NewUnauthorizedResponse(req *sip.Request, code sip.StatusCode, reason, nonce, realm string) *sip.Response {
|
||||
resp := newResponse(req, code, reason)
|
||||
|
||||
resp.AppendHeader(sip.NewHeader("WWW-Authenticate", fmt.Sprintf(`Digest realm="%s",nonce="%s",algorithm=MD5`, realm, nonce)))
|
||||
|
||||
return resp
|
||||
}
|
||||
package stack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/emiago/sipgo/sip"
|
||||
)
|
||||
|
||||
const TIME_LAYOUT = "2024-01-01T00:00:00"
|
||||
const EXPIRES_TIME = 3600
|
||||
|
||||
func newResponse(req *sip.Request, code sip.StatusCode, reason string) *sip.Response {
|
||||
resp := sip.NewResponseFromRequest(req, code, reason, nil)
|
||||
|
||||
newTo := &sip.ToHeader{Address: resp.To().Address, Params: sip.NewParams()}
|
||||
newTo.Params.Add("tag", sip.GenerateTagN(10))
|
||||
|
||||
resp.ReplaceHeader(newTo)
|
||||
resp.RemoveHeader("Allow")
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func NewRegisterResponse(req *sip.Request, code sip.StatusCode, reason string) *sip.Response {
|
||||
resp := newResponse(req, code, reason)
|
||||
|
||||
expires := sip.ExpiresHeader(EXPIRES_TIME)
|
||||
resp.AppendHeader(&expires)
|
||||
resp.AppendHeader(sip.NewHeader("Date", time.Now().Format(TIME_LAYOUT)))
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func NewUnauthorizedResponse(req *sip.Request, code sip.StatusCode, reason, nonce, realm string) *sip.Response {
|
||||
resp := newResponse(req, code, reason)
|
||||
|
||||
resp.AppendHeader(sip.NewHeader("WWW-Authenticate", fmt.Sprintf(`Digest realm="%s",nonce="%s",algorithm=MD5`, realm, nonce)))
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
@ -1,122 +1,122 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/emiago/sipgo"
|
||||
"github.com/emiago/sipgo/sip"
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
"github.com/ossrs/srs-sip/pkg/config"
|
||||
"github.com/ossrs/srs-sip/pkg/service/stack"
|
||||
)
|
||||
|
||||
const (
|
||||
UserAgent = "SRS-SIP/1.0"
|
||||
)
|
||||
|
||||
type UAC struct {
|
||||
*Cascade
|
||||
|
||||
SN uint32
|
||||
LocalIP string
|
||||
}
|
||||
|
||||
func NewUac() *UAC {
|
||||
ip, err := config.GetLocalIP()
|
||||
if err != nil {
|
||||
slog.Error("get local ip failed", "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
c := &UAC{
|
||||
Cascade: &Cascade{},
|
||||
LocalIP: ip,
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *UAC) Start(agent *sipgo.UserAgent, r0 interface{}) error {
|
||||
var err error
|
||||
|
||||
c.ctx = context.Background()
|
||||
c.conf = r0.(*config.MainConfig)
|
||||
|
||||
if agent == nil {
|
||||
ua, err := sipgo.NewUA(sipgo.WithUserAgent(UserAgent))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
agent = ua
|
||||
}
|
||||
|
||||
c.sipCli, err = sipgo.NewClient(agent, sipgo.WithClientHostname(c.LocalIP))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.sipSvr, err = sipgo.NewServer(agent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.sipSvr.OnInvite(c.onInvite)
|
||||
c.sipSvr.OnBye(c.onBye)
|
||||
c.sipSvr.OnMessage(c.onMessage)
|
||||
|
||||
go c.doRegister()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *UAC) Stop() {
|
||||
// TODO: 断开所有当前连接
|
||||
c.sipCli.Close()
|
||||
c.sipSvr.Close()
|
||||
}
|
||||
|
||||
func (c *UAC) doRegister() error {
|
||||
r, _ := stack.NewRegisterRequest(stack.OutboundConfig{
|
||||
From: "34020000001110000001",
|
||||
To: "34020000002000000001",
|
||||
Transport: "UDP",
|
||||
Via: fmt.Sprintf("%s:%d", c.LocalIP, c.conf.GB28181.Port),
|
||||
})
|
||||
tx, err := c.sipCli.TransactionRequest(c.ctx, r)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "transaction request error")
|
||||
}
|
||||
|
||||
rs, _ := c.getResponse(tx)
|
||||
slog.Info("register response", "response", rs.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *UAC) OnRequest(req *sip.Request, tx sip.ServerTransaction) {
|
||||
switch req.Method {
|
||||
case "INVITE":
|
||||
c.onInvite(req, tx)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *UAC) onInvite(req *sip.Request, tx sip.ServerTransaction) {
|
||||
slog.Debug("onInvite")
|
||||
}
|
||||
|
||||
func (c *UAC) onBye(req *sip.Request, tx sip.ServerTransaction) {
|
||||
slog.Debug("onBye")
|
||||
}
|
||||
|
||||
func (c *UAC) onMessage(req *sip.Request, tx sip.ServerTransaction) {
|
||||
slog.Debug("onMessage", "request", req.String())
|
||||
}
|
||||
|
||||
func (c *UAC) getResponse(tx sip.ClientTransaction) (*sip.Response, error) {
|
||||
select {
|
||||
case <-tx.Done():
|
||||
return nil, fmt.Errorf("transaction died")
|
||||
case res := <-tx.Responses():
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/emiago/sipgo"
|
||||
"github.com/emiago/sipgo/sip"
|
||||
"github.com/ossrs/go-oryx-lib/errors"
|
||||
"github.com/ossrs/srs-sip/pkg/config"
|
||||
"github.com/ossrs/srs-sip/pkg/service/stack"
|
||||
)
|
||||
|
||||
const (
|
||||
UserAgent = "SRS-SIP/1.0"
|
||||
)
|
||||
|
||||
type UAC struct {
|
||||
*Cascade
|
||||
|
||||
SN uint32
|
||||
LocalIP string
|
||||
}
|
||||
|
||||
func NewUac() *UAC {
|
||||
ip, err := config.GetLocalIP()
|
||||
if err != nil {
|
||||
slog.Error("get local ip failed", "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
c := &UAC{
|
||||
Cascade: &Cascade{},
|
||||
LocalIP: ip,
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *UAC) Start(agent *sipgo.UserAgent, r0 interface{}) error {
|
||||
var err error
|
||||
|
||||
c.ctx = context.Background()
|
||||
c.conf = r0.(*config.MainConfig)
|
||||
|
||||
if agent == nil {
|
||||
ua, err := sipgo.NewUA(sipgo.WithUserAgent(UserAgent))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
agent = ua
|
||||
}
|
||||
|
||||
c.sipCli, err = sipgo.NewClient(agent, sipgo.WithClientHostname(c.LocalIP))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.sipSvr, err = sipgo.NewServer(agent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.sipSvr.OnInvite(c.onInvite)
|
||||
c.sipSvr.OnBye(c.onBye)
|
||||
c.sipSvr.OnMessage(c.onMessage)
|
||||
|
||||
go c.doRegister()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *UAC) Stop() {
|
||||
// TODO: 断开所有当前连接
|
||||
c.sipCli.Close()
|
||||
c.sipSvr.Close()
|
||||
}
|
||||
|
||||
func (c *UAC) doRegister() error {
|
||||
r, _ := stack.NewRegisterRequest(stack.OutboundConfig{
|
||||
From: "34020000001110000001",
|
||||
To: "34020000002000000001",
|
||||
Transport: "UDP",
|
||||
Via: fmt.Sprintf("%s:%d", c.LocalIP, c.conf.GB28181.Port),
|
||||
})
|
||||
tx, err := c.sipCli.TransactionRequest(c.ctx, r)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "transaction request error")
|
||||
}
|
||||
|
||||
rs, _ := c.getResponse(tx)
|
||||
slog.Info("register response", "response", rs.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *UAC) OnRequest(req *sip.Request, tx sip.ServerTransaction) {
|
||||
switch req.Method {
|
||||
case "INVITE":
|
||||
c.onInvite(req, tx)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *UAC) onInvite(req *sip.Request, tx sip.ServerTransaction) {
|
||||
slog.Debug("onInvite")
|
||||
}
|
||||
|
||||
func (c *UAC) onBye(req *sip.Request, tx sip.ServerTransaction) {
|
||||
slog.Debug("onBye")
|
||||
}
|
||||
|
||||
func (c *UAC) onMessage(req *sip.Request, tx sip.ServerTransaction) {
|
||||
slog.Debug("onMessage", "request", req.String())
|
||||
}
|
||||
|
||||
func (c *UAC) getResponse(tx sip.ClientTransaction) (*sip.Response, error) {
|
||||
select {
|
||||
case <-tx.Done():
|
||||
return nil, fmt.Errorf("transaction died")
|
||||
case res := <-tx.Responses():
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user