重构国标播放与修复快照

This commit is contained in:
xugo
2025-06-14 01:20:46 +08:00
parent bf5ff1d748
commit d082fcedda
10 changed files with 135 additions and 80 deletions

1
.gitignore vendored
View File

@@ -35,3 +35,4 @@ tables/
*.zip *.zip
.idea/ .idea/
data/ data/
cover/

View File

@@ -1,3 +1,8 @@
# 2025-06-14
重构国标播放逻辑, play 接口仅返回播放地址,通过 webhook 来通知拉流
可能会导致首播慢一些
重构 main 函数,将文件移动到项目根目录下
# 2025-02-15 # 2025-02-15
国标流停止,实现 sip bye 请求 国标流停止,实现 sip bye 请求

View File

@@ -1,64 +1,64 @@
[Server] [Server]
Debug = false Debug = false
# rtmp 推流秘钥 # rtmp 推流秘钥
RTMPSecret = '123' RTMPSecret = '123'
# 对外提供的服务,建议由 nginx 代理 # 对外提供的服务,建议由 nginx 代理
[Server.HTTP] [Server.HTTP]
# http 端口 # http 端口
Port = 15123 Port = 15123
# 请求超时时间 # 请求超时时间
Timeout = '1m0s' Timeout = '1m0s'
# jwt 秘钥,空串时,每次启动程序将随机赋值 # jwt 秘钥,空串时,每次启动程序将随机赋值
JwtSecret = '' JwtSecret = ''
[Server.HTTP.PProf] [Server.HTTP.PProf]
# 是否启用 pprof, 建议设置为 true # 是否启用 pprof, 建议设置为 true
Enabled = true Enabled = true
# 访问白名单 # 访问白名单
AccessIps = ['::1', '127.0.0.1'] AccessIps = ['::1', '127.0.0.1']
[Data] [Data]
# 数据库支持 sqlite 和 postgres 两种,使用 sqlite 时 dsn 应当填写文件存储路径 # 数据库支持 sqlite 和 postgres 两种,使用 sqlite 时 dsn 应当填写文件存储路径
[Data.Database] [Data.Database]
Dsn = './configs/data.db' Dsn = './configs/data.db'
MaxIdleConns = 1 MaxIdleConns = 1
MaxOpenConns = 1 MaxOpenConns = 1
ConnMaxLifetime = '6h0m0s' ConnMaxLifetime = '6h0m0s'
SlowThreshold = '200ms' SlowThreshold = '200ms'
[Log] [Log]
# 日志存储目录,不能使用特殊符号 # 日志存储目录,不能使用特殊符号
Dir = './logs' Dir = './logs'
# 记录级别 debug/info/warn/error # 记录级别 debug/info/warn/error
Level = 'debug' Level = 'debug'
# 保留日志多久,超过时间自动删除 # 保留日志多久,超过时间自动删除
MaxAge = '744h0m0s' MaxAge = '744h0m0s'
# 多久时间,分割一个新的日志文件 # 多久时间,分割一个新的日志文件
RotationTime = '12h0m0s' RotationTime = '12h0m0s'
# 多大文件,分割一个新的日志文件(MB) # 多大文件,分割一个新的日志文件(MB)
RotationSize = 50 RotationSize = 50
[Sip] [Sip]
# 服务监听的 tcp/udp 端口号 # 服务监听的 tcp/udp 端口号
Port = 15060 Port = 15060
# gb/t28181 20 位国标 ID # gb/t28181 20 位国标 ID
ID = '3402000000200000001' ID = '3402000000200000001'
# 域 # 域
Domain = '3402000000' Domain = '3402000000'
# 注册密码 # 注册密码
Password = '' Password = ''
[Media] [Media]
# 媒体服务器 IP # 媒体服务器 IP
IP = '127.0.0.1' IP = '127.0.0.1'
# 媒体服务器 HTTP 端口 # 媒体服务器 HTTP 端口
HTTPPort = 8080 HTTPPort = 8080
# 媒体服务器密钥 # 媒体服务器密钥
Secret = 'jvRqCAzEg7AszBi4gm1cfhwXpmnVmJMG' Secret = 'jvRqCAzEg7AszBi4gm1cfhwXpmnVmJMG'
# 用于流媒体 webhook 回调 # 用于流媒体 webhook 回调
WebHookIP = '192.168.10.37' WebHookIP = '192.168.10.10'
# 媒体服务器 RTP 端口范围 # 媒体服务器 RTP 端口范围
RTPPortRange = '20000-20100' RTPPortRange = '20000-20100'
# 媒体服务器 SDP IP # 媒体服务器 SDP IP
SDPIP = '192.168.10.37' SDPIP = '192.168.10.10'

