NextGB, web demo powerd by vue

This commit is contained in:
chenhaibo
2025-02-03 16:27:46 +08:00
parent 0b7126b12b
commit c80247286e
113 changed files with 16731 additions and 9944 deletions

92
pkg/service/auth.go Normal file
View File

@ -0,0 +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))
}

View File

@ -1,64 +1,12 @@
package service
import (
"fmt"
"sync"
"github.com/ossrs/srs-sip/pkg/utils"
"github.com/ossrs/srs-sip/pkg/models"
)
// <Item>
// <DeviceID>34020000001320000002</DeviceID>
// <Name>209</Name>
// <Manufacturer>UNIVIEW</Manufacturer>
// <Model>HIC6622-IR@X33-VF</Model>
// <Owner>IPC-B2202.7.11.230222</Owner>
// <CivilCode>CivilCode</CivilCode>
// <Address>Address</Address>
// <Parental>1</Parental>
// <ParentID>75015310072008100002</ParentID>
// <SafetyWay>0</SafetyWay>
// <RegisterWay>1</RegisterWay>
// <Secrecy>0</Secrecy>
// <Status>ON</Status>
// <Longitude>0.0000000</Longitude>
// <Latitude>0.0000000</Latitude>
// <Info>
// <PTZType>1</PTZType>
// <Resolution>6/4/2</Resolution>
// <DownloadSpeed>0</DownloadSpeed>
// </Info>
// </Item>
type ChannelInfo struct {
DeviceID string `json:"device_id"`
ParentID string `json:"parent_id"`
Name string `json:"name"`
Manufacturer string `json:"manufacturer"`
Model string `json:"model"`
Owner string `json:"owner"`
CivilCode string `json:"civil_code"`
Address string `json:"address"`
Port int `json:"port"`
Parental int `json:"parental"`
SafetyWay int `json:"safety_way"`
RegisterWay int `json:"register_way"`
Secrecy int `json:"secrecy"`
IPAddress string `json:"ip_address"`
Status ChannelStatus `json:"status"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
Info struct {
PTZType int `json:"ptz_type"`
Resolution string `json:"resolution"`
DownloadSpeed string `json:"download_speed"` // 1/2/4/8
} `json:"info"`
// custom fields
Ssrc string `json:"ssrc"`
}
type ChannelStatus string
type DeviceInfo struct {
DeviceID string `json:"device_id"`
SourceAddr string `json:"source_addr"`
@ -83,6 +31,13 @@ func GetDeviceManager() *deviceManager {
}
func (dm *deviceManager) AddDevice(id string, info *DeviceInfo) {
channel := models.ChannelInfo{
DeviceID: id,
ParentID: id,
Name: id,
Status: models.ChannelStatus("ON"),
}
info.ChannelMap.Store(channel.DeviceID, channel)
dm.devices.Store(id, info)
}
@ -107,41 +62,88 @@ func (dm *deviceManager) GetDevice(id string) (*DeviceInfo, bool) {
return v.(*DeviceInfo), true
}
func (dm *deviceManager) UpdateChannels(deviceID string, list ...ChannelInfo) {
// ChannelParser defines interface for different manufacturer's channel parsing
type ChannelParser interface {
ParseChannels(list ...models.ChannelInfo) ([]models.ChannelInfo, error)
}
// channelParserRegistry manages registration and lookup of manufacturer-specific parsers
type channelParserRegistry struct {
parsers map[string]ChannelParser
mu sync.RWMutex
}
var (
parserRegistry = &channelParserRegistry{
parsers: make(map[string]ChannelParser),
}
)
// RegisterParser registers a parser for a specific manufacturer
func (r *channelParserRegistry) RegisterParser(manufacturer string, parser ChannelParser) {
r.mu.Lock()
defer r.mu.Unlock()
r.parsers[manufacturer] = parser
}
// GetParser retrieves parser for a specific manufacturer
func (r *channelParserRegistry) GetParser(manufacturer string) (ChannelParser, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
parser, ok := r.parsers[manufacturer]
return parser, ok
}
// UpdateChannels updates device channel information
func (dm *deviceManager) UpdateChannels(deviceID string, list ...models.ChannelInfo) error {
device, ok := dm.GetDevice(deviceID)
if !ok {
return
return fmt.Errorf("device not found: %s", deviceID)
}
for _, channel := range list {
// clear ChannelMap
device.ChannelMap.Range(func(key, value interface{}) bool {
device.ChannelMap.Delete(key)
return true
})
parser, ok := parserRegistry.GetParser(list[0].Manufacturer)
if !ok {
return fmt.Errorf("no parser found for manufacturer: %s", list[0].Manufacturer)
}
channels, err := parser.ParseChannels(list...)
if err != nil {
return fmt.Errorf("failed to parse channels: %v", err)
}
for _, channel := range channels {
device.ChannelMap.Store(channel.DeviceID, channel)
}
dm.devices.Store(deviceID, device)
return nil
}
func (dm *deviceManager) ApiGetChannelByDeviceId(deviceID string) []ChannelInfo {
func (dm *deviceManager) ApiGetChannelByDeviceId(deviceID string) []models.ChannelInfo {
device, ok := dm.GetDevice(deviceID)
if !ok {
return nil
}
channels := make([]ChannelInfo, 0)
channels := make([]models.ChannelInfo, 0)
device.ChannelMap.Range(func(key, value interface{}) bool {
channels = append(channels, value.(ChannelInfo))
channels = append(channels, value.(models.ChannelInfo))
return true
})
return channels
}
func (dm *deviceManager) GetAllVideoChannels() []ChannelInfo {
channels := make([]ChannelInfo, 0)
func (dm *deviceManager) GetAllVideoChannels() []models.ChannelInfo {
channels := make([]models.ChannelInfo, 0)
dm.devices.Range(func(key, value interface{}) bool {
device := value.(*DeviceInfo)
device.ChannelMap.Range(func(key, value interface{}) bool {
if utils.IsVideoChannel(value.(ChannelInfo).DeviceID) {
channels = append(channels, value.(ChannelInfo))
return true
}
channels = append(channels, value.(models.ChannelInfo))
return true
})
return true
@ -164,3 +166,37 @@ func (dm *deviceManager) GetDeviceInfoByChannel(channelID string) (*DeviceInfo,
})
return device, found
}
// Hikvision channel parser implementation
type HikvisionParser struct{}
func (p *HikvisionParser) ParseChannels(list ...models.ChannelInfo) ([]models.ChannelInfo, error) {
return list, nil
}
// Dahua channel parser implementation
type DahuaParser struct{}
func (p *DahuaParser) ParseChannels(list ...models.ChannelInfo) ([]models.ChannelInfo, error) {
return list, nil
}
// Uniview channel parser implementation
type UniviewParser struct{}
func (p *UniviewParser) ParseChannels(list ...models.ChannelInfo) ([]models.ChannelInfo, error) {
videoChannels := make([]models.ChannelInfo, 0)
for _, channel := range list {
// 只有Parental为1的通道才是视频通道
if channel.Parental == 1 {
videoChannels = append(videoChannels, channel)
}
}
return videoChannels, nil
}
func init() {
parserRegistry.RegisterParser("Hikvision", &HikvisionParser{})
parserRegistry.RegisterParser("DAHUA", &DahuaParser{})
parserRegistry.RegisterParser("UNIVIEW", &UniviewParser{})
}

View File

@ -8,21 +8,13 @@ import (
"github.com/emiago/sipgo/sip"
"github.com/ossrs/go-oryx-lib/logger"
"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
type VideoChannelStatus struct {
ID string
ParentID string
MediaHost string
MediaPort int
Ssrc string
Status string
}
func (s *UAS) onRegister(req *sip.Request, tx sip.ServerTransaction) {
id := req.From().Address.User
if len(id) != GB28181_ID_LENGTH {
@ -30,6 +22,27 @@ func (s *UAS) onRegister(req *sip.Request, tx sip.ServerTransaction) {
return
}
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) {
logger.Ef(s.ctx, "%s auth failed, source: %s", id, req.Source())
s.respondRegister(req, http.StatusForbidden, "Auth Failed", tx)
return
}
}
isUnregister := false
if exps := req.GetHeaders("Expires"); len(exps) > 0 {
exp := exps[0]
@ -88,19 +101,7 @@ func (s *UAS) onMessage(req *sip.Request, tx sip.ServerTransaction) {
//logger.Tf(s.ctx, "Received MESSAGE: %s", req.String())
temp := &struct {
XMLName xml.Name
CmdType string
SN int // 请求序列号,一般用于对应 request 和 response
DeviceID string
DeviceName string
Manufacturer string
Model string
Channel string
DeviceList []ChannelInfo `xml:"DeviceList>Item"`
// RecordList []*Record `xml:"RecordList>Item"`
// SumNum int
}{}
temp := &models.XmlMessageInfo{}
decoder := xml.NewDecoder(bytes.NewReader([]byte(req.Body())))
decoder.CharsetReader = charset.NewReaderLabel
if err := decoder.Decode(temp); err != nil {
@ -122,6 +123,14 @@ func (s *UAS) onMessage(req *sip.Request, tx sip.ServerTransaction) {
//go s.AutoInvite(temp.DeviceID, temp.DeviceList...)
case "Alarm":
logger.T(s.ctx, "Alarm")
case "RecordInfo":
logger.T(s.ctx, "RecordInfo")
// 从 recordQueryResults 中获取对应通道的结果通道
if ch, ok := s.recordQueryResults.Load(temp.DeviceID); ok {
// 发送查询结果
resultChan := ch.(chan *models.XmlMessageInfo)
resultChan <- temp
}
default:
logger.Wf(s.ctx, "Not supported CmdType: %s", temp.CmdType)
response := sip.NewResponseFromRequest(req, http.StatusBadRequest, "", nil)
@ -135,19 +144,3 @@ func (s *UAS) onNotify(req *sip.Request, tx sip.ServerTransaction) {
logger.T(s.ctx, "Received NOTIFY request")
tx.Respond(sip.NewResponseFromRequest(req, http.StatusOK, "OK", nil))
}
func (s *UAS) AddVideoChannelStatue(channelID string, status VideoChannelStatus) {
s.channelsStatue.Store(channelID, status)
}
func (s *UAS) GetVideoChannelStatue(channelID string) (VideoChannelStatus, bool) {
v, ok := s.channelsStatue.Load(channelID)
if !ok {
return VideoChannelStatus{}, false
}
return v.(VideoChannelStatus), true
}
func (s *UAS) RemoveVideoChannelStatue(channelID string) {
s.channelsStatue.Delete(channelID)
}

View File

@ -1,169 +1,531 @@
package service
import (
"fmt"
"strings"
"github.com/emiago/sipgo/sip"
"github.com/ossrs/go-oryx-lib/errors"
"github.com/ossrs/go-oryx-lib/logger"
"github.com/ossrs/srs-sip/pkg/service/stack"
"github.com/ossrs/srs-sip/pkg/utils"
)
func (s *UAS) AutoInvite(deviceID string, list ...ChannelInfo) {
for _, c := range list {
if c.Status == "ON" && utils.IsVideoChannel(c.DeviceID) {
if err := s.Invite(deviceID, c.DeviceID); err != nil {
logger.Ef(s.ctx, "invite error: %s", err.Error())
}
}
}
}
func (s *UAS) Invite(deviceID, channelID string) error {
if s.isPublishing(channelID) {
return nil
}
ssrc := utils.CreateSSRC(true)
mediaPort, err := s.signal.Publish(ssrc, ssrc)
if err != nil {
return errors.Wrapf(err, "api gb publish request error")
}
mediaHost := strings.Split(s.conf.MediaAddr, ":")[0]
if mediaHost == "" {
return errors.Errorf("media host is empty")
}
sdpInfo := []string{
"v=0",
fmt.Sprintf("o=%s 0 0 IN IP4 %s", channelID, mediaHost),
"s=" + "Play",
"u=" + channelID + ":0",
"c=IN IP4 " + mediaHost,
"t=0 0", // start time and end time
fmt.Sprintf("m=video %d TCP/RTP/AVP 96", mediaPort),
"a=recvonly",
"a=rtpmap:96 PS/90000",
"y=" + ssrc,
"\r\n",
}
if true { // support tcp only
sdpInfo = append(sdpInfo, "a=setup:passive", "a=connection:new")
}
// TODO: 需要考虑不同设备通道ID相同的情况
d, ok := DM.GetDeviceInfoByChannel(channelID)
if !ok {
return errors.Errorf("device not found by %s", channelID)
}
subject := fmt.Sprintf("%s:%s,%s:0", channelID, ssrc, s.conf.Serial)
req, err := stack.NewInviteRequest([]byte(strings.Join(sdpInfo, "\r\n")), subject, stack.OutboundConfig{
Via: d.SourceAddr,
To: d.DeviceID,
From: s.conf.Serial,
Transport: d.NetworkType,
})
if err != nil {
return errors.Wrapf(err, "build invite request error")
}
tx, err := s.sipCli.TransactionRequest(s.ctx, req)
if err != nil {
return errors.Wrapf(err, "transaction request error")
}
res, err := s.waitAnswer(tx)
if err != nil {
return errors.Wrapf(err, "wait answer error")
}
if res.StatusCode != 200 {
return errors.Errorf("invite response error: %s", res.String())
}
ack := sip.NewAckRequest(req, res, nil)
s.sipCli.WriteRequest(ack)
s.AddVideoChannelStatue(channelID, VideoChannelStatus{
ID: channelID,
ParentID: deviceID,
MediaHost: mediaHost,
MediaPort: mediaPort,
Ssrc: ssrc,
Status: "ON",
})
return nil
}
func (s *UAS) isPublishing(channelID string) bool {
c, err := s.GetVideoChannelStatue(channelID)
if !err {
return false
}
if p, err := s.signal.GetStreamStatus(c.Ssrc); err != nil || !p {
return false
}
return true
}
func (s *UAS) Bye() error {
return nil
}
func (s *UAS) Catalog(deviceID string) error {
var CatalogXML = `<?xml version="1.0"?><Query>
<CmdType>Catalog</CmdType>
<SN>%d</SN>
<DeviceID>%s</DeviceID>
</Query>
`
d, ok := DM.GetDevice(deviceID)
if !ok {
return errors.Errorf("device %s not found", deviceID)
}
body := fmt.Sprintf(CatalogXML, s.getSN(), deviceID)
req, err := stack.NewCatelogRequest([]byte(body), stack.OutboundConfig{
Via: d.SourceAddr,
To: d.DeviceID,
From: s.conf.Serial,
Transport: d.NetworkType,
})
if err != nil {
return errors.Wrapf(err, "build catalog request error")
}
tx, err := s.sipCli.TransactionRequest(s.ctx, req)
if err != nil {
return errors.Wrapf(err, "transaction request error")
}
res, err := s.waitAnswer(tx)
if err != nil {
return errors.Wrapf(err, "wait answer error")
}
logger.Tf(s.ctx, "catalog response: %s", res.String())
return nil
}
func (s *UAS) waitAnswer(tx sip.ClientTransaction) (*sip.Response, error) {
select {
case <-s.ctx.Done():
return nil, errors.Errorf("context done")
case res := <-tx.Responses():
if res.StatusCode == 100 || res.StatusCode == 101 || res.StatusCode == 180 || res.StatusCode == 183 {
return s.waitAnswer(tx)
}
return res, nil
}
}
package service
import (
"fmt"
"strings"
"time"
"github.com/emiago/sipgo/sip"
"github.com/ossrs/go-oryx-lib/errors"
"github.com/ossrs/go-oryx-lib/logger"
"github.com/ossrs/srs-sip/pkg/media"
"github.com/ossrs/srs-sip/pkg/models"
"github.com/ossrs/srs-sip/pkg/service/stack"
"github.com/ossrs/srs-sip/pkg/utils"
)
type Session struct {
ID string
ParentID string
MediaHost string
MediaPort int
Ssrc string
Status string
URL string
RefCount int
CSeq int
InviteReq *sip.Request
InviteRes *sip.Response
}
func (s *Session) NewRequest(method sip.RequestMethod, body []byte) *sip.Request {
request := sip.NewRequest(method, s.InviteReq.Recipient)
request.SipVersion = s.InviteRes.SipVersion
maxForwardsHeader := sip.MaxForwardsHeader(70)
request.AppendHeader(&maxForwardsHeader)
if h := s.InviteReq.From(); h != nil {
request.AppendHeader(h)
}
if h := s.InviteRes.To(); h != nil {
request.AppendHeader(h)
}
if h := s.InviteReq.CallID(); h != nil {
request.AppendHeader(h)
}
if h := s.InviteReq.CSeq(); h != nil {
h.SeqNo++
request.AppendHeader(h)
}
request.SetSource(s.InviteReq.Source())
request.SetDestination(s.InviteReq.Destination())
request.SetTransport(s.InviteReq.Transport())
request.SetBody(body)
s.CSeq++
return request
}
func (s *Session) NewByeRequest() *sip.Request {
return s.NewRequest(sip.BYE, nil)
}
// PAUSE RTSP/1.0
// CSeq:1
// PauseTime:now
func (s *Session) NewPauseRequest() *sip.Request {
body := []byte(fmt.Sprintf(`PAUSE RTSP/1.0
CSeq: %d
PauseTime: now
`, s.CSeq))
s.CSeq++
pauseRequest := s.NewRequest(sip.INFO, body)
pauseRequest.AppendHeader(sip.NewHeader("Content-Type", "Application/MANSRTSP"))
return pauseRequest
}
// PLAY RTSP/1.0
// CSeq:2
// Range:npt=now
func (s *Session) NewResumeRequest() *sip.Request {
body := []byte(fmt.Sprintf(`PLAY RTSP/1.0
CSeq: %d
Range: npt=now
`, s.CSeq))
s.CSeq++
resumeRequest := s.NewRequest(sip.INFO, body)
resumeRequest.AppendHeader(sip.NewHeader("Content-Type", "Application/MANSRTSP"))
return resumeRequest
}
// PLAY RTSP/1.0
// CSeq:3
// Scale:2.0
func (s *Session) NewSpeedRequest(speed float32) *sip.Request {
body := []byte(fmt.Sprintf(`PLAY RTSP/1.0
CSeq: %d
Scale: %.1f
`, s.CSeq, speed))
s.CSeq++
speedRequest := s.NewRequest(sip.INFO, body)
speedRequest.AppendHeader(sip.NewHeader("Content-Type", "Application/MANSRTSP"))
return speedRequest
}
func (s *UAS) AddSession(key string, status Session) {
logger.Tf(s.ctx, "AddSession: %s, %+v", key, status)
s.Streams.Store(key, status)
}
func (s *UAS) GetSession(key string) (Session, bool) {
v, ok := s.Streams.Load(key)
if !ok {
return Session{}, false
}
return v.(Session), true
}
func (s *UAS) GetSessionByURL(url string) (string, Session) {
var k string
var result Session
s.Streams.Range(func(key, value interface{}) bool {
stream := value.(Session)
if stream.URL == url {
k = key.(string)
result = stream
return false // break
}
return true // continue
})
return k, result
}
func (s *UAS) RemoveSession(key string) {
s.Streams.Delete(key)
}
func (s *UAS) InitMediaServer(req models.InviteRequest) error {
s.mediaLock.Lock()
defer s.mediaLock.Unlock()
mediaServer, err := MediaDB.GetMediaServer(req.MediaServerId)
if err != nil {
return errors.Wrapf(err, "get media server error")
}
if s.media != nil && s.media.GetAddr() == fmt.Sprintf("%s:%d", mediaServer.IP, mediaServer.Port) {
return nil
}
switch mediaServer.Type {
case "SRS", "srs":
s.media = &media.Srs{
Ctx: s.ctx,
Schema: "http",
Addr: fmt.Sprintf("%s:%d", mediaServer.IP, mediaServer.Port),
Username: mediaServer.Username,
Password: mediaServer.Password,
}
case "ZLM", "zlm":
s.media = &media.Zlm{
Ctx: s.ctx,
Schema: "http",
Addr: fmt.Sprintf("%s:%d", mediaServer.IP, mediaServer.Port),
Secret: mediaServer.Secret,
}
default:
return errors.Errorf("unsupported media server type: %s", mediaServer.Type)
}
return nil
}
func (s *UAS) Invite(req models.InviteRequest) (*Session, error) {
key := fmt.Sprintf("%d:%s:%s:%d:%d:%d:%d", req.MediaServerId, req.DeviceID, req.ChannelID, req.SubStream, req.PlayType, req.StartTime, req.EndTime)
// Check if stream already exists
if s.isPublishing(key) {
// Stream exists, increase reference count
c, _ := s.GetSession(key)
c.RefCount++
s.AddSession(key, c)
return &c, nil
}
ssrc := utils.CreateSSRC(req.PlayType == 0)
err := s.InitMediaServer(req)
if err != nil {
return nil, errors.Wrapf(err, "init media server error")
}
mediaPort, err := s.media.Publish(ssrc, ssrc)
if err != nil {
return nil, errors.Wrapf(err, "api gb publish request error")
}
mediaHost := strings.Split(s.media.GetAddr(), ":")[0]
if mediaHost == "" {
return nil, errors.Errorf("media host is empty")
}
sessionName := utils.GetSessionName(req.PlayType)
sdpInfo := []string{
"v=0",
fmt.Sprintf("o=%s 0 0 IN IP4 %s", req.ChannelID, mediaHost),
"s=" + sessionName,
"u=" + req.ChannelID + ":0",
"c=IN IP4 " + mediaHost,
"t=" + fmt.Sprintf("%d %d", req.StartTime, req.EndTime),
fmt.Sprintf("m=video %d TCP/RTP/AVP 96", mediaPort),
"a=recvonly",
"a=rtpmap:96 PS/90000",
"y=" + ssrc,
"\r\n",
}
if true { // support tcp only
sdpInfo = append(sdpInfo, "a=setup:passive", "a=connection:new")
}
// TODO: 需要考虑不同设备通道ID相同的情况
d, ok := DM.GetDeviceInfoByChannel(req.ChannelID)
if !ok {
return nil, errors.Errorf("device not found by %s", req.ChannelID)
}
subject := fmt.Sprintf("%s:%s,%s:0", req.ChannelID, ssrc, s.conf.GB28181.Serial)
reqInvite, err := stack.NewInviteRequest([]byte(strings.Join(sdpInfo, "\r\n")), subject, stack.OutboundConfig{
Via: d.SourceAddr,
To: d.DeviceID,
From: s.conf.GB28181.Serial,
Transport: d.NetworkType,
})
if err != nil {
return nil, errors.Wrapf(err, "build invite request error")
}
res, err := s.handleSipTransaction(reqInvite)
if err != nil {
return nil, err
}
ack := sip.NewAckRequest(reqInvite, res, nil)
s.sipCli.WriteRequest(ack)
session := Session{
ID: req.ChannelID,
ParentID: req.DeviceID,
MediaHost: mediaHost,
MediaPort: mediaPort,
Ssrc: ssrc,
Status: "ON",
URL: s.media.GetWebRTCAddr(ssrc),
RefCount: 1,
InviteReq: reqInvite,
InviteRes: res,
}
s.AddSession(key, session)
return &session, nil
}
func (s *UAS) isPublishing(key string) bool {
c, ok := s.GetSession(key)
if !ok {
return false
}
// Check if stream already exists
if p, err := s.media.GetStreamStatus(c.Ssrc); err != nil || !p {
return false
}
return true
}
func (s *UAS) Bye(req models.ByeRequest) error {
key, session := s.GetSessionByURL(req.URL)
if key == "" {
return errors.Errorf("stream not found: %s", req.URL)
}
session.RefCount--
if session.RefCount > 0 {
s.AddSession(key, session)
return nil
}
defer func() {
if err := s.media.Unpublish(session.Ssrc); err != nil {
logger.Ef(s.ctx, "unpublish stream error: %s", err)
}
s.RemoveSession(key)
}()
reqBye := session.NewByeRequest()
_, err := s.handleSipTransaction(reqBye)
if err != nil {
return err
}
return nil
}
func (s *UAS) Pause(req models.PauseRequest) error {
key, session := s.GetSessionByURL(req.URL)
if key == "" {
return errors.Errorf("stream not found: %s", req.URL)
}
pauseRequest := session.NewPauseRequest()
_, err := s.handleSipTransaction(pauseRequest)
if err != nil {
return err
}
return nil
}
func (s *UAS) Resume(req models.ResumeRequest) error {
key, session := s.GetSessionByURL(req.URL)
if key == "" {
return errors.Errorf("stream not found: %s", req.URL)
}
resumeRequest := session.NewResumeRequest()
_, err := s.handleSipTransaction(resumeRequest)
if err != nil {
return err
}
return nil
}
func (s *UAS) Speed(req models.SpeedRequest) error {
key, session := s.GetSessionByURL(req.URL)
if key == "" {
return errors.Errorf("stream not found: %s", req.URL)
}
speedRequest := session.NewSpeedRequest(req.Speed)
_, err := s.handleSipTransaction(speedRequest)
if err != nil {
return err
}
return nil
}
func (s *UAS) Catalog(deviceID string) error {
var CatalogXML = `<?xml version="1.0"?><Query>
<CmdType>Catalog</CmdType>
<SN>%d</SN>
<DeviceID>%s</DeviceID>
</Query>
`
d, ok := DM.GetDevice(deviceID)
if !ok {
return errors.Errorf("device %s not found", deviceID)
}
body := fmt.Sprintf(CatalogXML, s.getSN(), deviceID)
req, err := stack.NewMessageRequest([]byte(body), stack.OutboundConfig{
Via: d.SourceAddr,
To: d.DeviceID,
From: s.conf.GB28181.Serial,
Transport: d.NetworkType,
})
if err != nil {
return errors.Wrapf(err, "build catalog request error")
}
_, err = s.handleSipTransaction(req)
if err != nil {
return err
}
return nil
}
func (s *UAS) waitAnswer(tx sip.ClientTransaction) (*sip.Response, error) {
select {
case <-s.ctx.Done():
return nil, errors.Errorf("context done")
case res := <-tx.Responses():
if res.StatusCode == 100 || res.StatusCode == 101 || res.StatusCode == 180 || res.StatusCode == 183 {
return s.waitAnswer(tx)
}
return res, nil
}
}
// <?xml version="1.0"?>
// <Control>
// <CmdType>DeviceControl</CmdType>
// <SN>474</SN>
// <DeviceID>33010602001310019325</DeviceID>
// <PTZCmd>a50f4d0190000092</PTZCmd>
// <Info>
// <ControlPriority>150</ControlPriority>
// </Info>
// </Control>
func (s *UAS) ControlPTZ(deviceID, channelID, ptz, speed string) error {
var ptzXML = `<?xml version="1.0"?>
<Control>
<CmdType>DeviceControl</CmdType>
<SN>%d</SN>
<DeviceID>%s</DeviceID>
<PTZCmd>%s</PTZCmd>
<Info>
<ControlPriority>150</ControlPriority>
</Info>
</Control>
`
// d, ok := DM.GetDevice(deviceID)
d, ok := DM.GetDeviceInfoByChannel(channelID)
if !ok {
return errors.Errorf("device %s not found", deviceID)
}
ptzCmd, err := toPTZCmd(ptz, speed)
if err != nil {
return errors.Wrapf(err, "build ptz command error")
}
body := fmt.Sprintf(ptzXML, s.getSN(), channelID, ptzCmd)
req, err := stack.NewMessageRequest([]byte(body), stack.OutboundConfig{
Via: d.SourceAddr,
To: d.DeviceID,
From: s.conf.GB28181.Serial,
Transport: d.NetworkType,
})
if err != nil {
return errors.Wrapf(err, "build ptz request error")
}
_, err = s.handleSipTransaction(req)
return err
}
// QueryRecord 查询录像记录
func (s *UAS) QueryRecord(deviceID, channelID string, startTime, endTime int64) ([]*models.Record, error) {
var queryXML = `<?xml version="1.0"?>
<Query>
<CmdType>RecordInfo</CmdType>
<SN>%d</SN>
<DeviceID>%s</DeviceID>
<StartTime>%s</StartTime>
<EndTime>%s</EndTime>
<Secrecy>0</Secrecy>
<Type>all</Type>
</Query>
`
d, ok := DM.GetDeviceInfoByChannel(channelID)
if !ok {
return nil, errors.Errorf("device %s not found", deviceID)
}
// 时间原本是unix时间戳需要转换为YYYY-MM-DDTHH:MM:SS
startTimeStr := time.Unix(startTime, 0).Format("2006-01-02T00:00:00")
endTimeStr := time.Unix(endTime, 0).Format("2006-01-02T15:04:05")
body := fmt.Sprintf(queryXML, s.getSN(), channelID, startTimeStr, endTimeStr)
req, err := stack.NewMessageRequest([]byte(body), stack.OutboundConfig{
Via: d.SourceAddr,
To: d.DeviceID,
From: s.conf.GB28181.Serial,
Transport: d.NetworkType,
})
if err != nil {
return nil, errors.Wrapf(err, "build query request error")
}
if _, err := s.handleSipTransaction(req); err != nil {
return nil, err
}
// 创建一个通道来接收录像查询结果
resultChan := make(chan *models.XmlMessageInfo, 1)
s.recordQueryResults.Store(channelID, resultChan)
defer s.recordQueryResults.Delete(channelID)
// 等待结果或超时
var allRecords []*models.Record
timeout := time.After(10 * time.Second)
for {
select {
case <-timeout:
return allRecords, errors.Errorf("query record timeout after 30s")
case <-s.ctx.Done():
return nil, errors.Errorf("context done")
case records := <-resultChan:
allRecords = append(allRecords, records.RecordList...)
logger.Tf(s.ctx, "[channel %s] 应收总数 %d, 实收总数 %d, 本次收到 %d", channelID, records.SumNum, len(allRecords), len(records.RecordList))
if len(allRecords) == records.SumNum {
return allRecords, nil
}
}
}
}
func (s *UAS) handleSipTransaction(req *sip.Request) (*sip.Response, error) {
tx, err := s.sipCli.TransactionRequest(s.ctx, req)
if err != nil {
return nil, errors.Wrapf(err, "transaction request error")
}
res, err := s.waitAnswer(tx)
if err != nil {
return nil, errors.Wrapf(err, "wait answer error")
}
if res.StatusCode != 200 {
return nil, errors.Errorf("response error: %s", res.String())
}
return res, nil
}

81
pkg/service/ptz.go Normal file
View File

@ -0,0 +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
}

View File

@ -12,7 +12,7 @@ type OutboundConfig struct {
To string
}
func newRequest(method sip.RequestMethod, body []byte, conf OutboundConfig) (*sip.Request, error) {
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")
}
@ -37,7 +37,7 @@ func newRequest(method sip.RequestMethod, body []byte, conf OutboundConfig) (*si
}
func NewRegisterRequest(conf OutboundConfig) (*sip.Request, error) {
req, err := newRequest(sip.REGISTER, nil, conf)
req, err := NewRequest(sip.REGISTER, nil, conf)
if err != nil {
return nil, err
}
@ -47,7 +47,7 @@ func NewRegisterRequest(conf OutboundConfig) (*sip.Request, error) {
}
func NewInviteRequest(body []byte, subject string, conf OutboundConfig) (*sip.Request, error) {
req, err := newRequest(sip.INVITE, body, conf)
req, err := NewRequest(sip.INVITE, body, conf)
if err != nil {
return nil, err
}
@ -57,8 +57,8 @@ func NewInviteRequest(body []byte, subject string, conf OutboundConfig) (*sip.Re
return req, nil
}
func NewCatelogRequest(body []byte, conf OutboundConfig) (*sip.Request, error) {
req, err := newRequest(sip.MESSAGE, body, conf)
func NewMessageRequest(body []byte, conf OutboundConfig) (*sip.Request, error) {
req, err := NewRequest(sip.MESSAGE, body, conf)
if err != nil {
return nil, err
}

View File

@ -1,6 +1,7 @@
package stack
import (
"fmt"
"time"
"github.com/emiago/sipgo/sip"
@ -9,7 +10,7 @@ import (
const TIME_LAYOUT = "2024-01-01T00:00:00"
const EXPIRES_TIME = 3600
func NewRegisterResponse(req *sip.Request, code sip.StatusCode, reason string) *sip.Response {
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()}
@ -17,9 +18,24 @@ func NewRegisterResponse(req *sip.Request, code sip.StatusCode, reason string) *
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
}

View File

@ -81,7 +81,7 @@ func (c *UAC) doRegister() error {
From: "34020000001110000001",
To: "34020000002000000001",
Transport: "UDP",
Via: fmt.Sprintf("%s:%d", c.LocalIP, c.conf.SipPort),
Via: fmt.Sprintf("%s:%d", c.LocalIP, c.conf.GB28181.Port),
})
tx, err := c.sipCli.TransactionRequest(c.ctx, r)
if err != nil {

View File

@ -5,27 +5,31 @@ import (
"errors"
"fmt"
"net"
"os"
"sync"
"github.com/emiago/sipgo"
"github.com/emiago/sipgo/sip"
"github.com/ossrs/go-oryx-lib/logger"
"github.com/ossrs/srs-sip/pkg/config"
"github.com/ossrs/srs-sip/pkg/signaling"
"github.com/ossrs/srs-sip/pkg/db"
"github.com/ossrs/srs-sip/pkg/media"
)
type UAS struct {
*Cascade
SN uint32
channelsStatue sync.Map
signal signaling.ISignaling
SN uint32
Streams sync.Map
mediaLock sync.Mutex
media media.IMedia
recordQueryResults sync.Map // channelID -> chan []Record
sipConnUDP *net.UDPConn
sipConnTCP *net.TCPListener
}
var DM = GetDeviceManager()
var MediaDB, _ = db.GetInstance("./media_servers.db")
func NewUas() *UAS {
return &UAS{
@ -35,12 +39,6 @@ func NewUas() *UAS {
func (s *UAS) Start(agent *sipgo.UserAgent, r0 interface{}) error {
ctx := context.Background()
conf := r0.(*config.MainConfig)
sig := &signaling.Srs{
Ctx: ctx,
Addr: "http://" + conf.MediaAddr,
}
s.signal = sig
s.startSipServer(agent, ctx, r0)
return nil
}
@ -86,19 +84,23 @@ func (s *UAS) startSipServer(agent *sipgo.UserAgent, ctx context.Context, r0 int
return err
}
candidate := os.Getenv("CANDIDATE")
if candidate != "" {
MediaDB.AddMediaServer("Default", "SRS", candidate, 1985, "", "", "", 1)
}
return nil
}
func (s *UAS) startUDP() error {
lis, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: s.conf.SipPort,
Port: s.conf.GB28181.Port,
})
if err != nil {
return fmt.Errorf("cannot listen on the UDP signaling port %d: %w", s.conf.SipPort, err)
return fmt.Errorf("cannot listen on the UDP signaling port %d: %w", s.conf.GB28181.Port, err)
}
s.sipConnUDP = lis
logger.Tf(s.ctx, "sip signaling listening on UDP %s:%d", lis.LocalAddr().String(), s.conf.SipPort)
logger.Tf(s.ctx, "sip signaling listening on UDP %s:%d", lis.LocalAddr().String(), s.conf.GB28181.Port)
go func() {
if err := s.sipSvr.ServeUDP(lis); err != nil {
@ -111,13 +113,13 @@ func (s *UAS) startUDP() error {
func (s *UAS) startTCP() error {
lis, err := net.ListenTCP("tcp", &net.TCPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: s.conf.SipPort,
Port: s.conf.GB28181.Port,
})
if err != nil {
return fmt.Errorf("cannot listen on the TCP signaling port %d: %w", s.conf.SipPort, err)
return fmt.Errorf("cannot listen on the TCP signaling port %d: %w", s.conf.GB28181.Port, err)
}
s.sipConnTCP = lis
logger.Tf(s.ctx, "sip signaling listening on TCP %s:%d", lis.Addr().String(), s.conf.SipPort)
logger.Tf(s.ctx, "sip signaling listening on TCP %s:%d", lis.Addr().String(), s.conf.GB28181.Port)
go func() {
if err := s.sipSvr.ServeTCP(lis); err != nil && !errors.Is(err, net.ErrClosed) {
@ -127,10 +129,6 @@ func (s *UAS) startTCP() error {
return nil
}
func sipErrorResponse(tx sip.ServerTransaction, req *sip.Request) {
_ = tx.Respond(sip.NewResponseFromRequest(req, 400, "", nil))
}
func (s *UAS) getSN() uint32 {
s.SN++
return s.SN