mirror of
https://github.com/gowvp/gb28181.git
synced 2025-09-26 19:41:19 +08:00
重构国标播放与修复快照
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -35,3 +35,4 @@ tables/
|
|||||||
*.zip
|
*.zip
|
||||||
.idea/
|
.idea/
|
||||||
data/
|
data/
|
||||||
|
cover/
|
||||||
|
@@ -1,3 +1,8 @@
|
|||||||
|
# 2025-06-14
|
||||||
|
重构国标播放逻辑, play 接口仅返回播放地址,通过 webhook 来通知拉流
|
||||||
|
可能会导致首播慢一些
|
||||||
|
重构 main 函数,将文件移动到项目根目录下
|
||||||
|
|
||||||
# 2025-02-15
|
# 2025-02-15
|
||||||
|
|
||||||
国标流停止,实现 sip bye 请求
|
国标流停止,实现 sip bye 请求
|
||||||
|
@@ -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'
|
||||||
|
@@ -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(""),
|
||||||
|
@@ -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)
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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
|
||||||
|
}
|
||||||
|
@@ -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"` // 流虚拟主机
|
||||||
|
}
|
||||||
|
2
main.go
2
main.go
@@ -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)
|
||||||
|
@@ -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)
|
||||||
|
Reference in New Issue
Block a user