View File

@@ -168,6 +168,7 @@ func (n *NodeManager) connection(server *MediaServer, serverPort int) error {
// HookOnHTTPAccess: zlm.NewString(""), // HookOnHTTPAccess: zlm.NewString(""),
HookOnPublish: zlm.NewString(fmt.Sprintf("%s/on_publish", hookPrefix)), HookOnPublish: zlm.NewString(fmt.Sprintf("%s/on_publish", hookPrefix)),
HookOnStreamNoneReader: zlm.NewString(fmt.Sprintf("%s/on_stream_none_reader", hookPrefix)), HookOnStreamNoneReader: zlm.NewString(fmt.Sprintf("%s/on_stream_none_reader", hookPrefix)),
HookOnStreamNotFound: zlm.NewString(fmt.Sprintf("%s/on_stream_not_found", hookPrefix)),
HookOnRecordTs: zlm.NewString(""), HookOnRecordTs: zlm.NewString(""),
HookOnRtspAuth: zlm.NewString(""), HookOnRtspAuth: zlm.NewString(""),
HookOnRtspRealm: zlm.NewString(""), HookOnRtspRealm: zlm.NewString(""),

View File

@@ -28,6 +28,8 @@ var startRuntime = time.Now()
func setupRouter(r *gin.Engine, uc *Usecase) { func setupRouter(r *gin.Engine, uc *Usecase) {
uc.GB28181API.uc = uc uc.GB28181API.uc = uc
uc.SMSAPI.uc = uc uc.SMSAPI.uc = uc
uc.WebHookAPI.uc = uc
go stat.LoadTop(system.Getwd(), func(m map[string]any) { go stat.LoadTop(system.Getwd(), func(m map[string]any) {
_ = m _ = m
}) })
@@ -175,6 +177,8 @@ func sortExpvarMap(data *expvar.Map, top int) []KV {
} }
func (uc *Usecase) proxySMS(c *gin.Context) { func (uc *Usecase) proxySMS(c *gin.Context) {
defer recover()
rc := http.NewResponseController(c.Writer) rc := http.NewResponseController(c.Writer)
exp := time.Now().AddDate(99, 0, 0) exp := time.Now().AddDate(99, 0, 0)
_ = rc.SetReadDeadline(exp) _ = rc.SetReadDeadline(exp)

View File

@@ -15,12 +15,10 @@ import (
"github.com/gowvp/gb28181/internal/core/gb28181" "github.com/gowvp/gb28181/internal/core/gb28181"
"github.com/gowvp/gb28181/internal/core/media" "github.com/gowvp/gb28181/internal/core/media"
"github.com/gowvp/gb28181/internal/core/sms" "github.com/gowvp/gb28181/internal/core/sms"
"github.com/gowvp/gb28181/pkg/gbs"
"github.com/gowvp/gb28181/pkg/zlm" "github.com/gowvp/gb28181/pkg/zlm"
"github.com/ixugo/goddd/domain/uniqueid" "github.com/ixugo/goddd/domain/uniqueid"
"github.com/ixugo/goddd/pkg/orm" "github.com/ixugo/goddd/pkg/orm"
"github.com/ixugo/goddd/pkg/reason" "github.com/ixugo/goddd/pkg/reason"
"github.com/ixugo/goddd/pkg/system"
"github.com/ixugo/goddd/pkg/web" "github.com/ixugo/goddd/pkg/web"
) )
@@ -32,13 +30,13 @@ const (
// TODO: 快照不会删除,只会覆盖,设备删除时也不会删除快照,待实现 // TODO: 快照不会删除,只会覆盖,设备删除时也不会删除快照,待实现
func writeCover(dataDir, channelID string, body []byte) error { func writeCover(dataDir, channelID string, body []byte) error {
coverPath := filepath.Join(system.Getwd(), dataDir, coverDir) coverPath := filepath.Join(dataDir, coverDir)
os.MkdirAll(coverPath, 0o755) os.MkdirAll(coverPath, 0o755)
return os.WriteFile(filepath.Join(coverPath, channelID+".jpg"), body, 0o644) return os.WriteFile(filepath.Join(coverPath, channelID+".jpg"), body, 0o644)
} }
func readCoverPath(dataDir, channelID string) string { func readCoverPath(dataDir, channelID string) string {
coverPath := filepath.Join(system.Getwd(), dataDir, coverDir) coverPath := filepath.Join(dataDir, coverDir)
return filepath.Join(coverPath, channelID+".jpg") return filepath.Join(coverPath, channelID+".jpg")
} }
@@ -179,18 +177,6 @@ func (a GB28181API) play(c *gin.Context, _ *struct{}) (*playOutput, error) {
return nil, err return nil, err
} }
dev, err := a.gb28181Core.GetDeviceByDeviceID(c.Request.Context(), ch.DeviceID)
if err != nil {
return nil, err
}
if err := a.uc.SipServer.Play(&gbs.PlayInput{
Channel: ch,
StreamMode: dev.StreamMode,
SMS: svr,
}); err != nil {
return nil, ErrDevice.SetMsg(err.Error())
}
} else if strings.HasPrefix(channelID, bz.IDPrefixRTMP) { } else if strings.HasPrefix(channelID, bz.IDPrefixRTMP) {
push, err := a.uc.MediaAPI.mediaCore.GetStreamPush(c.Request.Context(), channelID) push, err := a.uc.MediaAPI.mediaCore.GetStreamPush(c.Request.Context(), channelID)
if err != nil { if err != nil {
@@ -335,7 +321,7 @@ func (a GB28181API) refreshSnapshot(c *gin.Context, in *refreshSnapshotInput) (a
} }
} }
return gin.H{"link": fmt.Sprintf("/api/channels/%s/snapshot", channelID)}, nil return gin.H{"link": fmt.Sprintf("/channels/%s/snapshot", channelID)}, nil
} }
func (a GB28181API) getSnapshot(c *gin.Context) { func (a GB28181API) getSnapshot(c *gin.Context) {

View File

@@ -20,6 +20,7 @@ type WebHookAPI struct {
conf *conf.Bootstrap conf *conf.Bootstrap
log *slog.Logger log *slog.Logger
gbs *gbs.Server gbs *gbs.Server
uc *Usecase
} }
func NewWebHookAPI(core sms.Core, mediaCore media.Core, conf *conf.Bootstrap, gbs *gbs.Server, gb28181 gb28181.Core) WebHookAPI { func NewWebHookAPI(core sms.Core, mediaCore media.Core, conf *conf.Bootstrap, gbs *gbs.Server, gb28181 gb28181.Core) WebHookAPI {
@@ -42,6 +43,7 @@ func registerZLMWebhookAPI(r gin.IRouter, api WebHookAPI, handler ...gin.Handler
group.POST("/on_play", web.WarpH(api.onPlay)) group.POST("/on_play", web.WarpH(api.onPlay))
group.POST("/on_stream_none_reader", web.WarpH(api.onStreamNoneReader)) group.POST("/on_stream_none_reader", web.WarpH(api.onStreamNoneReader))
group.POST("/on_rtp_server_timeout", web.WarpH(api.onRTPServerTimeout)) group.POST("/on_rtp_server_timeout", web.WarpH(api.onRTPServerTimeout))
group.POST("/on_stream_not_found", web.WarpH(api.onStreamNotFound))
} }
} }
@@ -159,3 +161,38 @@ func (w WebHookAPI) onRTPServerTimeout(c *gin.Context, in *onRTPServerTimeoutInp
w.log.Info("rtp 收流超时", "local_port", in.LocalPort, "ssrc", in.SSRC, "stream_id", in.StreamID, "mediaServerID", in.MediaServerID) w.log.Info("rtp 收流超时", "local_port", in.LocalPort, "ssrc", in.SSRC, "stream_id", in.StreamID, "mediaServerID", in.MediaServerID)
return newDefaultOutputOK(), nil return newDefaultOutputOK(), nil
} }
func (w WebHookAPI) onStreamNotFound(c *gin.Context, in *onStreamNotFoundInput) (DefaultOutput, error) {
w.log.Info("流不存在", "app", in.App, "stream", in.Stream, "schema", in.Schema, "mediaServerID", in.MediaServerID)
// 国标流处理
if in.App == "rtp" {
ch, err := w.gb28181Core.GetChannel(c.Request.Context(), in.Stream)
if err != nil {
// slog.Error("获取通道失败", "err", err)
return newDefaultOutputOK(), nil
}
dev, err := w.gb28181Core.GetDeviceByDeviceID(c.Request.Context(), ch.DeviceID)
if err != nil {
// slog.Error("获取设备失败", "err", err)
return newDefaultOutputOK(), nil
}
svr, err := w.uc.SMSAPI.smsCore.GetMediaServer(c.Request.Context(), sms.DefaultMediaServerID)
if err != nil {
// slog.Error("GetMediaServer", "err", err)
return newDefaultOutputOK(), nil
}
if err := w.gbs.Play(&gbs.PlayInput{
Channel: ch,
StreamMode: dev.StreamMode,
SMS: svr,
}); err != nil {
slog.Error("play", "err", err, "channel", ch.ID)
return newDefaultOutputOK(), nil
}
}
return newDefaultOutputOK(), nil
}

