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

View File

@ -1,137 +0,0 @@
package api
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/ossrs/srs-sip/pkg/service"
)
func (h *HttpApiServer) RegisterRoutes(router *mux.Router) {
apiV1Router := router.PathPrefix("/srs-sip/v1").Subrouter()
// Add Auth middleware
//apiV1Router.Use(authMiddleware)
apiV1Router.HandleFunc("/devices", h.ApiListDevices).Methods(http.MethodGet)
apiV1Router.HandleFunc("/devices/{id}/channels", h.ApiGetChannelByDeviceId).Methods(http.MethodGet)
apiV1Router.HandleFunc("/channels", h.ApiGetAllChannels).Methods(http.MethodGet)
apiV1Router.HandleFunc("/invite", h.ApiInvite).Methods(http.MethodPost)
apiV1Router.HandleFunc("/bye", h.ApiBye).Methods(http.MethodPost)
apiV1Router.HandleFunc("/ptz", h.ApiPTZControl).Methods(http.MethodPost)
apiV1Router.HandleFunc("", h.GetAPIRoutes(apiV1Router)).Methods(http.MethodGet)
router.HandleFunc("/srs-sip", h.ApiGetAPIVersion).Methods(http.MethodGet)
}
func (h *HttpApiServer) RespondWithJSON(w http.ResponseWriter, code int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
wrapper := map[string]interface{}{
"code": code,
"data": data,
}
json.NewEncoder(w).Encode(wrapper)
}
func (h *HttpApiServer) RespondWithJSONSimple(w http.ResponseWriter, jsonStr string) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(jsonStr))
}
func (h *HttpApiServer) GetAPIRoutes(router *mux.Router) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var routes []map[string]string
router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
path, err := route.GetPathTemplate()
if err != nil {
return err
}
methods, err := route.GetMethods()
if err != nil {
return err
}
for _, method := range methods {
routes = append(routes, map[string]string{
"method": method,
"path": path,
})
}
return nil
})
h.RespondWithJSON(w, 0, routes)
}
}
func (h *HttpApiServer) ApiGetAPIVersion(w http.ResponseWriter, r *http.Request) {
h.RespondWithJSONSimple(w, `{"version": "v1"}`)
}
func (h *HttpApiServer) ApiListDevices(w http.ResponseWriter, r *http.Request) {
list := service.DM.GetDevices()
h.RespondWithJSON(w, 0, list)
}
func (h *HttpApiServer) ApiGetChannelByDeviceId(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
channels := service.DM.ApiGetChannelByDeviceId(id)
h.RespondWithJSON(w, 0, channels)
}
func (h *HttpApiServer) ApiGetAllChannels(w http.ResponseWriter, r *http.Request) {
channels := service.DM.GetAllVideoChannels()
h.RespondWithJSON(w, 0, channels)
}
// request: {"device_id": "1", "channel_id": "1", "sub_stream": 0}
// response: {"code": 0, "data": {"channel_id": "1", "url": "webrtc://"}}
func (h *HttpApiServer) ApiInvite(w http.ResponseWriter, r *http.Request) {
// Parse request
var req map[string]string
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
// Get device and channel
deviceID := req["device_id"]
channelID := req["channel_id"]
//subStream := req["sub_stream"]
code := 0
url := ""
defer func() {
data := map[string]string{
"channel_id": channelID,
"url": url,
}
h.RespondWithJSON(w, code, data)
}()
if err := h.sipSvr.Uas.Invite(deviceID, channelID); err != nil {
code = http.StatusInternalServerError
return
}
c, ok := h.sipSvr.Uas.GetVideoChannelStatue(channelID)
if !ok {
code = http.StatusInternalServerError
return
}
url = "webrtc://" + h.conf.MediaAddr + "/live/" + c.Ssrc
}
func (h *HttpApiServer) ApiBye(w http.ResponseWriter, r *http.Request) {
h.RespondWithJSONSimple(w, `{"msg":"Not implemented"}`)
}
func (h *HttpApiServer) ApiPTZControl(w http.ResponseWriter, r *http.Request) {
h.RespondWithJSONSimple(w, `{"msg":"Not implemented"}`)
}

View File

