NextGB, web demo powerd by vue
This commit is contained in:
@ -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"}`)
|
||||
}
|
||||
@ -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
284
pkg/api/controller.go
Normal 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"})
|
||||
}
|
||||
@ -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
121
pkg/db/media_server.go
Normal 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()
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
59
pkg/media/zlm.go
Normal 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
83
pkg/models/gb28181.go
Normal 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
80
pkg/models/types.go
Normal 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
92
pkg/service/auth.go
Normal 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))
|
||||
}
|
||||
@ -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{})
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
81
pkg/service/ptz.go
Normal 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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user