|
|
// Copyright 2022, Chef. All rights reserved.
|
|
|
// https://github.com/q191201771/lal
|
|
|
//
|
|
|
// Use of this source code is governed by a MIT-style license
|
|
|
// that can be found in the License file.
|
|
|
//
|
|
|
// Author: Chef (191201771@qq.com)
|
|
|
|
|
|
package logic
|
|
|
|
|
|
import (
|
|
|
"errors"
|
|
|
"fmt"
|
|
|
"github.com/q191201771/lal/pkg/base"
|
|
|
"github.com/q191201771/lal/pkg/rtsp"
|
|
|
"github.com/q191201771/naza/pkg/nazalog"
|
|
|
"strings"
|
|
|
"time"
|
|
|
|
|
|
"github.com/q191201771/lal/pkg/rtmp"
|
|
|
)
|
|
|
|
|
|
// StartPull 外部命令主动触发pull拉流
|
|
|
//
|
|
|
func (group *Group) StartPull(info base.ApiCtrlStartRelayPullReq) (string, error) {
|
|
|
group.mutex.Lock()
|
|
|
defer group.mutex.Unlock()
|
|
|
|
|
|
group.pullProxy.apiEnable = true
|
|
|
group.pullProxy.pullUrl = info.Url
|
|
|
group.pullProxy.pullTimeoutMs = info.PullTimeoutMs
|
|
|
group.pullProxy.pullRetryNum = info.PullRetryNum
|
|
|
group.pullProxy.autoStopPullAfterNoOutMs = info.AutoStopPullAfterNoOutMs
|
|
|
group.pullProxy.rtspMode = info.RtspMode
|
|
|
group.pullProxy.debugDumpPacket = info.DebugDumpPacket
|
|
|
|
|
|
return group.pullIfNeeded()
|
|
|
}
|
|
|
|
|
|
// StopPull
|
|
|
//
|
|
|
// @return 如果PullSession存在,返回它的unique key
|
|
|
//
|
|
|
func (group *Group) StopPull() string {
|
|
|
group.mutex.Lock()
|
|
|
defer group.mutex.Unlock()
|
|
|
|
|
|
group.pullProxy.apiEnable = false
|
|
|
return group.stopPull()
|
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
|
|
|
|
type pullProxy struct {
|
|
|
staticRelayPullEnable bool // 是否开启pull TODO(chef): refactor 这两个bool可以考虑合并成一个
|
|
|
apiEnable bool
|
|
|
pullUrl string
|
|
|
pullTimeoutMs int
|
|
|
pullRetryNum int
|
|
|
autoStopPullAfterNoOutMs int // 没有观看者时,是否自动停止pull
|
|
|
rtspMode int
|
|
|
debugDumpPacket string
|
|
|
|
|
|
startCount int
|
|
|
lastHasOutTs int64
|
|
|
|
|
|
isSessionPulling bool // 是否正在pull,注意,这是一个内部状态,表示的是session的状态,而不是整体任务应该处于的状态
|
|
|
rtmpSession *rtmp.PullSession
|
|
|
rtspSession *rtsp.PullSession
|
|
|
}
|
|
|
|
|
|
// initRelayPullByConfig 根据配置文件中的静态回源配置来初始化回源设置
|
|
|
//
|
|
|
func (group *Group) initRelayPullByConfig() {
|
|
|
// 注意,这是配置文件中静态回源的配置值,不是HTTP-API的默认值
|
|
|
const (
|
|
|
staticRelayPullTimeoutMs = 5000 //
|
|
|
staticRelayPullRetryNum = base.PullRetryNumForever
|
|
|
staticRelayPullAutoStopPullAfterNoOutMs = base.AutoStopPullAfterNoOutMsImmediately
|
|
|
)
|
|
|
|
|
|
enable := group.config.StaticRelayPullConfig.Enable
|
|
|
addr := group.config.StaticRelayPullConfig.Addr
|
|
|
appName := group.appName
|
|
|
streamName := group.streamName
|
|
|
|
|
|
group.pullProxy = &pullProxy{
|
|
|
startCount: 0,
|
|
|
lastHasOutTs: time.Now().UnixNano() / 1e6,
|
|
|
}
|
|
|
|
|
|
var pullUrl string
|
|
|
if enable {
|
|
|
pullUrl = fmt.Sprintf("rtmp://%s/%s/%s", addr, appName, streamName)
|
|
|
}
|
|
|
|
|
|
group.pullProxy.pullUrl = pullUrl
|
|
|
group.pullProxy.staticRelayPullEnable = enable
|
|
|
group.pullProxy.pullTimeoutMs = staticRelayPullTimeoutMs
|
|
|
group.pullProxy.pullRetryNum = staticRelayPullRetryNum
|
|
|
group.pullProxy.autoStopPullAfterNoOutMs = staticRelayPullAutoStopPullAfterNoOutMs
|
|
|
}
|
|
|
|
|
|
func (group *Group) setRtmpPullSession(session *rtmp.PullSession) {
|
|
|
group.pullProxy.rtmpSession = session
|
|
|
}
|
|
|
|
|
|
func (group *Group) setRtspPullSession(session *rtsp.PullSession) {
|
|
|
group.pullProxy.rtspSession = session
|
|
|
if group.pullProxy.debugDumpPacket != "" {
|
|
|
group.rtspPullDumpFile = base.NewDumpFile()
|
|
|
if err := group.rtspPullDumpFile.OpenToWrite(group.pullProxy.debugDumpPacket); err != nil {
|
|
|
Log.Errorf("%+v", err)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func (group *Group) resetRelayPullSession() {
|
|
|
group.pullProxy.isSessionPulling = false
|
|
|
group.pullProxy.rtmpSession = nil
|
|
|
group.pullProxy.rtspSession = nil
|
|
|
if group.rtspPullDumpFile != nil {
|
|
|
group.rtspPullDumpFile.Close()
|
|
|
group.rtspPullDumpFile = nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func (group *Group) getStatPull() base.StatPull {
|
|
|
if group.pullProxy.rtmpSession != nil {
|
|
|
return base.Session2StatPull(group.pullProxy.rtmpSession)
|
|
|
}
|
|
|
if group.pullProxy.rtspSession != nil {
|
|
|
return base.Session2StatPull(group.pullProxy.rtspSession)
|
|
|
}
|
|
|
return base.StatPull{}
|
|
|
}
|
|
|
|
|
|
func (group *Group) disposeInactivePullSession() {
|
|
|
if group.pullProxy.rtmpSession != nil {
|
|
|
if readAlive, _ := group.pullProxy.rtmpSession.IsAlive(); !readAlive {
|
|
|
Log.Warnf("[%s] session timeout. session=%s", group.UniqueKey, group.pullProxy.rtmpSession.UniqueKey())
|
|
|
group.pullProxy.rtmpSession.Dispose()
|
|
|
}
|
|
|
}
|
|
|
if group.pullProxy.rtspSession != nil {
|
|
|
if readAlive, _ := group.pullProxy.rtspSession.IsAlive(); !readAlive {
|
|
|
Log.Warnf("[%s] session timeout. session=%s", group.UniqueKey, group.pullProxy.rtspSession.UniqueKey())
|
|
|
group.pullProxy.rtspSession.Dispose()
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func (group *Group) updatePullSessionStat() {
|
|
|
if group.pullProxy.rtmpSession != nil {
|
|
|
group.pullProxy.rtmpSession.UpdateStat(calcSessionStatIntervalSec)
|
|
|
}
|
|
|
if group.pullProxy.rtspSession != nil {
|
|
|
group.pullProxy.rtspSession.UpdateStat(calcSessionStatIntervalSec)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func (group *Group) isPullModuleAlive() bool {
|
|
|
if group.hasPullSession() || group.pullProxy.isSessionPulling {
|
|
|
return true
|
|
|
}
|
|
|
flag, _ := group.shouldStartPull()
|
|
|
return flag
|
|
|
}
|
|
|
|
|
|
func (group *Group) tickPullModule() {
|
|
|
if group.hasSubSession() {
|
|
|
group.pullProxy.lastHasOutTs = time.Now().UnixNano() / 1e6
|
|
|
}
|
|
|
|
|
|
if group.shouldAutoStopPull() {
|
|
|
group.stopPull()
|
|
|
} else {
|
|
|
group.pullIfNeeded()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func (group *Group) hasPullSession() bool {
|
|
|
return group.pullProxy.rtmpSession != nil || group.pullProxy.rtspSession != nil
|
|
|
}
|
|
|
|
|
|
func (group *Group) pullSessionUniqueKey() string {
|
|
|
if group.pullProxy.rtmpSession != nil {
|
|
|
return group.pullProxy.rtmpSession.UniqueKey()
|
|
|
}
|
|
|
if group.pullProxy.rtspSession != nil {
|
|
|
return group.pullProxy.rtspSession.UniqueKey()
|
|
|
}
|
|
|
return ""
|
|
|
}
|
|
|
|
|
|
// kickPull
|
|
|
//
|
|
|
// @return 返回true,表示找到对应的session,并关闭
|
|
|
//
|
|
|
func (group *Group) kickPull(sessionId string) bool {
|
|
|
if (group.pullProxy.rtmpSession != nil && group.pullProxy.rtmpSession.UniqueKey() == sessionId) ||
|
|
|
(group.pullProxy.rtspSession != nil && group.pullProxy.rtspSession.UniqueKey() == sessionId) {
|
|
|
group.pullProxy.apiEnable = false
|
|
|
group.stopPull()
|
|
|
return true
|
|
|
}
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
// 判断是否需要pull从远端拉流至本地,如果需要,则触发pull
|
|
|
//
|
|
|
// 当前调用时机:
|
|
|
// 1. 添加新sub session
|
|
|
// 2. 外部命令,比如http api
|
|
|
// 3. 定时器,比如pull的连接断了,通过定时器可以重启触发pull
|
|
|
//
|
|
|
func (group *Group) pullIfNeeded() (string, error) {
|
|
|
if flag, err := group.shouldStartPull(); !flag {
|
|
|
return "", err
|
|
|
}
|
|
|
|
|
|
Log.Infof("[%s] start relay pull. url=%s", group.UniqueKey, group.pullProxy.pullUrl)
|
|
|
|
|
|
group.pullProxy.isSessionPulling = true
|
|
|
group.pullProxy.startCount++
|
|
|
|
|
|
isPullByRtmp := strings.HasPrefix(group.pullProxy.pullUrl, "rtmp")
|
|
|
|
|
|
var rtmpSession *rtmp.PullSession
|
|
|
var rtspSession *rtsp.PullSession
|
|
|
var uk string
|
|
|
|
|
|
if isPullByRtmp {
|
|
|
rtmpSession = rtmp.NewPullSession(func(option *rtmp.PullSessionOption) {
|
|
|
option.PullTimeoutMs = group.pullProxy.pullTimeoutMs
|
|
|
}).WithOnPullSucc(func() {
|
|
|
err := group.AddRtmpPullSession(rtmpSession)
|
|
|
if err != nil {
|
|
|
rtmpSession.Dispose()
|
|
|
return
|
|
|
}
|
|
|
}).WithOnReadRtmpAvMsg(group.OnReadRtmpAvMsg)
|
|
|
|
|
|
uk = rtmpSession.UniqueKey()
|
|
|
} else {
|
|
|
rtspSession = rtsp.NewPullSession(group, func(option *rtsp.PullSessionOption) {
|
|
|
option.PullTimeoutMs = group.pullProxy.pullTimeoutMs
|
|
|
option.OverTcp = group.pullProxy.rtspMode == 0
|
|
|
}).WithOnDescribeResponse(func() {
|
|
|
err := group.AddRtspPullSession(rtspSession)
|
|
|
if err != nil {
|
|
|
rtspSession.Dispose()
|
|
|
return
|
|
|
}
|
|
|
})
|
|
|
|
|
|
uk = rtspSession.UniqueKey()
|
|
|
}
|
|
|
|
|
|
go func(rtPullUrl string, rtIsPullByRtmp bool, rtRtmpSession *rtmp.PullSession, rtRtspSession *rtsp.PullSession) {
|
|
|
if rtIsPullByRtmp {
|
|
|
// TODO(chef): 处理数据回调,是否应该等待Add成功之后。避免竞态条件中途加入了其他in session
|
|
|
err := rtRtmpSession.Pull(rtPullUrl)
|
|
|
if err != nil {
|
|
|
Log.Errorf("[%s] relay pull fail. err=%v", rtRtmpSession.UniqueKey(), err)
|
|
|
group.DelRtmpPullSession(rtRtmpSession)
|
|
|
return
|
|
|
}
|
|
|
|
|
|
err = <-rtRtmpSession.WaitChan()
|
|
|
Log.Infof("[%s] relay pull done. err=%v", rtRtmpSession.UniqueKey(), err)
|
|
|
group.DelRtmpPullSession(rtRtmpSession)
|
|
|
return
|
|
|
}
|
|
|
|
|
|
err := rtRtspSession.Pull(rtPullUrl)
|
|
|
if err != nil {
|
|
|
Log.Errorf("[%s] relay pull fail. err=%v", rtRtspSession.UniqueKey(), err)
|
|
|
group.DelRtspPullSession(rtRtspSession)
|
|
|
return
|
|
|
}
|
|
|
|
|
|
err = <-rtRtspSession.WaitChan()
|
|
|
Log.Infof("[%s] relay pull done. err=%v", rtRtspSession.UniqueKey(), err)
|
|
|
group.DelRtspPullSession(rtRtspSession)
|
|
|
return
|
|
|
}(group.pullProxy.pullUrl, isPullByRtmp, rtmpSession, rtspSession)
|
|
|
|
|
|
return uk, nil
|
|
|
}
|
|
|
|
|
|
func (group *Group) stopPull() string {
|
|
|
// 关闭时,清空用于重试的计数
|
|
|
group.pullProxy.startCount = 0
|
|
|
|
|
|
if group.pullProxy.rtmpSession != nil {
|
|
|
Log.Infof("[%s] stop pull session.", group.UniqueKey)
|
|
|
group.pullProxy.rtmpSession.Dispose()
|
|
|
return group.pullProxy.rtmpSession.UniqueKey()
|
|
|
}
|
|
|
if group.pullProxy.rtspSession != nil {
|
|
|
Log.Infof("[%s] stop pull session.", group.UniqueKey)
|
|
|
group.pullProxy.rtspSession.Dispose()
|
|
|
return group.pullProxy.rtspSession.UniqueKey()
|
|
|
}
|
|
|
return ""
|
|
|
}
|
|
|
|
|
|
func (group *Group) shouldStartPull() (bool, error) {
|
|
|
// 如果本地已经有输入型的流,就不需要pull了
|
|
|
if group.hasInSession() {
|
|
|
return false, base.ErrDupInStream
|
|
|
}
|
|
|
|
|
|
// 已经在pull中,就不需要pull了
|
|
|
if group.pullProxy.isSessionPulling {
|
|
|
return false, base.ErrDupInStream
|
|
|
}
|
|
|
|
|
|
if !group.pullProxy.staticRelayPullEnable && !group.pullProxy.apiEnable {
|
|
|
return false, errors.New("relay pull not enable")
|
|
|
}
|
|
|
|
|
|
// 没人观看自动停的逻辑,是否满足并且需要触发
|
|
|
if group.shouldAutoStopPull() {
|
|
|
return false, errors.New("should auto stop pull")
|
|
|
}
|
|
|
|
|
|
// 检查重试次数
|
|
|
if group.pullProxy.pullRetryNum >= 0 {
|
|
|
if group.pullProxy.startCount > group.pullProxy.pullRetryNum {
|
|
|
return false, errors.New("relay pull retry limited")
|
|
|
}
|
|
|
} else {
|
|
|
// 负数永远都重试
|
|
|
}
|
|
|
|
|
|
return true, nil
|
|
|
}
|
|
|
|
|
|
// shouldAutoStopPull 是否需要自动停,根据没人观看停的逻辑
|
|
|
//
|
|
|
func (group *Group) shouldAutoStopPull() bool {
|
|
|
// 没开启
|
|
|
if group.pullProxy.autoStopPullAfterNoOutMs < 0 {
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
// 还有观众
|
|
|
if group.hasOutSession() {
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
// 没有观众,并且设置为立即关闭
|
|
|
if group.pullProxy.autoStopPullAfterNoOutMs == 0 {
|
|
|
return true
|
|
|
}
|
|
|
|
|
|
// 是否达到时间阈值
|
|
|
nazalog.Debugf("%d %d %d", group.pullProxy.lastHasOutTs, time.Now().UnixNano(), group.pullProxy.autoStopPullAfterNoOutMs)
|
|
|
return group.pullProxy.lastHasOutTs != -1 && time.Now().UnixNano()/1e6-group.pullProxy.lastHasOutTs >= int64(group.pullProxy.autoStopPullAfterNoOutMs)
|
|
|
}
|