@ -2,13 +2,10 @@ package api
import (
"context"
"fmt"
"net/http"
"github.com/ossrs/go-oryx-lib/logger"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/ossrs/go-oryx-lib/logger"
"github.com/ossrs/srs-sip/pkg/config"
"github.com/ossrs/srs-sip/pkg/service"
)
@ -25,21 +22,24 @@ func NewHttpApiServer(r0 interface{}, svr *service.Service) (*HttpApiServer, err
}, nil
}
func (h *HttpApiServer) Start() {
router := mux.NewRouter().StrictSlash(true)
h.RegisterRoutes(router)
func (h *HttpApiServer) Start(router *mux.Router) {
// 添加版本检查路由到主路由器
router.HandleFunc("/srs-sip", h.ApiGetAPIVersion).Methods(http.MethodGet)
headers := handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"})
methods := handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS"})
origins := handlers.AllowedOrigins([]string{"*"})
// 创建一个子路由所有API都以/srs-sip/v1为前缀
apiRouter := router.PathPrefix("/srs-sip/v1").Subrouter()
go func() {
ctx := context.Background()
addr := fmt.Sprintf(":%v", h.conf.APIPort)
logger.Tf(ctx, "http api listen on %s", addr)
err := http.ListenAndServe(addr, handlers.CORS(headers, methods, origins)(router))
if err != nil {
panic(err)
}
}()
logger.Tf(context.Background(), "Registering API routes under /srs-sip/v1")
h.RegisterRoutes(apiRouter)
// 打印所有注册的路由,包含更详细的信息
router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
pathTemplate, _ := route.GetPathTemplate()
pathRegexp, _ := route.GetPathRegexp()
methods, _ := route.GetMethods()
queries, _ := route.GetQueriesTemplates()
logger.Tf(context.Background(), "Route Details: Path=%v, Regexp=%v, Methods=%v, Queries=%v",
pathTemplate, pathRegexp, methods, queries)
return nil
})
}

284
pkg/api/controller.go Normal file
View File