View File

@@ -201,3 +201,15 @@ type onRTPServerTimeoutInput struct {
TCPMode int `json:"tcp_mode"` // openRtpServer 输入的参数 TCPMode int `json:"tcp_mode"` // openRtpServer 输入的参数
MediaServerID string `json:"mediaServerId"` // 服务器 id,通过配置文件设置 MediaServerID string `json:"mediaServerId"` // 服务器 id,通过配置文件设置
} }
type onStreamNotFoundInput struct {
MediaServerID string `json:"mediaServerId"` // 服务器 id,通过配置文件设置
App string `json:"app"` // 流应用名
ID string `json:"id"` // TCP链接唯一ID
IP string `json:"ip"` // 播放器ip
Params string `json:"params"` // 播放url参数
Port int `json:"port"` // 播放器端口号
Schema string `json:"schema"` // 播放的协议可能是rtsp、rtmp、http
Stream string `json:"stream"` // 流 ID
Vhost string `json:"vhost"` // 流虚拟主机
}

View File

@@ -44,6 +44,8 @@ func main() {
} }
bc.Debug = !getBuildRelease() bc.Debug = !getBuildRelease()
bc.BuildVersion = buildVersion bc.BuildVersion = buildVersion
bc.ConfigDir = filedir
bc.ConfigPath = filePath
{ {
expvar.NewString("version").Set(buildVersion) expvar.NewString("version").Set(buildVersion)

View File

@@ -24,23 +24,18 @@ type StopPlayInput struct {
Channel *gb28181.Channel Channel *gb28181.Channel
} }
func (g *GB28181API) StopPlay(in *StopPlayInput) error { // stopPlay 不加锁的
ch, ok := g.svr.memoryStorer.GetChannel(in.Channel.DeviceID, in.Channel.ChannelID) func (g *GB28181API) stopPlay(ch *Channel, in *StopPlayInput) error {
if !ok {
return ErrDeviceNotExist
}
ch.device.playMutex.Lock()
defer ch.device.playMutex.Unlock()
key := "play:" + in.Channel.DeviceID + ":" + in.Channel.ChannelID key := "play:" + in.Channel.DeviceID + ":" + in.Channel.ChannelID
stream, ok := g.streams.LoadAndDelete(key) stream, ok := g.streams.LoadAndDelete(key)
if !ok { if !ok {
return nil return nil
} }
if stream.Resp == nil { if stream.Resp == nil {
return nil return nil
} }
req := sip.NewRequestFromResponse(sip.MethodBYE, stream.Resp) req := sip.NewRequestFromResponse(sip.MethodBYE, stream.Resp)
req.SetDestination(ch.Source()) req.SetDestination(ch.Source())
req.SetConnection(ch.Conn()) req.SetConnection(ch.Conn())
@@ -53,6 +48,18 @@ func (g *GB28181API) StopPlay(in *StopPlayInput) error {
return err return err
} }
// StopPlay 加锁的停止播放
func (g *GB28181API) StopPlay(in *StopPlayInput) error {
ch, ok := g.svr.memoryStorer.GetChannel(in.Channel.DeviceID, in.Channel.ChannelID)
if !ok {
return ErrDeviceNotExist
}
ch.device.playMutex.Lock()
defer ch.device.playMutex.Unlock()
return g.stopPlay(ch, in)
}
func (g *GB28181API) Play(in *PlayInput) error { func (g *GB28181API) Play(in *PlayInput) error {
ch, ok := g.svr.memoryStorer.GetChannel(in.Channel.DeviceID, in.Channel.ChannelID) ch, ok := g.svr.memoryStorer.GetChannel(in.Channel.DeviceID, in.Channel.ChannelID)
if !ok { if !ok {
@@ -68,7 +75,7 @@ func (g *GB28181API) Play(in *PlayInput) error {
if ok { if ok {
// TODO: 临时解决方案,每次播放,先停止再播放 // TODO: 临时解决方案,每次播放,先停止再播放
// https://github.com/gowvp/gb28181/issues/16 // https://github.com/gowvp/gb28181/issues/16
if err := g.StopPlay(&StopPlayInput{ if err := g.stopPlay(ch, &StopPlayInput{
Channel: in.Channel, Channel: in.Channel,
}); err != nil { }); err != nil {
slog.Error("stop play failed", "err", err) slog.Error("stop play failed", "err", err)