@ -0,0 +1,284 @@
package api
import (
"encoding/json"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/ossrs/srs-sip/pkg/models"
"github.com/ossrs/srs-sip/pkg/service"
)
func (h *HttpApiServer) RegisterRoutes(router *mux.Router) {
// Add Auth middleware
//apiV1Router.Use(authMiddleware)
router.HandleFunc("/devices", h.ApiListDevices).Methods(http.MethodGet)
router.HandleFunc("/devices/{id}/channels", h.ApiGetChannelByDeviceId).Methods(http.MethodGet)
router.HandleFunc("/channels", h.ApiGetAllChannels).Methods(http.MethodGet)
router.HandleFunc("/invite", h.ApiInvite).Methods(http.MethodPost)
router.HandleFunc("/bye", h.ApiBye).Methods(http.MethodPost)
router.HandleFunc("/ptz", h.ApiPTZControl).Methods(http.MethodPost)
router.HandleFunc("/pause", h.ApiPause).Methods(http.MethodPost)
router.HandleFunc("/resume", h.ApiResume).Methods(http.MethodPost)
router.HandleFunc("/speed", h.ApiSpeed).Methods(http.MethodPost)
router.HandleFunc("/query-record", h.ApiQueryRecord).Methods(http.MethodPost)
// 媒体服务器相关接口查询新增删除用restful风格
router.HandleFunc("/media-servers", h.ApiListMediaServers).Methods(http.MethodGet)
router.HandleFunc("/media-servers", h.ApiAddMediaServer).Methods(http.MethodPost)
router.HandleFunc("/media-servers/{id}", h.ApiDeleteMediaServer).Methods(http.MethodDelete)
router.HandleFunc("/media-servers/default/{id}", h.ApiSetDefaultMediaServer).Methods(http.MethodPost)
router.HandleFunc("", h.GetAPIRoutes(router)).Methods(http.MethodGet)
}
func (h *HttpApiServer) RespondWithJSON(w http.ResponseWriter, code int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
wrapper := models.CommonResponse{
Code: code,
Data: data,
}
json.NewEncoder(w).Encode(wrapper)
}
func (h *HttpApiServer) RespondWithJSONSimple(w http.ResponseWriter, jsonStr string) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(jsonStr))
}
func (h *HttpApiServer) GetAPIRoutes(router *mux.Router) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var routes []map[string]string
router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
path, err := route.GetPathTemplate()
if err != nil {
return err
}
methods, err := route.GetMethods()
if err != nil {
return err
}
for _, method := range methods {
routes = append(routes, map[string]string{
"method": method,
"path": path,
})
}
return nil
})
h.RespondWithJSON(w, 0, routes)
}
}
func (h *HttpApiServer) ApiGetAPIVersion(w http.ResponseWriter, r *http.Request) {
h.RespondWithJSONSimple(w, `{"version": "v1"}`)
}
func (h *HttpApiServer) ApiListDevices(w http.ResponseWriter, r *http.Request) {
list := service.DM.GetDevices()
h.RespondWithJSON(w, 0, list)
}
func (h *HttpApiServer) ApiGetChannelByDeviceId(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
channels := service.DM.ApiGetChannelByDeviceId(id)
h.RespondWithJSON(w, 0, channels)
}
func (h *HttpApiServer) ApiGetAllChannels(w http.ResponseWriter, r *http.Request) {
channels := service.DM.GetAllVideoChannels()
h.RespondWithJSON(w, 0, channels)
}
func (h *HttpApiServer) ApiInvite(w http.ResponseWriter, r *http.Request) {
var req models.InviteRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
h.RespondWithJSON(w, http.StatusBadRequest, map[string]string{"msg": err.Error()})
return
}
session, err := h.sipSvr.Uas.Invite(req)
if err != nil {
h.RespondWithJSON(w, http.StatusInternalServerError, map[string]string{"msg": err.Error()})
return
}
response := models.InviteResponse{
ChannelID: req.ChannelID,
URL: session.URL,
}
h.RespondWithJSON(w, 0, response)
}
func (h *HttpApiServer) ApiBye(w http.ResponseWriter, r *http.Request) {
var req models.ByeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.RespondWithJSON(w, http.StatusBadRequest, map[string]string{"msg": err.Error()})
return
}
if err := h.sipSvr.Uas.Bye(req); err != nil {
h.RespondWithJSON(w, http.StatusInternalServerError, map[string]string{"msg": err.Error()})
return
}
h.RespondWithJSON(w, 0, map[string]string{"msg": "success"})
}
func (h *HttpApiServer) ApiPause(w http.ResponseWriter, r *http.Request) {
var req models.PauseRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.RespondWithJSON(w, http.StatusBadRequest, map[string]string{"msg": err.Error()})
return
}
if err := h.sipSvr.Uas.Pause(req); err != nil {
h.RespondWithJSON(w, http.StatusInternalServerError, map[string]string{"msg": err.Error()})
return
}
h.RespondWithJSON(w, 0, map[string]string{"msg": "success"})
}
func (h *HttpApiServer) ApiResume(w http.ResponseWriter, r *http.Request) {
var req models.ResumeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.RespondWithJSON(w, http.StatusBadRequest, map[string]string{"msg": err.Error()})
return
}
if err := h.sipSvr.Uas.Resume(req); err != nil {
h.RespondWithJSON(w, http.StatusInternalServerError, map[string]string{"msg": err.Error()})
return
}
h.RespondWithJSON(w, 0, map[string]string{"msg": "success"})
}
func (h *HttpApiServer) ApiSpeed(w http.ResponseWriter, r *http.Request) {
var req models.SpeedRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.RespondWithJSON(w, http.StatusBadRequest, map[string]string{"msg": err.Error()})
return
}
if err := h.sipSvr.Uas.Speed(req); err != nil {
h.RespondWithJSON(w, http.StatusInternalServerError, map[string]string{"msg": err.Error()})
return
}
h.RespondWithJSON(w, 0, map[string]string{"msg": "success"})
}
// request: {"device_id": "1", "channel_id": "1", "ptz": "up", "speed": "1}
func (h *HttpApiServer) ApiPTZControl(w http.ResponseWriter, r *http.Request) {
var req models.PTZControlRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
h.RespondWithJSON(w, http.StatusBadRequest, map[string]string{"msg": err.Error()})
return
}
code := 0
msg := ""
defer func() {
h.RespondWithJSON(w, code, map[string]string{"msg": msg})
}()
if err := h.sipSvr.Uas.ControlPTZ(req.DeviceID, req.ChannelID, req.PTZ, req.Speed); err != nil {
code = http.StatusInternalServerError
msg = err.Error()
return
}
msg = "success"
}
func (h *HttpApiServer) ApiQueryRecord(w http.ResponseWriter, r *http.Request) {
var req models.QueryRecordRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
h.RespondWithJSON(w, http.StatusBadRequest, map[string]string{"msg": err.Error()})
return
}
records, err := h.sipSvr.Uas.QueryRecord(req.DeviceID, req.ChannelID, req.StartTime, req.EndTime)
if err != nil {
h.RespondWithJSON(w, http.StatusInternalServerError, map[string]string{"msg": err.Error()})
return
}
h.RespondWithJSON(w, 0, records)
}
func (h *HttpApiServer) ApiListMediaServers(w http.ResponseWriter, r *http.Request) {
servers, err := service.MediaDB.ListMediaServers()
if err != nil {
h.RespondWithJSON(w, http.StatusInternalServerError, map[string]string{"msg": err.Error()})
return
}
h.RespondWithJSON(w, 0, servers)
}
// request: {"name": "srs1", "ip": "192.168.1.100", "port": 1935, "type": "SRS", "username": "admin", "password": "123456"}
func (h *HttpApiServer) ApiAddMediaServer(w http.ResponseWriter, r *http.Request) {
var req models.MediaServerRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.RespondWithJSON(w, http.StatusBadRequest, map[string]string{"msg": err.Error()})
return
}
// 验证必填字段
if req.Name == "" || req.IP == "" || req.Port == 0 || req.Type == "" {
h.RespondWithJSON(w, http.StatusBadRequest, map[string]string{"msg": "name, ip, port and type are required"})
return
}
// 添加到数据库
if err := service.MediaDB.AddMediaServer(req.Name, req.Type, req.IP, req.Port, req.Username, req.Password, req.Secret, req.IsDefault); err != nil {
h.RespondWithJSON(w, http.StatusInternalServerError, map[string]string{"msg": err.Error()})
return
}
h.RespondWithJSON(w, 0, map[string]string{"msg": "success"})
}
func (h *HttpApiServer) ApiDeleteMediaServer(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
h.RespondWithJSON(w, http.StatusBadRequest, map[string]string{"msg": "invalid id"})
return
}
if err := service.MediaDB.DeleteMediaServer(id); err != nil {
h.RespondWithJSON(w, http.StatusInternalServerError, map[string]string{"msg": err.Error()})
return
}
h.RespondWithJSON(w, 0, map[string]string{"msg": "success"})
}
func (h *HttpApiServer) ApiSetDefaultMediaServer(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
h.RespondWithJSON(w, http.StatusBadRequest, map[string]string{"msg": "invalid id"})
return
}
if err := service.MediaDB.SetDefaultMediaServer(id); err != nil {
h.RespondWithJSON(w, http.StatusInternalServerError, map[string]string{"msg": err.Error()})
return
}
h.RespondWithJSON(w, 0, map[string]string{"msg": "success"})
}

View File

@ -3,16 +3,85 @@ package config
import (
"fmt"
"net"
"os"
"gopkg.in/yaml.v3"
)
// 通用配置
type CommonConfig struct {
LogLevel string `yaml:"log-level"`
LogFile string `yaml:"log-file"`
}
// GB28181配置
type GB28181AuthConfig struct {
Enable bool `yaml:"enable"`
Password string `yaml:"password"`
}
type GB28181Config struct {
Serial string `yaml:"serial"`
Realm string `yaml:"realm"`
Host string `yaml:"host"`
Port int `yaml:"port"`
Auth GB28181AuthConfig `yaml:"auth"`
}
// HTTP服务配置
type HttpConfig struct {
Port int `yaml:"listen"`
Dir string `yaml:"dir"`
}
// 主配置结构
type MainConfig struct {
Serial string `ymal:"serial"`
Realm string `ymal:"realm"`
SipHost string `ymal:"sip-host"`
SipPort int `ymal:"sip-port"`
MediaAddr string `ymal:"media-addr"`
HttpServerPort int `ymal:"http-server-port"`
APIPort int `ymal:"api-port"`
Common CommonConfig `yaml:"common"`
GB28181 GB28181Config `yaml:"gb28181"`
Http HttpConfig `yaml:"http"`
}
// 获取默认配置
func DefaultConfig() *MainConfig {
return &MainConfig{
Common: CommonConfig{
LogLevel: "info",
LogFile: "app.log",
},
GB28181: GB28181Config{
Serial: "34020000002000000001",
Realm: "3402000000",
Host: "0.0.0.0",
Port: 5060,
Auth: GB28181AuthConfig{
Enable: false,
Password: "123456",
},
},
Http: HttpConfig{
Port: 8025,
Dir: "./html",
},
}
}
func LoadConfig(filename string) (*MainConfig, error) {
// 如果配置文件不存在,返回默认配置
if _, err := os.Stat(filename); os.IsNotExist(err) {
return DefaultConfig(), nil
}
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("read config file failed: %v", err)
}
var config MainConfig
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("parse config file failed: %v", err)
}
return &config, nil
}
func GetLocalIP() (string, error) {

121
pkg/db/media_server.go Normal file
View File

@ -0,0 +1,121 @@
package db
import (
"database/sql"
"sync"
"github.com/ossrs/srs-sip/pkg/models"
_ "modernc.org/sqlite"
)
var (
instance *MediaServerDB
once sync.Once
)
type MediaServerDB struct {
models.MediaServerResponse
db *sql.DB
}
// GetInstance 返回 MediaServerDB 的单例实例
func GetInstance(dbPath string) (*MediaServerDB, error) {
var err error
once.Do(func() {
instance, err = NewMediaServerDB(dbPath)
})
if err != nil {
return nil, err
}
return instance, nil
}
func NewMediaServerDB(dbPath string) (*MediaServerDB, error) {
db, err := sql.Open("sqlite", dbPath)
if err != nil {
return nil, err
}
// 创建媒体服务器表
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS media_servers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL,
name TEXT NOT NULL,
ip TEXT NOT NULL,
port INTEGER NOT NULL,
username TEXT,
password TEXT,
secret TEXT,
is_default INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
if err != nil {
return nil, err
}
return &MediaServerDB{db: db}, nil
}
func (m *MediaServerDB) AddMediaServer(name, serverType, ip string, port int, username, password, secret string, isDefault int) error {
_, err := m.db.Exec(`
INSERT INTO media_servers (name, type, ip, port, username, password, secret, is_default)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, name, serverType, ip, port, username, password, secret, isDefault)
return err
}
func (m *MediaServerDB) DeleteMediaServer(id int) error {
_, err := m.db.Exec("DELETE FROM media_servers WHERE id = ?", id)
return err
}
func (m *MediaServerDB) GetMediaServer(id int) (*models.MediaServerResponse, error) {
var ms models.MediaServerResponse
err := m.db.QueryRow(`
SELECT id, name, type, ip, port, username, password, secret, is_default, created_at
FROM media_servers WHERE id = ?
`, id).Scan(&ms.ID, &ms.Name, &ms.Type, &ms.IP, &ms.Port, &ms.Username, &ms.Password, &ms.Secret, &ms.IsDefault, &ms.CreatedAt)
if err != nil {
return nil, err
}
return &ms, nil
}
func (m *MediaServerDB) ListMediaServers() ([]models.MediaServerResponse, error) {
rows, err := m.db.Query(`
SELECT id, name, type, ip, port, username, password, secret, is_default, created_at
FROM media_servers ORDER BY created_at DESC
`)
if err != nil {
return nil, err
}
defer rows.Close()
var servers []models.MediaServerResponse
for rows.Next() {
var ms models.MediaServerResponse
err := rows.Scan(&ms.ID, &ms.Name, &ms.Type, &ms.IP, &ms.Port, &ms.Username, &ms.Password, &ms.Secret, &ms.IsDefault, &ms.CreatedAt)
if err != nil {
return nil, err
}
servers = append(servers, ms)
}
return servers, nil
}
func (m *MediaServerDB) SetDefaultMediaServer(id int) error {
// 先将所有服务器设置为非默认
if _, err := m.db.Exec("UPDATE media_servers SET is_default = 0"); err != nil {
return err
}
// 将指定ID的服务器设置为默认
_, err := m.db.Exec("UPDATE media_servers SET is_default = 1 WHERE id = ?", id)
return err
}
func (m *MediaServerDB) Close() error {
return m.db.Close()
}

View File

@ -1,75 +1,77 @@
package signaling
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"time"
"github.com/ossrs/go-oryx-lib/errors"
"github.com/ossrs/go-oryx-lib/logger"
)
type ISignaling interface {
Publish(id, ssrc string) (int, error)
Unpublish(id string) error
GetStreamStatus(id string) (bool, error)
}
// The r is HTTP API to request, like "http://localhost:1985/gb/v1/publish".
// The req is the HTTP request body, will be marshal to JSON object. nil is no body
// The res is the HTTP response body, already unmarshal to JSON object.
func apiRequest(ctx context.Context, r string, req interface{}, res interface{}) error {
var buf bytes.Buffer
if req != nil {
if err := json.NewEncoder(&buf).Encode(req); err != nil {
return errors.Wrapf(err, "Marshal body %v", req)
}
}
logger.Tf(ctx, "Request url api=%v with %v bytes", r, buf.Len())
method := "POST"
if req == nil {
method = "GET"
}
reqObj, err := http.NewRequest(method, r, &buf)
if err != nil {
return errors.Wrapf(err, "HTTP request %v", buf.String())
}
client := &http.Client{Timeout: 10 * time.Second}
resObj, err := client.Do(reqObj.WithContext(ctx))
if err != nil {
return errors.Wrapf(err, "Do HTTP request %v", buf.String())
}
defer resObj.Body.Close()
if resObj.StatusCode != http.StatusOK {
return errors.Errorf("Server returned status code=%v", resObj.StatusCode)
}
b2, err := io.ReadAll(resObj.Body)
if err != nil {
return errors.Wrapf(err, "Read response for %v", buf.String())
}
logger.Tf(ctx, "Response from %v is %v bytes", r, len(b2))
errorCode := struct {
Code int `json:"code"`
}{}
if err := json.Unmarshal(b2, &errorCode); err != nil {
return errors.Wrapf(err, "Unmarshal %v", string(b2))
}
if errorCode.Code != 0 {
return errors.Errorf("Server fail code=%v %v", errorCode.Code, string(b2))
}
if err := json.Unmarshal(b2, res); err != nil {
return errors.Wrapf(err, "Unmarshal %v", string(b2))
}
logger.Tf(ctx, "Parse response to code=%v ok, %v", errorCode.Code, res)
return nil
}
package media
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"time"
"github.com/ossrs/go-oryx-lib/errors"
"github.com/ossrs/go-oryx-lib/logger"
)
type IMedia interface {
Publish(id, ssrc string) (int, error)
Unpublish(id string) error
GetStreamStatus(id string) (bool, error)
GetAddr() string
GetWebRTCAddr(id string) string
}
// The r is HTTP API to request, like "http://localhost:1985/gb/v1/publish".
// The req is the HTTP request body, will be marshal to JSON object. nil is no body
// The res is the HTTP response body, already unmarshal to JSON object.
func apiRequest(ctx context.Context, r string, req interface{}, res interface{}) error {
var buf bytes.Buffer
if req != nil {
if err := json.NewEncoder(&buf).Encode(req); err != nil {
return errors.Wrapf(err, "Marshal body %v", req)
}
}
logger.Tf(ctx, "Request url api=%v with %v bytes", r, buf.Len())
method := "POST"
if req == nil {
method = "GET"
}
reqObj, err := http.NewRequest(method, r, &buf)
if err != nil {
return errors.Wrapf(err, "HTTP request %v", buf.String())
}
client := &http.Client{Timeout: 10 * time.Second}
resObj, err := client.Do(reqObj.WithContext(ctx))
if err != nil {
return errors.Wrapf(err, "Do HTTP request %v", buf.String())
}
defer resObj.Body.Close()
if resObj.StatusCode != http.StatusOK {
return errors.Errorf("Server returned status code=%v", resObj.StatusCode)
}
b2, err := io.ReadAll(resObj.Body)
if err != nil {
return errors.Wrapf(err, "Read response for %v", buf.String())
}
logger.Tf(ctx, "Response from %v is %v bytes", r, len(b2))
errorCode := struct {
Code int `json:"code"`
}{}
if err := json.Unmarshal(b2, &errorCode); err != nil {
return errors.Wrapf(err, "Unmarshal %v", string(b2))
}
if errorCode.Code != 0 {
return errors.Errorf("Server fail code=%v %v", errorCode.Code, string(b2))
}
if err := json.Unmarshal(b2, res); err != nil {
return errors.Wrapf(err, "Unmarshal %v", string(b2))
}
logger.Tf(ctx, "Parse response to code=%v ok, %v", errorCode.Code, res)
return nil
}

View File

@ -1,95 +1,106 @@
package signaling
import (
"context"
"github.com/ossrs/go-oryx-lib/errors"
)
type Srs struct {
Ctx context.Context
Addr string // The address of SRS, eg: http://localhost:1985
}
func (s *Srs) Publish(id, ssrc string) (int, error) {
req := struct {
Id string `json:"id"`
SSRC string `json:"ssrc"`
}{
id, ssrc,
}
res := struct {
Code int `json:"code"`
Port int `json:"port"`
}{}
if err := apiRequest(s.Ctx, s.Addr+"/gb/v1/publish/", req, &res); err != nil {
return 0, errors.Wrapf(err, "gb/v1/publish")
}
return res.Port, nil
}
func (s *Srs) Unpublish(id string) error {
return nil
}
// {
// "code": 0,
// "server": "vid-y19n6nm",
// "service": "382k456r",
// "pid": "9495",
// "streams": [{
// "id": "vid-9y0ozy0",
// "name": "0551954854",
// "vhost": "vid-v2ws53u",
// "app": "live",
// "tcUrl": "webrtc://127.0.0.1:1985/live",
// "url": "/live/0551954854",
// "live_ms": 1720428680003,
// "clients": 1,
// "frames": 8431,
// "send_bytes": 66463941,
// "recv_bytes": 89323998,
// "kbps": {
// "recv_30s": 0,
// "send_30s": 0
// },
// "publish": {
// "active": false,
// "cid": "b3op069g"
// },
// "video": null,
// "audio": null
// }]
// }
func (s *Srs) GetStreamStatus(id string) (bool, error) {
type Stream struct {
Id string `json:"id"`
Name string `json:"name"`
Publish struct {
Active bool `json:"active"`
Cid string `json:"cid"`
} `json:"publish"`
}
res := struct {
Code int `json:"code"`
Streams []Stream `json:"streams"`
}{}
if err := apiRequest(s.Ctx, s.Addr+"/api/v1/streams?count=99", nil, &res); err != nil {
return false, errors.Wrapf(err, "api/v1/stream")
}
if len(res.Streams) == 0 {
return false, nil
} else {
for _, v := range res.Streams {
if v.Name == id {
return v.Publish.Active, nil
}
}
}
return false, nil
}
package media
import (
"context"
"github.com/ossrs/go-oryx-lib/errors"
)
type Srs struct {
Ctx context.Context
Schema string // The schema of SRS, eg: http
Addr string // The address of SRS, eg: localhost:1985
Username string // The username of SRS, eg: admin
Password string // The password of SRS, eg: 123456
}
func (s *Srs) Publish(id, ssrc string) (int, error) {
req := struct {
Id string `json:"id"`
SSRC string `json:"ssrc"`
}{
id, ssrc,
}
res := struct {
Code int `json:"code"`
Port int `json:"port"`
}{}
if err := apiRequest(s.Ctx, s.Schema+"://"+s.Addr+"/gb/v1/publish/", req, &res); err != nil {
return 0, errors.Wrapf(err, "gb/v1/publish")
}
return res.Port, nil
}
func (s *Srs) Unpublish(id string) error {
return nil
}
// {
// "code": 0,
// "server": "vid-y19n6nm",
// "service": "382k456r",
// "pid": "9495",
// "streams": [{
// "id": "vid-9y0ozy0",
// "name": "0551954854",
// "vhost": "vid-v2ws53u",
// "app": "live",
// "tcUrl": "webrtc://127.0.0.1:1985/live",
// "url": "/live/0551954854",
// "live_ms": 1720428680003,
// "clients": 1,
// "frames": 8431,
// "send_bytes": 66463941,
// "recv_bytes": 89323998,
// "kbps": {
// "recv_30s": 0,
// "send_30s": 0
// },
// "publish": {
// "active": false,
// "cid": "b3op069g"
// },
// "video": null,
// "audio": null
// }]
// }
func (s *Srs) GetStreamStatus(id string) (bool, error) {
type Stream struct {
Id string `json:"id"`
Name string `json:"name"`
Publish struct {
Active bool `json:"active"`
Cid string `json:"cid"`
} `json:"publish"`
}
res := struct {
Code int `json:"code"`
Streams []Stream `json:"streams"`
}{}
if err := apiRequest(s.Ctx, s.Schema+"://"+s.Addr+"/api/v1/streams?count=99", nil, &res); err != nil {
return false, errors.Wrapf(err, "api/v1/stream")
}
if len(res.Streams) == 0 {
return false, nil
} else {
for _, v := range res.Streams {
if v.Name == id {
return v.Publish.Active, nil
}
}
}
return false, nil
}
func (s *Srs) GetAddr() string {
return s.Addr
}
func (s *Srs) GetWebRTCAddr(id string) string {
return "webrtc://" + s.Addr + "/live/" + id
}

59
pkg/media/zlm.go Normal file
View File

@ -0,0 +1,59 @@
package media
import (
"context"
"github.com/ossrs/go-oryx-lib/errors"
)
type Zlm struct {
Ctx context.Context
Schema string // The schema of ZLM, eg: http
Addr string // The address of ZLM, eg: localhost:8085
Secret string // The secret of ZLM, eg: ZLMediaKit_secret
}
// /index/api/openRtpServer
// secret={{ZLMediaKit_secret}}&port=0&enable_tcp=1&stream_id=test2
func (z *Zlm) Publish(id, ssrc string) (int, error) {
res := struct {
Code int `json:"code"`
Port int `json:"port"`
}{}
if err := apiRequest(z.Ctx, z.Schema+"://"+z.Addr+"/index/api/openRtpServer?secret="+z.Secret+"&port=0&enable_tcp=1&stream_id="+id+"&ssrc="+ssrc, nil, &res); err != nil {
return 0, errors.Wrapf(err, "gb/v1/publish")
}
return res.Port, nil
}
// /index/api/closeRtpServer
func (z *Zlm) Unpublish(id string) error {
res := struct {
Code int `json:"code"`
}{}
if err := apiRequest(z.Ctx, z.Schema+"://"+z.Addr+"/index/api/closeRtpServer?secret="+z.Secret+"&stream_id="+id, nil, &res); err != nil {
return errors.Wrapf(err, "gb/v1/publish")
}
return nil
}
// /index/api/getMediaList
func (z *Zlm) GetStreamStatus(id string) (bool, error) {
res := struct {
Code int `json:"code"`
}{}
if err := apiRequest(z.Ctx, z.Schema+"://"+z.Addr+"/index/api/getMediaList?secret="+z.Secret+"&stream_id="+id, nil, &res); err != nil {
return false, errors.Wrapf(err, "gb/v1/publish")
}
return res.Code == 0, nil
}
func (z *Zlm) GetAddr() string {
return z.Addr
}
func (z *Zlm) GetWebRTCAddr(id string) string {
return "http://" + z.Addr + "/index/api/webrtc?app=rtp&stream=" + id + "&type=play"
}

83
pkg/models/gb28181.go Normal file
View File

@ -0,0 +1,83 @@
package models
import "encoding/xml"
type Record struct {
DeviceID string `xml:"DeviceID" json:"device_id"`
Name string `xml:"Name" json:"name"`
FilePath string `xml:"FilePath" json:"file_path"`
Address string `xml:"Address" json:"address"`
StartTime string `xml:"StartTime" json:"start_time"`
EndTime string `xml:"EndTime" json:"end_time"`
Secrecy int `xml:"Secrecy" json:"secrecy"`
Type string `xml:"Type" json:"type"`
}
// Example XML structure for channel info:
//
// <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"` // Speed levels: 1/2/4/8
} `json:"info"`
// Custom fields
Ssrc string `json:"ssrc"`
}
type ChannelStatus string
type XmlMessageInfo struct {
XMLName xml.Name
CmdType string
SN int
DeviceID string
DeviceName string
Manufacturer string
Model string
Channel string
DeviceList []ChannelInfo `xml:"DeviceList>Item"`
RecordList []*Record `xml:"RecordList>Item"`
SumNum int
}

80
pkg/models/types.go Normal file
View File

@ -0,0 +1,80 @@
package models
type BaseRequest struct {
DeviceID string `json:"device_id"`
ChannelID string `json:"channel_id"`
}
type InviteRequest struct {
BaseRequest
MediaServerId int `json:"media_server_id"`
PlayType int `json:"play_type"` // 0: live, 1: playback, 2: download
SubStream int `json:"sub_stream"`
StartTime int64 `json:"start_time"`
EndTime int64 `json:"end_time"`
}
type InviteResponse struct {
ChannelID string `json:"channel_id"`
URL string `json:"url"`
}
type SessionRequest struct {
BaseRequest
URL string `json:"url"`
}
type ByeRequest struct {
SessionRequest
}
type PauseRequest struct {
SessionRequest
}
type ResumeRequest struct {
SessionRequest
}
type SpeedRequest struct {
SessionRequest
Speed float32 `json:"speed"`
}
type PTZControlRequest struct {
BaseRequest
PTZ string `json:"ptz"`
Speed string `json:"speed"`
}
type QueryRecordRequest struct {
BaseRequest
StartTime int64 `json:"start_time"`
EndTime int64 `json:"end_time"`
}
type MediaServer struct {
Name string `json:"name"`
Type string `json:"type"`
IP string `json:"ip"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
Secret string `json:"secret"`
IsDefault int `json:"is_default"`
}
type MediaServerRequest struct {
MediaServer
}
type MediaServerResponse struct {
MediaServer
ID int `json:"id"`
CreatedAt string `json:"created_at"`
}
type CommonResponse struct {
Code int `json:"code"`
Data interface{} `json:"data"`
}

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

View File

@ -1,44 +1,10 @@
package utils
import (
"context"
"crypto/rand"
"flag"
"math/big"
"os"
"github.com/ossrs/srs-sip/pkg/config"
)
func Parse(ctx context.Context) interface{} {
fl := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
var conf config.MainConfig
fl.StringVar(&conf.Serial, "serial", "34020000002000000001", "The serial number")
fl.StringVar(&conf.Realm, "realm", "3402000000", "The realm")
fl.StringVar(&conf.SipHost, "sip-host", "0.0.0.0", "The SIP host")
fl.IntVar(&conf.SipPort, "sip-port", 5060, "The SIP port")
fl.StringVar(&conf.MediaAddr, "media-addr", "127.0.0.1:1985", "The api address of media server. like: 127.0.0.1:1985")
fl.IntVar(&conf.HttpServerPort, "http-server-port", 8888, "The port of http server")
fl.IntVar(&conf.APIPort, "api-port", 2020, "The port of http api server")
fl.Usage = func() {
fl.PrintDefaults()
}
if err := fl.Parse(os.Args[1:]); err == flag.ErrHelp {
os.Exit(0)
}
showHelp := conf.MediaAddr == ""
if showHelp {
fl.Usage()
os.Exit(-1)
}
return &conf
}
func GenRandomNumber(n int) string {
var result string
for i := 0; i < n; i++ {
@ -64,3 +30,17 @@ func IsVideoChannel(channelID string) bool {
deviceType := channelID[10:13]
return deviceType == "131" || deviceType == "132"
}
// GetSessionName 根据播放类型返回会话名称
func GetSessionName(playType int) string {
switch playType {
case 1:
return "Playback"
case 2:
return "Download"
case 3:
return "Talk"
default:
return "Play"
}
}