diff --git a/CHANGELOG.md b/CHANGELOG.md index 88a55af..ee7e792 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,46 @@ +#### v0.32.0 (2022-11-10) + +- [feat] 自动叠加静音音频。所有协议、所有类型的输入流都已支持,文档见: https://pengrl.com/lal/#/dummy_audio +- [feat] 支持rtmps、rtsps(server端) +- [feat] rtp: 支持解析rtp header中的padding和csrc +- [feat] demo: pullhttpflv拉取http-flv时可以存储为flv文件 +- [opt] 二次开发: 当DelCustomizePubSession后,调用被删除对象的FeedAvPacket方法将返回错误 +- [opt] 二次开发: 支持直接使用json字符串作为配置内容初始化ILalServer +- [opt] 兼容性优化。转ts时,如果调整时间戳失败则使用调整前的时间戳。 +- [opt] 兼容性优化。当rtmps和rtsps加载签名文件失败时,只打印日志而不退出lalserver +- [fix] rtsp: 修复aac rtp type不是标准值导致无法合帧的问题。提高兼容性 +- [fix] http-api: 修复sub http-flv remote_addr字段没有值的bug +- [fix] rtsp: 修复auth可能失败的bug +- [log] 打印rtsp信令。丰富多处错误日志,比如转hls异常 +- [doc] 新增文档:重要概念 https://pengrl.com/lal/#/concept + +#### v0.31.1 (2022-10-07) + +- [feat] HTTP-API增加`start_rtp_pub`接口,用于支持GB28181的ps流 +- [feat] 向外暴露IAuthentication,用于定制化鉴权 +- [feat] 向外暴露ModConfigGroupCreator,支持为特定的Group独立配置 +- [opt] rtsp: 允许rtsp先拉再推,也即没有输入流时,可以先创建rtsp SubSession +- [feat] rtp: unpacker支持hevc ap格式 +- [fix] rtmp: 优化metadata @SetDataFrame的处理,解决flv录制文件用ffmpeg查看fps不准的问题 #201 +- [fix] rtmp: 修复PubSession发送publish信令中字段错误导致推流至youtube失败的问题 #199 +- [perf] rtmp: PullSession支持配置是否复用接收message时的内存块 +- [opt] rtmp: ClientSession推流兼容vhou url格式 +- [opt] rtmp: add float64 support to amf0::WriteObject +- [opt] rtsp: PullSession在setup阶段如果对端没有回复server port,依然尝试继续拉流,增强兼容性 +- [fix] rtsp: server端没有收到前面的信令直接收到PLAY信令,主动关闭连接,避免崩溃 +- [fix] rtsp: 解析sdp中MPEG4-GENERIC大小写导致aac音频无法正常合帧的问题 +- [fix] hls: 修复hls鉴权时streamName取值错误导致无法正常鉴权的问题 +- [fix] hls: 修复流名称中包含-中划线时hls异常的问题 +- [opt] mpegts: rtmp2mpegts的时间戳重打从0开始,兼容时间戳太大时vlc播放不了的问题 +- [opt] remux: 新增RtspRemuxerAddSpsPps2KeyFrameFlag参数,用于强制在关键帧数据包前加sps、pps。目的是增强兼容性。 +- [opt] remux: Rtmp2AvPacketRemuxer可携带自定义参数 +- [fix] remux: avpacket2rtmp nal以00 00 01开头时崩溃,丢弃aud +- [refactor] rtprtcp: 重构RtpPacketList +- [chore] 构建windows可执行文件时增加.exe后缀 +- [opt] HTTP-API和Notify: bitrate重命名为bitrate_kbits +- [opt] HTTP-API和Notify: StatGroup增加AppName字段 +- [opt] HTTP-Notify: session相关的回调增加ReadBytesSum和WroteBytesSum字段 + #### v0.30.1 (2022-06-15) - [feat] HTTP-API:新增start/stop_relay_pull接口,支持rtmp和rtsp,支持设置超时时间,自动关闭,重试次数,rtsp类型等参数 diff --git a/README.md b/README.md index 9746fdd..1717416 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,11 @@ [![CI](https://github.com/q191201771/lal/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/q191201771/lal/actions/workflows/ci.yml) [![goreportcard](https://goreportcard.com/badge/github.com/q191201771/lal)](https://goreportcard.com/report/github.com/q191201771/lal) ![wechat](https://img.shields.io/:微信-q191201771-blue.svg) -![qqgroup](https://img.shields.io/:QQ群-1090510973-blue.svg) +![qqgroup](https://img.shields.io/:QQ群-635846365-blue.svg) [中文文档](https://pengrl.com/lal/#/) -LAL is an audio/video live streaming broadcast server written in Go. It's sort of like `nginx-rtmp-module`, but easier to use and with more features, e.g RTMP, RTSP(RTP/RTCP), HLS, HTTP[S]/WebSocket[s]-FLV/TS, H264/H265/AAC, relay, cluster, record, HTTP API/Notify, GOP cache. +LAL is an audio/video live streaming broadcast server written in Go. It's sort of like `nginx-rtmp-module`, but easier to use and with more features, e.g RTMP, RTSP(RTP/RTCP), HLS, HTTP[S]/WebSocket[s]-FLV/TS, GB28181, H264/H265/AAC, relay, cluster, record, HTTP API/Notify, GOP cache. ## Install @@ -109,4 +109,4 @@ Bugs, questions, suggestions, anything related or not, feel free to contact me w MIT, see [License](https://github.com/q191201771/lal/blob/master/LICENSE). -updated by yoko, 20211204 +this note updated by yoko, 202209 diff --git a/app/demo/customize_lalserver/customize_lalserver.go b/app/demo/customize_lalserver/customize_lalserver.go index 26e28ce..5ab1f5c 100644 --- a/app/demo/customize_lalserver/customize_lalserver.go +++ b/app/demo/customize_lalserver/customize_lalserver.go @@ -11,13 +11,16 @@ package main import ( "flag" "fmt" - "github.com/q191201771/lal/pkg/aac" - "github.com/q191201771/lal/pkg/avc" - "github.com/q191201771/naza/pkg/nazalog" "io/ioutil" "os" "time" + "github.com/q191201771/lal/pkg/aac" + "github.com/q191201771/lal/pkg/avc" + "github.com/q191201771/lal/pkg/httpflv" + "github.com/q191201771/lal/pkg/remux" + "github.com/q191201771/naza/pkg/nazalog" + "github.com/q191201771/lal/pkg/base" "github.com/q191201771/lal/pkg/logic" @@ -38,6 +41,7 @@ func main() { // 比常规lalserver多加了这一行 go showHowToCustomizePub(lals) + go showHowToFlvCustomizePub(lals) err := lals.RunLoop() nazalog.Infof("server manager done. err=%+v", err) @@ -57,6 +61,35 @@ func parseFlag() string { return *cf } +func showHowToFlvCustomizePub(lals logic.ILalServer) { + const ( + flvfilename = "/tmp/test.flv" + customizePubStreamName = "f110" + ) + + time.Sleep(200 * time.Millisecond) + + tags, err := httpflv.ReadAllTagsFromFlvFile(flvfilename) + nazalog.Assert(nil, err) + + session, err := lals.AddCustomizePubSession(customizePubStreamName) + nazalog.Assert(nil, err) + + startRealTime := time.Now() + startTs := int64(0) + for _, tag := range tags { + msg := remux.FlvTag2RtmpMsg(tag) + diffTs := int64(msg.Header.TimestampAbs) - startTs + diffReal := time.Since(startRealTime).Milliseconds() + if diffReal < diffTs { + time.Sleep(time.Duration(diffTs-diffReal) * time.Millisecond) + } + session.FeedRtmpMsg(msg) + } + + lals.DelCustomizePubSession(session) +} + func showHowToCustomizePub(lals logic.ILalServer) { const ( h264filename = "/tmp/test.h264" @@ -104,7 +137,6 @@ func showHowToCustomizePub(lals logic.ILalServer) { } // readAudioPacketsFromFile 从aac es流文件读取所有音频包 -// func readAudioPacketsFromFile(filename string) (audioContent []byte, audioPackets []base.AvPacket) { var err error audioContent, err = ioutil.ReadFile(filename) @@ -136,7 +168,6 @@ func readAudioPacketsFromFile(filename string) (audioContent []byte, audioPacket } // readVideoPacketsFromFile 从h264 es流文件读取所有视频包 -// func readVideoPacketsFromFile(filename string) (videoContent []byte, videoPackets []base.AvPacket) { var err error videoContent, err = ioutil.ReadFile(filename) @@ -166,7 +197,6 @@ func readVideoPacketsFromFile(filename string) (videoContent []byte, videoPacket } // mergePackets 将音频队列和视频队列按时间戳有序合并为一个队列 -// func mergePackets(audioPackets, videoPackets []base.AvPacket) (packets []base.AvPacket) { var i, j int for { diff --git a/app/demo/pullhttpflv/pullhttpflv.go b/app/demo/pullhttpflv/pullhttpflv.go index fe6f51b..52b296c 100644 --- a/app/demo/pullhttpflv/pullhttpflv.go +++ b/app/demo/pullhttpflv/pullhttpflv.go @@ -9,19 +9,17 @@ package main import ( - "encoding/hex" "flag" + "os" "github.com/q191201771/lal/pkg/base" "github.com/q191201771/lal/pkg/httpflv" + "github.com/q191201771/lal/pkg/rtmp" + "github.com/q191201771/naza/pkg/bele" "github.com/q191201771/naza/pkg/nazalog" ) -// 拉取HTTP-FLV的流 -// -// TODO -// - 存储成flv文件 -// - 拉取HTTP-FLV流进行分析参见另外一个demo:analyseflv。 这个demo可能可以删除掉了。 +// 拉取http-flv的流并存储为flv文件 func main() { _ = nazalog.Init(func(option *nazalog.Option) { @@ -30,27 +28,75 @@ func main() { defer nazalog.Sync() base.LogoutStartInfo() - url := parseFlag() + url, flvname := parseFlag() + flvfile, err := os.Create(flvname) + if err != nil { + nazalog.Errorf("create flv file failed, err=%+v", err) + return + } + + defer flvfile.Close() + session := httpflv.NewPullSession() - err := session.Pull(url, func(tag httpflv.Tag) { - switch tag.Header.Type { - case httpflv.TagTypeMetadata: - nazalog.Info(hex.Dump(tag.Payload())) - case httpflv.TagTypeAudio: - case httpflv.TagTypeVideo: + err = session.Pull(url, func(tag httpflv.Tag) { + if tag.Header.Type == httpflv.TagTypeMetadata { + // TODO(chef): httpflv.PullSession支持返回flv header,可供业务方选择使用 202210 + // 根据metadata填写flv头 + opa, err := rtmp.ParseMetadata(tag.Payload()) + if err != nil { + nazalog.Errorf("ParseMetadata failed, err=%+v", err) + return + } + + b := make([]byte, 13) + var flags uint8 + + audiocodecid := opa.Find("audiocodecid") + videocodecid := opa.Find("videocodecid") + if audiocodecid != 0 { + flags |= 0x04 + } + + if videocodecid != 0 { + flags |= 0x01 + } + + writeFlvHeader(b, flags) + flvfile.Write(b) } + + nazalog.Infof("tag Type:%d, tag Size:%d", tag.Header.Type, tag.Header.DataSize) + + flvfile.Write(tag.Raw) }) nazalog.Assert(nil, err) err = <-session.WaitChan() nazalog.Assert(nil, err) } -func parseFlag() string { - url := flag.String("i", "", "specify http-flv url") +func parseFlag() (url, flvfile string) { + i := flag.String("i", "", "specify http-flv url") + o := flag.String("o", "", "specify output flv file") flag.Parse() - if *url == "" { + if *i == "" || *o == "" { flag.Usage() base.OsExitAndWaitPressIfWindows(1) } - return *url + return *i, *o +} + +func writeFlvHeader(b []byte, flags uint8) { + + // 'FLV', version 1 + bele.BePutUint32(b, 0x464c5601) + b[4] = flags + + // DataOffset: UI32 Offset in bytes from start of file to start of body (that is, size of header) + // The DataOffset field usually has a value of 9 for FLV version 1. + bele.BePutUint32(b[5:9], 9) + + // PreviousTagSize0: UI32 Always 0 + bele.BePutUint32(b[9:13], 0) + + return } diff --git a/app/demo/pullrtmp2pushrtmp/stream_exist.go b/app/demo/pullrtmp2pushrtmp/stream_exist.go index 2b995c8..9dd1a06 100644 --- a/app/demo/pullrtmp2pushrtmp/stream_exist.go +++ b/app/demo/pullrtmp2pushrtmp/stream_exist.go @@ -17,7 +17,6 @@ import ( ) // StreamExist 检查远端rtmp流是否能正常拉取 -// func StreamExist(url string) error { const ( timeoutMs = 10000 diff --git a/app/demo/pullrtmp2pushrtmp/tunnel.go b/app/demo/pullrtmp2pushrtmp/tunnel.go index 1acc289..a4b3b2d 100644 --- a/app/demo/pullrtmp2pushrtmp/tunnel.go +++ b/app/demo/pullrtmp2pushrtmp/tunnel.go @@ -49,7 +49,6 @@ type ErrorCode struct { // @param inUrl 拉流rtmp url地址 // @param outUrlList 推流rtmp url地址列表 -// func NewTunnel(inUrl string, outUrlList []string) *Tunnel { var streamName string ctx, err := base.ParseRtmpUrl(inUrl) @@ -76,8 +75,11 @@ func NewTunnel(inUrl string, outUrlList []string) *Tunnel { } } -// @return err 为nil时,表示任务启动成功,拉流和推流通道都已成功建立,并开始转推数据 -// 不为nil时,表示任务失败,可以通过`code`得到是拉流还是推流失败 +// Start +// +// @return ErrorCode.err: +// - 为nil时,表示任务启动成功,拉流和推流通道都已成功建立,并开始转推数据。 +// - 不为nil时,表示任务失败,可以通过`code`得到是拉流还是推流失败。 func (t *Tunnel) Start() (ret ErrorCode) { const ( pullTimeoutMs = 10000 @@ -236,14 +238,12 @@ func (t *Tunnel) Start() (ret ErrorCode) { } // `Start`函数调用成功后,可调用`Wait`函数,等待任务结束 -// func (t *Tunnel) Wait() chan ErrorCode { return t.waitChan } // `Start`函数调用成功后,可调用`Close`函数,主动关闭转推任务 // `Close`函数允许调用多次 -// func (t *Tunnel) Close() { t.notifyClose() } diff --git a/app/demo/pullrtsp2pushrtsp/pullrtsp2pushrtsp.go b/app/demo/pullrtsp2pushrtsp/pullrtsp2pushrtsp.go index 3687e28..cca4a2c 100644 --- a/app/demo/pullrtsp2pushrtsp/pullrtsp2pushrtsp.go +++ b/app/demo/pullrtsp2pushrtsp/pullrtsp2pushrtsp.go @@ -57,11 +57,9 @@ func NewRtspTunnel(pullUrl string, pushUrl string, pullOverTcp bool, pushOverTcp } } -// Start 开启任务,阻塞直到任务开启成功或失败 -// -// @return true 表示任务启动成功,此时数据已经在后台转发 -// false 表示任务启动失败 +// Start 开启任务,阻塞直到任务开启成功或失败。 // +// @return: 如果为nil,表示任务启动成功,此时数据已经在后台转发 func (r *RtspTunnel) Start() error { r.pullSession = rtsp.NewPullSession(r, func(option *rtsp.PullSessionOption) { option.PullTimeoutMs = 5000 @@ -117,7 +115,6 @@ func (r *RtspTunnel) Start() error { // 注意,只有 Start 成功后的tunnel才能调用,否则行为未定义 // // 更详细的说明参考 IClientSessionLifecycle interface -// func (r *RtspTunnel) Dispose() error { return r.dispose(nil) } @@ -125,7 +122,6 @@ func (r *RtspTunnel) Dispose() error { // WaitChan Start 成功后,可使用这个channel来接收tunnel结束的消息 // // 更详细的说明参考 IClientSessionLifecycle interface -// func (r *RtspTunnel) WaitChan() chan error { return r.waitChan } diff --git a/conf/lalserver.conf.json b/conf/lalserver.conf.json index e9c4e12..4f73dc6 100644 --- a/conf/lalserver.conf.json +++ b/conf/lalserver.conf.json @@ -1,11 +1,18 @@ { "# doc of config": "https://pengrl.com/lal/#/ConfigBrief", - "conf_version": "v0.3.3", + "conf_version": "v0.4.1", "rtmp": { "enable": true, "addr": ":1935", + "rtmps_enable": true, + "rtmps_addr": ":4935", + "rtmps_cert_file": "./conf/cert.pem", + "rtmps_key_file": "./conf/key.pem", "gop_num": 0, - "merge_write_size": 0, + "single_gop_max_frame_num": 0, + "merge_write_size": 0 + }, + "in_session": { "add_dummy_audio_enable": false, "add_dummy_audio_wait_audio_ms": 150 }, @@ -19,7 +26,8 @@ "enable": true, "enable_https": true, "url_pattern": "/", - "gop_num": 0 + "gop_num": 0, + "single_gop_max_frame_num": 0 }, "hls": { "enable": true, @@ -38,11 +46,16 @@ "enable": true, "enable_https": true, "url_pattern": "/", - "gop_num": 0 + "gop_num": 0, + "single_gop_max_frame_num": 0 }, "rtsp": { "enable": true, "addr": ":5544", + "rtsps_enable": true, + "rtsps_addr": ":5322", + "rtsps_cert_file": "./conf/cert.pem", + "rtsps_key_file": "./conf/key.pem", "out_wait_key_frame_flag": true, "auth_enable": false, "auth_method": 1, diff --git a/conf/lalserver.conf.json.tmpl b/conf/lalserver.conf.json.tmpl index 818b365..b31ecc8 100644 --- a/conf/lalserver.conf.json.tmpl +++ b/conf/lalserver.conf.json.tmpl @@ -1,11 +1,18 @@ { "# doc of config": "https://pengrl.com/lal/#/ConfigBrief", - "conf_version": "v0.3.3", + "conf_version": "v0.4.1", "rtmp": { "enable": true, "addr": ":1935", + "rtmps_enable": true, + "rtmps_addr": ":4935", + "rtmps_cert_file": "./conf/cert.pem", + "rtmps_key_file": "./conf/key.pem", "gop_num": 0, - "merge_write_size": 0, + "single_gop_max_frame_num": 0, + "merge_write_size": 0 + }, + "in_session": { "add_dummy_audio_enable": false, "add_dummy_audio_wait_audio_ms": 150 }, @@ -19,7 +26,8 @@ "enable": true, "enable_https": true, "url_pattern": "/", - "gop_num": 0 + "gop_num": 0, + "single_gop_max_frame_num": 0 }, "hls": { "enable": true, @@ -36,11 +44,16 @@ "enable": true, "enable_https": true, "url_pattern": "/", - "gop_num": 0 + "gop_num": 0, + "single_gop_max_frame_num": 0 }, "rtsp": { "enable": true, "addr": ":5544", + "rtsps_enable": true, + "rtsps_addr": ":5322", + "rtsps_cert_file": "./conf/cert.pem", + "rtsps_key_file": "./conf/key.pem", "out_wait_key_frame_flag": true, "auth_enable": false, "auth_method": 1, diff --git a/gen_tag.sh b/gen_tag.sh index f32514f..6a16723 100755 --- a/gen_tag.sh +++ b/gen_tag.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# 根据CHANGELOG.md中的最新版本号,决定是否更新version.go和以及打git tag +# 根据CHANGELOG.md中的最新版本号,决定是否更新t_version.go和以及打git tag # # TODO(chef): 新电脑没有gsed导致失败了 # @@ -22,16 +22,16 @@ echo "newest version in git tag: " $GitTag # 源码中的版本号 FileVersion=`cat pkg/base/t_version.go | grep 'const LalVersion' | awk -F\" '{print $2}'` -echo "newest version in version.go: " $FileVersion +echo "newest version in t_version.go: " $FileVersion # CHANGELOG.md和源码中的不一致,更新源码,并提交修改 if [ "$NewVersion" == "$FileVersion" ];then echo 'same tag, noop.' else - echo 'update version.go' - gsed -i "/^const LalVersion/cconst LalVersion = \"${NewVersion}\"" pkg/base/version.go - git add pkg/base/version.go - git commit -m "${NewVersion} -> version.go" + echo 'update t_version.go' + gsed -i "/^const LalVersion/cconst LalVersion = \"${NewVersion}\"" pkg/base/t_version.go + git add pkg/base/t_version.go + git commit -m "${NewVersion} -> t_version.go" git push fi diff --git a/go.mod b/go.mod index d37c238..5d81c90 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module github.com/q191201771/lal go 1.14 -require github.com/q191201771/naza v0.30.3 +require github.com/q191201771/naza v0.30.8 diff --git a/go.sum b/go.sum index 8a90518..8169027 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -github.com/q191201771/naza v0.30.3 h1:zK3cumtPY8Wj+HQpXNs2JendbGCw96qbsGosRez7oLo= -github.com/q191201771/naza v0.30.3/go.mod h1:n+dpJjQSh90PxBwxBNuifOwQttywvSIN5TkWSSYCeBk= +github.com/q191201771/naza v0.30.8 h1:Lhh29o65C4PmTDj2l+eKfsw9dddpgWZk4bFICtcnSaA= +github.com/q191201771/naza v0.30.8/go.mod h1:n+dpJjQSh90PxBwxBNuifOwQttywvSIN5TkWSSYCeBk= diff --git a/pkg/aac/aac.go b/pkg/aac/aac.go index 097c8b4..663220e 100644 --- a/pkg/aac/aac.go +++ b/pkg/aac/aac.go @@ -62,7 +62,6 @@ const ( // audio object type [5b] 1=AAC MAIN 2=AAC LC // samplingFrequencyIndex [4b] 0=96000, 1=88200, 2=64000, 3=48000, 4=44100, 5=32000, 6=24000, 7=22050, 8=16000, 9=12000, 10=11025, 11=11025, 12=7350 // channelConfiguration [4b] 1=center front speaker 2=left, right front speakers -// type AscContext struct { AudioObjectType uint8 // [5b] SamplingFrequencyIndex uint8 // [4b] @@ -79,10 +78,10 @@ func NewAscContext(asc []byte) (*AscContext, error) { // Unpack // -// @param asc: 2字节的AAC Audio Specifc Config -// 注意,如果是rtmp/flv的message/tag,应去除Seq Header头部的2个字节 -// 函数调用结束后,内部不持有该内存块 +// @param asc: 2字节的AAC Audio Specifc Config。 // +// 注意,如果是rtmp/flv的message/tag,应去除Seq Header头部的2个字节。 +// 函数调用结束后,内部不持有该内存块。 func (ascCtx *AscContext) Unpack(asc []byte) error { if len(asc) < minAscLength { return nazaerrors.Wrap(base.ErrShortBuffer) @@ -98,7 +97,6 @@ func (ascCtx *AscContext) Unpack(asc []byte) error { // Pack // // @return asc: 内存块为独立新申请;函数调用结束后,内部不持有该内存块 -// func (ascCtx *AscContext) Pack() (asc []byte) { asc = make([]byte, minAscLength) bw := nazabits.NewBitWriter(asc) @@ -110,13 +108,13 @@ func (ascCtx *AscContext) Pack() (asc []byte) { // PackAdtsHeader // -// 获取ADTS头,由于ADTS头中的字段依赖包的长度,而每个包的长度可能不同,所以每个包的ADTS头都需要独立生成 +// 获取ADTS头,由于ADTS头中的字段依赖包的长度,而每个包的长度可能不同,所以每个包的ADTS头都需要独立生成。 // -// @param frameLength: raw aac frame的大小 -// 注意,如果是rtmp/flv的message/tag,应去除Seq Header头部的2个字节 +// @param frameLength: raw aac frame的大小。 // -// @return h: 内存块为独立新申请;函数调用结束后,内部不持有该内存块 +// 注意,如果是rtmp/flv的message/tag,应去除Seq Header头部的2个字节。 // +// @return h: 内存块为独立新申请;函数调用结束后,内部不持有该内存块 func (ascCtx *AscContext) PackAdtsHeader(frameLength int) (out []byte) { out = make([]byte, AdtsHeaderLength) _ = ascCtx.PackToAdtsHeader(out, frameLength) @@ -126,7 +124,6 @@ func (ascCtx *AscContext) PackAdtsHeader(frameLength int) (out []byte) { // PackToAdtsHeader // // @param out: 函数调用结束后,内部不持有该内存块 -// func (ascCtx *AscContext) PackToAdtsHeader(out []byte, frameLength int) error { if len(out) < AdtsHeaderLength { return nazaerrors.Wrap(base.ErrShortBuffer) @@ -239,7 +236,6 @@ func NewAdtsHeaderContext(adtsHeader []byte) (*AdtsHeaderContext, error) { // Unpack // // @param adtsHeader: 函数调用结束后,内部不持有该内存块 -// func (ctx *AdtsHeaderContext) Unpack(adtsHeader []byte) error { if len(adtsHeader) < AdtsHeaderLength { return nazaerrors.Wrap(base.ErrShortBuffer) @@ -267,7 +263,6 @@ func (ctx *AdtsHeaderContext) Unpack(adtsHeader []byte) error { // @param adtsHeader: 函数调用结束后,内部不持有该内存块 // // @return asc: 内存块为独立新申请;函数调用结束后,内部不持有该内存块 -// func MakeAscWithAdtsHeader(adtsHeader []byte) (asc []byte, err error) { var ctx *AdtsHeaderContext if ctx, err = NewAdtsHeaderContext(adtsHeader); err != nil { diff --git a/pkg/aac/aac_test.go b/pkg/aac/aac_test.go index 61f4a51..1ad54c8 100644 --- a/pkg/aac/aac_test.go +++ b/pkg/aac/aac_test.go @@ -85,6 +85,13 @@ func TestAscContext(t *testing.T) { sf, err = ascCtx.GetSamplingFrequency() assert.Equal(t, -1, sf) assert.IsNotNil(t, err) + + // case + ascCtx, err = aac.NewAscContext([]byte{0x14, 0x08}) + assert.Equal(t, nil, err) + sf, err = ascCtx.GetSamplingFrequency() + assert.Equal(t, 16000, sf) + assert.Equal(t, nil, err) } func TestMakeAudioDataSeqHeader(t *testing.T) { diff --git a/pkg/aac/seqheader.go b/pkg/aac/seqheader.go index 16401d7..4896d5b 100644 --- a/pkg/aac/seqheader.go +++ b/pkg/aac/seqheader.go @@ -26,7 +26,6 @@ import ( // soundSize [1b] 0=snd8Bit, 1=snd16Bit // soundType [1b] 0=sndMono, 1=sndStereo. AAC always 1 // aacPackageType [8b] 0=seq header, 1=AAC raw -// type SequenceHeaderContext struct { SoundFormat uint8 // [4b] SoundRate uint8 // [2b] @@ -37,9 +36,9 @@ type SequenceHeaderContext struct { // Unpack // -// @param b: rtmp/flv的message/tag的payload的前2个字节 -// 函数调用结束后,内部不持有该内存块 +// @param b: rtmp/flv的message/tag的payload的前2个字节。 // +// 函数调用结束后,内部不持有该内存块 func (shCtx *SequenceHeaderContext) Unpack(b []byte) { br := nazabits.NewBitReader(b) shCtx.SoundFormat, _ = br.ReadBits8(4) @@ -54,7 +53,6 @@ func (shCtx *SequenceHeaderContext) Unpack(b []byte) { // @param asc: 函数调用结束后,内部不持有该内存块 // // @return out: 内存块为独立新申请;函数调用结束后,内部不持有该内存块 -// func MakeAudioDataSeqHeaderWithAsc(asc []byte) (out []byte, err error) { if len(asc) < minAscLength { return nil, nazaerrors.Wrap(base.ErrShortBuffer) @@ -73,7 +71,6 @@ func MakeAudioDataSeqHeaderWithAsc(asc []byte) (out []byte, err error) { // @param adtsHeader: 函数调用结束后,内部不持有该内存块 // // @return out: 内存块为独立新申请;函数调用结束后,内部不持有该内存块 -// func MakeAudioDataSeqHeaderWithAdtsHeader(adtsHeader []byte) (out []byte, err error) { var asc []byte if asc, err = MakeAscWithAdtsHeader(adtsHeader); err != nil { diff --git a/pkg/avc/avc.go b/pkg/avc/avc.go index 94da6a0..268689b 100644 --- a/pkg/avc/avc.go +++ b/pkg/avc/avc.go @@ -43,7 +43,6 @@ var ( // // H.264-AVC-ISO_IEC_14496-15.pdf // Table 1 - NAL unit types in elementary streams -// var NaluTypeMapping = map[uint8]string{ 1: "SLICE", 5: "IDR", @@ -96,7 +95,6 @@ type Context struct { // // H.264-AVC-ISO_IEC_14496-15.pdf // 5.2.4 Decoder configuration information -// type DecoderConfigurationRecord struct { ConfigurationVersion uint8 AvcProfileIndication uint8 @@ -114,7 +112,6 @@ type DecoderConfigurationRecord struct { // ISO-14496-10.pdf // 7.3.2.1 Sequence parameter set RBSP syntax // 7.4.2.1 Sequence parameter set RBSP semantics -// type Sps struct { ProfileIdc uint8 ConstraintSet0Flag uint8 @@ -220,14 +217,14 @@ func ParseSliceTypeReadable(nalu []byte) (string, error) { // SpsPpsSeqHeader2Annexb // -// AVCC Seq Header -> Annexb +// AVCC Seq Header转换为Annexb格式。 // // @param payload: -// rtmp message的payload部分或者flv tag的payload部分。 -// 注意,包含了头部2字节类型以及3字节的cts。 // -// @return 返回的内存块为内部独立新申请。 +// rtmp message的payload部分或者flv tag的payload部分。 +// 注意,包含了头部2字节类型以及3字节的cts。 // +// @return 返回的内存块为内部独立新申请。 func SpsPpsSeqHeader2Annexb(payload []byte) ([]byte, error) { // TODO(chef): [refactor] 这里没有使用 ParseSpsPpsFromSeqHeaderWithoutMalloc // 因为遇到了sps>1个的情况 @@ -256,7 +253,6 @@ func SpsPpsSeqHeader2Annexb(payload []byte) ([]byte, error) { // 见func ParseSpsPpsFromSeqHeaderWithoutMalloc // // @return sps, pps: 内存块为内部独立新申请 -// func ParseSpsPpsFromSeqHeader(payload []byte) (sps, pps []byte, err error) { s, p, e := ParseSpsPpsFromSeqHeaderWithoutMalloc(payload) if e != nil { @@ -270,8 +266,6 @@ func ParseSpsPpsFromSeqHeader(payload []byte) (sps, pps []byte, err error) { // BuildSpsPps2Annexb // // 根据sps pps构建payload -// -// func BuildSpsPps2Annexb(sps, pps []byte) []byte { var ret []byte ret = append(ret, NaluStartCode4...) @@ -283,13 +277,13 @@ func BuildSpsPps2Annexb(sps, pps []byte) []byte { // ParseSpsPpsFromSeqHeaderWithoutMalloc // -// 从AVCC格式的Seq Header中得到SPS和PPS内存块 +// 从AVCC格式的Seq Header中得到SPS和PPS内存块。 // -// @param payload: rtmp message的payload部分或者flv tag的payload部分 -// 注意,包含了头部2字节类型以及3字节的cts +// @param payload: rtmp message的payload部分或者flv tag的payload部分。 // -// @return sps, pps: 复用传入参数`payload`的内存块 +// 注意,包含了头部2字节类型以及3字节的cts。 // +// @return sps, pps: 复用传入参数`payload`的内存块 func ParseSpsPpsFromSeqHeaderWithoutMalloc(payload []byte) (sps, pps []byte, err error) { if len(payload) < 13 { return nil, nil, nazaerrors.Wrap(base.ErrShortBuffer) @@ -337,7 +331,6 @@ func ParseSpsPpsFromSeqHeaderWithoutMalloc(payload []byte) (sps, pps []byte, err // BuildSeqHeaderFromSpsPps // // @return 内存块为内部独立新申请 -// func BuildSeqHeaderFromSpsPps(sps, pps []byte) ([]byte, error) { var sh []byte sh = make([]byte, 16+len(sps)+len(pps)) @@ -383,11 +376,11 @@ func BuildSeqHeaderFromSpsPps(sps, pps []byte) ([]byte, error) { // CaptureAvcc2Annexb // -// AVCC -> Annexb +// AVCC转换为Annexb格式。 // -// @param payload: rtmp message的payload部分或者flv tag的payload部分 -// 注意,包含了头部2字节类型以及3字节的cts +// @param payload: rtmp message的payload部分或者flv tag的payload部分。 // +// 注意,包含了头部2字节类型以及3字节的cts func CaptureAvcc2Annexb(w io.Writer, payload []byte) error { // sps pps if payload[0] == 0x17 && payload[1] == 0x00 { @@ -414,14 +407,16 @@ func CaptureAvcc2Annexb(w io.Writer, payload []byte) error { // IterateNaluStartCode // -// 遍历直到找到第一个nalu start code的位置 +// 遍历直到找到第一个nalu start code的位置。 +// +// @param start: 从`nalu`的start位置开始查找。 // -// @param start: 从`nalu`的start位置开始查找 +// @return pos: start code的起始位置(包含start code自身)。 // -// @return pos: start code的起始位置(包含start code自身) -// length: start code的长度,可能是3或者4 -// 注意,如果找不到start code,则返回-1, -1 +// @return length: // +// start code的长度,可能是3或者4。 +// 注意,如果找不到start code,则返回-1, -1。 func IterateNaluStartCode(nalu []byte, start int) (pos, length int) { if nalu == nil || start >= len(nalu) { return -1, -1 @@ -450,7 +445,6 @@ func IterateNaluStartCode(nalu []byte, start int) (pos, length int) { // 具体见单元测试 // // @return nalList: 内存块元素引用输入参数`nals`的内存 -// func SplitNaluAnnexb(nals []byte) (nalList [][]byte, err error) { err = IterateNaluAnnexb(nals, func(nal []byte) { nalList = append(nalList, nal) @@ -463,7 +457,6 @@ func SplitNaluAnnexb(nals []byte) (nalList [][]byte, err error) { // 遍历AVCC格式,去掉4字节长度,获取nal包,正常情况下可能返回1个或多个,异常情况下可能一个也没有 // // 具体见单元测试 -// func SplitNaluAvcc(nals []byte) (nalList [][]byte, err error) { err = IterateNaluAvcc(nals, func(nal []byte) { nalList = append(nalList, nal) @@ -474,7 +467,6 @@ func SplitNaluAvcc(nals []byte) (nalList [][]byte, err error) { // IterateNaluAnnexb // // @param handler: 回调函数中的`nal`参数引用`nals`中的内存 -// func IterateNaluAnnexb(nals []byte, handler func(nal []byte)) error { if nals == nil { return nazaerrors.Wrap(base.ErrShortBuffer) @@ -549,6 +541,7 @@ func IterateNaluAvcc(nals []byte, handler func(nal []byte)) error { } func Avcc2Annexb(nals []byte) ([]byte, error) { + // TODO(chef): 增加原地转换,不申请内存的方式 202206 ret := make([]byte, len(nals)) ret = ret[0:0] err := IterateNaluAvcc(nals, func(nal []byte) { @@ -559,8 +552,9 @@ func Avcc2Annexb(nals []byte) ([]byte, error) { } func Annexb2Avcc(nals []byte) ([]byte, error) { + // TODO(chef): 增加原地转换,不申请内存的方式。考虑原地内存不够大的情况 202206 var buf nazabytes.Buffer - buf.Grow(len(nals)) + buf.Grow(len(nals) + 16) // perf: start code是三字节0 0 1时,转换时每个nal会多需要一个字节,预先申请16个字节,减少后续扩容的可能性 err := IterateNaluAnnexb(nals, func(nal []byte) { bele.BePutUint32(buf.ReserveBytes(4), uint32(len(nal))) buf.Flush(4) @@ -573,15 +567,16 @@ func Annexb2Avcc(nals []byte) ([]byte, error) { // parseSpsPpsListFromSeqHeaderWithoutMalloc // -// 从AVCC格式的Seq Header中得到SPS和PPS内存块 +// 从AVCC格式的Seq Header中得到SPS和PPS内存块。 // // @param payload: -// rtmp message的payload部分或者flv tag的payload部分。 -// 注意,包含了头部2字节类型以及3字节的cts。 +// +// rtmp message的payload部分或者flv tag的payload部分。 +// 注意,包含了头部2字节类型以及3字节的cts。 // // @return spsList, ppsList: -// 复用传入参数`payload`的内存块 // +// 复用传入参数`payload`的内存块 func parseSpsPpsListFromSeqHeaderWithoutMalloc(payload []byte) (spsList, ppsList [][]byte, err error) { if len(payload) < 5 { return nil, nil, nazaerrors.Wrap(base.ErrShortBuffer) diff --git a/pkg/avc/beta.go b/pkg/avc/beta.go index 4016561..a0f5b37 100644 --- a/pkg/avc/beta.go +++ b/pkg/avc/beta.go @@ -54,11 +54,11 @@ func TryParsePps(payload []byte) error { return nil } -// TryParseSeqHeader 尝试解析SeqHeader所有字段,实验中,请勿直接使用该函数 -// -// @param rtmp message的payload部分或者flv tag的payload部分 -// 注意,包含了头部2字节类型以及3字节的cts +// TryParseSeqHeader 尝试解析SeqHeader所有字段,实验中,请勿直接使用该函数。 // +// @param payload: +// rtmp message的payload部分或者flv tag的payload部分。 +// 注意,包含了头部2字节类型以及3字节的cts。 func TryParseSeqHeader(payload []byte) error { if len(payload) < 5 { return nazaerrors.Wrap(base.ErrShortBuffer) diff --git a/pkg/base/avpacket.go b/pkg/base/avpacket.go index d44025d..046cb32 100644 --- a/pkg/base/avpacket.go +++ b/pkg/base/avpacket.go @@ -44,10 +44,7 @@ func (a AvPacketPt) ReadableString() string { // AvPacket // // 不同场景使用时,字段含义可能不同。 -// 使用AvPacket的地方,应注明各字段的含义。 -// -// -// +// 使用 AvPacket 的地方,应注明各字段的含义。 type AvPacket struct { PayloadType AvPacketPt Timestamp int64 // 如无特殊说明,此字段是Dts @@ -65,7 +62,7 @@ func (packet *AvPacket) IsVideo() bool { func (packet *AvPacket) DebugString() string { return fmt.Sprintf("[%p] type=%s, timestamp=%d, pts=%d, len=%d, payload=%s", - packet, packet.PayloadType.ReadableString(), packet.Timestamp, packet.Pts, len(packet.Payload), hex.Dump(nazabytes.Prefix(packet.Payload, 64))) + packet, packet.PayloadType.ReadableString(), packet.Timestamp, packet.Pts, len(packet.Payload), hex.Dump(nazabytes.Prefix(packet.Payload, 8))) } // --------------------------------------------------------------------------------------------------------------------- diff --git a/pkg/base/avpacket_stream.go b/pkg/base/avpacket_stream.go index 1dcb472..3229f7d 100644 --- a/pkg/base/avpacket_stream.go +++ b/pkg/base/avpacket_stream.go @@ -46,7 +46,7 @@ type IAvPacketStream interface { // 注意,调用 FeedAvPacket 传入AAC音频数据前,需要先调用 FeedAudioSpecificConfig。 // FeedAudioSpecificConfig 在最开始总共调用一次,后面就可以一直调用 FeedAvPacket // - FeedAudioSpecificConfig(asc []byte) + FeedAudioSpecificConfig(asc []byte) error // FeedAvPacket // @@ -66,5 +66,5 @@ type IAvPacketStream interface { // Avcc也即[<4字节长度 + nal>...],Annexb也即[<4字节start code 00 00 00 01 + nal>...]。 // 注意,sps和pps也通过 FeedAvPacket 传入。sps和pps可以单独调用 FeedAvPacket,也可以sps+pps+I帧组合在一起调用一次 FeedAvPacket // - FeedAvPacket(packet AvPacket) + FeedAvPacket(packet AvPacket) error } diff --git a/pkg/base/base.go b/pkg/base/base.go index 40192aa..a7047e0 100644 --- a/pkg/base/base.go +++ b/pkg/base/base.go @@ -6,6 +6,7 @@ // // Author: Chef (191201771@qq.com) +// Package base 提供被其他多个package依赖的基础内容,自身不依赖任何package package base import ( @@ -16,14 +17,11 @@ import ( "github.com/q191201771/naza/pkg/bininfo" ) -// base包提供被其他多个package依赖的基础内容,自身不依赖任何package -// // TODO chef: 考虑部分内容放入关联的协议package的子package中 var startTime string // ReadableNowTime 当前时间,可读字符串形式 -// func ReadableNowTime() string { return time.Now().Format("2006-01-02 15:04:05.999") } diff --git a/pkg/base/basic_http_sub_session.go b/pkg/base/basic_http_sub_session.go index 5b41fb4..b3f3d7b 100644 --- a/pkg/base/basic_http_sub_session.go +++ b/pkg/base/basic_http_sub_session.go @@ -36,7 +36,7 @@ func NewBasicHttpSubSession(option BasicHttpSubSessionOption) *BasicHttpSubSessi s := &BasicHttpSubSession{ BasicHttpSubSessionOption: option, conn: connection.New(option.Conn, option.ConnModOption), - sessionStat: NewBasicSessionStat(option.SessionType, ""), + sessionStat: NewBasicSessionStat(option.SessionType, option.Conn.RemoteAddr().String()), } return s } diff --git a/pkg/base/basic_session_stat.go b/pkg/base/basic_session_stat.go index eb132f4..c6776f6 100644 --- a/pkg/base/basic_session_stat.go +++ b/pkg/base/basic_session_stat.go @@ -24,7 +24,6 @@ type IStatable interface { // 2. 计算带宽 // // 计算带宽有两种方式,一种是通过外部的 connection.Connection 获取最新状态,一种是内部自己管理状态 -// type BasicSessionStat struct { stat StatSession @@ -39,7 +38,6 @@ type BasicSessionStat struct { // NewBasicSessionStat // // @param remoteAddr: 如果当前未知,填入""空字符串 -// func NewBasicSessionStat(sessionType SessionType, remoteAddr string) BasicSessionStat { var s BasicSessionStat s.stat.typ = sessionType @@ -150,7 +148,6 @@ func (s *BasicSessionStat) UniqueKey() string { // --------------------------------------------------------------------------------------------------------------------- // updateStat 根据两次调用间隔计算bitrate -// func (s *BasicSessionStat) updateStat(readBytesSum, wroteBytesSum uint64, typ string, intervalSec uint32) { rDiff := readBytesSum - s.prevConnStat.ReadBytesSum s.stat.ReadBitrateKbits = int(rDiff * 8 / 1024 / uint64(intervalSec)) @@ -171,7 +168,6 @@ func (s *BasicSessionStat) updateStat(readBytesSum, wroteBytesSum uint64, typ st } // isAlive 根据两次调用间隔计算是否存活 -// func (s *BasicSessionStat) isAlive(readBytesSum, wroteBytesSum uint64) (readAlive, writeAlive bool) { if s.staleStat == nil { s.staleStat = new(connection.Stat) diff --git a/pkg/base/dump_file.go b/pkg/base/dump_file.go index a478fa6..a715f8d 100644 --- a/pkg/base/dump_file.go +++ b/pkg/base/dump_file.go @@ -20,6 +20,13 @@ import ( // TODO(chef): [refactor] move to naza 202208 +// +// lal中的支持情况列表: +// 支持情况 | 协议 | 类型 | 应用 | 开关手段 | 方式 +// 已支持 | ps | pub | lalserver | http-api参数 | hook到logic中 +// 未支持 | rtsp | pull | lalserver | http-api参数 | hook到logic中 +// 未支持 | customize pub | + type DumpFile struct { file *os.File } diff --git a/pkg/base/error.go b/pkg/base/error.go index 3101d9a..6fb7265 100644 --- a/pkg/base/error.go +++ b/pkg/base/error.go @@ -76,15 +76,22 @@ var ( var ErrSdp = errors.New("lal.sdp: fxxk") -// ----- pkg/logic ------------------------------------------------------------------------------------------------------- +// ----- pkg/logic ----------------------------------------------------------------------------------------------------- var ( - ErrDupInStream = errors.New("lal.logic: in stream already exist at group") + ErrDupInStream = errors.New("lal.logic: in stream already exist at group") + ErrDisposedInStream = errors.New("lal.logic: in stream already disposed") ErrSimpleAuthParamNotFound = errors.New("lal.logic: simple auth failed since url param lal_secret not found") ErrSimpleAuthFailed = errors.New("lal.logic: simple auth failed since url param lal_secret invalid") ) +// ----- pkg/gb28181 --------------------------------------------------------------------------------------------------- + +var ( + ErrGb28181 = errors.New("lal.gb28181: fxxk") +) + // --------------------------------------------------------------------------------------------------------------------- func NewErrAmfInvalidType(b byte) error { diff --git a/pkg/base/http_server.go b/pkg/base/http_server.go index 21a497d..4a56b5e 100644 --- a/pkg/base/http_server.go +++ b/pkg/base/http_server.go @@ -17,9 +17,7 @@ import ( "github.com/q191201771/naza/pkg/nazaerrors" ) -// TODO(chef) -// - 考虑移入naza中 -// - 考虑增加一个pattern全部未命中的mux回调 +// TODO(chef): [refactor] 考虑移入naza中 202211 const ( NetworkTcp = "tcp" @@ -56,19 +54,21 @@ type Handler func(http.ResponseWriter, *http.Request) // AddListen // -// @param addrCtx IsHttps 是否为https -// 注意,如果要为相同的路由同时绑定http和https,那么应该调用该函数两次,分别将该参数设置为true和false -// Addr 监听地址,内部会为其建立监听 -// http和https不能够使用相同的地址 -// 注意,多次调用,允许使用相同的地址绑定不同的`pattern` -// CertFile -// KeyFile -// Network 如果为空默认为NetworkTcp="tcp" +// @param addrCtx: // -// @param pattern 必须以`/`开始,并以`/`结束 -// 注意,如果是`/`,则在其他所有pattern都匹配失败后,做为兜底匹配成功 -// 相同的pattern不能绑定不同的`handler`回调函数(显然,我们无法为相同的监听地址,相同的路径绑定多个回调函数) +// LocalAddrCtx.IsHttps 是否为https。 +// 注意,如果要为相同的路由同时绑定http和https,那么应该调用该函数两次,分别将该参数设置为true和false。 +// LocalAddrCtx.Addr 监听地址,内部会为其建立监听。 +// http和https不能够使用相同的地址。 +// 注意,多次调用,允许使用相同的地址绑定不同的`pattern`。 +// LocalAddrCtx.CertFile ... +// LocalAddrCtx.KeyFile ... +// LocalAddrCtx.Network 如果为空默认为NetworkTcp="tcp"。 // +// @param pattern: 必须以`/`开始,并以`/`结束。 +// +// 注意,如果是`/`,则在其他所有pattern都匹配失败后,做为兜底匹配成功。 +// 相同的pattern不能绑定不同的`handler`回调函数(显然,我们无法为相同的监听地址,相同的路径绑定多个回调函数)。 func (s *HttpServerManager) AddListen(addrCtx LocalAddrCtx, pattern string, handler Handler) error { var ( ctx *ServerCtx @@ -143,7 +143,6 @@ func (s *HttpServerManager) Dispose() error { // --------------------------------------------------------------------------------------------------------------------- // 为传入的`Addr`地址创建http或https监听 -// func listen(ctx LocalAddrCtx) (net.Listener, error) { if ctx.Network == "" { ctx.Network = NetworkTcp diff --git a/pkg/base/log.go b/pkg/base/log.go index cfa74e1..c767004 100644 --- a/pkg/base/log.go +++ b/pkg/base/log.go @@ -24,7 +24,6 @@ type LogDump struct { // NewLogDump // // @param debugMaxNum: 日志最小级别为debug时,使用debug打印日志次数的阈值 -// func NewLogDump(log nazalog.Logger, debugMaxNum int) LogDump { return LogDump{ log: log, @@ -52,7 +51,6 @@ func (ld *LogDump) ShouldDump() bool { // 将 ShouldDump 独立出来的目的是避免不需要打印日志时, Outf 调用前构造实参的开销,比如 // ld.Outf("hex=%s", hex.Dump(buf)) // 这个hex.Dump调用 -// func (ld *LogDump) Outf(format string, v ...interface{}) { ld.log.Out(ld.log.GetOption().Level, 3, fmt.Sprintf(format, v...)) } diff --git a/pkg/base/merge_writer.go b/pkg/base/merge_writer.go index 2313ff2..eb4e638 100644 --- a/pkg/base/merge_writer.go +++ b/pkg/base/merge_writer.go @@ -17,7 +17,6 @@ import ( // MergeWriter 合并多个内存块,达到阈值后一次性将内存块数组返回给上层 // // 注意,输入时的单个内存块,回调时不会出现拆分切割的情况 -// type MergeWriter struct { onWritev OnWritev size int @@ -32,7 +31,6 @@ type OnWritev func(bs net.Buffers) // // @param onWritev 回调缓存的1~n个内存块 // @param size 回调阈值 -// func NewMergeWriter(onWritev OnWritev, size int) *MergeWriter { return &MergeWriter{ onWritev: onWritev, @@ -43,7 +41,6 @@ func NewMergeWriter(onWritev OnWritev, size int) *MergeWriter { // Write // // 注意,函数调用结束后,`b`内存块会被内部持有 -// func (w *MergeWriter) Write(b []byte) { Log.Debugf("[%p] MergeWriter::Write. len=%d", w, len(b)) w.bs = append(w.bs, b) @@ -54,7 +51,6 @@ func (w *MergeWriter) Write(b []byte) { } // Flush 强制将内部缓冲的数据全部回调排空 -// func (w *MergeWriter) Flush() { Log.Debugf("[%p] MergeWriter::Flush.", w) if w.currSize > 0 { @@ -63,7 +59,6 @@ func (w *MergeWriter) Flush() { } // flush 将内部缓冲的数据全部回调排空 -// func (w *MergeWriter) flush() { // only for debug log var n int diff --git a/pkg/base/signal_unix.go b/pkg/base/signal_unix.go index 85cf1ef..04d3603 100644 --- a/pkg/base/signal_unix.go +++ b/pkg/base/signal_unix.go @@ -20,7 +20,6 @@ import ( // RunSignalHandler 监听SIGUSR1和SIGUSR2信号并回调 // // TODO(chef): refactor 函数名应与SIGUSR1挂钩 -// func RunSignalHandler(cb func()) { c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGUSR1, syscall.SIGUSR2) diff --git a/pkg/base/t_http_an__api.go b/pkg/base/t_http_an__api.go index 56aefc7..a95c03a 100644 --- a/pkg/base/t_http_an__api.go +++ b/pkg/base/t_http_an__api.go @@ -30,6 +30,7 @@ type ApiCtrlStartRelayPullReq struct { PullRetryNum int `json:"pull_retry_num"` AutoStopPullAfterNoOutMs int `json:"auto_stop_pull_after_no_out_ms"` RtspMode int `json:"rtsp_mode"` + DebugDumpPacket string `json:"debug_dump_packet"` } type ApiCtrlKickSessionReq struct { @@ -41,6 +42,7 @@ type ApiCtrlStartRtpPubReq struct { StreamName string `json:"stream_name"` Port int `json:"port"` TimeoutMs int `json:"timeout_ms"` + IsTcpFlag int `json:"is_tcp_flag"` DebugDumpPacket string `json:"debug_dump_packet"` } @@ -61,10 +63,11 @@ const ( ErrorCodeListenUdpPortFail = 2002 ) +// TODO(chef): [refactor] 所有Resp类型的结构体名加上Resp后缀 202209 + // HttpResponseBasic // // TODO(chef): 因为ILalserver会直接使用这个接口,所以重命名为ApiResponseBasic -// type HttpResponseBasic struct { ErrorCode int `json:"error_code"` Desp string `json:"desp"` diff --git a/pkg/base/t_http_an__notify.go b/pkg/base/t_http_an__notify.go index cb3f35b..9e12d59 100644 --- a/pkg/base/t_http_an__notify.go +++ b/pkg/base/t_http_an__notify.go @@ -11,13 +11,11 @@ package base // 文档见: https://pengrl.com/lal/#/HTTPNotify // EventCommonInfo 所有事件共有的字段 -// type EventCommonInfo struct { ServerId string `json:"server_id"` } // SessionEventCommonInfo session相关的事件的共有的字段 -// type SessionEventCommonInfo struct { EventCommonInfo diff --git a/pkg/base/t_rtmp.go b/pkg/base/t_rtmp.go index cc8cb26..517a078 100644 --- a/pkg/base/t_rtmp.go +++ b/pkg/base/t_rtmp.go @@ -144,7 +144,6 @@ func (msg RtmpMsg) Dts() uint32 { // Pts // // 注意,只有视频才能调用该函数获取pts,音频的dts和pts都直接使用 RtmpMsg.Header.TimestampAbs -// func (msg RtmpMsg) Pts() uint32 { return msg.Header.TimestampAbs + bele.BeUint24(msg.Payload[2:]) } diff --git a/pkg/base/t_session.go b/pkg/base/t_session.go index aee240e..a33d6dc 100644 --- a/pkg/base/t_session.go +++ b/pkg/base/t_session.go @@ -10,7 +10,7 @@ package base // ----- 所有session ----- // -// server.pub: rtmp(ServerSession), rtsp(PubSession) +// server.pub: rtmp(ServerSession), rtsp(PubSession), customize(CustomizePubSessionContext), ps(gb28181.PubSession) // server.sub: rtmp(ServerSession), rtsp(SubSession), flv(SubSession), ts(SubSession), 还有一个比较特殊的hls // // client.push: rtmp(PushSession), rtsp(PushSession) @@ -143,7 +143,6 @@ type IServerSessionLifecycle interface { // ISessionStat // // 调用约束:对于Client类型的Session,调用Start函数并返回成功后才能调用,否则行为未定义 -// type ISessionStat interface { // UpdateStat // @@ -176,7 +175,6 @@ type ISessionStat interface { // ISessionUrlContext 获取和流地址相关的信息 // // 调用约束:对于Client类型的Session,调用Start函数并返回成功后才能调用,否则行为未定义 -// type ISessionUrlContext interface { Url() string AppName() string diff --git a/pkg/base/t_version.go b/pkg/base/t_version.go index 81881f3..8cce129 100644 --- a/pkg/base/t_version.go +++ b/pkg/base/t_version.go @@ -16,19 +16,15 @@ import "strings" // 并且将这些信息打入可执行文件、日志、各协议中的标准版本字段中 // LalVersion 整个lal工程的版本号。注意,该变量由外部脚本修改维护,不要手动在代码中修改 -// -const LalVersion = "v0.30.1" +const LalVersion = "v0.32.0" // ConfVersion lalserver的配置文件的版本号 -// -const ConfVersion = "v0.3.3" +const ConfVersion = "v0.4.1" // HttpApiVersion lalserver的HTTP-API功能的版本号 -// -const HttpApiVersion = "v0.4.1" +const HttpApiVersion = "v0.4.2" // HttpNotifyVersion lalserver的HTTP-Notify功能的版本号 -// const HttpNotifyVersion = "v0.2.1" var ( diff --git a/pkg/base/url.go b/pkg/base/url.go index a463e0d..4cd1476 100644 --- a/pkg/base/url.go +++ b/pkg/base/url.go @@ -27,6 +27,7 @@ const ( DefaultHttpsPort = 443 DefaultRtspPort = 554 DefaultRtmpsPort = 443 + DefaultRtspsPort = 322 ) type UrlPathContext struct { @@ -44,16 +45,16 @@ type UrlContext struct { Username string Password string StdHost string // host or host:port - HostWithPort string - Host string - Port int + HostWithPort string // 当原始url中不包含port时,填充scheme对应的默认port + Host string // 不包含port + Port int // 当原始url中不包含port时,填充scheme对应的默认port //UrlPathContext - PathWithRawQuery string - Path string + PathWithRawQuery string // 注意,有前面的'/' + Path string // 注意,有前面的'/' PathWithoutLastItem string // 注意,没有前面的'/',也没有后面的'/' LastItemOfPath string // 注意,没有前面的'/' - RawQuery string // 参数 + RawQuery string // 参数,注意,没有前面的'?' RawUrlWithoutUserInfo string @@ -85,9 +86,9 @@ func (u *UrlContext) calcFilenameAndTypeIfNeeded() { // ParseUrl // -// @param defaultPort: 注意,如果rawUrl中显示指定了端口,则该参数不生效 -// 注意,如果设置为-1,内部依然会对常见协议(http, https, rtmp, rtsp)设置官方默认端口 -// +// @param defaultPort: +// 注意,如果rawUrl中显示指定了端口,则该参数不生效。 +// 注意,如果设置为-1,内部依然会对常见协议(http, https, rtmp, rtsp)设置官方默认端口。 func ParseUrl(rawUrl string, defaultPort int) (ctx UrlContext, err error) { ctx.Url = rawUrl @@ -112,6 +113,8 @@ func ParseUrl(rawUrl string, defaultPort int) (ctx UrlContext, err error) { defaultPort = DefaultRtspPort case "rtmps": defaultPort = DefaultRtmpsPort + case "rtsps": + defaultPort = DefaultRtspsPort } } @@ -203,7 +206,7 @@ func ParseRtspUrl(rawUrl string) (ctx UrlContext, err error) { return } // 注意,存在一种情况,使用rtsp pull session,直接拉取没有url path的流,所以不检查ctx.Path - if ctx.Scheme != "rtsp" || ctx.Host == "" { + if (ctx.Scheme != "rtsp" && ctx.Scheme != "rtsps") || ctx.Host == "" { return ctx, fmt.Errorf("%w. url=%s", ErrInvalidUrl, rawUrl) } @@ -219,7 +222,6 @@ func ParseHttpflvUrl(rawUrl string) (ctx UrlContext, err error) { // ParseHttpRequest // // @return 完整url -// func ParseHttpRequest(req *http.Request) string { // TODO(chef): [refactor] scheme是否能从从req.URL.Scheme获取 var scheme string diff --git a/pkg/base/websocket.go b/pkg/base/websocket.go index 5ba564e..75ea7ce 100644 --- a/pkg/base/websocket.go +++ b/pkg/base/websocket.go @@ -49,10 +49,11 @@ import ( // Payload length: 7 bits, 7+16 bits, or 7+64 bits // Masking-key: 0 or 4 bytes // mark 加密 -// for i := 0; i < datalen; i { -// m := markingkeys[i%4] -// data[i] = msg[i] ^ m -// } +// +// for i := 0; i < datalen; i { +// m := markingkeys[i%4] +// data[i] = msg[i] ^ m +// } type WsOpcode = uint8 const ( diff --git a/pkg/gb28181/gb28181.go b/pkg/gb28181/gb28181.go index 3aa94f2..ecf7647 100644 --- a/pkg/gb28181/gb28181.go +++ b/pkg/gb28181/gb28181.go @@ -24,13 +24,13 @@ var ( ) // ErrGb28181 TODO(chef): [refactor] move to pkg base 202207 -// var ErrGb28181 = errors.New("lal.gb28181: fxxk") +// TODO(chef): [opt] 除了队列长度,还可以通过时长控制 202209 var maxUnpackRtpListSize = 1024 var ( - defaultPubSessionPortMin = uint16(30000) + defaultPubSessionPortMin = uint16(30000) // 注意,udp和tcp都使用这个端口范围 defaultPubSessionPortMax = uint16(60000) ) diff --git a/pkg/gb28181/pub_session.go b/pkg/gb28181/pub_session.go index 9ebe1eb..ed6e694 100644 --- a/pkg/gb28181/pub_session.go +++ b/pkg/gb28181/pub_session.go @@ -11,20 +11,30 @@ package gb28181 import ( "fmt" "github.com/q191201771/lal/pkg/base" + "github.com/q191201771/naza/pkg/bele" + "github.com/q191201771/naza/pkg/nazabytes" + "github.com/q191201771/naza/pkg/nazalog" "github.com/q191201771/naza/pkg/nazanet" + "io" "net" "sync" ) +type OnReadPacket func(b []byte) + type PubSession struct { unpacker *PsUnpacker streamName string - hookOnReadUdpPacket nazanet.OnReadUdpPacket + hookOnReadPacket OnReadPacket + + isTcpFlag bool disposeOnce sync.Once - conn *nazanet.UdpConnection + udpConn *nazanet.UdpConnection + listener net.Listener + tcpConn net.Conn sessionStat base.BasicSessionStat } @@ -35,10 +45,9 @@ func NewPubSession() *PubSession { } } -// WithOnAvPacket 设置音视频的回调 -// -// @param onAvPacket 见 PsUnpacker.WithOnAvPacket 的注释 +// WithOnAvPacket 设置音视频的回调。 // +// @param onAvPacket: 见 PsUnpacker.WithOnAvPacket 的注释 func (session *PubSession) WithOnAvPacket(onAvPacket base.OnAvPacketFunc) *PubSession { session.unpacker.WithOnAvPacket(onAvPacket) return session @@ -49,61 +58,34 @@ func (session *PubSession) WithStreamName(streamName string) *PubSession { return session } -// WithHookReadUdpPacket +// WithHookReadPacket // -// 将udp接收数据返回给上层。 +// 将接收的数据返回给上层。 // 注意,底层的解析逻辑依然走。 // 可以用这个方式来截取数据进行调试。 -// -func (session *PubSession) WithHookReadUdpPacket(fn nazanet.OnReadUdpPacket) *PubSession { - session.hookOnReadUdpPacket = fn +func (session *PubSession) WithHookReadPacket(fn OnReadPacket) *PubSession { + session.hookOnReadPacket = fn return session } -// Listen +// Listen 非阻塞函数 // // 注意,当`port`参数为0时,内部会自动选择一个可用端口监听,并通过返回值返回该端口 -// -func (session *PubSession) Listen(port int) (int, error) { - var err error - var uconn *net.UDPConn - var addr string - - if port == 0 { - uconn, _, err = defaultUdpConnPoll.Acquire() - if err != nil { - return -1, err - } +func (session *PubSession) Listen(port int, isTcpFlag bool) (int, error) { + session.isTcpFlag = isTcpFlag - port = uconn.LocalAddr().(*net.UDPAddr).Port - } else { - addr = fmt.Sprintf(":%d", port) + if isTcpFlag { + return session.listenTcp(port) } - - session.conn, err = nazanet.NewUdpConnection(func(option *nazanet.UdpConnectionOption) { - option.LAddr = addr - option.Conn = uconn - }) - return port, err + return session.listenUdp(port) } -// RunLoop ... -// +// RunLoop 阻塞函数 func (session *PubSession) RunLoop() error { - err := session.conn.RunLoop(func(b []byte, raddr *net.UDPAddr, err error) bool { - if len(b) == 0 && err != nil { - return false - } - - if session.hookOnReadUdpPacket != nil { - session.hookOnReadUdpPacket(b, raddr, err) - } - - session.sessionStat.AddReadBytes(len(b)) - session.unpacker.FeedRtpPacket(b) - return true - }) - return err + if session.isTcpFlag { + return session.runLoopTcp() + } + return session.runLoopUdp() } // ----- IServerSessionLifecycle --------------------------------------------------------------------------------------- @@ -159,15 +141,125 @@ func (session *PubSession) IsAlive() (readAlive, writeAlive bool) { // --------------------------------------------------------------------------------------------------------------------- +func (session *PubSession) listenUdp(port int) (int, error) { + var err error + var uconn *net.UDPConn + var addr string + + if port == 0 { + uconn, _, err = defaultUdpConnPoll.Acquire() + if err != nil { + return -1, err + } + + port = uconn.LocalAddr().(*net.UDPAddr).Port + } else { + addr = fmt.Sprintf(":%d", port) + } + + session.udpConn, err = nazanet.NewUdpConnection(func(option *nazanet.UdpConnectionOption) { + option.LAddr = addr + option.Conn = uconn + }) + return port, err +} + +func (session *PubSession) listenTcp(port int) (int, error) { + var err error + + // TODO(chef): [refactor] 考虑挪到naza中,udp在naza中有类似的实现 202209 + if port == 0 { + for i := defaultPubSessionPortMin; i < defaultPubSessionPortMax; i++ { + addr := fmt.Sprintf(":%d", i) + if session.listener, err = net.Listen("tcp", addr); err == nil { + return int(i), nil + } + } + return 0, err + } + + addr := fmt.Sprintf(":%d", port) + session.listener, err = net.Listen("tcp", addr) + return port, err +} + +func (session *PubSession) runLoopUdp() error { + err := session.udpConn.RunLoop(func(b []byte, raddr *net.UDPAddr, err error) bool { + if len(b) == 0 && err != nil { + return false + } + + session.feedPacket(b) + return true + }) + return err +} + +func (session *PubSession) runLoopTcp() error { + for { + conn, err := session.listener.Accept() + if err != nil { + nazalog.Debugf("[%s] stop accept. err=%+v", session.UniqueKey(), err) + return err + } + + if session.tcpConn != nil { + nazalog.Warnf("[%s] tcp conn already exist, close the prev. err=%+v", session.UniqueKey(), err) + session.tcpConn.Close() + // TODO(chef): [fix] reset unpack 202209 + } + + session.tcpConn = conn + + go func() { + lb := make([]byte, 2) + buf := nazabytes.NewBuffer(1500) + for { + if _, rErr := io.ReadFull(conn, lb); rErr != nil { + nazalog.Debugf("[%s] read failed. err=%+v", session.UniqueKey(), rErr) + break + } + length := int(bele.BeUint16(lb)) + b := buf.ReserveBytes(length) + if _, rErr := io.ReadFull(conn, b); rErr != nil { + nazalog.Debugf("[%s] read failed. err=%+v", session.UniqueKey(), rErr) + break + } + + session.feedPacket(b) + } + }() + } +} + +func (session *PubSession) feedPacket(b []byte) { + if session.hookOnReadPacket != nil { + session.hookOnReadPacket(b) + } + + session.sessionStat.AddReadBytes(len(b)) + session.unpacker.FeedRtpPacket(b) +} + func (session *PubSession) dispose(err error) error { var retErr error session.disposeOnce.Do(func() { Log.Infof("[%s] lifecycle dispose gb28181 PubSession. err=%+v", session.UniqueKey(), err) - if session.conn == nil { - retErr = base.ErrSessionNotStarted - return + if session.isTcpFlag { + if session.tcpConn == nil { + retErr = base.ErrSessionNotStarted + return + } + retErr = session.tcpConn.Close() + } else { + if session.udpConn == nil { + retErr = base.ErrSessionNotStarted + return + } + retErr = session.udpConn.Dispose() } - retErr = session.conn.Dispose() + + session.unpacker.Dispose() }) return retErr } diff --git a/pkg/gb28181/pub_session_test.go b/pkg/gb28181/pub_session_test.go index 43b09ba..cdd985e 100644 --- a/pkg/gb28181/pub_session_test.go +++ b/pkg/gb28181/pub_session_test.go @@ -13,9 +13,11 @@ import ( "fmt" "github.com/q191201771/lal/pkg/aac" "github.com/q191201771/lal/pkg/base" + "github.com/q191201771/naza/pkg/bele" "github.com/q191201771/naza/pkg/nazalog" "github.com/q191201771/naza/pkg/nazanet" "io/ioutil" + "net" "os" "testing" "time" @@ -39,13 +41,14 @@ func TestReplayPubSession(t *testing.T) { // go test -test.run TestReplayPubSession // filename := "/tmp/record.psdata" + isTcpFlag := 1 b, err := ioutil.ReadFile(filename) if len(b) == 0 || err != nil { return } - //testPushFile("127.0.0.1:10002", filename) + testPushFile("127.0.0.1:10002", filename, isTcpFlag) } func TestPubSession(t *testing.T) { @@ -64,6 +67,8 @@ func TestPubSession(t *testing.T) { //testPubSession() } +// --------------------------------------------------------------------------------------------------------------------- + func testPubSession() { // 一个udp包一个文件,按行分隔,hex stream格式如下 // 8060 0000 0000 0000 0beb c567 0000 01ba @@ -99,7 +104,7 @@ func testPubSession() { helpUdpSend(addr) }() - _, runErr := session.Listen(int(port)) + _, runErr := session.Listen(int(port), false) nazalog.Assert(nil, runErr) runErr = session.RunLoop() nazalog.Assert(nil, runErr) @@ -121,16 +126,26 @@ func helpUdpSend(addr string) { } } -func testPushFile(addr string, filename string) { - conn, err := nazanet.NewUdpConnection(func(option *nazanet.UdpConnectionOption) { - option.RAddr = addr - }) - nazalog.Assert(nil, err) +func testPushFile(addr string, filename string, isTcpFlag int) { + var udpConn *nazanet.UdpConnection + var tcpConn net.Conn + var err error + + if isTcpFlag != 0 { + tcpConn, err = net.Dial("tcp", addr) + nazalog.Assert(nil, err) + } else { + udpConn, err = nazanet.NewUdpConnection(func(option *nazanet.UdpConnectionOption) { + option.RAddr = addr + }) + nazalog.Assert(nil, err) + } df := base.NewDumpFile() err = df.OpenToRead(filename) nazalog.Assert(nil, err) + lb := make([]byte, 2) for { m, err := df.ReadOneMessage() if err != nil { @@ -139,8 +154,16 @@ func testPushFile(addr string, filename string) { } nazalog.Debugf("%s", m.DebugString()) - conn.Write(m.Body) + if isTcpFlag != 0 { + bele.BePutUint16(lb, uint16(m.Len)) + _, err = tcpConn.Write(lb) + nazalog.Assert(nil, err) + _, err = tcpConn.Write(m.Body) + nazalog.Assert(nil, err) + } else { + udpConn.Write(m.Body) + } - time.Sleep(10 * time.Millisecond) + //time.Sleep(10 * time.Millisecond) } } diff --git a/pkg/gb28181/unpack.go b/pkg/gb28181/unpack.go index b417dc2..39d9714 100644 --- a/pkg/gb28181/unpack.go +++ b/pkg/gb28181/unpack.go @@ -22,7 +22,6 @@ import ( ) // PsUnpacker 解析ps(Program Stream)流 -// type PsUnpacker struct { list rtprtcp.RtpPacketList buf *nazabytes.Buffer @@ -43,6 +42,13 @@ type PsUnpacker struct { preVideoRtpts int64 onAvPacket base.OnAvPacketFunc + + waitSpsFlag bool + + feedPacketCount int + feedBodyCount int + onAvPacketWrapCount int + onAvPacketCount int } func NewPsUnpacker() *PsUnpacker { @@ -52,7 +58,7 @@ func NewPsUnpacker() *PsUnpacker { preAudioPts: -1, preVideoRtpts: -1, preAudioRtpts: -1, - onAvPacket: defaultOnAvPacket, + waitSpsFlag: true, } p.list.InitMaxSize(maxUnpackRtpListSize) @@ -62,12 +68,12 @@ func NewPsUnpacker() *PsUnpacker { // WithOnAvPacket // // @param onAvPacket: 回调函数中 base.AvPacket 字段说明: -// PayloadType AvPacketPt 见 base.AvPacketPt -// Timestamp int64 dts,单位毫秒 -// Pts int64 pts,单位毫秒 -// Payload []byte 对于视频,h264和h265是AnnexB格式 -// 对于音频,AAC是前面携带adts的格式 -// +// PayloadType AvPacketPt 见 base.AvPacketPt。 +// Timestamp int64 dts,单位毫秒。 +// Pts int64 pts,单位毫秒。 +// Payload []byte +// 对于视频,h264和h265是AnnexB格式。 +// 对于音频,AAC是前面携带adts的格式。 func (p *PsUnpacker) WithOnAvPacket(onAvPacket base.OnAvPacketFunc) *PsUnpacker { p.onAvPacket = onAvPacket return p @@ -77,17 +83,25 @@ func (p *PsUnpacker) WithOnAvPacket(onAvPacket base.OnAvPacketFunc) *PsUnpacker // // 注意,内部会处理丢包、乱序等问题 // -// @param b: rtp包,注意,包含rtp包头部分 -// +// @param b: rtp包,注意,包含rtp包头部分,内部不持有该内存块 func (p *PsUnpacker) FeedRtpPacket(b []byte) error { - //nazalog.Debugf("> FeedRtpPacket. len=%d", len(b)) + // TODO(chef): [opt] 当前遇到的场景都是,音频和视频共用一个ssrc,并且音频和视频的seq是打在一起的,是否存在两个ssrc的情况? 202209 + + p.feedPacketCount++ + + //defer func() { + // nazalog.Debugf("<<<<<<<<<< PsUnpacker. list=%s", p.list.DebugString()) + //}() ipkt, err := rtprtcp.ParseRtpPacket(b) if err != nil { + nazalog.Errorf("PsUnpacker ParseRtpPacket failed. b=%s, err=%+v", + hex.Dump(nazabytes.Prefix(b, 64)), err) return err } - //nazalog.Debugf("FeedRtpPacket. h=%+v", ipkt.Header) + //nazalog.Debugf(">>>>>>>>>> PsUnpacker FeedRtpPacket. h=%+v, len=%d, body=%s", + // ipkt.Header, len(ipkt.Raw), hex.Dump(nazabytes.Prefix(ipkt.Raw[12:], 8))) var isStartPositionFn = func(pkt rtprtcp.RtpPacket) bool { body := pkt.Body() @@ -98,11 +112,11 @@ func (p *PsUnpacker) FeedRtpPacket(b []byte) error { // 过期了直接丢掉 if p.list.IsStale(ipkt.Header.Seq) { - //nazalog.Debugf("FeedRtpPacket stale, drop. %d", ipkt.Header.Seq) + //nazalog.Debugf("PsUnpacker NOTICE stale, drop. %d", ipkt.Header.Seq) return ErrGb28181 } // 插入队列 - //nazalog.Debugf("FeedRtpPacket insert. %d", ipkt.Header.Seq) + //nazalog.Debugf("PsUnpacker FeedRtpPacket insert. %d", ipkt.Header.Seq) p.list.Insert(ipkt) for { // 循环判断头部是否是顺序的 @@ -112,14 +126,17 @@ func (p *PsUnpacker) FeedRtpPacket(b []byte) error { opkt := p.list.PopFirst() p.list.SetDoneSeq(opkt.Header.Seq) - p.FeedRtpBody(opkt.Body(), opkt.Header.Timestamp) - //nazalog.Debugf("FeedRtpPacket feed. %d", opkt.Header.Seq) + //nazalog.Debugf("PsUnpacker FeedRtpBody. %d", opkt.Header.Seq) + errFeedRtpBody := p.FeedRtpBody(opkt.Body(), opkt.Header.Timestamp) + if errFeedRtpBody != nil { + p.list.Reset() + } } else { // 不是顺序的,如果还没达到容器阈值,就先缓存在容器中,直接退出了 // 注意,如果队列为空,也会走到这,然后通过!full退出 if !p.list.Full() { - //nazalog.Debugf("FeedRtpPacket exit check !full.") + //nazalog.Debugf("PsUnpacker exit check !full.") break } @@ -130,17 +147,17 @@ func (p *PsUnpacker) FeedRtpPacket(b []byte) error { // 再丢弃连续的,直到下一个可解析帧位置 // 因为不连续的话,没法判断和正在丢弃的是否同一帧的,可以给个机会看后续是否能收到 prev := p.list.PopFirst() - //nazalog.Debugf("FeedRtpPacket, drop. %d", prev.Header.Seq) + //nazalog.Debugf("PsUnpacker NOTICE drop. %d", prev.Header.Seq) for p.list.Size > 0 { curr := p.list.PeekFirst() if rtprtcp.SubSeq(curr.Header.Seq, prev.Header.Seq) != 1 { - //nazalog.Debugf("FeedRtpPacket exit drop !sequential.") + //nazalog.Debugf("PsUnpacker exit drop !sequential.") break } if isStartPositionFn(curr) { - //nazalog.Debugf("FeedRtpPacket exit drop start.") + //nazalog.Debugf("PsUnpacker exit drop start.") // 注意,这里需要设置done seq,确保这个seq在以后的判断中可被使用 p.list.SetDoneSeq(curr.Header.Seq - 1) @@ -148,7 +165,7 @@ func (p *PsUnpacker) FeedRtpPacket(b []byte) error { } prev = p.list.PopFirst() - //nazalog.Debugf("FeedRtpPacket, drop. %d", prev.Header.Seq) + //nazalog.Debugf("PsUnpacker NOTICE drop. %d", prev.Header.Seq) } // 注意,缓存的数据也需要清除 @@ -162,8 +179,9 @@ func (p *PsUnpacker) FeedRtpPacket(b []byte) error { } // FeedRtpBody 注意,传入的数据应该是连续的,属于完整帧的 -// -func (p *PsUnpacker) FeedRtpBody(rtpBody []byte, rtpts uint32) { +func (p *PsUnpacker) FeedRtpBody(rtpBody []byte, rtpts uint32) error { + p.feedBodyCount++ + //nazalog.Debugf("> FeedRtpBody. len=%d, prev buf=%d", len(rtpBody), p.buf.Len()) p.buf.Write(rtpBody) // ISO/IEC iso13818-1 @@ -198,7 +216,7 @@ func (p *PsUnpacker) FeedRtpBody(rtpBody []byte, rtpts uint32) { //nazalog.Debugf("----------video stream----------") consumed = p.parseAvStream(int(code), rtpts, rb, i) case psPackStartCodePackEnd: - nazalog.Errorf("----------skip----------. %s", hex.Dump(nazabytes.Prefix(rb[i-4:], 32))) + //nazalog.Errorf("----------skip----------. %s", hex.Dump(nazabytes.Prefix(rb[i-4:], 32))) consumed = 0 case psPackStartCodeHikStream: consumed = parsePackStreamBody(rb, i) @@ -220,7 +238,7 @@ func (p *PsUnpacker) FeedRtpBody(rtpBody []byte, rtpts uint32) { p.buf.Reset() p.audioBuf = nil p.videoBuf = nil - return + return base.ErrGb28181 } if consumed < 0 { @@ -229,11 +247,17 @@ func (p *PsUnpacker) FeedRtpBody(rtpBody []byte, rtpts uint32) { nazalog.Warnf("consumed failed. code=%d, len=(%d,%d), i=%d, buf=%s", code, len(rtpBody), len(rb), i, hex.Dump(nazabytes.Prefix(rb[i-4:], 128))) } - return + return nil } p.buf.Skip(i + consumed) //nazalog.Debugf("skip. %d", i+consumed) } + return nil +} + +func (p *PsUnpacker) Dispose() { + nazalog.Debugf("PsUnpacker Dispose. (%d, %d, %d, %d, %d)", + p.feedPacketCount, p.feedBodyCount, p.list.Size, p.onAvPacketWrapCount, p.onAvPacketCount) } func (p *PsUnpacker) parsePsm(rb []byte, index int) int { @@ -359,7 +383,7 @@ func (p *PsUnpacker) parseAvStream(code int, rtpts uint32, rb []byte, index int) // noop } else { if p.preAudioRtpts != int64(rtpts) { - p.onAvPacket(&base.AvPacket{ + p.onAvPacketWrap(&base.AvPacket{ PayloadType: p.audioPayloadType, Timestamp: p.preAudioDts / 90, Pts: p.preAudioPts / 90, @@ -376,7 +400,7 @@ func (p *PsUnpacker) parseAvStream(code int, rtpts uint32, rb []byte, index int) } } else { if pts != p.preAudioPts && p.preAudioPts >= 0 { - p.onAvPacket(&base.AvPacket{ + p.onAvPacketWrap(&base.AvPacket{ PayloadType: p.audioPayloadType, Timestamp: p.preAudioDts / 90, Pts: p.preAudioPts / 90, @@ -392,7 +416,7 @@ func (p *PsUnpacker) parseAvStream(code int, rtpts uint32, rb []byte, index int) p.preAudioPts = pts p.preAudioDts = dts } else { - nazalog.Errorf("unknown audio stream type. ast=%d", p.audioStreamType) + nazalog.Warnf("unknown audio stream type. ast=%d", p.audioStreamType) } } else if code == psPackStartCodeVideoStream { // 判断出当前pes是否是新的帧,然后将缓存中的帧回调给上层 @@ -446,7 +470,6 @@ func (p *PsUnpacker) parseAvStream(code int, rtpts uint32, rb []byte, index int) } // parsePackHeader 注意,`rb[index:]`为待解析的内存块 -// func parsePackHeader(rb []byte, index int) int { // 2.5.3.3 Pack layer of Program Stream // Table 2-33 - Program Stream pack header @@ -473,13 +496,13 @@ func parsePackStreamBody(rb []byte, index int) int { i := index if len(rb) < i+2 { - nazalog.Warnf("needed=%d, actual=%d", i+2, len(rb)) + nazalog.Debugf("needed=%d, actual=%d", i+2, len(rb)) return -1 } l := int(bele.BeUint16(rb[i:])) i += 2 + l if len(rb) < i { - nazalog.Warnf("needed=%d, actual=%d", i, len(rb)) + nazalog.Debugf("needed=%d, actual=%d", i, len(rb)) return -1 } @@ -487,7 +510,6 @@ func parsePackStreamBody(rb []byte, index int) int { } // iterateNaluByStartCode 通过nal start code分隔缓存数据,将nal回调给上层 -// func (p *PsUnpacker) iterateNaluByStartCode(code int, pts, dts int64) { leading, preLeading, startPos := 0, 0, 0 startPos, preLeading = h2645.IterateNaluStartCode(p.videoBuf, 0) @@ -510,7 +532,7 @@ func (p *PsUnpacker) iterateNaluByStartCode(code int, pts, dts int64) { } startPos = nextPos - p.onAvPacket(&base.AvPacket{ + p.onAvPacketWrap(&base.AvPacket{ PayloadType: p.videoPayloadType, Timestamp: dts / 90, Pts: pts / 90, @@ -523,6 +545,38 @@ func (p *PsUnpacker) iterateNaluByStartCode(code int, pts, dts int64) { } } +func (p *PsUnpacker) onAvPacketWrap(packet *base.AvPacket) { + p.onAvPacketWrapCount++ + //nazalog.Debugf("PsUnpacker > onAvPacketWrap. packet=%s", packet.DebugString()) + if packet.IsVideo() { + typ := h2645.ParseNaluType(packet.PayloadType == base.AvPacketPtAvc, packet.Payload[4]) + //nazalog.Debugf("PsUnpacker onAvPacketWrap. type=%d", typ) + // TODO(chef): [opt] 等待sps等信息再开始回调,这个逻辑不完整简化了 202209 + if p.waitSpsFlag { + if packet.PayloadType == base.AvPacketPtAvc { + if typ == h2645.H264NaluTypeSps || typ == h2645.H264NaluTypePps { + p.waitSpsFlag = false + } else { + //nazalog.Debugf("PsUnpacker onAvPacketWrap. drop. %d", typ) + return + } + } else if packet.PayloadType == base.AvPacketPtHevc { + if typ == h2645.H265NaluTypeVps || typ == h2645.H265NaluTypeSps || typ == h2645.H265NaluTypePps { + p.waitSpsFlag = false + } else { + //nazalog.Debugf("PsUnpacker onAvPacketWrap. drop. %d", typ) + return + } + } + } + } + if p.onAvPacket != nil { + p.onAvPacketCount++ + //nazalog.Debugf("PsUnpacker > onAvPacket. packet=%s", packet.DebugString()) + p.onAvPacket(packet) + } +} + // --------------------------------------------------------------------------------------------------------------------- // TODO(chef): [refactor] 以下代码拷贝来自package mpegts,重复了 @@ -599,7 +653,3 @@ func readPts(b []byte) (fb uint8, pts int64) { pts |= (int64(b[3])<<8 | int64(b[4])) >> 1 return } - -func defaultOnAvPacket(packet *base.AvPacket) { - // noop -} diff --git a/pkg/h2645/h2645.go b/pkg/h2645/h2645.go index 4b832e4..06598a4 100644 --- a/pkg/h2645/h2645.go +++ b/pkg/h2645/h2645.go @@ -64,7 +64,6 @@ const ( ) // IterateNaluAvcc 遍历Avcc格式的nalu流 -// func IterateNaluAvcc(nals []byte, handler func(nal []byte)) error { return avc.IterateNaluAvcc(nals, handler) } diff --git a/pkg/hevc/hevc.go b/pkg/hevc/hevc.go index c2c0781..416af96 100644 --- a/pkg/hevc/hevc.go +++ b/pkg/hevc/hevc.go @@ -133,7 +133,6 @@ func ParseNaluTypeReadable(v uint8) string { // ParseNaluType // // @param v 第一个字节 -// func ParseNaluType(v uint8) uint8 { // 6 bit in middle // 0*** ***0 @@ -144,7 +143,6 @@ func ParseNaluType(v uint8) uint8 { // IsIrapNalu 是否是关键帧 // // @param typ 帧类型。注意,是经过 ParseNaluType 解析后的帧类型 -// func IsIrapNalu(typ uint8) bool { // [16, 23] irap nal // [19, 20] idr nal @@ -156,7 +154,6 @@ func IsIrapNalu(typ uint8) bool { // HVCC Seq Header -> Annexb // // @return 返回的内存块为内部独立新申请 -// func VpsSpsPpsSeqHeader2Annexb(payload []byte) ([]byte, error) { vps, sps, pps, err := ParseVpsSpsPpsFromSeqHeaderWithoutMalloc(payload) if err != nil { @@ -195,7 +192,6 @@ func BuildVpsSpsPps2Annexb(vps, sps, pps []byte) ([]byte, error) { // 见func ParseVpsSpsPpsFromSeqHeaderWithoutMalloc // // @return vps, sps, pps: 内存块为内部独立新申请 -// func ParseVpsSpsPpsFromSeqHeader(payload []byte) (vps, sps, pps []byte, err error) { v, s, p, e := ParseVpsSpsPpsFromSeqHeaderWithoutMalloc(payload) if e != nil { @@ -209,13 +205,13 @@ func ParseVpsSpsPpsFromSeqHeader(payload []byte) (vps, sps, pps []byte, err erro // ParseVpsSpsPpsFromSeqHeaderWithoutMalloc // -// 从HVCC格式的Seq Header中得到VPS,SPS,PPS内存块 +// 从HVCC格式的Seq Header中得到VPS,SPS,PPS内存块。 // -// @param rtmp message的payload部分或者flv tag的payload部分 -// 注意,包含了头部2字节类型以及3字节的cts +// @param payload: rtmp message的payload部分或者flv tag的payload部分。 // -// @return vps, sps, pps: 复用传入参数`payload`的内存块 +// 注意,包含了头部2字节类型以及3字节的cts。 // +// @return vps, sps, pps: 复用传入参数`payload`的内存块。 func ParseVpsSpsPpsFromSeqHeaderWithoutMalloc(payload []byte) (vps, sps, pps []byte, err error) { if len(payload) < 5 { return nil, nil, nil, nazaerrors.Wrap(base.ErrShortBuffer) @@ -289,7 +285,6 @@ func ParseVpsSpsPpsFromSeqHeaderWithoutMalloc(payload []byte) (vps, sps, pps []b // BuildSeqHeaderFromVpsSpsPps // // @return 内存块为内部独立新申请 -// func BuildSeqHeaderFromVpsSpsPps(vps, sps, pps []byte) ([]byte, error) { var sh []byte sh = make([]byte, 43+len(vps)+len(sps)+len(pps)) diff --git a/pkg/hls/m3u8.go b/pkg/hls/m3u8.go index b7a7d51..b6d3a92 100644 --- a/pkg/hls/m3u8.go +++ b/pkg/hls/m3u8.go @@ -22,7 +22,6 @@ import ( // @param content 需写入文件的内容 // @param filename m3u8文件名 // @param filenameBak m3u8临时文件名 -// func writeM3u8File(content []byte, filename string, filenameBak string) error { if err := fslCtx.WriteFile(filenameBak, content, 0666); err != nil { return err @@ -37,7 +36,6 @@ func writeM3u8File(content []byte, filename string, filenameBak string) error { // @param currDuration 当前duration // // @return 处理后的m3u8文件内容 -// func updateTargetDurationInM3u8(content []byte, currDuration int) ([]byte, error) { l := bytes.Index(content, []byte("#EXT-X-TARGETDURATION:")) if l == -1 { @@ -67,7 +65,6 @@ func updateTargetDurationInM3u8(content []byte, currDuration int) ([]byte, error // @param content 传入m3u8文件内容 // // @return durationSec m3u8中所有ts的时间总和。注意,使用的是m3u8文件中描述的ts时间,而不是读取ts文件中实际音视频数据的时间。 -// func CalcM3u8Duration(content []byte) (durationSec float64, err error) { lines := bytes.Split(content, []byte{'\n'}) for _, line := range lines { diff --git a/pkg/hls/muxer.go b/pkg/hls/muxer.go index d7a3ad8..545ed7a 100644 --- a/pkg/hls/muxer.go +++ b/pkg/hls/muxer.go @@ -34,7 +34,6 @@ type IMuxerObserver interface { // MuxerConfig // // 各字段含义见文档: https://pengrl.com/lal/#/ConfigBrief -// type MuxerConfig struct { OutPath string `json:"out_path"` FragmentDurationMs int `json:"fragment_duration_ms"` @@ -52,7 +51,6 @@ const ( // Muxer // // 输入mpegts流,输出hls(m3u8+ts)至文件中 -// type Muxer struct { UniqueKey string @@ -96,7 +94,6 @@ type fragmentInfo struct { // NewMuxer // // @param observer 可以为nil,如果不为nil,TS流将回调给上层 -// func NewMuxer(streamName string, config *MuxerConfig, observer IMuxerObserver) *Muxer { uk := base.GenUkHlsMuxer() op := PathStrategy.GetMuxerOutPath(config.OutPath, streamName) @@ -137,7 +134,6 @@ func (m *Muxer) Dispose() { // OnPatPmt OnTsPackets // // 实现 remux.IRtmp2MpegtsRemuxerObserver,方便直接将 remux.Rtmp2MpegtsRemuxer 的数据喂入 hls.Muxer -// func (m *Muxer) OnPatPmt(b []byte) { m.FeedPatPmt(b) } @@ -156,7 +152,7 @@ func (m *Muxer) FeedMpegts(tsPackets []byte, frame *mpegts.Frame, boundary bool) //Log.Debugf("> FeedMpegts. boundary=%v, frame=%p, sid=%d", boundary, frame, frame.Sid) if frame.Sid == mpegts.StreamIdAudio { // TODO(chef): 为什么音频用pts,视频用dts - if err := m.updateFragment(frame.Pts, boundary); err != nil { + if err := m.updateFragment(frame.Pts, boundary, frame); err != nil { Log.Errorf("[%s] update fragment error. err=%+v", m.UniqueKey, err) return } @@ -166,7 +162,7 @@ func (m *Muxer) FeedMpegts(tsPackets []byte, frame *mpegts.Frame, boundary bool) } //Log.Debugf("[%s] WriteFrame A. dts=%d, len=%d", m.UniqueKey, frame.DTS, len(frame.Raw)) } else { - if err := m.updateFragment(frame.Dts, boundary); err != nil { + if err := m.updateFragment(frame.Dts, boundary, frame); err != nil { Log.Errorf("[%s] update fragment error. err=%+v", m.UniqueKey, err) return } @@ -196,9 +192,10 @@ func (m *Muxer) OutPath() string { // // @param boundary: 调用方认为可能是开启新TS切片的时间点 // -// @return: 理论上,只有文件操作失败才会返回错误 +// @param frame: 内部只在打日志时使用 // -func (m *Muxer) updateFragment(ts uint64, boundary bool) error { +// @return: 理论上,只有文件操作失败才会返回错误 +func (m *Muxer) updateFragment(ts uint64, boundary bool, frame *mpegts.Frame) error { discont := true // 如果已经有TS切片,检查是否需要强制开启新的切片,以及切片是否发生跳跃 @@ -215,7 +212,7 @@ func (m *Muxer) updateFragment(ts uint64, boundary bool) error { // maxfraglen := uint64(m.config.FragmentDurationMs * 90 * 10) if (ts > m.fragTs && ts-m.fragTs > maxfraglen) || (m.fragTs > ts && m.fragTs-ts > negMaxfraglen) { - Log.Warnf("[%s] force fragment split. fragTs=%d, ts=%d", m.UniqueKey, m.fragTs, ts) + Log.Warnf("[%s] force fragment split. fragTs=%d, ts=%d, frame=%s", m.UniqueKey, m.fragTs, ts, frame.DebugString()) if err := m.closeFragment(false); err != nil { return err @@ -267,7 +264,6 @@ func (m *Muxer) updateFragment(ts uint64, boundary bool) error { // @param discont: 不连续标志,会在m3u8文件的fragment前增加`#EXT-X-DISCONTINUITY` // // @return: 理论上,只有文件操作失败才会返回错误 -// func (m *Muxer) openFragment(ts uint64, discont bool) error { if m.opened { return nazaerrors.Wrap(base.ErrHls) @@ -316,7 +312,6 @@ func (m *Muxer) openFragment(ts uint64, discont bool) error { // closeFragment // // @return: 理论上,只有文件操作失败才会返回错误 -// func (m *Muxer) closeFragment(isLast bool) error { if !m.opened { // 注意,首次调用closeFragment时,有可能opened为false diff --git a/pkg/hls/path_strategy.go b/pkg/hls/path_strategy.go index a629290..e19ed9e 100644 --- a/pkg/hls/path_strategy.go +++ b/pkg/hls/path_strategy.go @@ -34,7 +34,6 @@ type IPathStrategy interface { // // 路由策略 // 接到HTTP请求时,对应文件路径的映射逻辑 -// type IPathRequestStrategy interface { // GetRequestInfo // @@ -87,7 +86,6 @@ const ( // - test110-1620540716095-1.ts // - ... 一系列的TS文件 // -// // 假设 // 流名称="test110" // rootPath="/tmp/lal/hls/" @@ -100,7 +98,6 @@ const ( // http://127.0.0.1:8080/hls/test110.m3u8 -> /tmp/lal/hls/test110/playlist.m3u8 // http://127.0.0.1:8080/hls/test110-1620540712084-0.ts -> /tmp/lal/hls/test110/test110-1620540712084-0.ts // 最下面这两个做了特殊映射 -// type DefaultPathStrategy struct { } @@ -113,7 +110,6 @@ type DefaultPathStrategy struct { // /hls/test110/record.m3u8 -> record.m3u8 test110 m3u8 {rootOutPath}/test110/record.m3u8 // /hls/test110/test110-1620540712084-.ts -> test110-1620540712084-.ts test110 ts {rootOutPath/test110/test110-1620540712084-.ts // /hls/test110-1620540712084-.ts -> test110-1620540712084-.ts test110 ts {rootOutPath/test110/test110-1620540712084-.ts -// func (dps *DefaultPathStrategy) GetRequestInfo(urlCtx base.UrlContext, rootOutPath string) (ri RequestInfo) { filename := urlCtx.LastItemOfPath filetype := urlCtx.GetFileType() @@ -158,5 +154,16 @@ func (*DefaultPathStrategy) GetTsFileName(streamName string, index int, timestam } func (*DefaultPathStrategy) getStreamNameFromTsFileName(fileName string) string { - return strings.Split(fileName, "-")[0] + sum := 0 + index := strings.LastIndexFunc(fileName, func(r rune) bool { + if r == '-' { + sum++ + } + // GetTsFileName 格式固定为%s-%d-%d.ts streamName取%s部分 + return sum == 2 + }) + if index == -1 { + return fileName + } + return fileName[:index] } diff --git a/pkg/hls/path_strategy_test.go b/pkg/hls/path_strategy_test.go index b68b738..3edfe20 100644 --- a/pkg/hls/path_strategy_test.go +++ b/pkg/hls/path_strategy_test.go @@ -9,6 +9,7 @@ package hls_test import ( + "runtime" "testing" "github.com/q191201771/lal/pkg/base" @@ -18,36 +19,100 @@ import ( ) func TestDefaultPathStrategy_GetRequestInfo(t *testing.T) { - dps := &hls.DefaultPathStrategy{} - rootOutPath := "/tmp/lal/hls/" - - golden := map[string]hls.RequestInfo{ - "http://127.0.0.1:8080/hls/test110.m3u8": { - StreamName: "test110", - FileNameWithPath: "/tmp/lal/hls/test110/playlist.m3u8", + testCases := []struct { + name string + url string + wantStreamName string + wantFileNameWithPath string + wantWinFileNameWithPath string + }{ + { + name: "1 hls/[].m3u8 格式测试", + url: "http://127.0.0.1:8080/hls/test1.m3u8", + wantStreamName: "test1", + wantFileNameWithPath: "/tmp/lal/hls/test1/playlist.m3u8", + wantWinFileNameWithPath: "\\tmp\\lal\\hls\\test1\\playlist.m3u8", + }, + { + name: "2 hls/[]/playlist.m3u8 格式测试", + url: "http://127.0.0.1:8080/hls/test2.m3u8", + wantStreamName: "test2", + wantFileNameWithPath: "/tmp/lal/hls/test2/playlist.m3u8", + wantWinFileNameWithPath: "\\tmp\\lal\\hls\\test2\\playlist.m3u8", + }, + { + name: "3 hls/[]/record.m3u8 格式测试", + url: "http://127.0.0.1:8080/hls/test3/record.m3u8", + wantStreamName: "test3", + wantFileNameWithPath: "/tmp/lal/hls/test3/record.m3u8", + wantWinFileNameWithPath: "\\tmp\\lal\\hls\\test3\\record.m3u8", }, - "http://127.0.0.1:8080/hls/test110/playlist.m3u8": { - StreamName: "test110", - FileNameWithPath: "/tmp/lal/hls/test110/playlist.m3u8", + { + name: "4 hls/[]/[]-timestamp-seq.ts 格式测试", + url: "http://127.0.0.1:8080/hls/test4/test4-1620540712084-0.ts", + wantStreamName: "test4", + wantFileNameWithPath: "/tmp/lal/hls/test4/test4-1620540712084-0.ts", + wantWinFileNameWithPath: "\\tmp\\lal\\hls\\test4\\test4-1620540712084-0.ts", }, - "http://127.0.0.1:8080/hls/test110/record.m3u8": { - StreamName: "test110", - FileNameWithPath: "/tmp/lal/hls/test110/record.m3u8", + { + name: "5 hls/[]-timestamp-seq.ts 格式测试", + url: "http://127.0.0.1:8080/hls/test5-1620540712084-0.ts", + wantStreamName: "test5", + wantFileNameWithPath: "/tmp/lal/hls/test5/test5-1620540712084-0.ts", + wantWinFileNameWithPath: "\\tmp\\lal\\hls\\test5\\test5-1620540712084-0.ts", }, - "http://127.0.0.1:8080/hls/test110/test110-1620540712084-0.ts": { - StreamName: "test110", - FileNameWithPath: "/tmp/lal/hls/test110/test110-1620540712084-0.ts", + { + name: "6 hls/[]/[]-timestamp-seq.ts 名称带-符号格式测试", + url: "http://127.0.0.1:8080/hls/test6/test6-0-1620540712084-0.ts", + wantStreamName: "test6-0", + wantFileNameWithPath: "/tmp/lal/hls/test6-0/test6-0-1620540712084-0.ts", + wantWinFileNameWithPath: "\\tmp\\lal\\hls\\test6-0\\test6-0-1620540712084-0.ts", }, - "http://127.0.0.1:8080/hls/test110-1620540712084-0.ts": { - StreamName: "test110", - FileNameWithPath: "/tmp/lal/hls/test110/test110-1620540712084-0.ts", + { + name: "7 hls/[]-timestamp-seq.ts 名称带-符号格式测试", + url: "http://127.0.0.1:8080/hls/test7-0-1620540712084-0.ts", + wantStreamName: "test7-0", + wantFileNameWithPath: "/tmp/lal/hls/test7-0/test7-0-1620540712084-0.ts", + wantWinFileNameWithPath: "\\tmp\\lal\\hls\\test7-0\\test7-0-1620540712084-0.ts", + }, + { + name: "8 hls/[]-timestamp-seq.ts 名称带中文测试", + url: "http://127.0.0.1:8080/hls/中文测试-1620540712084-0.ts", + wantStreamName: "中文测试", + wantFileNameWithPath: "/tmp/lal/hls/中文测试/中文测试-1620540712084-0.ts", + wantWinFileNameWithPath: "\\tmp\\lal\\hls\\中文测试\\中文测试-1620540712084-0.ts", + }, + { + name: "9 hls/[]-timestamp-seq.ts 名称带中文和-符号测试", + url: "http://127.0.0.1:8080/hls/中文测试-zh-0-1620540712084-0.ts", + wantStreamName: "中文测试-zh-0", + wantFileNameWithPath: "/tmp/lal/hls/中文测试-zh-0/中文测试-zh-0-1620540712084-0.ts", + wantWinFileNameWithPath: "\\tmp\\lal\\hls\\中文测试-zh-0\\中文测试-zh-0-1620540712084-0.ts", + }, + { + name: "10 hls/[]-timestamp.ts 非标准格式", + url: "http://127.0.0.1:8080/hls/中文测试-1620540712084.ts", + wantStreamName: "中文测试-1620540712084.ts", + wantFileNameWithPath: "/tmp/lal/hls/中文测试-1620540712084.ts/中文测试-1620540712084.ts", + wantWinFileNameWithPath: "\\tmp\\lal\\hls\\中文测试-1620540712084.ts\\中文测试-1620540712084.ts", }, } - for k, v := range golden { - ctx, err := base.ParseUrl(k, -1) - hls.Log.Assert(nil, err) - out := dps.GetRequestInfo(ctx, rootOutPath) - assert.Equal(t, v, out) + dps := &hls.DefaultPathStrategy{} + rootOutPath := "/tmp/lal/hls/" + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx, err := base.ParseUrl(tc.url, -1) + hls.Log.Assert(nil, err) + out := dps.GetRequestInfo(ctx, rootOutPath) + tmp := hls.RequestInfo{ + StreamName: tc.wantStreamName, + FileNameWithPath: tc.wantFileNameWithPath, + } + if runtime.GOOS == "windows" { + tmp.FileNameWithPath = tc.wantWinFileNameWithPath + } + assert.Equal(t, tmp, out) + }) } } diff --git a/pkg/httpflv/client_pull_session.go b/pkg/httpflv/client_pull_session.go index 7c3138b..22198dd 100644 --- a/pkg/httpflv/client_pull_session.go +++ b/pkg/httpflv/client_pull_session.go @@ -67,16 +67,15 @@ func NewPullSession(modOptions ...ModPullSessionOption) *PullSession { // OnReadFlvTag @param tag: 底层保证回调上来的Raw数据长度是完整的(但是不会分析Raw内部的编码数据) type OnReadFlvTag func(tag Tag) -// Pull 阻塞直到和对端完成拉流前,握手部分的工作,或者发生错误 +// Pull 阻塞直到和对端完成拉流前,握手部分的工作,或者发生错误。 // -// 注意,握手指的是发送完HTTP Request,不包含接收任何数据,因为有的httpflv服务端,如果流不存在不会发送任何内容,此时我们也应该认为是握手完成了 +// 注意,握手指的是发送完HTTP Request,不包含接收任何数据,因为有的httpflv服务端,如果流不存在不会发送任何内容,此时我们也应该认为是握手完成了。 // -// @param rawUrl 支持如下两种格式。(当然,关键点是对端支持) -// http://{domain}/{app_name}/{stream_name}.flv -// http://{ip}/{domain}/{app_name}/{stream_name}.flv +// @param rawUrl 支持如下两种格式(当然,关键点是对端支持): +// 1. `http://{domain}/{app_name}/{stream_name}.flv` +// 2. `http://{ip}/{domain}/{app_name}/{stream_name}.flv` // // @param onReadFlvTag 读取到 flv tag 数据时回调。回调结束后,PullSession 不会再使用这块 数据。 -// func (session *PullSession) Pull(rawUrl string, onReadFlvTag OnReadFlvTag) error { Log.Debugf("[%s] pull. url=%s", session.UniqueKey(), rawUrl) @@ -98,13 +97,11 @@ func (session *PullSession) Pull(rawUrl string, onReadFlvTag OnReadFlvTag) error // --------------------------------------------------------------------------------------------------------------------- // Dispose 文档请参考: IClientSessionLifecycle interface -// func (session *PullSession) Dispose() error { return session.dispose(nil) } // WaitChan 文档请参考: IClientSessionLifecycle interface -// func (session *PullSession) WaitChan() <-chan error { return session.conn.Done() } diff --git a/pkg/httpflv/flv_file_pump.go b/pkg/httpflv/flv_file_pump.go index 35b2aea..b884567 100644 --- a/pkg/httpflv/flv_file_pump.go +++ b/pkg/httpflv/flv_file_pump.go @@ -19,7 +19,6 @@ var Clock = mock.NewStdClock() // FlvFilePumpOption // // 读取flv文件,将tag按时间戳间隔缓慢(类似于ffmpeg的-re)返回 -// type FlvFilePumpOption struct { IsRecursive bool // 如果为true,则循环返回文件内容(类似于ffmpeg的-stream_loop -1) } @@ -48,7 +47,6 @@ type OnPumpFlvTag func(tag Tag) bool // Pump // // @param onFlvTag 如果回调中返回false,则停止Pump -// func (f *FlvFilePump) Pump(filename string, onFlvTag OnPumpFlvTag) error { // 一次性将文件所有内容读入内存,后续不再读取文件 tags, err := ReadAllTagsFromFlvFile(filename) @@ -60,7 +58,6 @@ func (f *FlvFilePump) Pump(filename string, onFlvTag OnPumpFlvTag) error { } // PumpWithTags @return error 暂时只做预留,目前只会返回nil -// func (f *FlvFilePump) PumpWithTags(tags []Tag, onFlvTag OnPumpFlvTag) error { var totalBaseTs uint32 // 整体的基础时间戳。每轮最后更新 diff --git a/pkg/httpflv/tag.go b/pkg/httpflv/tag.go index 7fe1f16..18837d9 100644 --- a/pkg/httpflv/tag.go +++ b/pkg/httpflv/tag.go @@ -27,7 +27,6 @@ type Tag struct { } // Payload 只包含数据部分,去除了前面11字节的tag header和后面4字节的prev tag size -// func (tag *Tag) Payload() []byte { return tag.Raw[TagHeaderSize : len(tag.Raw)-PrevTagSizeFieldSize] } @@ -103,7 +102,6 @@ func PackHttpflvTag(t uint8, timestamp uint32, in []byte) []byte { } // ReadTag 从`rd`中读取数据并解析至`tag` -// func ReadTag(rd io.Reader) (tag Tag, err error) { rawHeader := make([]byte, TagHeaderSize) if _, err = io.ReadAtLeast(rd, rawHeader, TagHeaderSize); err != nil { diff --git a/pkg/innertest/dump_test.go b/pkg/innertest/dump_test.go new file mode 100644 index 0000000..aacfe62 --- /dev/null +++ b/pkg/innertest/dump_test.go @@ -0,0 +1,11 @@ +// 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 innertest + +// TODO(chef): [refactor] 把gb28181 ps的测试也挪过来 202209 diff --git a/pkg/innertest/innertest.go b/pkg/innertest/innertest.go index 1482418..8a59202 100644 --- a/pkg/innertest/innertest.go +++ b/pkg/innertest/innertest.go @@ -13,6 +13,7 @@ import ( "io/ioutil" "net/http" "os" + "runtime" "strings" "sync" "testing" @@ -107,6 +108,15 @@ func (r RtspPullObserver) OnAvPacket(pkt base.AvPacket) { } func Entry(tt *testing.T) { + // 在MacOS只测试一次 + // 其他环境(比如github CI)上则每个package都执行,因为要生产测试覆盖率 + if runtime.GOOS == "darwin" { + _, file, _, _ := runtime.Caller(1) + if !strings.HasSuffix(file, "innertest_test.go") { + return + } + } + t = tt mode = 0 @@ -312,7 +322,8 @@ func entry() { fileTagCount, httpflvPullTagCount.Load(), rtmpPullTagCount.Load(), rtspPullAvPacketCount.Load()) compareFile() - assert.Equal(t, strings.ReplaceAll(goldenRtspSdpList[mode], "\n", "\r\n"), string(rtspSdpCtx.RawSdp)) + goldenRtspSdpTmplList[mode] = strings.ReplaceAll(goldenRtspSdpTmplList[mode], "{atoolv}", base.LalPackSdp) + assert.Equal(t, strings.ReplaceAll(goldenRtspSdpTmplList[mode], "\n", "\r\n"), string(rtspSdpCtx.RawSdp)) } func compareFile() { @@ -607,13 +618,13 @@ innertest-1642375465000-7.ts `, } -var goldenRtspSdpList = []string{ +var goldenRtspSdpTmplList = []string{ `v=0 o=- 0 0 IN IP4 127.0.0.1 s=No Name c=IN IP4 127.0.0.1 t=0 0 -a=tool:lal 0.30.1 +a=tool:{atoolv} m=video 0 RTP/AVP 96 a=rtpmap:96 H264/90000 a=fmtp:96 packetization-mode=1; sprop-parameter-sets=Z2QAFqyyAUBf8uAiAAADAAIAAAMAPB4sXJA=,aOvDyyLA; profile-level-id=640016 @@ -629,7 +640,7 @@ o=- 0 0 IN IP4 127.0.0.1 s=No Name c=IN IP4 127.0.0.1 t=0 0 -a=tool:lal 0.30.1 +a=tool:{atoolv} m=audio 0 RTP/AVP 97 b=AS:128 a=rtpmap:97 MPEG4-GENERIC/44100/2 @@ -641,7 +652,7 @@ o=- 0 0 IN IP4 127.0.0.1 s=No Name c=IN IP4 127.0.0.1 t=0 0 -a=tool:lal 0.30.1 +a=tool:{atoolv} m=video 0 RTP/AVP 96 a=rtpmap:96 H264/90000 a=fmtp:96 packetization-mode=1; sprop-parameter-sets=Z2QAFqyyAUBf8uAiAAADAAIAAAMAPB4sXJA=,aOvDyyLA; profile-level-id=640016 diff --git a/pkg/logic/config.go b/pkg/logic/config.go index a5bdba9..31f3888 100644 --- a/pkg/logic/config.go +++ b/pkg/logic/config.go @@ -11,7 +11,6 @@ package logic import ( "encoding/json" "fmt" - "io/ioutil" "os" "strings" @@ -32,6 +31,7 @@ const ( type Config struct { ConfVersion string `json:"conf_version"` RtmpConfig RtmpConfig `json:"rtmp"` + InSessionConfig InSessionConfig `json:"in_session"` DefaultHttpConfig DefaultHttpConfig `json:"default_http"` HttpflvConfig HttpflvConfig `json:"httpflv"` HlsConfig HlsConfig `json:"hls"` @@ -51,12 +51,20 @@ type Config struct { } type RtmpConfig struct { - Enable bool `json:"enable"` - Addr string `json:"addr"` - GopNum int `json:"gop_num"` // TODO(chef): refactor 更名为gop_cache_num - MergeWriteSize int `json:"merge_write_size"` - AddDummyAudioEnable bool `json:"add_dummy_audio_enable"` - AddDummyAudioWaitAudioMs int `json:"add_dummy_audio_wait_audio_ms"` + Enable bool `json:"enable"` + Addr string `json:"addr"` + RtmpsEnable bool `json:"rtmps_enable"` + RtmpsAddr string `json:"rtmps_addr"` + RtmpsCertFile string `json:"rtmps_cert_file"` + RtmpsKeyFile string `json:"rtmps_key_file"` + GopNum int `json:"gop_num"` // TODO(chef): refactor 更名为gop_cache_num + SingleGopMaxFrameNum int `json:"single_gop_max_frame_num"` + MergeWriteSize int `json:"merge_write_size"` +} + +type InSessionConfig struct { + AddDummyAudioEnable bool `json:"add_dummy_audio_enable"` + AddDummyAudioWaitAudioMs int `json:"add_dummy_audio_wait_audio_ms"` } type DefaultHttpConfig struct { @@ -66,13 +74,15 @@ type DefaultHttpConfig struct { type HttpflvConfig struct { CommonHttpServerConfig - GopNum int `json:"gop_num"` + GopNum int `json:"gop_num"` + SingleGopMaxFrameNum int `json:"single_gop_max_frame_num"` } type HttptsConfig struct { CommonHttpServerConfig - GopNum int `json:"gop_num"` + GopNum int `json:"gop_num"` + SingleGopMaxFrameNum int `json:"single_gop_max_frame_num"` } type HlsConfig struct { @@ -87,6 +97,10 @@ type HlsConfig struct { type RtspConfig struct { Enable bool `json:"enable"` Addr string `json:"addr"` + RtspsEnable bool `json:"rtsps_enable"` + RtspsAddr string `json:"rtsps_addr"` + RtspsCertFile string `json:"rtsps_cert_file"` + RtspsKeyFile string `json:"rtsps_key_file"` OutWaitKeyFrameFlag bool `json:"out_wait_key_frame_flag"` rtsp.ServerAuthConfig } @@ -166,23 +180,18 @@ type CommonHttpAddrConfig struct { HttpsKeyFile string `json:"https_key_file"` } -func LoadConfAndInitLog(confFile string) *Config { +func LoadConfAndInitLog(rawContent []byte) *Config { var config *Config - // 读取配置文件并解析原始内容 - rawContent, err := ioutil.ReadFile(confFile) - if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "read conf file failed. file=%s err=%+v", confFile, err) - base.OsExitAndWaitPressIfWindows(1) - } - if err = json.Unmarshal(rawContent, &config); err != nil { - _, _ = fmt.Fprintf(os.Stderr, "unmarshal conf file failed. file=%s err=%+v", confFile, err) + // 读取配置并解析原始内容 + if err := json.Unmarshal(rawContent, &config); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "unmarshal conf file failed. raw content=%s err=%+v", rawContent, err) base.OsExitAndWaitPressIfWindows(1) } j, err := nazajson.New(rawContent) if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "nazajson unmarshal conf file failed. file=%s err=%+v", confFile, err) + _, _ = fmt.Fprintf(os.Stderr, "nazajson unmarshal conf file failed. raw content=%s err=%+v", rawContent, err) base.OsExitAndWaitPressIfWindows(1) } @@ -326,7 +335,7 @@ func LoadConfAndInitLog(confFile string) *Config { tlines = append(tlines, strings.TrimSpace(l)) } compactRawContent := strings.Join(tlines, " ") - Log.Infof("load conf file succ. filename=%s, raw content=%s parsed=%+v", confFile, compactRawContent, config) + Log.Infof("load conf succ. raw content=%s parsed=%+v", compactRawContent, config) return config } diff --git a/pkg/logic/customize_pubsession.go b/pkg/logic/customize_pubsession.go index 43c4bdd..cc35016 100644 --- a/pkg/logic/customize_pubsession.go +++ b/pkg/logic/customize_pubsession.go @@ -11,6 +11,8 @@ package logic import ( "github.com/q191201771/lal/pkg/base" "github.com/q191201771/lal/pkg/remux" + "github.com/q191201771/naza/pkg/nazaatomic" + "github.com/q191201771/naza/pkg/nazalog" ) type CustomizePubSessionContext struct { @@ -19,17 +21,22 @@ type CustomizePubSessionContext struct { streamName string remuxer *remux.AvPacket2RtmpRemuxer onRtmpMsg func(msg base.RtmpMsg) + + disposeFlag nazaatomic.Bool } func NewCustomizePubSessionContext(streamName string) *CustomizePubSessionContext { - return &CustomizePubSessionContext{ + s := &CustomizePubSessionContext{ uniqueKey: base.GenUkCustomizePubSession(), streamName: streamName, remuxer: remux.NewAvPacket2RtmpRemuxer(), } + nazalog.Infof("[%s] NewCustomizePubSessionContext.", s.uniqueKey) + return s } func (ctx *CustomizePubSessionContext) WithOnRtmpMsg(onRtmpMsg func(msg base.RtmpMsg)) *CustomizePubSessionContext { + ctx.onRtmpMsg = onRtmpMsg ctx.remuxer.WithOnRtmpMsg(onRtmpMsg) return ctx } @@ -42,16 +49,39 @@ func (ctx *CustomizePubSessionContext) StreamName() string { return ctx.streamName } +func (ctx *CustomizePubSessionContext) Dispose() { + nazalog.Infof("[%s] CustomizePubSessionContext::Dispose.", ctx.uniqueKey) + ctx.disposeFlag.Store(true) +} + // --------------------------------------------------------------------------------------------------------------------- func (ctx *CustomizePubSessionContext) WithOption(modOption func(option *base.AvPacketStreamOption)) { ctx.remuxer.WithOption(modOption) } -func (ctx *CustomizePubSessionContext) FeedAudioSpecificConfig(asc []byte) { +func (ctx *CustomizePubSessionContext) FeedAudioSpecificConfig(asc []byte) error { + if ctx.disposeFlag.Load() { + return base.ErrDisposedInStream + } + //nazalog.Debugf("[%s] FeedAudioSpecificConfig. asc=%s", ctx.uniqueKey, hex.Dump(asc)) ctx.remuxer.InitWithAvConfig(asc, nil, nil, nil) + return nil } -func (ctx *CustomizePubSessionContext) FeedAvPacket(packet base.AvPacket) { +func (ctx *CustomizePubSessionContext) FeedAvPacket(packet base.AvPacket) error { + if ctx.disposeFlag.Load() { + return base.ErrDisposedInStream + } + //nazalog.Debugf("[%s] FeedAvPacket. packet=%s", ctx.uniqueKey, packet.DebugString()) ctx.remuxer.FeedAvPacket(packet) + return nil +} + +func (ctx *CustomizePubSessionContext) FeedRtmpMsg(msg base.RtmpMsg) error { + if ctx.disposeFlag.Load() { + return base.ErrDisposedInStream + } + ctx.onRtmpMsg(msg) + return nil } diff --git a/pkg/logic/group__.go b/pkg/logic/group__.go index ee05c9f..3f7fe67 100644 --- a/pkg/logic/group__.go +++ b/pkg/logic/group__.go @@ -10,10 +10,11 @@ package logic import ( "encoding/json" - "github.com/q191201771/lal/pkg/gb28181" "strings" "sync" + "github.com/q191201771/lal/pkg/gb28181" + "github.com/q191201771/lal/pkg/base" "github.com/q191201771/lal/pkg/hls" "github.com/q191201771/lal/pkg/httpflv" @@ -43,24 +44,24 @@ import ( // | group.inSessionUniqueKey() | Y | Y | // --------------------------------------------------------------------------------------------------------------------- -// 输入流到输出流的转换路径关系 -// -// customizePubSession.WithOnRtmpMsg -> [dummyAudioFilter] -> OnReadRtmpAvMsg -> rtmp2RtspRemuxer -> rtsp -// -> rtmp -// -> http-flv, ts, hls +// 输入流到输出流的转换路径关系(一共6种输入): // -// --------------------------------------------------------------------------------------------------------------------- -// rtmpPubSession 和customizePubSession一样,省略 +// rtmpPullSession.WithOnReadRtmpAvMsg -> +// rtmpPubSession.SetPubSessionObserver -> +// customizePubSession.WithOnRtmpMsg -> OnReadRtmpAvMsg(enter Lock) -> [dummyAudioFilter] -> broadcastByRtmpMsg -> rtmp, http-flv +// -> rtmp2RtspRemuxer -> rtsp +// -> rtmp2MpegtsRemuxer -> ts, hls // // --------------------------------------------------------------------------------------------------------------------- -// rtspPubSession -> OnRtpPacket -> rtsp -// -> OnAvPacket -> rtsp2RtmpRemuxer -> onRtmpMsgFromRemux -> broadcastByRtmpMsg -> rtmp -// -> http-flv, ts, hls +// rtspPullSession -> +// rtspPubSession -> OnRtpPacket(enter Lock) -> rtsp +// -> OnAvPacket(enter Lock) -> rtsp2RtmpRemuxer -> onRtmpMsgFromRemux -> [dummyAudioFilter] -> broadcastByRtmpMsg -> rtmp, http-flv +// -> rtmp2MpegtsRemuxer -> ts, hls // // --------------------------------------------------------------------------------------------------------------------- -// psPubSession -> OnAvPacketFromPsPubSession -> rtsp2RtmpRemuxer -> onRtmpMsgFromRemux -> broadcastByRtmpMsg -> rtmp2RtspRemuxer -> rtsp -// -> rtmp -// -> http-flv, ts, hls +// psPubSession -> OnAvPacketFromPsPubSession(enter Lock) -> rtsp2RtmpRemuxer -> onRtmpMsgFromRemux -> [dummyAudioFilter] -> broadcastByRtmpMsg -> ... +// -> ... +// -> ... type IGroupObserver interface { CleanupHlsIfNeeded(appName string, streamName string, path string) @@ -89,12 +90,11 @@ type Group struct { rtmp2MpegtsRemuxer *remux.Rtmp2MpegtsRemuxer // pull pullProxy *pullProxy - // rtmp pub使用 + // rtmp pub使用 TODO(chef): [doc] 更新这个注释,是共同使用 202210 dummyAudioFilter *remux.DummyAudioFilter // ps pub使用 psPubTimeoutSec uint32 // 超时时间 psPubPrevInactiveCheckTick int64 // 上次检查时间 - psPubDumpFile *base.DumpFile // rtmp sub使用 rtmpGopCache *remux.GopCache // httpflv sub使用 @@ -124,8 +124,11 @@ type Group struct { rtmpMergeWriter *base.MergeWriter // TODO(chef): 后面可以在业务层加一个定时Flush // stat base.StatGroup - + // hlsCalcSessionStatIntervalSec uint32 + // + psPubDumpFile *base.DumpFile + rtspPullDumpFile *base.DumpFile } func NewGroup(appName string, streamName string, config *Config, observer IGroupObserver) *Group { @@ -148,9 +151,9 @@ func NewGroup(appName string, streamName string, config *Config, observer IGroup rtspSubSessionSet: make(map[*rtsp.SubSession]struct{}), waitRtspSubSessionSet: make(map[*rtsp.SubSession]struct{}), hlsSubSessionSet: make(map[*hls.SubSession]struct{}), - rtmpGopCache: remux.NewGopCache("rtmp", uk, config.RtmpConfig.GopNum), - httpflvGopCache: remux.NewGopCache("httpflv", uk, config.HttpflvConfig.GopNum), - httptsGopCache: remux.NewGopCacheMpegts(uk, config.HttptsConfig.GopNum), + rtmpGopCache: remux.NewGopCache("rtmp", uk, config.RtmpConfig.GopNum, config.RtmpConfig.SingleGopMaxFrameNum), + httpflvGopCache: remux.NewGopCache("httpflv", uk, config.HttpflvConfig.GopNum, config.HttpflvConfig.SingleGopMaxFrameNum), + httptsGopCache: remux.NewGopCacheMpegts(uk, config.HttptsConfig.GopNum, config.HttptsConfig.SingleGopMaxFrameNum), psPubPrevInactiveCheckTick: -1, hlsCalcSessionStatIntervalSec: uint32(config.HlsConfig.FragmentDurationMs/1000) * 10, } @@ -588,7 +591,7 @@ func (group *Group) inSessionUniqueKey() string { } func (group *Group) shouldStartRtspRemuxer() bool { - return group.config.RtspConfig.Enable + return group.config.RtspConfig.Enable || group.config.RtspConfig.RtspsEnable } func (group *Group) shouldStartMpegtsRemuxer() bool { diff --git a/pkg/logic/group__core_streaming.go b/pkg/logic/group__core_streaming.go index 54ab578..50ba611 100644 --- a/pkg/logic/group__core_streaming.go +++ b/pkg/logic/group__core_streaming.go @@ -9,9 +9,10 @@ package logic import ( + "net" + "github.com/q191201771/lal/pkg/rtmp" "github.com/q191201771/naza/pkg/nazalog" - "net" "github.com/q191201771/lal/pkg/mpegts" @@ -34,11 +35,14 @@ import ( // // 输入rtmp数据. // 来自 rtmp.ServerSession(Pub), rtmp.PullSession, CustomizePubSessionContext(remux.AvPacket2RtmpRemuxer), (remux.DummyAudioFilter) 的回调. -// func (group *Group) OnReadRtmpAvMsg(msg base.RtmpMsg) { group.mutex.Lock() defer group.mutex.Unlock() - group.broadcastByRtmpMsg(msg) + if group.dummyAudioFilter != nil { + group.dummyAudioFilter.Feed(msg) + } else { + group.broadcastByRtmpMsg(msg) + } } // --------------------------------------------------------------------------------------------------------------------- @@ -47,7 +51,6 @@ func (group *Group) OnReadRtmpAvMsg(msg base.RtmpMsg) { // // 输入rtsp(rtp)和rtp合帧之后的数据. // 来自 rtsp.PubSession 的回调. -// func (group *Group) OnSdp(sdpCtx sdp.LogicContext) { group.mutex.Lock() defer group.mutex.Unlock() @@ -63,6 +66,9 @@ func (group *Group) OnRtpPacket(pkt rtprtcp.RtpPacket) { group.mutex.Lock() defer group.mutex.Unlock() group.feedRtpPacket(pkt) + if group.rtspPullDumpFile != nil { + group.rtspPullDumpFile.Write(pkt.Raw) + } } // OnAvPacket ... @@ -83,7 +89,6 @@ func (group *Group) OnAvPacket(pkt base.AvPacket) { // OnAvPacketFromPsPubSession // // 来自 gb28181.PubSession 的回调. -// func (group *Group) OnAvPacketFromPsPubSession(pkt *base.AvPacket) { // TODO(chef): [refactor] 统一所有回调,AvPacket和*AvPacket 202208 @@ -103,7 +108,6 @@ func (group *Group) OnAvPacketFromPsPubSession(pkt *base.AvPacket) { // // 输入mpegts数据. // 来自 remux.Rtmp2MpegtsRemuxer 的回调. -// func (group *Group) OnPatPmt(b []byte) { group.patpmt = b @@ -129,9 +133,12 @@ func (group *Group) OnTsPackets(tsPackets []byte, frame *mpegts.Frame, boundary // // 输入rtmp数据. // 来自 remux.AvPacket2RtmpRemuxer 的回调. -// func (group *Group) onRtmpMsgFromRemux(msg base.RtmpMsg) { - group.broadcastByRtmpMsg(msg) + if group.dummyAudioFilter != nil { + group.dummyAudioFilter.Feed(msg) + } else { + group.broadcastByRtmpMsg(msg) + } } // --------------------------------------------------------------------------------------------------------------------- @@ -140,7 +147,6 @@ func (group *Group) onRtmpMsgFromRemux(msg base.RtmpMsg) { // // 输入rtsp(rtp)数据. // 来自 remux.Rtmp2RtspRemuxer 的回调. -// func (group *Group) onSdpFromRemux(sdpCtx sdp.LogicContext) { group.sdpCtx = &sdpCtx group.feedWaitRtspSubSessions() @@ -156,7 +162,6 @@ func (group *Group) onRtpPacketFromRemux(pkt rtprtcp.RtpPacket) { // OnFragmentOpen // // 来自 hls.Muxer 的回调 -// func (group *Group) OnFragmentOpen() { group.rtmp2MpegtsRemuxer.FlushAudio() } @@ -168,7 +173,6 @@ func (group *Group) OnFragmentOpen() { // 使用rtmp类型的数据做为输入,广播给各协议的输出 // // @param msg 调用结束后,内部不持有msg.Payload内存块 -// func (group *Group) broadcastByRtmpMsg(msg base.RtmpMsg) { //Log.Debugf("> broadcastByRtmpMsg. %s", msg.DebugString()) @@ -367,7 +371,7 @@ func (group *Group) broadcastByRtmpMsg(msg base.RtmpMsg) { } // # 缓存关键信息,以及gop - if group.config.RtmpConfig.Enable { + if group.config.RtmpConfig.Enable || group.config.RtmpConfig.RtmpsEnable { group.rtmpGopCache.Feed(msg, lazyRtmpChunkDivider.GetEnsureWithoutSdf()) if msg.Header.MsgTypeId == base.RtmpTypeIdMetadata { group.rtmpGopCache.SetMetadata(lazyRtmpChunkDivider.GetEnsureWithSdf(), lazyRtmpChunkDivider.GetEnsureWithoutSdf()) diff --git a/pkg/logic/group__in.go b/pkg/logic/group__in.go index 8bd1495..77888bf 100644 --- a/pkg/logic/group__in.go +++ b/pkg/logic/group__in.go @@ -11,7 +11,6 @@ package logic import ( "github.com/q191201771/lal/pkg/gb28181" "github.com/q191201771/naza/pkg/nazalog" - "net" "time" "github.com/q191201771/lal/pkg/base" @@ -31,6 +30,8 @@ func (group *Group) AddCustomizePubSession(streamName string) (ICustomizePubSess } group.customizePubSession = NewCustomizePubSessionContext(streamName) + Log.Debugf("[%s] [%s] add customize pub session into group.", group.UniqueKey, group.customizePubSession.UniqueKey()) + group.addIn() if group.shouldStartRtspRemuxer() { @@ -40,12 +41,7 @@ func (group *Group) AddCustomizePubSession(streamName string) (ICustomizePubSess ) } - if group.config.RtmpConfig.AddDummyAudioEnable { - group.dummyAudioFilter = remux.NewDummyAudioFilter(group.UniqueKey, group.config.RtmpConfig.AddDummyAudioWaitAudioMs, group.OnReadRtmpAvMsg) - group.customizePubSession.WithOnRtmpMsg(group.dummyAudioFilter.OnReadRtmpAvMsg) - } else { - group.customizePubSession.WithOnRtmpMsg(group.OnReadRtmpAvMsg) - } + group.customizePubSession.WithOnRtmpMsg(group.OnReadRtmpAvMsg) return group.customizePubSession, nil } @@ -72,17 +68,7 @@ func (group *Group) AddRtmpPubSession(session *rtmp.ServerSession) error { ) } - // TODO(chef): feat 为其他输入流也添加假音频。比如rtmp pull以及rtsp - // TODO(chef): refactor 可考虑抽象出一个输入流的配置块 - // TODO(chef): refactor 考虑放入addIn中 - if group.config.RtmpConfig.AddDummyAudioEnable { - // TODO(chef): 从整体控制和锁关系来说,应该让pub的数据回调到group中进锁后再让数据流入filter - // TODO(chef): 这里用OnReadRtmpAvMsg正确吗,是否会重复进锁 - group.dummyAudioFilter = remux.NewDummyAudioFilter(group.UniqueKey, group.config.RtmpConfig.AddDummyAudioWaitAudioMs, group.OnReadRtmpAvMsg) - session.SetPubSessionObserver(group.dummyAudioFilter) - } else { - session.SetPubSessionObserver(group) - } + session.SetPubSessionObserver(group) return nil } @@ -124,11 +110,10 @@ func (group *Group) StartRtpPub(req base.ApiCtrlStartRtpPubReq) (ret base.ApiCtr } pubSession := gb28181.NewPubSession().WithStreamName(req.StreamName).WithOnAvPacket(group.OnAvPacketFromPsPubSession) - pubSession.WithHookReadUdpPacket(func(b []byte, raddr *net.UDPAddr, err error) bool { + pubSession.WithHookReadPacket(func(b []byte) { if group.psPubDumpFile != nil { group.psPubDumpFile.Write(b) } - return true }) Log.Debugf("[%s] [%s] add RTP PubSession into group.", group.UniqueKey, pubSession.UniqueKey()) @@ -151,7 +136,7 @@ func (group *Group) StartRtpPub(req base.ApiCtrlStartRtpPubReq) (ret base.ApiCtr ) } - port, err := pubSession.Listen(req.Port) + port, err := pubSession.Listen(req.Port, req.IsTcpFlag != 0) if err != nil { group.delPsPubSession(pubSession) @@ -310,7 +295,7 @@ func (group *Group) delPsPubSession(session *gb28181.PubSession) { if session != group.psPubSession { Log.Warnf("[%s] del ps pub session but not match. del session=%s, group session=%p", - group.UniqueKey, session.UniqueKey(), group.customizePubSession) + group.UniqueKey, session.UniqueKey(), group.psPubSession) return } @@ -318,10 +303,10 @@ func (group *Group) delPsPubSession(session *gb28181.PubSession) { } func (group *Group) delCustomizePubSession(sessionCtx ICustomizePubSessionContext) { - Log.Debugf("[%s] [%s] del rtmp PubSession from group.", group.UniqueKey, sessionCtx.UniqueKey()) + Log.Debugf("[%s] [%s] del customize PubSession from group.", group.UniqueKey, sessionCtx.UniqueKey()) if sessionCtx != group.customizePubSession { - Log.Warnf("[%s] del rtmp pub session but not match. del session=%s, group session=%p", + Log.Warnf("[%s] del customize pub session but not match. del session=%s, group session=%p", group.UniqueKey, sessionCtx.UniqueKey(), group.customizePubSession) return } @@ -364,12 +349,16 @@ func (group *Group) delPullSession(session base.IObject) { // --------------------------------------------------------------------------------------------------------------------- // addIn 有pub或pull的输入型session加入时,需要调用该函数 -// func (group *Group) addIn() { now := time.Now().Unix() if group.shouldStartMpegtsRemuxer() { group.rtmp2MpegtsRemuxer = remux.NewRtmp2MpegtsRemuxer(group) + nazalog.Debugf("[%s] [%s] NewRtmp2MpegtsRemuxer in group.", group.UniqueKey, group.rtmp2MpegtsRemuxer.UniqueKey()) + } + + if group.config.InSessionConfig.AddDummyAudioEnable { + group.dummyAudioFilter = remux.NewDummyAudioFilter(group.UniqueKey, group.config.InSessionConfig.AddDummyAudioWaitAudioMs, group.broadcastByRtmpMsg) } group.startPushIfNeeded() @@ -379,7 +368,6 @@ func (group *Group) addIn() { } // delIn 有pub或pull的输入型session离开时,需要调用该函数 -// func (group *Group) delIn() { // 注意,remuxer放前面,使得有机会将内部缓存的数据吐出来 if group.rtmp2MpegtsRemuxer != nil { diff --git a/pkg/logic/group__record_flv.go b/pkg/logic/group__record_flv.go index 42c942d..1dcbb6d 100644 --- a/pkg/logic/group__record_flv.go +++ b/pkg/logic/group__record_flv.go @@ -16,7 +16,6 @@ import ( ) // startRecordFlvIfNeeded 必要时开启flv录制 -// func (group *Group) startRecordFlvIfNeeded(nowUnix int64) { if !group.config.RecordConfig.EnableFlv { return diff --git a/pkg/logic/group__record_hls.go b/pkg/logic/group__record_hls.go index dc9d9f5..b4c4e6d 100644 --- a/pkg/logic/group__record_hls.go +++ b/pkg/logic/group__record_hls.go @@ -17,7 +17,6 @@ func (group *Group) IsHlsMuxerAlive() bool { } // startHlsIfNeeded 必要时启动hls -// func (group *Group) startHlsIfNeeded() { if !group.config.HlsConfig.Enable && !group.config.HlsConfig.EnableHttps { return diff --git a/pkg/logic/group__record_mpegts.go b/pkg/logic/group__record_mpegts.go index 4e7b9a7..213d227 100644 --- a/pkg/logic/group__record_mpegts.go +++ b/pkg/logic/group__record_mpegts.go @@ -16,7 +16,6 @@ import ( ) // startRecordMpegtsIfNeeded 必要时开启ts录制 -// func (group *Group) startRecordMpegtsIfNeeded(nowUnix int64) { if !group.config.RecordConfig.EnableMpegts { return diff --git a/pkg/logic/group__relay_pull.go b/pkg/logic/group__relay_pull.go index ef2f1f8..3f53231 100644 --- a/pkg/logic/group__relay_pull.go +++ b/pkg/logic/group__relay_pull.go @@ -21,7 +21,6 @@ import ( ) // StartPull 外部命令主动触发pull拉流 -// func (group *Group) StartPull(info base.ApiCtrlStartRelayPullReq) (string, error) { group.mutex.Lock() defer group.mutex.Unlock() @@ -32,6 +31,7 @@ func (group *Group) StartPull(info base.ApiCtrlStartRelayPullReq) (string, error group.pullProxy.pullRetryNum = info.PullRetryNum group.pullProxy.autoStopPullAfterNoOutMs = info.AutoStopPullAfterNoOutMs group.pullProxy.rtspMode = info.RtspMode + group.pullProxy.debugDumpPacket = info.DebugDumpPacket return group.pullIfNeeded() } @@ -39,7 +39,6 @@ func (group *Group) StartPull(info base.ApiCtrlStartRelayPullReq) (string, error // StopPull // // @return 如果PullSession存在,返回它的unique key -// func (group *Group) StopPull() string { group.mutex.Lock() defer group.mutex.Unlock() @@ -58,6 +57,7 @@ type pullProxy struct { pullRetryNum int autoStopPullAfterNoOutMs int // 没有观看者时,是否自动停止pull rtspMode int + debugDumpPacket string startCount int lastHasOutTs int64 @@ -68,7 +68,6 @@ type pullProxy struct { } // initRelayPullByConfig 根据配置文件中的静态回源配置来初始化回源设置 -// func (group *Group) initRelayPullByConfig() { // 注意,这是配置文件中静态回源的配置值,不是HTTP-API的默认值 const ( @@ -105,12 +104,22 @@ func (group *Group) setRtmpPullSession(session *rtmp.PullSession) { 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 { @@ -184,7 +193,6 @@ func (group *Group) pullSessionUniqueKey() string { // 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) { @@ -201,7 +209,6 @@ func (group *Group) kickPull(sessionId string) bool { // 1. 添加新sub session // 2. 外部命令,比如http api // 3. 定时器,比如pull的连接断了,通过定时器可以重启触发pull -// func (group *Group) pullIfNeeded() (string, error) { if flag, err := group.shouldStartPull(); !flag { return "", err @@ -327,7 +334,6 @@ func (group *Group) shouldStartPull() (bool, error) { } // shouldAutoStopPull 是否需要自动停,根据没人观看停的逻辑 -// func (group *Group) shouldAutoStopPull() bool { // 没开启 if group.pullProxy.autoStopPullAfterNoOutMs < 0 { diff --git a/pkg/logic/group__relay_push.go b/pkg/logic/group__relay_push.go index e3b995b..191cc21 100644 --- a/pkg/logic/group__relay_push.go +++ b/pkg/logic/group__relay_push.go @@ -64,7 +64,6 @@ func (group *Group) initRelayPushByConfig() { } // startPushIfNeeded 必要时进行replay push转推 -// func (group *Group) startPushIfNeeded() { // push转推功能没开 if !group.pushEnable { diff --git a/pkg/logic/group_manager.go b/pkg/logic/group_manager.go index 29ed2ea..148fd5d 100644 --- a/pkg/logic/group_manager.go +++ b/pkg/logic/group_manager.go @@ -20,7 +20,6 @@ type ModConfigGroupCreator func(appName, streamName string, baseConfig *Config) // // 封装管理Group的容器 // 管理流标识(appName,streamName)与Group的映射关系。比如appName是否参与映射匹配 -// type IGroupManager interface { // GetOrCreateGroup // @@ -49,7 +48,6 @@ type IGroupManager interface { // --------------------------------------------------------------------------------------------------------------------- // SimpleGroupManager 忽略appName,只使用streamName -// type SimpleGroupManager struct { groupCreator IGroupCreator groups map[string]*Group // streamName -> Group @@ -114,16 +112,16 @@ func (s *SimpleGroupManager) Len() int { // --------------------------------------------------------------------------------------------------------------------- // // 背景: -// 有的协议需要结合appName和streamName作为流唯一标识(比如rtmp,httpflv,httpts) -// 有的协议不需要appName,只使用streamName作为流唯一标识(比如rtsp?) +// - 有的协议需要结合appName和streamName作为流唯一标识(比如rtmp,httpflv,httpts)。 +// - 有的协议不需要appName,只使用streamName作为流唯一标识(比如rtsp?)。 +// // 目标: -// 有appName的协议,需要参考appName -// 没appName的协议,需要和有appName的协议互通 -// 注意: -// - 当以上两种类型的协议混用时,系统使用者应避免第二种协议的streamName,在第一种协议中存在相同的streamName,但是appName不止一个 -// 这种情况下,内部无法知道该如何对应 -// - group可能由第一种协议创建,也可能由第二种协议创建 +// - 有appName的协议,需要参考appName。 +// - 没appName的协议,需要和有appName的协议互通。 // +// 注意: +// - 当以上两种类型的协议混用时,系统使用者应避免第二种协议的streamName,在第一种协议中存在相同的streamName,但是appName不止一个,这种情况下,内部无法知道该如何对应。 +// - group可能由第一种协议创建,也可能由第二种协议创建。 type ComplexGroupManager struct { groupCreator IGroupCreator // 注意,一个group只可能在一个容器中,两个容器中的group加起来才是全量 diff --git a/pkg/logic/http_api.go b/pkg/logic/http_api.go index ea40a95..f0a6b99 100644 --- a/pkg/logic/http_api.go +++ b/pkg/logic/http_api.go @@ -193,12 +193,16 @@ func (h *HttpApiServer) ctrlStartRtpPubHandler(w http.ResponseWriter, req *http. return } - if !j.Exist("port") { - info.Port = 0 - } if !j.Exist("timeout_ms") { info.TimeoutMs = 60000 } + // 不存在时默认0值的,不需要手动写了 + //if !j.Exist("port") { + // info.Port = 0 + //} + //if !j.Exist("is_tcp_flag") { + // info.IsTcpFlag = 0 + //} Log.Infof("http api start rtp pub. req info=%+v", info) @@ -223,7 +227,6 @@ func feedback(v interface{}, w http.ResponseWriter) { // unmarshalRequestJsonBody // // TODO(chef): [refactor] 搬到naza中 202205 -// func unmarshalRequestJsonBody(r *http.Request, info interface{}, keyFieldList ...string) (nazajson.Json, error) { body, err := ioutil.ReadAll(r.Body) if err != nil { diff --git a/pkg/logic/logic.go b/pkg/logic/logic.go index 137d295..9aa3231 100644 --- a/pkg/logic/logic.go +++ b/pkg/logic/logic.go @@ -9,8 +9,9 @@ package logic import ( - "github.com/q191201771/lal/pkg/base" "path/filepath" + + "github.com/q191201771/lal/pkg/base" ) // --------------------------------------------------------------------------------------------------------------------- @@ -27,7 +28,9 @@ type ILalServer interface { // AddCustomizePubSession(streamName string) (ICustomizePubSessionContext, error) - // DelCustomizePubSession 将 ICustomizePubSessionContext 从 ILalServer 中删除 + // DelCustomizePubSession 将 ICustomizePubSessionContext 对象从 ILalServer 中删除 + // + // 注意,业务方调用该函数后,就不要再使用该 ICustomizePubSessionContext 对象的方法了,比如继续 FeedAvPacket 是无效的 // DelCustomizePubSession(ICustomizePubSessionContext) @@ -47,7 +50,6 @@ type ILalServer interface { // NewLalServer 创建一个lal server // // @param modOption: 定制化配置。可变参数,如果不关心,可以不填,具体字段见 Option -// func NewLalServer(modOption ...ModOption) ILalServer { return NewServerManager(modOption...) } @@ -59,6 +61,8 @@ type ICustomizePubSessionContext interface { // base.IAvPacketStream + FeedRtmpMsg(msg base.RtmpMsg) error + UniqueKey() string StreamName() string } @@ -66,7 +70,6 @@ type ICustomizePubSessionContext interface { // --------------------------------------------------------------------------------------------------------------------- // INotifyHandler 事件通知接口 -// type INotifyHandler interface { OnServerStart(info base.LalInfo) OnUpdate(info base.UpdateInfo) @@ -81,10 +84,18 @@ type INotifyHandler interface { } type Option struct { - // ConfFilename 配置文件,注意,如果为空,内部会尝试从 DefaultConfFilenameList 读取默认配置文件 + // ConfFilename 配置文件。 // + // 注意,如果为空,内部会尝试从 DefaultConfFilenameList 读取默认配置文件 ConfFilename string + // ConfRawContent 配置内容,json格式。 + // + // 应用场景:有的业务方配置内容并非从配置文件中读取,比如集成 ILalServer 时配置内容来自配置中心网络下发,所以提供这个字段供业务方直接传入配置内容。 + // + // 注意,读取加载配置的优先级是 ConfRawContent > ConfFilename > DefaultConfFilenameList + ConfRawContent []byte + // NotifyHandler // // 事件监听 @@ -114,7 +125,6 @@ var defaultOption = Option{ type ModOption func(option *Option) // DefaultConfFilenameList 没有指定配置文件时,按顺序作为优先级,找到第一个存在的并使用 -// var DefaultConfFilenameList = []string{ filepath.FromSlash("lalserver.conf.json"), filepath.FromSlash("./conf/lalserver.conf.json"), diff --git a/pkg/logic/server_manager__.go b/pkg/logic/server_manager__.go index a33901e..3b1a483 100644 --- a/pkg/logic/server_manager__.go +++ b/pkg/logic/server_manager__.go @@ -11,28 +11,22 @@ package logic import ( "flag" "fmt" + "io/ioutil" "net/http" + _ "net/http/pprof" "os" "path/filepath" "sync" "time" - "github.com/q191201771/naza/pkg/nazalog" - - "github.com/q191201771/naza/pkg/defertaskthread" - - "github.com/q191201771/lal/pkg/hls" - "github.com/q191201771/lal/pkg/base" - - "github.com/q191201771/lal/pkg/httpts" - - "github.com/q191201771/lal/pkg/rtsp" - - _ "net/http/pprof" - + "github.com/q191201771/lal/pkg/hls" "github.com/q191201771/lal/pkg/httpflv" + "github.com/q191201771/lal/pkg/httpts" "github.com/q191201771/lal/pkg/rtmp" + "github.com/q191201771/lal/pkg/rtsp" + "github.com/q191201771/naza/pkg/defertaskthread" + "github.com/q191201771/naza/pkg/nazalog" //"github.com/felixge/fgprof" ) @@ -46,7 +40,9 @@ type ServerManager struct { hlsServerHandler *hls.ServerHandler rtmpServer *rtmp.Server + rtmpsServer *rtmp.Server rtspServer *rtsp.Server + rtspsServer *rtsp.Server httpApiServer *HttpApiServer pprofServer *http.Server exitChan chan struct{} @@ -67,28 +63,37 @@ func NewServerManager(modOption ...ModOption) *ServerManager { fn(&sm.option) } - confFile := sm.option.ConfFilename - // 运行参数中没有配置文件,尝试从几个默认位置读取 - if confFile == "" { - nazalog.Warnf("config file did not specify in the command line, try to load it in the usual path.") - confFile = firstExistDefaultConfFilename() - - // 所有默认位置都找不到配置文件,退出程序 + rawContent := sm.option.ConfRawContent + if len(rawContent) == 0 { + confFile := sm.option.ConfFilename + // 运行参数中没有配置文件,尝试从几个默认位置读取 if confFile == "" { - // TODO(chef): refactor ILalserver既然已经作为package提供了,那么内部就不应该包含flag和os exit的操作,应该返回给上层 - // TODO(chef): refactor new中逻辑是否该往后移 - flag.Usage() - _, _ = fmt.Fprintf(os.Stderr, ` + nazalog.Warnf("config file did not specify in the command line, try to load it in the usual path.") + confFile = firstExistDefaultConfFilename() + + // 所有默认位置都找不到配置文件,退出程序 + if confFile == "" { + // TODO(chef): refactor ILalserver既然已经作为package提供了,那么内部就不应该包含flag和os exit的操作,应该返回给上层 + // TODO(chef): refactor new中逻辑是否该往后移 + flag.Usage() + _, _ = fmt.Fprintf(os.Stderr, ` Example: %s -c %s Github: %s Doc: %s `, os.Args[0], filepath.FromSlash("./conf/lalserver.conf.json"), base.LalGithubSite, base.LalDocSite) + base.OsExitAndWaitPressIfWindows(1) + } + } + var err error + rawContent, err = ioutil.ReadFile(confFile) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "read conf file failed. file=%s err=%+v", confFile, err) base.OsExitAndWaitPressIfWindows(1) } } - sm.config = LoadConfAndInitLog(confFile) + sm.config = LoadConfAndInitLog(rawContent) base.LogoutStartInfo() if sm.config.HlsConfig.Enable && sm.config.HlsConfig.UseMemoryAsDiskFlag { @@ -123,9 +128,15 @@ Doc: %s if sm.config.RtmpConfig.Enable { sm.rtmpServer = rtmp.NewServer(sm.config.RtmpConfig.Addr, sm) } + if sm.config.RtmpConfig.RtmpsEnable { + sm.rtmpsServer = rtmp.NewServer(sm.config.RtmpConfig.RtmpsAddr, sm) + } if sm.config.RtspConfig.Enable { sm.rtspServer = rtsp.NewServer(sm.config.RtspConfig.Addr, sm, sm.config.RtspConfig.ServerAuthConfig) } + if sm.config.RtspConfig.RtspsEnable { + sm.rtspsServer = rtsp.NewServer(sm.config.RtspConfig.RtspsAddr, sm, sm.config.RtspConfig.ServerAuthConfig) + } if sm.config.HttpApiConfig.Enable { sm.httpApiServer = NewHttpApiServer(sm.config.HttpApiConfig.Addr, sm) } @@ -220,6 +231,18 @@ func (sm *ServerManager) RunLoop() error { }() } + if sm.rtmpsServer != nil { + err := sm.rtmpsServer.ListenWithTLS(sm.config.RtmpConfig.RtmpsCertFile, sm.config.RtmpConfig.RtmpsKeyFile) + // rtmps启动失败影响降级:当rtmps启动时我们并不返回错误,保证不因为rtmps影响其他服务 + if err == nil { + go func() { + if errRun := sm.rtmpsServer.RunLoop(); errRun != nil { + Log.Error(errRun) + } + }() + } + } + if sm.rtspServer != nil { if err := sm.rtspServer.Listen(); err != nil { return err @@ -231,6 +254,18 @@ func (sm *ServerManager) RunLoop() error { }() } + if sm.rtspsServer != nil { + err := sm.rtspsServer.ListenWithTLS(sm.config.RtspConfig.RtspsCertFile, sm.config.RtspConfig.RtspsKeyFile) + // rtsps启动失败影响降级:当rtsps启动时我们并不返回错误,保证不因为rtsps影响其他服务 + if err == nil { + go func() { + if errRun := sm.rtspsServer.RunLoop(); errRun != nil { + Log.Error(errRun) + } + }() + } + } + if sm.httpApiServer != nil { if err := sm.httpApiServer.Listen(); err != nil { return err @@ -308,10 +343,18 @@ func (sm *ServerManager) Dispose() { sm.rtmpServer.Dispose() } + if sm.rtmpsServer != nil { + sm.rtmpsServer.Dispose() + } + if sm.rtspServer != nil { sm.rtspServer.Dispose() } + if sm.rtspsServer != nil { + sm.rtspsServer.Dispose() + } + if sm.httpServerManager != nil { sm.httpServerManager.Dispose() } diff --git a/pkg/logic/server_manager__api.go b/pkg/logic/server_manager__api.go index 82878eb..522ea3f 100644 --- a/pkg/logic/server_manager__api.go +++ b/pkg/logic/server_manager__api.go @@ -87,7 +87,6 @@ func (sm *ServerManager) CtrlStartRelayPull(info base.ApiCtrlStartRelayPullReq) // CtrlStopRelayPull // // TODO(chef): 整理错误值 -// func (sm *ServerManager) CtrlStopRelayPull(streamName string) (ret base.ApiCtrlStopRelayPull) { sm.mutex.Lock() defer sm.mutex.Unlock() @@ -114,7 +113,6 @@ func (sm *ServerManager) CtrlStopRelayPull(streamName string) (ret base.ApiCtrlS // CtrlKickSession // // TODO(chef): refactor 不要返回http结果,返回error吧 -// func (sm *ServerManager) CtrlKickSession(info base.ApiCtrlKickSessionReq) (ret base.HttpResponseBasic) { sm.mutex.Lock() defer sm.mutex.Unlock() diff --git a/pkg/mpegts/pack.go b/pkg/mpegts/pack.go index 7b5958b..f35091f 100644 --- a/pkg/mpegts/pack.go +++ b/pkg/mpegts/pack.go @@ -15,7 +15,6 @@ import ( ) // Frame 帧数据,用于打包成mpegts格式的数据 -// type Frame struct { Pts uint64 // =(毫秒 * 90) Dts uint64 @@ -45,7 +44,6 @@ type Frame struct { // 注意,内部会增加 Frame.Cc 的值. // // @return: 内存块为独立申请,调度结束后,内部不再持有 -// func (frame *Frame) Pack() []byte { bufLen := len(frame.Raw) * 2 // 预分配一块足够大的内存 if bufLen < 1024 { @@ -259,6 +257,15 @@ func packPcr(out []byte, pcr uint64) { out[3] = uint8(pcr >> 1) out[4] = uint8(pcr<<7) | 0x7e out[5] = 0 + + //pcrLow := pcr % 300 + //pcrHigh := pcr / 300 + //out[0] = uint8(pcrHigh >> 25) + //out[1] = uint8(pcrHigh >> 17) + //out[2] = uint8(pcrHigh >> 9) + //out[3] = uint8(pcrHigh >> 1) + //out[4] = uint8(pcrHigh<<7) | uint8(pcrLow>>8) | 0x7e + //out[5] = uint8(pcrLow) } // 注意,除PTS外,DTS也使用这个函数打包 diff --git a/pkg/mpegts/pmt.go b/pkg/mpegts/pmt.go index 60f66c4..00e19d2 100644 --- a/pkg/mpegts/pmt.go +++ b/pkg/mpegts/pmt.go @@ -41,7 +41,6 @@ import ( // -------------- // CRC32 [32b] **** // ---------------------------------------- -// type Pmt struct { tid uint8 ssi uint8 diff --git a/pkg/remux/avpacket2rtmp.go b/pkg/remux/avpacket2rtmp.go index 30f7213..b717482 100644 --- a/pkg/remux/avpacket2rtmp.go +++ b/pkg/remux/avpacket2rtmp.go @@ -27,7 +27,6 @@ import ( // - gb28181 ps: rtp的合帧包 // - customize: 业务方通过接口向lalserver输入的流 // - 理论上也支持webrtc,后续接入webrtc时再验证 -// type AvPacket2RtmpRemuxer struct { option base.AvPacketStreamOption onRtmpMsg rtmp.OnReadRtmpAvMsg @@ -54,7 +53,6 @@ func NewAvPacket2RtmpRemuxer() *AvPacket2RtmpRemuxer { // WithOption // // TODO(chef): [refactor] 返回*AvPacket2RtmpRemuxer 202208 -// func (r *AvPacket2RtmpRemuxer) WithOption(modOption func(option *base.AvPacketStreamOption)) { modOption(&r.option) } @@ -69,7 +67,6 @@ func (r *AvPacket2RtmpRemuxer) WithOnRtmpMsg(onRtmpMsg rtmp.OnReadRtmpAvMsg) *Av // OnRtpPacket OnSdp OnAvPacket // // 实现RTSP回调数据的接口 rtsp.IBaseInSessionObserver ,使得接入时方便些 -// func (r *AvPacket2RtmpRemuxer) OnRtpPacket(pkt rtprtcp.RtpPacket) { // noop } @@ -86,7 +83,6 @@ func (r *AvPacket2RtmpRemuxer) OnAvPacket(pkt base.AvPacket) { // 这里提供输入sdp的sps、pps等信息的机会,如果没有,可以不调用 // // 内部不持有输入参数的内存块 -// func (r *AvPacket2RtmpRemuxer) InitWithAvConfig(asc, vps, sps, pps []byte) { var err error var bVsh []byte @@ -145,12 +141,9 @@ func (r *AvPacket2RtmpRemuxer) InitWithAvConfig(asc, vps, sps, pps []byte) { // 输入 base.AvPacket 数据 // // @param pkt: -// -// - 如果是aac,格式是裸数据或带adts头,具体取决于前面的配置 -// - 如果是h264,格式是avcc或Annexb,具体取决于前面的配置 -// -// 内部不持有该内存块 -// +// - 如果是aac,格式是裸数据或带adts头,具体取决于前面的配置。 +// - 如果是h264,格式是avcc或Annexb,具体取决于前面的配置。 +// 内部不持有该内存块。 func (r *AvPacket2RtmpRemuxer) FeedAvPacket(pkt base.AvPacket) { switch pkt.PayloadType { case base.AvPacketPtAvc: diff --git a/pkg/remux/dummy_audio_filter.go b/pkg/remux/dummy_audio_filter.go index 60f8c21..16c8be5 100644 --- a/pkg/remux/dummy_audio_filter.go +++ b/pkg/remux/dummy_audio_filter.go @@ -38,7 +38,6 @@ type DummyAudioFilter struct { // // @param waitAudioMs 等待音频数据时间,如果超出这个时间还没有接收到音频数据,则开始制造静音数据 // @param onPop 注意,所有回调都发生在输入函数调用中 -// func NewDummyAudioFilter(uk string, waitAudioMs int, onPop rtmp.OnReadRtmpAvMsg) *DummyAudioFilter { return &DummyAudioFilter{ uk: uk, diff --git a/pkg/remux/dummy_audio_filter_test.go b/pkg/remux/dummy_audio_filter_test.go index 9c61ea5..6201c1d 100644 --- a/pkg/remux/dummy_audio_filter_test.go +++ b/pkg/remux/dummy_audio_filter_test.go @@ -103,7 +103,7 @@ func TestDummyAudioFilter(t *testing.T) { } // @param logstr e.g. "header={Csid:4 MsgLen:378 MsgTypeId:18 MsgStreamId:1 TimestampAbs:0}" -/// +// / func helperUnpackRtmpMsg(logstr string) base.RtmpMsg { var fetchItemFn = func(str string, prefix string, suffix string) string { b := strings.Index(str, prefix) diff --git a/pkg/remux/flv2rtmp.go b/pkg/remux/flv2rtmp.go index 8407511..29586e2 100644 --- a/pkg/remux/flv2rtmp.go +++ b/pkg/remux/flv2rtmp.go @@ -31,7 +31,6 @@ func FlvTagHeader2RtmpHeader(in httpflv.TagHeader) (out base.RtmpHeader) { } // FlvTag2RtmpMsg @return msg: 返回的内存块引用参数`tag`的内存块 -// func FlvTag2RtmpMsg(tag httpflv.Tag) (msg base.RtmpMsg) { msg.Header = FlvTagHeader2RtmpHeader(tag.Header) msg.Payload = tag.Payload() @@ -39,7 +38,6 @@ func FlvTag2RtmpMsg(tag httpflv.Tag) (msg base.RtmpMsg) { } // FlvTag2RtmpChunks @return 返回的内存块为内部新申请 -// func FlvTag2RtmpChunks(tag httpflv.Tag) []byte { rtmpHeader := FlvTagHeader2RtmpHeader(tag.Header) return rtmp.Message2Chunks(tag.Payload(), &rtmpHeader) diff --git a/pkg/remux/gop_cache.go b/pkg/remux/gop_cache.go index 4f93a4a..fe5d3b9 100644 --- a/pkg/remux/gop_cache.go +++ b/pkg/remux/gop_cache.go @@ -15,42 +15,41 @@ import ( // GopCache // // 提供两个功能: -// 1. 缓存Metadata, VideoSeqHeader, AacSeqHeader -// 2. 缓存音视频GOP数据 +// 1. 缓存Metadata, VideoSeqHeader, AacSeqHeader +// 2. 缓存音视频GOP数据 // // 以下,只讨论GopCache的第2点功能 // -// 音频和视频都会缓存 +// 音频和视频都会缓存。 // -// GopCache也可能不缓存GOP数据,见NewGopCache函数的gopNum参数说明 +// GopCache也可能不缓存GOP数据,见NewGopCache函数的gopNum参数说明。 // -// 以下,我们只讨论gopNum > 0(也即gopSize > 1)的情况 +// 以下,我们只讨论gopNum > 0(也即gopSize > 1)的情况。 // -// GopCache为空时,只有输入了关键帧,才能开启GOP缓存,非关键帧以及音频数据不会被缓存 -// 因此,单音频的流是ok的,相当于不缓存任何数据 +// GopCache为空时,只有输入了关键帧,才能开启GOP缓存,非关键帧以及音频数据不会被缓存。 +// 因此,单音频的流是ok的,相当于不缓存任何数据。 // -// GopCache不为空时,输入关键帧触发生成新的GOP元素,其他情况则往最后一个GOP元素一直追加 +// GopCache不为空时,输入关键帧触发生成新的GOP元素,其他情况则往最后一个GOP元素一直追加。 // -// first用于读取第一个GOP(可能不完整),last的前一个用于写入当前GOP +// first用于读取第一个GOP(可能不完整),last的前一个用于写入当前GOP。 // -// 最近不完整的GOP也会被缓存,见NewGopCache函数的gopNum参数说明 +// 最近不完整的GOP也会被缓存,见NewGopCache函数的gopNum参数说明。 // // ----- // gopNum = 1 // gopSize = 2 // -// first | first | first | 在后面两个状态间转换,就不画了 -// | | | | | | -// 0 1 | 0 1 | 0 1 | -// * * | * * | * * | -// | | | | | | -// last | last | last | -// | | | -// (empty) | (full) | (full) | +// first | first | first | 在后面两个状态间转换,就不画了 +// | | | | | | +// 0 1 | 0 1 | 0 1 | +// * * | * * | * * | +// | | | | | | +// last | last | last | +// | | | +// (empty) | (full) | (full) | +// // GetGopCount: 0 | 1 | 1 | // ----- -// -// type GopCache struct { t string uniqueKey string @@ -60,28 +59,27 @@ type GopCache struct { VideoSeqHeader []byte AacSeqHeader []byte - gopRing []Gop - gopRingFirst int - gopRingLast int - gopSize int + gopRing []Gop + gopRingFirst int + gopRingLast int + gopSize int + singleGopMaxFrameNum int } // NewGopCache // -// @param gopNum: -// gop缓存大小 -// -// - 如果为0,则不缓存音频数据,也即GOP缓存功能不生效 -// - 如果>0,则缓存[0, gopNum]个GOP,最多缓存 gopNum 个GOP。注意,最后一个GOP可能是不完整的 -// -func NewGopCache(t string, uniqueKey string, gopNum int) *GopCache { +// @param gopNum: gop缓存大小。 +// - 如果为0,则不缓存音频数据,也即GOP缓存功能不生效。 +// - 如果>0,则缓存[0, gopNum]个GOP,最多缓存 gopNum 个GOP。注意,最后一个GOP可能是不完整的。 +func NewGopCache(t string, uniqueKey string, gopNum int, singleGopMaxFrameNum int) *GopCache { return &GopCache{ - t: t, - uniqueKey: uniqueKey, - gopSize: gopNum + 1, - gopRing: make([]Gop, gopNum+1, gopNum+1), - gopRingFirst: 0, - gopRingLast: 0, + t: t, + uniqueKey: uniqueKey, + gopSize: gopNum + 1, + gopRing: make([]Gop, gopNum+1, gopNum+1), + gopRingFirst: 0, + gopRingLast: 0, + singleGopMaxFrameNum: singleGopMaxFrameNum, } } @@ -98,7 +96,6 @@ func (gc *GopCache) SetMetadata(w []byte, wo []byte) { // Feed // // @param lg: 内部可能持有lg返回的内存块 -// func (gc *GopCache) Feed(msg base.RtmpMsg, b []byte) { // TODO(chef): [refactor] 重构lg两个参数这种方式 202207 @@ -130,7 +127,6 @@ func (gc *GopCache) Feed(msg base.RtmpMsg, b []byte) { } // GetGopCount 获取GOP数量,注意,最后一个可能是不完整的 -// func (gc *GopCache) GetGopCount() int { return (gc.gopRingLast + gc.gopSize - gc.gopRingFirst) % gc.gopSize } @@ -157,17 +153,18 @@ func (gc *GopCache) Clear() { // // 往最后一个GOP元素追加一个msg // 注意,如果GopCache为空,则不缓存msg -// func (gc *GopCache) feedLastGop(msg base.RtmpMsg, b []byte) { if !gc.isGopRingEmpty() { - gc.gopRing[(gc.gopRingLast-1+gc.gopSize)%gc.gopSize].Feed(msg, b) + gopPos := (gc.gopRingLast - 1 + gc.gopSize) % gc.gopSize + if gc.gopRing[gopPos].len() <= gc.singleGopMaxFrameNum || gc.singleGopMaxFrameNum == 0 { + gc.gopRing[gopPos].Feed(msg, b) + } } } // feedNewGop // // 生成一个最新的GOP元素,并往里追加一个msg -// func (gc *GopCache) feedNewGop(msg base.RtmpMsg, b []byte) { if gc.isGopRingFull() { gc.gopRingFirst = (gc.gopRingFirst + 1) % gc.gopSize @@ -194,7 +191,6 @@ type Gop struct { // Feed // // @param b: 内部持有`b`内存块 -// func (g *Gop) Feed(msg base.RtmpMsg, b []byte) { g.data = append(g.data, b) } @@ -202,3 +198,6 @@ func (g *Gop) Feed(msg base.RtmpMsg, b []byte) { func (g *Gop) Clear() { g.data = g.data[:0] } +func (g *Gop) len() int { + return len(g.data) +} diff --git a/pkg/remux/gop_cache_mpegts.go b/pkg/remux/gop_cache_mpegts.go index c7fb1d5..73f898a 100644 --- a/pkg/remux/gop_cache_mpegts.go +++ b/pkg/remux/gop_cache_mpegts.go @@ -17,27 +17,28 @@ type GopCacheMpegts struct { uniqueKey string gopNum int - gopRing []GopMpegts - gopRingFirst int - gopRingLast int - gopSize int + gopRing []GopMpegts + gopRingFirst int + gopRingLast int + gopSize int + singleGopMaxFrameNum int } -func NewGopCacheMpegts(uniqueKey string, gopNum int) *GopCacheMpegts { +func NewGopCacheMpegts(uniqueKey string, gopNum int, singleGopMaxFrameNum int) *GopCacheMpegts { return &GopCacheMpegts{ - uniqueKey: uniqueKey, - gopNum: gopNum, - gopSize: gopNum + 1, - gopRing: make([]GopMpegts, gopNum+1, gopNum+1), - gopRingFirst: 0, - gopRingLast: 0, + uniqueKey: uniqueKey, + gopNum: gopNum, + gopSize: gopNum + 1, + gopRing: make([]GopMpegts, gopNum+1, gopNum+1), + gopRingFirst: 0, + gopRingLast: 0, + singleGopMaxFrameNum: singleGopMaxFrameNum, } } // Feed // // @param b: 内部持有该内存块 -// func (gc *GopCacheMpegts) Feed(b []byte, boundary bool) { if gc.gopSize > 1 { if boundary { @@ -49,7 +50,6 @@ func (gc *GopCacheMpegts) Feed(b []byte, boundary bool) { } // GetGopCount 获取GOP数量,注意,最后一个可能是不完整的 -// func (gc *GopCacheMpegts) GetGopCount() int { return (gc.gopRingLast + gc.gopSize - gc.gopRingFirst) % gc.gopSize } @@ -72,17 +72,19 @@ func (gc *GopCacheMpegts) Clear() { // // 往最后一个GOP元素追加一个msg // 注意,如果GopCache为空,则不缓存msg -// func (gc *GopCacheMpegts) feedLastGop(b []byte) { if !gc.isGopRingEmpty() { - gc.gopRing[(gc.gopRingLast-1+gc.gopSize)%gc.gopSize].Feed(b) + gopPos := (gc.gopRingLast - 1 + gc.gopSize) % gc.gopSize + if gc.gopRing[gopPos].len() <= gc.singleGopMaxFrameNum || gc.singleGopMaxFrameNum == 0 { + gc.gopRing[gopPos].Feed(b) + } + } } // feedNewGop // // 生成一个最新的GOP元素,并往里追加一个msg -// func (gc *GopCacheMpegts) feedNewGop(b []byte) { if gc.isGopRingFull() { gc.gopRingFirst = (gc.gopRingFirst + 1) % gc.gopSize @@ -105,7 +107,6 @@ func (gc *GopCacheMpegts) isGopRingEmpty() bool { // GopMpegts // // 单个Gop,包含多帧数据 -// type GopMpegts struct { data [][]byte } @@ -113,7 +114,6 @@ type GopMpegts struct { // Feed // // @param b: 内部持有`b`内存块 -// func (g *GopMpegts) Feed(b []byte) { g.data = append(g.data, b) } @@ -121,3 +121,6 @@ func (g *GopMpegts) Feed(b []byte) { func (g *GopMpegts) Clear() { g.data = g.data[:0] } +func (g *GopMpegts) len() int { + return len(g.data) +} diff --git a/pkg/remux/gop_cache_test.go b/pkg/remux/gop_cache_test.go index 2487eb2..86d4d0f 100644 --- a/pkg/remux/gop_cache_test.go +++ b/pkg/remux/gop_cache_test.go @@ -59,7 +59,7 @@ func TestGopCache_Feed(t *testing.T) { i4f := func() []byte { return []byte{1, 4} } p4f := func() []byte { return []byte{0, 4} } - nc := NewGopCache("rtmp", "test", 3) + nc := NewGopCache("rtmp", "test", 3, 0) assert.Equal(t, 0, nc.GetGopCount()) assert.Equal(t, nil, nc.GetGopDataAt(0)) assert.Equal(t, nil, nc.GetGopDataAt(1)) diff --git a/pkg/remux/rtmp.go b/pkg/remux/rtmp.go index 9cb9a01..ab59ef7 100644 --- a/pkg/remux/rtmp.go +++ b/pkg/remux/rtmp.go @@ -18,7 +18,6 @@ import ( // 使用场景:一般是输入流转换为输出流时。 // 目的:使得流格式更标准。 // 做法:设置 MsgStreamId 和 Csid,其他字段保持`in`的值。 -// func MakeDefaultRtmpHeader(in base.RtmpHeader) (out base.RtmpHeader) { out.MsgLen = in.MsgLen out.TimestampAbs = in.TimestampAbs @@ -38,7 +37,6 @@ func MakeDefaultRtmpHeader(in base.RtmpHeader) (out base.RtmpHeader) { // --------------------------------------------------------------------------------------------------------------------- // LazyRtmpChunkDivider 在必要时,有且仅有一次做切分成chunk的操作 -// type LazyRtmpChunkDivider struct { msg base.RtmpMsg chunksWithSdf []byte diff --git a/pkg/remux/rtmp2avpacket.go b/pkg/remux/rtmp2avpacket.go index 1e9f8c6..fd1e531 100644 --- a/pkg/remux/rtmp2avpacket.go +++ b/pkg/remux/rtmp2avpacket.go @@ -20,7 +20,6 @@ import ( // // 用途: // - 将rtmp流中的视频转换成ffmpeg可解码的格式 -// type Rtmp2AvPacketRemuxer struct { option Rtmp2AvPacketRemuxerOption onAvPacket func(pkt base.AvPacket, arg interface{}) @@ -52,7 +51,6 @@ func (r *Rtmp2AvPacketRemuxer) WithOption(modOption func(option *Rtmp2AvPacketRe // WithOnAvPacket // // @param onAvPacket: pkt 内存由内部新申请,回调后内部不再使用 -// func (r *Rtmp2AvPacketRemuxer) WithOnAvPacket(onAvPacket func(pkt base.AvPacket, arg interface{})) *Rtmp2AvPacketRemuxer { r.onAvPacket = onAvPacket return r diff --git a/pkg/remux/rtmp2flv.go b/pkg/remux/rtmp2flv.go index 0c1544d..bf6486c 100644 --- a/pkg/remux/rtmp2flv.go +++ b/pkg/remux/rtmp2flv.go @@ -28,7 +28,6 @@ func RtmpMsg2FlvTag(msg base.RtmpMsg) *httpflv.Tag { // ------------------------------------------------------------------------------------------------------------------- // LazyRtmpMsg2FlvTag 在必要时,有且仅有一次做转换操作 -// type LazyRtmpMsg2FlvTag struct { msg base.RtmpMsg //tagWithSdf []byte diff --git a/pkg/remux/rtmp2mpegts.go b/pkg/remux/rtmp2mpegts.go index bae4425..391e27a 100644 --- a/pkg/remux/rtmp2mpegts.go +++ b/pkg/remux/rtmp2mpegts.go @@ -17,6 +17,7 @@ import ( "github.com/q191201771/lal/pkg/mpegts" "github.com/q191201771/naza/pkg/bele" "github.com/q191201771/naza/pkg/nazabytes" + "github.com/q191201771/naza/pkg/nazalog" "math" ) @@ -50,9 +51,8 @@ type IRtmp2MpegtsRemuxerObserver interface { } // Rtmp2MpegtsRemuxer 输入rtmp流,输出mpegts流 -// type Rtmp2MpegtsRemuxer struct { - UniqueKey string + uk string observer IRtmp2MpegtsRemuxerObserver filter *rtmp2MpegtsFilter @@ -109,7 +109,7 @@ type Rtmp2MpegtsRemuxer struct { func NewRtmp2MpegtsRemuxer(observer IRtmp2MpegtsRemuxerObserver) *Rtmp2MpegtsRemuxer { uk := base.GenUkRtmp2MpegtsRemuxer() r := &Rtmp2MpegtsRemuxer{ - UniqueKey: uk, + uk: uk, observer: observer, basicAudioDts: math.MaxUint64, basicAudioPts: math.MaxUint64, @@ -120,13 +120,15 @@ func NewRtmp2MpegtsRemuxer(observer IRtmp2MpegtsRemuxerObserver) *Rtmp2MpegtsRem r.videoOut = make([]byte, initialVideoOutBufferSize) r.videoOut = r.videoOut[0:0] r.filter = newRtmp2MpegtsFilter(calcFragmentHeaderQueueSize, r) + + nazalog.Debugf("[%s] NewRtmp2MpegtsRemuxer", r.uk) + return r } // FeedRtmpMessage // // @param msg: msg.Payload 调用结束后,函数内部不会持有这块内存 -// func (s *Rtmp2MpegtsRemuxer) FeedRtmpMessage(msg base.RtmpMsg) { s.filter.Push(msg) } @@ -143,7 +145,6 @@ func (s *Rtmp2MpegtsRemuxer) Dispose() { // 1. 收到音频或视频时,音频缓存队列已达到一定长度(内部判断) // 2. 打开一个新的TS文件切片时 // 3. 输入流关闭时 -// func (s *Rtmp2MpegtsRemuxer) FlushAudio() { if s.audioCacheEmpty() { return @@ -166,12 +167,15 @@ func (s *Rtmp2MpegtsRemuxer) FlushAudio() { s.audioCc = frame.Cc } +func (s *Rtmp2MpegtsRemuxer) UniqueKey() string { + return s.uk +} + // ----- implement of iRtmp2MpegtsFilterObserver ---------------------------------------------------------------------------------------------------------------- // onPatPmt onPop // // 实现 iRtmp2MpegtsFilterObserver -// func (s *Rtmp2MpegtsRemuxer) onPatPmt(b []byte) { s.observer.OnPatPmt(b) } @@ -189,7 +193,7 @@ func (s *Rtmp2MpegtsRemuxer) onPop(msg base.RtmpMsg) { func (s *Rtmp2MpegtsRemuxer) feedVideo(msg base.RtmpMsg) { if len(msg.Payload) <= 5 { - Log.Warnf("[%s] rtmp msg too short, ignore. header=%+v, payload=%s", s.UniqueKey, msg.Header, hex.Dump(msg.Payload)) + Log.Warnf("[%s] rtmp msg too short, ignore. header=%+v, payload=%s", s.uk, msg.Header, hex.Dump(msg.Payload)) return } @@ -204,12 +208,12 @@ func (s *Rtmp2MpegtsRemuxer) feedVideo(msg base.RtmpMsg) { var err error if msg.IsAvcKeySeqHeader() { if s.spspps, err = avc.SpsPpsSeqHeader2Annexb(msg.Payload); err != nil { - Log.Errorf("[%s] cache spspps failed. err=%+v", s.UniqueKey, err) + Log.Errorf("[%s] cache spspps failed. err=%+v", s.uk, err) } return } else if msg.IsHevcKeySeqHeader() { if s.spspps, err = hevc.VpsSpsPpsSeqHeader2Annexb(msg.Payload); err != nil { - Log.Errorf("[%s] cache vpsspspps failed. err=%+v", s.UniqueKey, err) + Log.Errorf("[%s] cache vpsspspps failed. err=%+v", s.uk, err) } return } @@ -223,7 +227,7 @@ func (s *Rtmp2MpegtsRemuxer) feedVideo(msg base.RtmpMsg) { // msg中可能有多个NALU,逐个获取 nals, err := avc.SplitNaluAvcc(msg.Payload[5:]) if err != nil { - Log.Errorf("[%s] iterate nalu failed. err=%+v, header=%+v, payload=%s", err, s.UniqueKey, msg.Header, hex.Dump(nazabytes.Prefix(msg.Payload, 32))) + Log.Errorf("[%s] iterate nalu failed. err=%+v, header=%+v, payload=%s", err, s.uk, msg.Header, hex.Dump(nazabytes.Prefix(msg.Payload, 32))) return } @@ -312,7 +316,7 @@ func (s *Rtmp2MpegtsRemuxer) feedVideo(msg base.RtmpMsg) { case avc.NaluTypeIdrSlice: if !spsppsSent { if s.videoOut, err = s.appendSpsPps(s.videoOut); err != nil { - Log.Warnf("[%s] append spspps by not exist.", s.UniqueKey) + Log.Warnf("[%s] append spspps by not exist.", s.uk) return } } @@ -326,7 +330,7 @@ func (s *Rtmp2MpegtsRemuxer) feedVideo(msg base.RtmpMsg) { if hevc.IsIrapNalu(nalType) { if !spsppsSent { if s.videoOut, err = s.appendSpsPps(s.videoOut); err != nil { - Log.Warnf("[%s] append spspps by not exist.", s.UniqueKey) + Log.Warnf("[%s] append spspps by not exist.", s.uk) return } } @@ -374,24 +378,24 @@ func (s *Rtmp2MpegtsRemuxer) feedVideo(msg base.RtmpMsg) { func (s *Rtmp2MpegtsRemuxer) feedAudio(msg base.RtmpMsg) { if len(msg.Payload) <= 2 { - Log.Warnf("[%s] rtmp msg too short, ignore. header=%+v, payload=%s", s.UniqueKey, msg.Header, hex.Dump(msg.Payload)) + Log.Warnf("[%s] rtmp msg too short, ignore. header=%+v, payload=%s", s.uk, msg.Header, hex.Dump(msg.Payload)) return } if msg.Payload[0]>>4 != base.RtmpSoundFormatAac { return } - //Log.Debugf("[%s] hls: feedAudio. dts=%d len=%d", s.UniqueKey, msg.Header.TimestampAbs, len(msg.Payload)) + //Log.Debugf("[%s] hls: feedAudio. dts=%d len=%d", s.uk, msg.Header.TimestampAbs, len(msg.Payload)) if msg.Payload[1] == base.RtmpAacPacketTypeSeqHeader { if err := s.cacheAacSeqHeader(msg); err != nil { - Log.Errorf("[%s] cache aac seq header failed. err=%+v", s.UniqueKey, err) + Log.Errorf("[%s] cache aac seq header failed. err=%+v", s.uk, err) } return } if !s.audioSeqHeaderCached() { - Log.Warnf("[%s] feed audio message but aac seq header not exist.", s.UniqueKey) + Log.Warnf("[%s] feed audio message but aac seq header not exist.", s.uk) return } @@ -484,8 +488,8 @@ func (s *Rtmp2MpegtsRemuxer) adjustDtsPts(frame *mpegts.Frame) { if s.basicAudioPts == math.MaxUint64 { s.basicAudioPts = frame.Pts } - frame.Dts = subSafe(frame.Dts, s.basicAudioDts) - frame.Pts = subSafe(frame.Pts, s.basicAudioPts) + frame.Dts = subSafe(frame.Dts, s.basicAudioDts, s.uk, frame) + frame.Pts = subSafe(frame.Pts, s.basicAudioPts, s.uk, frame) } else if frame.Sid == mpegts.StreamIdVideo { if s.basicVideoDts == math.MaxUint64 { s.basicVideoDts = frame.Dts @@ -493,15 +497,15 @@ func (s *Rtmp2MpegtsRemuxer) adjustDtsPts(frame *mpegts.Frame) { if s.basicVideoPts == math.MaxUint64 { s.basicVideoPts = frame.Pts } - frame.Dts = subSafe(frame.Dts, s.basicVideoDts) - frame.Pts = subSafe(frame.Pts, s.basicVideoPts) + frame.Dts = subSafe(frame.Dts, s.basicVideoDts, s.uk, frame) + frame.Pts = subSafe(frame.Pts, s.basicVideoPts, s.uk, frame) } } -func subSafe(a, b uint64) uint64 { +func subSafe(a, b uint64, uk string, frame *mpegts.Frame) uint64 { if a >= b { return a - b } - Log.Warnf("subSafe. a=%d, b=%d", a, b) - return 0 + Log.Warnf("[%s] subSafe. a=%d, b=%d, frame=%s", uk, a, b, frame.DebugString()) + return a } diff --git a/pkg/remux/rtmp2mpegts_filter_.go b/pkg/remux/rtmp2mpegts_filter_.go index 681725a..908af20 100644 --- a/pkg/remux/rtmp2mpegts_filter_.go +++ b/pkg/remux/rtmp2mpegts_filter_.go @@ -18,7 +18,6 @@ import ( // 缓存流起始的一些数据,判断流中是否存在音频、视频,以及编码格式,生成正确的mpegts PatPmt头信息 // // 一旦判断结束,该队列变成直进直出,不再有实际缓存 -// type rtmp2MpegtsFilter struct { maxMsgSize int data []base.RtmpMsg @@ -46,7 +45,6 @@ type iRtmp2MpegtsFilterObserver interface { // NewRtmp2MpegtsFilter // // @param maxMsgSize: 最大缓存多少个包 -// func newRtmp2MpegtsFilter(maxMsgSize int, observer iRtmp2MpegtsFilterObserver) *rtmp2MpegtsFilter { return &rtmp2MpegtsFilter{ maxMsgSize: maxMsgSize, @@ -61,7 +59,6 @@ func newRtmp2MpegtsFilter(maxMsgSize int, observer iRtmp2MpegtsFilterObserver) * // Push // // @param msg: 函数调用结束后,内部不持有该内存块 -// func (q *rtmp2MpegtsFilter) Push(msg base.RtmpMsg) { if q.done { q.observer.onPop(msg) diff --git a/pkg/remux/rtmp2rtsp.go b/pkg/remux/rtmp2rtsp.go index c53daea..e157597 100644 --- a/pkg/remux/rtmp2rtsp.go +++ b/pkg/remux/rtmp2rtsp.go @@ -53,7 +53,6 @@ type OnRtpPacket func(pkt rtprtcp.RtpPacket) // NewRtmp2RtspRemuxer @param onSdp: 每次回调为独立的内存块,回调结束后,内部不再使用该内存块 // @param onRtpPacket: 每次回调为独立的内存块,回调结束后,内部不再使用该内存块 -// func NewRtmp2RtspRemuxer(onSdp OnSdp, onRtpPacket OnRtpPacket) *Rtmp2RtspRemuxer { return &Rtmp2RtspRemuxer{ onSdp: onSdp, @@ -64,7 +63,6 @@ func NewRtmp2RtspRemuxer(onSdp OnSdp, onRtpPacket OnRtpPacket) *Rtmp2RtspRemuxer } // FeedRtmpMsg @param msg: 函数调用结束后,内部不持有`msg`内存块 -// func (r *Rtmp2RtspRemuxer) FeedRtmpMsg(msg base.RtmpMsg) { var err error diff --git a/pkg/rtmp/amf0.go b/pkg/rtmp/amf0.go index c8ca811..3b9a0ea 100644 --- a/pkg/rtmp/amf0.go +++ b/pkg/rtmp/amf0.go @@ -33,7 +33,7 @@ const ( Amf0TypeMarkerObject = uint8(0x03) Amf0TypeMarkerNull = uint8(0x05) Amf0TypeMarkerEcmaArray = uint8(0x08) - Amf0TypeMarkerObjectEnd = uint8(0x09) + Amf0TypeMarkerObjectEnd = uint8(0x09) // end for both Object and Array Amf0TypeMarkerLongString = uint8(0x0c) // 还没用到的类型 @@ -48,7 +48,12 @@ const ( //Amf0TypeMarkerTypedObject = uint8(0x10) ) -var Amf0TypeMarkerObjectEndBytes = []byte{0, 0, Amf0TypeMarkerObjectEnd} +var ( + // Amf0TypeMarkerObjectEndBytes Amf0TypeMarkerArrayEndBytes: + // object-end-type(0x00 0x00 0x09) 表示Object和EcmaArray类型的结束标识 + Amf0TypeMarkerObjectEndBytes = []byte{0, 0, Amf0TypeMarkerObjectEnd} + Amf0TypeMarkerArrayEndBytes = []byte{0, 0, Amf0TypeMarkerObjectEnd} +) // --------------------------------------------------------------------------------------------------------------------- @@ -396,10 +401,9 @@ func (amf0) ReadArray(b []byte) (ObjectPairArray, int, error) { } } - if len(b)-index >= 3 && bytes.Equal(b[index:index+3], Amf0TypeMarkerObjectEndBytes) { + if len(b)-index >= 3 && bytes.Equal(b[index:index+3], Amf0TypeMarkerArrayEndBytes) { index += 3 } else { - // 测试时发现Array最后也是以00 00 09结束,不确定是否是标准规定的,加个日志在这 Log.Warn("amf ReadArray without suffix Amf0TypeMarkerObjectEndBytes.") } return ops, index, nil diff --git a/pkg/rtmp/chunk_composer.go b/pkg/rtmp/chunk_composer.go index aeedd67..d1ecde8 100644 --- a/pkg/rtmp/chunk_composer.go +++ b/pkg/rtmp/chunk_composer.go @@ -23,7 +23,6 @@ import ( // ChunkComposer // // 读取chunk,并合并chunk,生成message返回给上层 -// type ChunkComposer struct { peerChunkSize uint32 reuseBufferFlag bool // TODO(chef): [fix] RtmpTypeIdAggregateMessage时,reuseBufferFlag==false的处理 202206 @@ -50,17 +49,18 @@ type OnCompleteMessage func(stream *Stream) error // RunLoop 将rtmp chunk合并为message // -// @param cb: stream.msg: 注意,回调结束后,`msg`的内存块会被`ChunkComposer`重复使用 -// 也即多次回调的`msg`是复用的同一块内存块 -// 如果业务方需要在回调结束后,依然持有`msg`,那么需要对`msg`进行拷贝 -// 只在回调中使用`msg`,则不需要拷贝 +// @param cb: // -// cb return: 如果cb返回的error不为nil,则`RunLoop`停止阻塞,并返回这个错误 +// @param cb.Stream.msg: +// 注意,回调结束后,`msg`的内存块会被`ChunkComposer`重复使用。 +// 也即多次回调的`msg`是复用的同一块内存块。 +// 如果业务方需要在回调结束后,依然持有`msg`,那么需要对`msg`进行拷贝。 +// 只在回调中使用`msg`,则不需要拷贝。 +// @return(回调函数`cb`的返回值): 如果cb返回的error不为nil,则`RunLoop`停止阻塞,并返回这个错误。 // // @return 阻塞直到发生错误 // // TODO chef: msglen支持最大阈值,超过可以认为对端是非法的 -// func (c *ChunkComposer) RunLoop(reader io.Reader, cb OnCompleteMessage) error { var aggregateStream *Stream bootstrap := make([]byte, 11) @@ -270,7 +270,7 @@ func (c *ChunkComposer) RunLoop(reader io.Reader, cb OnCompleteMessage) error { // TODO(chef): 这里应该永远执行不到,可以删除掉 if stream.msg.Len() > stream.header.MsgLen { - return base.NewErrRtmpShortBuffer(int(stream.header.MsgLen), int(stream.msg.Len()), "len of msg bigger tthan msg len of header") + return base.NewErrRtmpShortBuffer(int(stream.header.MsgLen), int(stream.msg.Len()), "len of msg bigger than msg len of header") } } } diff --git a/pkg/rtmp/chunk_divider.go b/pkg/rtmp/chunk_divider.go index 8aea675..a265ac3 100644 --- a/pkg/rtmp/chunk_divider.go +++ b/pkg/rtmp/chunk_divider.go @@ -13,6 +13,7 @@ package rtmp import ( "github.com/q191201771/lal/pkg/base" "github.com/q191201771/naza/pkg/bele" + "net" ) type ChunkDivider struct { @@ -23,16 +24,33 @@ var defaultChunkDivider = ChunkDivider{ localChunkSize: LocalChunkSize, } -// Message2Chunks @return 返回的内存块由内部申请,不依赖参数内存块 +// Message2Chunks +// +// @return 返回的内存块由内部申请,不依赖参数内存块 func Message2Chunks(message []byte, header *base.RtmpHeader) []byte { return defaultChunkDivider.Message2Chunks(message, header) } -// Message2Chunks TODO chef: 新的 message 的第一个 chunk 始终使用 fmt0 格式,没有参考前一个 message +// Message2ChunksV +// +// @param message: 待打包的message支持放在多个字节切片中 +func Message2ChunksV(message net.Buffers, header *base.RtmpHeader) []byte { + return defaultChunkDivider.Message2ChunksV(message, header) +} + +// Message2Chunks +// +// TODO chef: [opt] 新的 message 的第一个 chunk 始终使用 fmt0 格式,没有参考前一个 message func (d *ChunkDivider) Message2Chunks(message []byte, header *base.RtmpHeader) []byte { return message2Chunks(message, header, nil, d.localChunkSize) } +func (d *ChunkDivider) Message2ChunksV(message net.Buffers, header *base.RtmpHeader) []byte { + return message2ChunksV(message, header, nil, d.localChunkSize) +} + +// --------------------------------------------------------------------------------------------------------------------- + // @param 返回头的大小 func calcHeader(header *base.RtmpHeader, prevHeader *base.RtmpHeader, out []byte) int { var index int @@ -151,3 +169,50 @@ func message2Chunks(message []byte, header *base.RtmpHeader, prevHeader *base.Rt return out[:index] } + +// copyBufferFromBuffers +// +// TODO(chef): [refactor] move to naza 202206 +// TODO(chef): [perf] impl me +func copyBufferFromBuffers(out []byte, bs net.Buffers, pos int, length int) { +} + +func message2ChunksV(message net.Buffers, header *base.RtmpHeader, prevHeader *base.RtmpHeader, chunkSize int) []byte { + var totalLen int + for _, b := range message { + totalLen += len(b) + } + + // 计算chunk数量,最后一个chunk的大小 + numOfChunk := totalLen / chunkSize + lastChunkSize := chunkSize + if totalLen%chunkSize != 0 { + numOfChunk++ + lastChunkSize = totalLen % chunkSize + } + + maxNeededLen := (chunkSize + maxHeaderSize) * numOfChunk + out := make([]byte, maxNeededLen) + + var index int + + // NOTICE 和srs交互时,发现srs要求message中的非第一个chunk不能使用fmt0 + // 将message切割成chunk放入chunk body中 + for i := 0; i < numOfChunk; i++ { + headLen := calcHeader(header, prevHeader, out[index:]) + index += headLen + + if i != numOfChunk-1 { + //copy(out[index:], message[i*chunkSize:i*chunkSize+chunkSize]) + copyBufferFromBuffers(out[index:], message, i*chunkSize, chunkSize) + index += chunkSize + } else { + //copy(out[index:], message[i*chunkSize:i*chunkSize+lastChunkSize]) + copyBufferFromBuffers(out[index:], message, i*chunkSize, lastChunkSize) + index += lastChunkSize + } + prevHeader = header + } + + return out[:index] +} diff --git a/pkg/rtmp/client_pull_session.go b/pkg/rtmp/client_pull_session.go index 7dfee03..cb05544 100644 --- a/pkg/rtmp/client_pull_session.go +++ b/pkg/rtmp/client_pull_session.go @@ -65,7 +65,6 @@ func NewPullSession(modOptions ...ModPullSessionOption) *PullSession { // WithOnPullSucc Pull成功 // // 如果你想保证绝对时序,在 WithOnReadRtmpAvMsg 回调音视频数据前,做一些操作,那么使用这个回调替代 Pull 返回成功 -// func (s *PullSession) WithOnPullSucc(onPullResult func()) *PullSession { s.core.onDoResult = onPullResult return s @@ -74,24 +73,22 @@ func (s *PullSession) WithOnPullSucc(onPullResult func()) *PullSession { // WithOnReadRtmpAvMsg // // @param onReadRtmpAvMsg: -// msg: 关于内存块的说明: -// ReuseReadMessageBufferFlag 为true时: -// 回调结束后,`msg`的内存块会被`PullSession`重复使用。 -// 也即多次回调的`msg`是复用的同一块内存块。 -// 如果业务方需要在回调结束后,依然持有`msg`,那么需要对`msg`进行拷贝,比如调用`msg.Clone()`。 -// 只在回调中使用`msg`,则不需要拷贝。 -// ReuseReadMessageBufferFlag 为false时: -// 回调接收后,`PullSession`不再使用该内存块。 -// 业务方可以自由持有释放该内存块。 // +// msg: 关于内存块的说明: +// ReuseReadMessageBufferFlag 为true时: +// 回调结束后,`msg`的内存块会被`PullSession`重复使用。 +// 也即多次回调的`msg`是复用的同一块内存块。 +// 如果业务方需要在回调结束后,依然持有`msg`,那么需要对`msg`进行拷贝,比如调用`msg.Clone()`。 +// 只在回调中使用`msg`,则不需要拷贝。 +// ReuseReadMessageBufferFlag 为false时: +// 回调接收后,`PullSession`不再使用该内存块。 +// 业务方可以自由持有释放该内存块。 func (s *PullSession) WithOnReadRtmpAvMsg(onReadRtmpAvMsg OnReadRtmpAvMsg) *PullSession { s.core.onReadRtmpAvMsg = onReadRtmpAvMsg return s } // Pull 阻塞直到和对端完成拉流前的所有准备工作(也即收到RTMP Play response),或者发生错误 -// -// func (s *PullSession) Pull(rawUrl string) error { return s.core.Do(rawUrl) } @@ -101,13 +98,11 @@ func (s *PullSession) Pull(rawUrl string) error { // --------------------------------------------------------------------------------------------------------------------- // Dispose 文档请参考: IClientSessionLifecycle interface -// func (s *PullSession) Dispose() error { return s.core.Dispose() } // WaitChan 文档请参考: IClientSessionLifecycle interface -// func (s *PullSession) WaitChan() <-chan error { return s.core.WaitChan() } diff --git a/pkg/rtmp/client_push_session.go b/pkg/rtmp/client_push_session.go index 26a9e59..dbacf98 100644 --- a/pkg/rtmp/client_push_session.go +++ b/pkg/rtmp/client_push_session.go @@ -62,7 +62,6 @@ func (s *PushSession) Push(rawUrl string) error { // Write 发送数据 // // @param msg: 注意,`msg`数据应该是已经打包成rtmp chunk格式的数据。这里的数据就对应socket发送的数据,内部不会再修改数据内容。 -// func (s *PushSession) Write(msg []byte) error { // TODO(chef): [opt] 使用Write函数时确保metadata有@SetDataFrame 202207 @@ -80,13 +79,11 @@ func (s *PushSession) Flush() error { // --------------------------------------------------------------------------------------------------------------------- // Dispose 文档请参考: IClientSessionLifecycle interface -// func (s *PushSession) Dispose() error { return s.core.Dispose() } // WaitChan 文档请参考: IClientSessionLifecycle interface -// func (s *PushSession) WaitChan() <-chan error { return s.core.WaitChan() } diff --git a/pkg/rtmp/client_session.go b/pkg/rtmp/client_session.go index 12feb3f..43d52ba 100644 --- a/pkg/rtmp/client_session.go +++ b/pkg/rtmp/client_session.go @@ -172,13 +172,11 @@ func (s *ClientSession) Flush() error { // --------------------------------------------------------------------------------------------------------------------- // Dispose 文档请参考: IClientSessionLifecycle interface -// func (s *ClientSession) Dispose() error { return s.dispose(nil) } // WaitChan 文档请参考: IClientSessionLifecycle interface -// func (s *ClientSession) WaitChan() <-chan error { return s.conn.Done() } diff --git a/pkg/rtmp/message_packer.go b/pkg/rtmp/message_packer.go index 9b41081..d33e1b4 100644 --- a/pkg/rtmp/message_packer.go +++ b/pkg/rtmp/message_packer.go @@ -24,7 +24,6 @@ const ( ) // MessagePacker 打包并发送 rtmp 信令 -// type MessagePacker struct { b *Buffer } @@ -36,7 +35,6 @@ func NewMessagePacker() *MessagePacker { } // 注意,这个函数只会打包一个chunk头,所以调用方应自己保证在`bodyLen`小于chunk size时使用 -// func writeSingleChunkHeader(out []byte, csid int, bodyLen int, typeid uint8, streamid int) { // 目前这个函数只供发送信令时调用,信令的 csid 都是小于等于 63 的,如果传入的 csid 大于 63,直接 panic if csid > 63 { diff --git a/pkg/rtmp/metadata.go b/pkg/rtmp/metadata.go index 62c6a66..dd75588 100644 --- a/pkg/rtmp/metadata.go +++ b/pkg/rtmp/metadata.go @@ -41,7 +41,6 @@ func ParseMetadata(b []byte) (ObjectPairArray, error) { // 确保metadata中包含@setDataFrame // // @return 返回的内存块为内部独立申请 -// func MetadataEnsureWithSdf(b []byte) ([]byte, error) { var ret []byte v, _, err := Amf0.ReadString(b) @@ -69,7 +68,6 @@ func MetadataEnsureWithSdf(b []byte) ([]byte, error) { // 确保metadata中不包含@setDataFrame // // @return 返回的内存块为内部独立申请 -// func MetadataEnsureWithoutSdf(b []byte) ([]byte, error) { var ret []byte v, l, err := Amf0.ReadString(b) @@ -110,10 +108,14 @@ func MetadataEnsureWithoutSdf(b []byte) ([]byte, error) { // @param width 如果为-1,则metadata中不写入该字段 // @param height 如果为-1,则metadata中不写入该字段 // @param audiocodecid 如果为-1,则metadata中不写入该字段 -// AAC 10 +// +// AAC 10 +// // @param videocodecid 如果为-1,则metadata中不写入该字段 -// H264 7 -// H265 12 +// +// H264 7 +// H265 12 +// // @return 返回的内存块为新申请的独立内存块 func BuildMetadata(width int, height int, audiocodecid int, videocodecid int) ([]byte, error) { buf := &bytes.Buffer{} @@ -150,6 +152,10 @@ func BuildMetadata(width int, height int, audiocodecid int, videocodecid int) ([ Key: "version", Value: base.LalRtmpBuildMetadataEncoder, }) + opa = append(opa, ObjectPair{ + Key: "lal", + Value: base.LalVersionDot, + }) if err := Amf0.WriteObject(buf, opa); err != nil { return nil, err diff --git a/pkg/rtmp/metadata_test.go b/pkg/rtmp/metadata_test.go index 5445d1e..64f6f9c 100644 --- a/pkg/rtmp/metadata_test.go +++ b/pkg/rtmp/metadata_test.go @@ -10,6 +10,7 @@ package rtmp_test import ( "encoding/hex" + "strings" "testing" "github.com/q191201771/lal/pkg/base" @@ -19,21 +20,18 @@ import ( ) func TestMetadata(t *testing.T) { - cache := base.LalRtmpBuildMetadataEncoder - base.LalRtmpBuildMetadataEncoder = "lal0.30.1" - defer func() { - base.LalRtmpBuildMetadataEncoder = cache - }() - // ----- b, err := rtmp.BuildMetadata(1024, 768, 10, 7) assert.Equal(t, nil, err) - assert.Equal(t, "02000a6f6e4d6574614461746103000577696474680040900000000000000006686569676874004088000000000000000c617564696f636f6465636964004024000000000000000c766964656f636f646563696400401c000000000000000776657273696f6e0200096c616c302e33302e31000009", hex.EncodeToString(b)) + ver := hex.EncodeToString([]byte(base.LalVersionDot)) + expected := "02000a6f6e4d6574614461746103000577696474680040900000000000000006686569676874004088000000000000000c617564696f636f6465636964004024000000000000000c766964656f636f646563696400401c000000000000000776657273696f6e0200096c616c" + + ver + "00036c616c020006" + ver + "000009" + assert.Equal(t, expected, hex.EncodeToString(b)) // ----- opa, err := rtmp.ParseMetadata(b) assert.Equal(t, nil, err) - assert.Equal(t, 5, len(opa)) + assert.Equal(t, 6, len(opa)) v := opa.Find("width") assert.Equal(t, float64(1024), v.(float64)) v = opa.Find("height") @@ -44,14 +42,20 @@ func TestMetadata(t *testing.T) { assert.Equal(t, float64(7), v.(float64)) v = opa.Find("version") assert.Equal(t, base.LalRtmpBuildMetadataEncoder, v.(string)) + v = opa.Find("lal") + assert.Equal(t, base.LalVersionDot, v.(string)) // ----- wo, err := rtmp.MetadataEnsureWithoutSdf(b) assert.Equal(t, nil, err) assert.Equal(t, b, wo) + w, err := rtmp.MetadataEnsureWithSdf(b) assert.Equal(t, nil, err) - assert.Equal(t, "02000d40736574446174614672616d6502000a6f6e4d6574614461746103000577696474680040900000000000000006686569676874004088000000000000000c617564696f636f6465636964004024000000000000000c766964656f636f646563696400401c000000000000000776657273696f6e0200096c616c302e33302e31000009", hex.EncodeToString(w)) + exp2 := "02000d40736574446174614672616d6502000a6f6e4d6574614461746103000577696474680040900000000000000006686569676874004088000000000000000c617564696f636f6465636964004024000000000000000c766964656f636f646563696400401c000000000000000776657273696f6e0200096c616c302e33322e3000036c616c020006302e33322e30000009" + strings.Replace(exp2, "302e33322e3", ver, -1) + assert.Equal(t, exp2, hex.EncodeToString(w)) + wo, err = rtmp.MetadataEnsureWithoutSdf(b) assert.Equal(t, nil, err) assert.Equal(t, b, wo) diff --git a/pkg/rtmp/server.go b/pkg/rtmp/server.go index 75b3b49..50e21d6 100644 --- a/pkg/rtmp/server.go +++ b/pkg/rtmp/server.go @@ -9,8 +9,10 @@ package rtmp import ( - "github.com/q191201771/lal/pkg/base" + "crypto/tls" "net" + + "github.com/q191201771/lal/pkg/base" ) type IServerObserver interface { @@ -55,6 +57,21 @@ func (server *Server) Listen() (err error) { return } +func (server *Server) ListenWithTLS(certFile, keyFile string) (err error) { + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + Log.Errorf("start rtmps server listen failed. certFile=%s, keyFile=%s, err=%+v", certFile, keyFile, err) + return + } + tlsConfig := &tls.Config{Certificates: []tls.Certificate{cert}} + if server.ln, err = tls.Listen("tcp", server.addr, tlsConfig); err != nil { + Log.Errorf("start rtmps server listen failed. addr=%s, err=%+v", server.addr, err) + return + } + Log.Infof("start rtmps server listen. addr=%s", server.addr) + return +} + func (server *Server) RunLoop() error { for { conn, err := server.ln.Accept() diff --git a/pkg/rtprtcp/ntp.go b/pkg/rtprtcp/ntp.go index 9226446..c1ce712 100644 --- a/pkg/rtprtcp/ntp.go +++ b/pkg/rtprtcp/ntp.go @@ -9,13 +9,13 @@ package rtprtcp // (70 * 365 + 17) * 24 * 60 * 60 -const offset uint64 = 2208988800 +const ntpOffset uint64 = 2208988800 // Ntp2UnixNano 将ntp时间戳转换为Unix时间戳,Unix时间戳单位是纳秒 func Ntp2UnixNano(v uint64) uint64 { msw := v >> 32 lsw := v & 0xFFFFFFFF - return (msw-offset)*1e9 + (lsw*1e9)>>32 + return (msw-ntpOffset)*1e9 + (lsw*1e9)>>32 } // MswLsw2UnixNano 将ntp时间戳(高32位低32位分开的形式)转换为Unix时间戳 diff --git a/pkg/rtprtcp/rtcp_rr_producer.go b/pkg/rtprtcp/rtcp_rr_producer.go index 3764ef7..655885c 100644 --- a/pkg/rtprtcp/rtcp_rr_producer.go +++ b/pkg/rtprtcp/rtcp_rr_producer.go @@ -66,7 +66,6 @@ func (r *RrProducer) FeedRtpPacket(seq uint16) { // // @param lsr: 从sr包中获取,见func SR.GetMiddleNtp // @return: rr包的二进制数据 -// func (r *RrProducer) Produce(lsr uint32) []byte { if r.baseSeq == -1 { return nil diff --git a/pkg/rtprtcp/rtp.go b/pkg/rtprtcp/rtp.go index 146bd78..507b7f2 100644 --- a/pkg/rtprtcp/rtp.go +++ b/pkg/rtprtcp/rtp.go @@ -41,15 +41,17 @@ const ( ) // CompareSeq 比较序号的值,内部处理序号翻转问题,见单元测试中的例子 -// @return 0 a和b相等 -// 1 a大于b -// -1 a小于b +// +// @return +// - 0 a和b相等 +// - 1 a大于b +// - -1 a小于b func CompareSeq(a, b uint16) int { if a == b { return 0 } if a > b { - if a-b < 16384 { + if a-b < 32768 { return 1 } @@ -57,7 +59,7 @@ func CompareSeq(a, b uint16) int { } // must be a < b - if b-a < 16384 { + if b-a < 32768 { return -1 } diff --git a/pkg/rtprtcp/rtp_packer.go b/pkg/rtprtcp/rtp_packer.go index d857888..baf11a3 100644 --- a/pkg/rtprtcp/rtp_packer.go +++ b/pkg/rtprtcp/rtp_packer.go @@ -52,9 +52,12 @@ func NewRtpPacker(payloadPacker IRtpPackerPayload, clockRate int, ssrc uint32, m } } -// Pack @param pkt: pkt.Timestamp 绝对时间戳,单位毫秒 -// pkt.PayloadType rtp包头中的packet type +// Pack // +// @param pkt: +// +// - pkt.Timestamp 绝对时间戳,单位毫秒。 +// - pkt.PayloadType rtp包头中的packet type。 func (r *RtpPacker) Pack(pkt base.AvPacket) (out []RtpPacket) { payloads := r.payloadPacker.Pack(pkt.Payload, r.option.MaxPayloadSize) for i, payload := range payloads { diff --git a/pkg/rtprtcp/rtp_packer_payload_avc_hevc.go b/pkg/rtprtcp/rtp_packer_payload_avc_hevc.go index e9fa13a..163100a 100644 --- a/pkg/rtprtcp/rtp_packer_payload_avc_hevc.go +++ b/pkg/rtprtcp/rtp_packer_payload_avc_hevc.go @@ -59,7 +59,6 @@ func NewRtpPackerPayloadAvcHevc(payloadType base.AvPacketPt, modOptions ...ModRt // Pack @param in: AVCC格式 // // @return out: 内存块为独立新申请;函数返回后,内部不再持有该内存块 -// func (r *RtpPackerPayloadAvcHevc) Pack(in []byte, maxSize int) (out [][]byte) { if in == nil || maxSize <= 0 { return diff --git a/pkg/rtprtcp/rtp_packet.go b/pkg/rtprtcp/rtp_packet.go index 1966bc1..09bc328 100644 --- a/pkg/rtprtcp/rtp_packet.go +++ b/pkg/rtprtcp/rtp_packet.go @@ -58,7 +58,17 @@ type RtpHeader struct { Timestamp uint32 // 32b **** samples Ssrc uint32 // 32b **** Synchronization source - payloadOffset uint32 + Csrc []uint32 + + ExtensionProfile uint16 + + // Extensions 包含了整个extension,引用的是包体的内存 + // + // TODO(chef): [opt] 后续考虑解析extension中的单独个item存储至结构体中 202211 + Extensions []byte + + payloadOffset uint32 // body部分,真正数据部分的起始位置 + paddingLength int // 末尾padding的长度 } type RtpPacket struct { @@ -74,6 +84,8 @@ func (h *RtpHeader) PackTo(out []byte) { bele.BePutUint16(out[2:], h.Seq) bele.BePutUint32(out[4:], h.Timestamp) bele.BePutUint32(out[8:], h.Ssrc) + + // TODO(chef): pack csrc 202210 } func MakeDefaultRtpHeader() RtpHeader { @@ -96,8 +108,7 @@ func MakeRtpPacket(h RtpHeader, payload []byte) (pkt RtpPacket) { func ParseRtpHeader(b []byte) (h RtpHeader, err error) { if len(b) < RtpFixedHeaderLength { - err = base.ErrRtpRtcpShortBuffer - return + return h, base.ErrRtpRtcpShortBuffer } h.Version = b[0] >> 6 @@ -110,7 +121,44 @@ func ParseRtpHeader(b []byte) (h RtpHeader, err error) { h.Timestamp = bele.BeUint32(b[4:]) h.Ssrc = bele.BeUint32(b[8:]) - h.payloadOffset = RtpFixedHeaderLength + offset := RtpFixedHeaderLength + + if h.CsrcCount > 0 { + h.Csrc = make([]uint32, h.CsrcCount) + } + + for i := uint8(0); i < h.CsrcCount; i++ { + if offset+4 > len(b) { + return h, base.ErrRtpRtcpShortBuffer + } + + h.Csrc[i] = bele.BeUint32(b[offset:]) + offset += 4 + } + + if h.Extension != 0 { + if offset+4 > len(b) { + return h, base.ErrRtpRtcpShortBuffer + } + + // rfc3550#section-5.3.1 + h.ExtensionProfile = bele.BeUint16(b[offset:]) + offset += 2 + extensionLength := bele.BeUint16(b[offset:]) + offset += 2 + h.Extensions = b[offset : offset+int(extensionLength)] + + } + + if offset >= len(b) { + return h, base.ErrRtpRtcpShortBuffer + } + + h.payloadOffset = uint32(offset) + + if h.Padding == 1 { + h.paddingLength = int(b[len(b)-1]) + } return } @@ -130,11 +178,16 @@ func (p *RtpPacket) Body() []byte { Log.Warnf("CHEFNOTICEME. payloadOffset=%d", p.Header.payloadOffset) p.Header.payloadOffset = RtpFixedHeaderLength } + if p.Header.Padding == 1 { + return p.Raw[p.Header.payloadOffset : len(p.Raw)-p.Header.paddingLength] + } + return p.Raw[p.Header.payloadOffset:] } -// IsAvcHevcBoundary @param pt: 取值范围为AvPacketPtAvc或AvPacketPtHevc,否则直接返回false +// IsAvcHevcBoundary // +// @param pt: 取值范围为AvPacketPtAvc或AvPacketPtHevc,否则直接返回false func IsAvcHevcBoundary(pkt RtpPacket, pt base.AvPacketPt) bool { switch pt { case base.AvPacketPtAvc: @@ -152,6 +205,7 @@ func IsAvcBoundary(pkt RtpPacket) bool { avc.NaluTypeIdrSlice: {}, } + // TODO(chef): [fix] 检查数据长度有效性 202211 b := pkt.Body() outerNaluType := avc.ParseNaluType(b[0]) @@ -193,6 +247,7 @@ func IsHevcBoundary(pkt RtpPacket) bool { hevc.NaluTypeSliceRsvIrapVcl23: {}, } + // TODO(chef): [fix] 检查数据长度有效性 202211 b := pkt.Body() outerNaluType := hevc.ParseNaluType(b[0]) diff --git a/pkg/rtprtcp/rtp_packet_list.go b/pkg/rtprtcp/rtp_packet_list.go index 4583c15..faf4155 100644 --- a/pkg/rtprtcp/rtp_packet_list.go +++ b/pkg/rtprtcp/rtp_packet_list.go @@ -8,7 +8,10 @@ package rtprtcp -import "github.com/q191201771/naza/pkg/nazalog" +import ( + "fmt" + "github.com/q191201771/naza/pkg/nazabytes" +) type RtpPacketListItem struct { Packet RtpPacket @@ -21,7 +24,6 @@ type RtpPacketListItem struct { // 第一,容器有最大值,这个数量级用啥容器都差不多, // 第二,插入时,99.99%的seq号是当前最大号附近的,遍历找就可以了, // 注意,这个链表并不是一个定长容器,当数据有序时,容器内缓存的数据是一个帧的数据。 -// type RtpPacketList struct { // TODO(chef): [refactor] 隐藏这两个变量的访问权限 202207 Head RtpPacketListItem // 哨兵,自身不存放rtp包,第一个rtp包存在在head.next中 @@ -34,7 +36,6 @@ type RtpPacketList struct { } // IsStale 是否过期 -// func (l *RtpPacketList) IsStale(seq uint16) bool { if !l.doneSeqFlag { return false @@ -45,7 +46,6 @@ func (l *RtpPacketList) IsStale(seq uint16) bool { } // Insert 插入有序链表,并去重 -// func (l *RtpPacketList) Insert(pkt RtpPacket) { // 遍历查找插入位置 p := &l.Head @@ -79,7 +79,6 @@ func (l *RtpPacketList) Insert(pkt RtpPacket) { } // PopFirst 弹出第一个包。注意,调用方保证容器不为空时调用 -// func (l *RtpPacketList) PopFirst() RtpPacket { pkt := l.Head.Next.Packet l.Head.Next = l.Head.Next.Next @@ -88,26 +87,21 @@ func (l *RtpPacketList) PopFirst() RtpPacket { } // PeekFirst 查看第一个包。注意,调用方保证容器不为空时调用 -// func (l *RtpPacketList) PeekFirst() RtpPacket { return l.Head.Next.Packet } // InitMaxSize 设置容器最大容量 -// func (l *RtpPacketList) InitMaxSize(maxSize int) { l.maxSize = maxSize } // Full 是否已经满了 -// func (l *RtpPacketList) Full() bool { - nazalog.Debugf("%d %d", l.Size, l.maxSize) return l.Size >= l.maxSize } // IsFirstSequential 第一个包是否是需要的(与之前已处理的是连续的) -// func (l *RtpPacketList) IsFirstSequential() bool { first := l.Head.Next if first == nil { @@ -122,8 +116,26 @@ func (l *RtpPacketList) IsFirstSequential() bool { } // SetDoneSeq 设置已处理的包序号,比如已经成功合成了,或者主动丢弃到该位置结束丢弃了 -// func (l *RtpPacketList) SetDoneSeq(seq uint16) { l.doneSeqFlag = true l.doneSeq = seq } + +func (l *RtpPacketList) Reset() { + l.doneSeqFlag = false + l.doneSeq = 0 + l.Head.Next = nil +} + +func (l *RtpPacketList) DebugString() string { + p := l.Head.Next + buf := nazabytes.NewBuffer(65535) + buf.WriteString(fmt.Sprintf("size=%d, doneSeq=%d", l.Size, l.doneSeq)) + buf.WriteString(" [") + for p != nil { + buf.WriteString(fmt.Sprintf("%d ", p.Packet.Header.Seq)) + p = p.Next + } + buf.WriteString("]") + return buf.String() +} diff --git a/pkg/rtprtcp/rtp_test.go b/pkg/rtprtcp/rtp_test.go index c73c0dd..7175777 100644 --- a/pkg/rtprtcp/rtp_test.go +++ b/pkg/rtprtcp/rtp_test.go @@ -24,7 +24,6 @@ func TestCompareSeq(t *testing.T) { assert.Equal(t, 1, rtprtcp.CompareSeq(1, 0)) assert.Equal(t, 1, rtprtcp.CompareSeq(16383, 0)) - assert.Equal(t, -1, rtprtcp.CompareSeq(16384, 0)) assert.Equal(t, -1, rtprtcp.CompareSeq(65534, 0)) assert.Equal(t, -1, rtprtcp.CompareSeq(65535, 0)) assert.Equal(t, -1, rtprtcp.CompareSeq(65534, 1)) @@ -33,11 +32,14 @@ func TestCompareSeq(t *testing.T) { assert.Equal(t, -1, rtprtcp.CompareSeq(0, 1)) assert.Equal(t, -1, rtprtcp.CompareSeq(0, 16383)) - assert.Equal(t, 1, rtprtcp.CompareSeq(0, 16384)) assert.Equal(t, 1, rtprtcp.CompareSeq(0, 65534)) assert.Equal(t, 1, rtprtcp.CompareSeq(0, 65535)) assert.Equal(t, 1, rtprtcp.CompareSeq(1, 65534)) assert.Equal(t, 1, rtprtcp.CompareSeq(1, 65535)) + + assert.Equal(t, 1, rtprtcp.CompareSeq(16384, 0)) + assert.Equal(t, -1, rtprtcp.CompareSeq(0, 16384)) + assert.Equal(t, -1, rtprtcp.CompareSeq(10400, 33489)) } func TestSubSeq(t *testing.T) { diff --git a/pkg/rtprtcp/rtp_unpack_container.go b/pkg/rtprtcp/rtp_unpack_container.go index b21a5d8..9199295 100644 --- a/pkg/rtprtcp/rtp_unpack_container.go +++ b/pkg/rtprtcp/rtp_unpack_container.go @@ -70,7 +70,6 @@ func (r *RtpUnpackContainer) Feed(pkt RtpPacket) { } // tryUnpackOneSequential 从队列头部,尝试合成一个完整的帧。保证这次合成的帧的首个seq和上次合成帧的尾部seq是连续的 -// func (r *RtpUnpackContainer) tryUnpackOneSequential() bool { if !r.list.IsFirstSequential() { return false @@ -80,7 +79,6 @@ func (r *RtpUnpackContainer) tryUnpackOneSequential() bool { } // tryUnpackOne 从队列头部,尝试合成一个完整的帧。不保证这次合成的帧的首个seq和上次合成帧的尾部seq是连续的 -// func (r *RtpUnpackContainer) tryUnpackOne() bool { unpackedFlag, unpackedSeq := r.unpackerProtocol.TryUnpackOne(&r.list) if unpackedFlag { diff --git a/pkg/rtprtcp/rtp_unpacker.go b/pkg/rtprtcp/rtp_unpacker.go index 3a1aca2..590bddf 100644 --- a/pkg/rtprtcp/rtp_unpacker.go +++ b/pkg/rtprtcp/rtp_unpacker.go @@ -10,6 +10,7 @@ package rtprtcp import ( "github.com/q191201771/lal/pkg/base" + "github.com/q191201771/naza/pkg/nazalog" ) // 传入RTP包,合成帧数据,并回调返回 @@ -46,21 +47,30 @@ type IRtpUnpackerProtocol interface { TryUnpackOne(list *RtpPacketList) (unpackedFlag bool, unpackedSeq uint16) } -// OnAvPacket @param pkt: pkt.Timestamp RTP包头中的时间戳(pts)经过clockrate换算后的时间戳,单位毫秒 -// 注意,不支持带B帧的视频流,pts和dts永远相同 -// pkt.PayloadType base.AvPacketPTXXX -// pkt.Payload AAC: -// 返回的是raw frame,一个AvPacket只包含一帧 -// 引用的是接收到的RTP包中的内存块 -// AVC或HEVC: -// AVCC格式,每个NAL前包含4字节NAL的长度 -// 新申请的内存块,回调结束后,内部不再使用该内存块 -// 注意,这一层只做RTP包的合并,假如sps和pps是两个RTP single包,则合并结果为两个AvPacket, -// 假如sps和pps是一个stapA包,则合并结果为一个AvPacket +// OnAvPacket +// +// @param pkt: +// +// pkt.Timestamp: +// RTP包头中的时间戳(pts)经过clockrate换算后的时间戳,单位毫秒。 +// 注意,不支持带B帧的视频流,pts和dts永远相同。 +// +// pkt.PayloadType: base.AvPacketPTXXX。 +// +// pkt.Payload: +// AAC: +// 返回的是raw frame,一个AvPacket只包含一帧。 +// 引用的是接收到的RTP包中的内存块。 +// AVC或HEVC: +// AVCC格式,每个NAL前包含4字节NAL的长度。 +// 新申请的内存块,回调结束后,内部不再使用该内存块。 +// 注意,这一层只做RTP包的合并,假如sps和pps是两个RTP single包,则合并结果为两个AvPacket, +// 假如sps和pps是一个stapA包,则合并结果为一个AvPacket。 type OnAvPacket func(pkt base.AvPacket) // DefaultRtpUnpackerFactory 目前支持AVC,HEVC和AAC MPEG4-GENERIC,业务方也可以自己实现IRtpUnpackerProtocol,甚至是IRtpUnpackContainer func DefaultRtpUnpackerFactory(payloadType base.AvPacketPt, clockRate int, maxSize int, onAvPacket OnAvPacket) IRtpUnpacker { + nazalog.Debugf("DefaultRtpUnpackerFactory. type=%d, clockRate=%d, maxSize=%d", payloadType, clockRate, maxSize) var protocol IRtpUnpackerProtocol switch payloadType { case base.AvPacketPtAac: diff --git a/pkg/rtprtcp/rtp_unpacker_aac.go b/pkg/rtprtcp/rtp_unpacker_aac.go index 32bdcfc..860c0d1 100644 --- a/pkg/rtprtcp/rtp_unpacker_aac.go +++ b/pkg/rtprtcp/rtp_unpacker_aac.go @@ -65,8 +65,7 @@ func (unpacker *RtpUnpackerAac) TryUnpackOne(list *RtpPacketList) (unpackedFlag if p == nil { return false, 0 } - b := p.Packet.Raw[p.Packet.Header.payloadOffset:] - //Log.Debugf("%d, %d, %s", len(pkt.Raw), pkt.Header.timestamp, hex.Dump(b)) + b := p.Packet.Body() aus := parseAu(b) @@ -115,7 +114,7 @@ func (unpacker *RtpUnpackerAac) TryUnpackOne(list *RtpPacketList) (unpackedFlag } // 注意,非第一个fragment,也会包含au,au的size和第一个fragment里au的size应该相等 - b = p.Packet.Raw[p.Packet.Header.payloadOffset:] + b = p.Packet.Body() aus := parseAu(b) if len(aus) != 1 { Log.Errorf("shall be a single fragment. len(aus)=%d", len(aus)) diff --git a/pkg/rtprtcp/rtp_unpacker_avc_hevc.go b/pkg/rtprtcp/rtp_unpacker_avc_hevc.go index d4af5a8..04e143f 100644 --- a/pkg/rtprtcp/rtp_unpacker_avc_hevc.go +++ b/pkg/rtprtcp/rtp_unpacker_avc_hevc.go @@ -52,9 +52,9 @@ func (unpacker *RtpUnpackerAvcHevc) TryUnpackOne(list *RtpPacketList) (unpackedF pkt.PayloadType = unpacker.payloadType pkt.Timestamp = int64(first.Packet.Header.Timestamp / uint32(unpacker.clockRate/1000)) - pkt.Payload = make([]byte, len(first.Packet.Raw)-int(first.Packet.Header.payloadOffset)+4) - bele.BePutUint32(pkt.Payload, uint32(len(first.Packet.Raw))-first.Packet.Header.payloadOffset) - copy(pkt.Payload[4:], first.Packet.Raw[first.Packet.Header.payloadOffset:]) + pkt.Payload = make([]byte, len(first.Packet.Body())+4) + bele.BePutUint32(pkt.Payload, uint32(len(first.Packet.Body()))) + copy(pkt.Payload[4:], first.Packet.Body()) list.Head.Next = first.Next list.Size-- @@ -74,13 +74,16 @@ func (unpacker *RtpUnpackerAvcHevc) TryUnpackOne(list *RtpPacketList) (unpackedF pkt.Timestamp = int64(first.Packet.Header.Timestamp / uint32(unpacker.clockRate/1000)) // 跳过前面的字节,并且将多nalu前的2字节长度,替换成4字节长度 - buf := first.Packet.Raw[first.Packet.Header.payloadOffset+skip:] + // skip后: + // rtp中的数据格式 [<2字节的nalu长度>, , <2字节的nalu长度>, ...] + // 转变后的数据格式 [<4字节的nalu长度>, , <4字节的nalu长度>, ...] + buf := first.Packet.Body()[skip:] // 使用两次遍历,第一次遍历找出总大小,第二次逐个拷贝,目的是使得内存块一次就申请好,不用动态扩容造成额外性能开销 totalSize := 0 for i := 0; i != len(buf); { if len(buf)-i < 2 { - Log.Errorf("invalid STAP-A packet. len(buf)=%d, i=%d", len(buf), i) + Log.Errorf("[%p] invalid STAP-A packet. len(buf)=%d, i=%d", unpacker, len(buf), i) return false, 0 } naluSize := int(bele.BeUint16(buf[i:])) @@ -130,14 +133,14 @@ func (unpacker *RtpUnpackerAvcHevc) TryUnpackOne(list *RtpPacketList) (unpackedF naluTypeLen = 1 naluType = make([]byte, naluTypeLen) - fuIndicator := first.Packet.Raw[first.Packet.Header.payloadOffset] - fuHeader := first.Packet.Raw[first.Packet.Header.payloadOffset+1] + fuIndicator := first.Packet.Body()[0] + fuHeader := first.Packet.Body()[1] naluType[0] = (fuIndicator & 0xE0) | (fuHeader & 0x1F) } else { naluTypeLen = 2 naluType = make([]byte, naluTypeLen) - buf := first.Packet.Raw[first.Packet.Header.payloadOffset:] + buf := first.Packet.Body() fuType := buf[2] & 0x3f // ffmpeg rtpdec_hevc.c // 取buf[0]的头尾各1位 @@ -149,15 +152,20 @@ func (unpacker *RtpUnpackerAvcHevc) TryUnpackOne(list *RtpPacketList) (unpackedF totalSize := 0 pp := first for { - totalSize += len(pp.Packet.Raw) - int(pp.Packet.Header.payloadOffset) - (naluTypeLen + 1) + // naluTypeLen表示的是合帧之后的nalu type的长度。而+1,是在rtp时头的长度。 + // 比如h264,帧数据时naluTypeLen是1字节,rtp包时长度是2字节。 + totalSize += len(pp.Packet.Body()) - (naluTypeLen + 1) if pp == p { break } pp = pp.Next } - pkt.Payload = make([]byte, totalSize+4+naluTypeLen) + // 三部分组成: len + type + data + pkt.Payload = make([]byte, 4+naluTypeLen+totalSize) + // len bele.BePutUint32(pkt.Payload, uint32(totalSize+naluTypeLen)) + // type var index int if unpacker.payloadType == base.AvPacketPtAvc { pkt.Payload[4] = naluType[0] @@ -167,11 +175,12 @@ func (unpacker *RtpUnpackerAvcHevc) TryUnpackOne(list *RtpPacketList) (unpackedF pkt.Payload[5] = naluType[1] index = 6 } + // data packetCount := 0 pp = first for { - copy(pkt.Payload[index:], pp.Packet.Raw[int(pp.Packet.Header.payloadOffset)+(naluTypeLen+1):]) - index += len(pp.Packet.Raw) - int(pp.Packet.Header.payloadOffset) - (naluTypeLen + 1) + copy(pkt.Payload[index:], pp.Packet.Body()[naluTypeLen+1:]) + index += len(pp.Packet.Body()) - (naluTypeLen + 1) packetCount++ if pp == p { @@ -187,8 +196,8 @@ func (unpacker *RtpUnpackerAvcHevc) TryUnpackOne(list *RtpPacketList) (unpackedF return true, p.Packet.Header.Seq } else { // 不应该出现其他类型 - Log.Errorf("invalid position type. position=%d, first=(h=%+v, pos=%d), prev=(h=%+v, pos=%d), p=(h=%+v, pos=%d)", - p.Packet.positionType, first.Packet.Header, first.Packet.positionType, prev.Packet.Header, prev.Packet.positionType, p.Packet.Header, p.Packet.positionType) + Log.Errorf("[%p] invalid position type. position=%d, first=(h=%+v, pos=%d), prev=(h=%+v, pos=%d), p=(h=%+v, pos=%d)", + unpacker, p.Packet.positionType, first.Packet.Header, first.Packet.positionType, prev.Packet.Header, prev.Packet.positionType, p.Packet.Header, p.Packet.positionType) return false, 0 } } @@ -203,8 +212,9 @@ func (unpacker *RtpUnpackerAvcHevc) TryUnpackOne(list *RtpPacketList) (unpackedF return false, 0 } + func calcPositionIfNeededAvc(pkt *RtpPacket) { - b := pkt.Raw[pkt.Header.payloadOffset:] + b := pkt.Body() // rfc3984 5.3. NAL Unit Octet Usage // @@ -278,7 +288,7 @@ func calcPositionIfNeededAvc(pkt *RtpPacket) { } func calcPositionIfNeededHevc(pkt *RtpPacket) { - b := pkt.Raw[pkt.Header.payloadOffset:] + b := pkt.Body() // +---------------+---------------+ // |0|1|2|3|4|5|6|7|0|1|2|3|4|5|6|7| diff --git a/pkg/rtprtcp/rtp_unpacker_test.go b/pkg/rtprtcp/rtp_unpacker_test.go index 490024d..12cdf4e 100644 --- a/pkg/rtprtcp/rtp_unpacker_test.go +++ b/pkg/rtprtcp/rtp_unpacker_test.go @@ -10,6 +10,7 @@ package rtprtcp import ( "encoding/hex" + "github.com/q191201771/naza/pkg/nazalog" "testing" "github.com/q191201771/naza/pkg/bele" @@ -34,12 +35,12 @@ func TestAvcCase1(t *testing.T) { { Timestamp: 10340642, PayloadType: base.AvPacketPtAvc, - Payload: testHelperAddPrefixLength(rtpPackets[0].Raw[12:]), + Payload: testHelperAddPrefixLength(rtpPackets[0].Raw), }, { Timestamp: 10340642, PayloadType: base.AvPacketPtAvc, - Payload: testHelperAddPrefixLength(rtpPackets[1].Raw[12:]), + Payload: testHelperAddPrefixLength(rtpPackets[1].Raw), }, } }) @@ -62,22 +63,22 @@ func TestHevcCase1(t *testing.T) { { Timestamp: 25753900, PayloadType: base.AvPacketPtHevc, - Payload: testHelperAddPrefixLength(rtpPackets[0].Raw[12:]), + Payload: testHelperAddPrefixLength(rtpPackets[0].Raw), }, { Timestamp: 25753900, PayloadType: base.AvPacketPtHevc, - Payload: testHelperAddPrefixLength(rtpPackets[1].Raw[12:]), + Payload: testHelperAddPrefixLength(rtpPackets[1].Raw), }, { Timestamp: 25753900, PayloadType: base.AvPacketPtHevc, - Payload: testHelperAddPrefixLength(rtpPackets[2].Raw[12:]), + Payload: testHelperAddPrefixLength(rtpPackets[2].Raw), }, { Timestamp: 25753900, PayloadType: base.AvPacketPtHevc, - Payload: testHelperAddPrefixLength(rtpPackets[3].Raw[12:]), + Payload: testHelperAddPrefixLength(rtpPackets[3].Raw), }, } }) @@ -181,7 +182,6 @@ func TestAacCase2(t *testing.T) { // @param expectedFn: // []RtpPacket: `hexRtpPackets`解析成的rtp包数组 // []base.AvPacket: rtp包数组解析成的AvPacket数组 -// func testHelperTemplete(t *testing.T, payloadType base.AvPacketPt, clockRate int, maxSize int, hexRtpPackets []string, expectedFn func([]RtpPacket) []base.AvPacket) { rtpPackets, err := testHelperHexstream2rtppackets(hexRtpPackets) assert.Equal(t, nil, err) @@ -191,9 +191,11 @@ func testHelperTemplete(t *testing.T, payloadType base.AvPacketPt, clockRate int } func testHelperAddPrefixLength(in []byte) (out []byte) { - out = make([]byte, len(in)+4) - bele.BePutUint32(out, uint32(len(in))) - copy(out[4:], in) + pkt, err := ParseRtpPacket(in) + nazalog.Assert(nil, err) + out = make([]byte, len(pkt.Body())+4) + bele.BePutUint32(out, uint32(len(pkt.Body()))) + copy(out[4:], pkt.Body()) return } @@ -202,7 +204,7 @@ func testHelperAddPrefixLength(in []byte) (out []byte) { func testHelperUnpack(payloadType base.AvPacketPt, clockRate int, maxSize int, rtpPackets []RtpPacket) []base.AvPacket { var outPkts []base.AvPacket unpacker := DefaultRtpUnpackerFactory(payloadType, clockRate, maxSize, func(pkt base.AvPacket) { - Log.Debugf("%s", hex.EncodeToString(pkt.Payload)) + //Log.Debugf("%s", hex.EncodeToString(pkt.Payload)) outPkts = append(outPkts, pkt) }) for _, pkt := range rtpPackets { diff --git a/pkg/rtprtcp/rtprtcp_test.go b/pkg/rtprtcp/rtprtcp_test.go index bbb1c3c..88cb532 100644 --- a/pkg/rtprtcp/rtprtcp_test.go +++ b/pkg/rtprtcp/rtprtcp_test.go @@ -9,9 +9,8 @@ package rtprtcp_test import ( - "testing" - "github.com/q191201771/lal/pkg/innertest" + "testing" ) func TestRtpRtcp(t *testing.T) { diff --git a/pkg/rtsp/auth.go b/pkg/rtsp/auth.go index 5375832..d6602c0 100644 --- a/pkg/rtsp/auth.go +++ b/pkg/rtsp/auth.go @@ -12,9 +12,10 @@ import ( "crypto/rand" "encoding/base64" "fmt" - "github.com/q191201771/lal/pkg/base" "strings" + "github.com/q191201771/lal/pkg/base" + "github.com/q191201771/naza/pkg/nazamd5" ) @@ -41,12 +42,11 @@ type Auth struct { } // ParseAuthorization 解析字段,server side使用 -// func (a *Auth) ParseAuthorization(authStr string) (err error) { switch { case strings.HasPrefix(authStr, "Basic "): a.Typ = AuthTypeDigest - authBase64Str := strings.TrimLeft(authStr, "Basic ") + authBase64Str := strings.TrimPrefix(authStr, "Basic ") authInfo, err := base64.StdEncoding.DecodeString(authBase64Str) if err != nil { @@ -63,7 +63,7 @@ func (a *Auth) ParseAuthorization(authStr string) (err error) { case strings.HasPrefix(authStr, "Digest "): a.Typ = AuthTypeDigest - authDigestStr := strings.TrimLeft(authStr, "Digest ") + authDigestStr := strings.TrimPrefix(authStr, "Digest ") a.Username = a.getV(authDigestStr, `username="`) a.Realm = a.getV(authDigestStr, `realm="`) a.Nonce = a.getV(authDigestStr, `nonce="`) @@ -78,7 +78,6 @@ func (a *Auth) ParseAuthorization(authStr string) (err error) { } // FeedWwwAuthenticate 使用第一轮回复,client side使用 -// func (a *Auth) FeedWwwAuthenticate(auths []string, username, password string) { a.Username = username a.Password = password @@ -123,7 +122,6 @@ func (a *Auth) FeedWwwAuthenticate(auths []string, username, password string) { // MakeAuthorization 生成第二轮请求,client side使用 // // 如果没有调用`FeedWwwAuthenticate`初始化过,则直接返回空字符串 -// func (a *Auth) MakeAuthorization(method, uri string) string { if a.Username == "" { return "" @@ -143,7 +141,6 @@ func (a *Auth) MakeAuthorization(method, uri string) string { } // MakeAuthenticate 生成第一轮的回复,server side使用 -// func (a *Auth) MakeAuthenticate(method string) string { switch method { case AuthTypeBasic: @@ -155,7 +152,6 @@ func (a *Auth) MakeAuthenticate(method string) string { } // CheckAuthorization 验证第二轮请求,server side使用 -// func (a *Auth) CheckAuthorization(method, username, password string) bool { switch a.Typ { case AuthTypeBasic: diff --git a/pkg/rtsp/base_in_session.go b/pkg/rtsp/base_in_session.go index 58eedb0..700a993 100644 --- a/pkg/rtsp/base_in_session.go +++ b/pkg/rtsp/base_in_session.go @@ -31,7 +31,6 @@ import ( // BaseInSession会向上层回调两种格式的数据(本质上是一份数据,业务方可自由选择使用): // 1. 原始的rtp packet // 2. rtp合并后的av packet -// type IBaseInSessionObserver interface { OnSdp(sdpCtx sdp.LogicContext) @@ -133,7 +132,6 @@ func (session *BaseInSession) InitWithSdp(sdpCtx sdp.LogicContext) { } // SetObserver 如果没有设置回调监听对象,可以通过该函数设置,调用方保证调用该函数发生在调用InitWithSdp之后 -// func (session *BaseInSession) SetObserver(observer IBaseInSessionObserver) { session.observer = observer @@ -178,7 +176,6 @@ func (session *BaseInSession) SetupWithChannel(uri string, rtpChannel, rtcpChann // --------------------------------------------------------------------------------------------------------------------- // Dispose 文档请参考: IClientSessionLifecycle interface -// func (session *BaseInSession) Dispose() error { return session.dispose(nil) } @@ -186,7 +183,6 @@ func (session *BaseInSession) Dispose() error { // WaitChan 文档请参考: IClientSessionLifecycle interface // // 注意,目前只有一种情况,即上层主动调用Dispose函数,此时error为nil -// func (session *BaseInSession) WaitChan() <-chan error { return session.waitChan } diff --git a/pkg/rtsp/base_out_session.go b/pkg/rtsp/base_out_session.go index 084ce5b..b38390a 100644 --- a/pkg/rtsp/base_out_session.go +++ b/pkg/rtsp/base_out_session.go @@ -25,7 +25,6 @@ import ( ) // BaseOutSession out的含义是音视频由本端发送至对端 -// type BaseOutSession struct { cmdSession IInterleavedPacketWriter @@ -106,7 +105,6 @@ func (session *BaseOutSession) SetupWithChannel(uri string, rtpChannel, rtcpChan // --------------------------------------------------------------------------------------------------------------------- // Dispose 文档请参考: IClientSessionLifecycle interface -// func (session *BaseOutSession) Dispose() error { return session.dispose(nil) } @@ -114,7 +112,6 @@ func (session *BaseOutSession) Dispose() error { // WaitChan 文档请参考: IClientSessionLifecycle interface // // 注意,目前只有一种情况,即上层主动调用Dispose函数,此时error为nil -// func (session *BaseOutSession) WaitChan() <-chan error { return session.waitChan } diff --git a/pkg/rtsp/client_command_session.go b/pkg/rtsp/client_command_session.go index a6eb011..b2c432a 100644 --- a/pkg/rtsp/client_command_session.go +++ b/pkg/rtsp/client_command_session.go @@ -127,13 +127,11 @@ func (session *ClientCommandSession) Do(rawUrl string) error { // --------------------------------------------------------------------------------------------------------------------- // Dispose 文档请参考: IClientSessionLifecycle interface -// func (session *ClientCommandSession) Dispose() error { return session.dispose(nil) } // WaitChan 文档请参考: IClientSessionLifecycle interface -// func (session *ClientCommandSession) WaitChan() <-chan error { return session.conn.Done() } @@ -443,8 +441,26 @@ func (session *ClientCommandSession) writeOneSetup(setupUri string) error { session.sessionId = strings.Split(ctx.Headers.Get(HeaderSession), ";")[0] rRtpPort, rRtcpPort, err := parseServerPort(ctx.Headers.Get(HeaderTransport)) + var rtpRAddr, rtcpRAddr string if err != nil { - return err + // 增强兼容性逻辑 + // 有用户反馈,存在对端不返回server_port的情况,对端是easydrawin + // 其实在pull的情况下,没有对端端口也可以,因为不发数据,或者需要发送时,使用接收时获取到的对端地址即可 + + Log.Warnf("[%s] init conn, parseServerPort failed. lRtpPort=%d, lRtcpPort=%d", + session.uniqueKey, lRtpPort, lRtcpPort) + + switch session.t { + case CcstPullSession: + // noop + case CcstPushSession: + fallthrough + default: + return err + } + } else { + rtpRAddr = net.JoinHostPort(session.urlCtx.Host, fmt.Sprintf("%d", rRtpPort)) + rtcpRAddr = net.JoinHostPort(session.urlCtx.Host, fmt.Sprintf("%d", rRtcpPort)) } Log.Debugf("[%s] init conn. lRtpPort=%d, lRtcpPort=%d, rRtpPort=%d, rRtcpPort=%d", @@ -452,7 +468,7 @@ func (session *ClientCommandSession) writeOneSetup(setupUri string) error { rtpConn, err := nazanet.NewUdpConnection(func(option *nazanet.UdpConnectionOption) { option.Conn = rtpC - option.RAddr = net.JoinHostPort(session.urlCtx.Host, fmt.Sprintf("%d", rRtpPort)) + option.RAddr = rtpRAddr option.MaxReadPacketSize = rtprtcp.MaxRtpRtcpPacketSize }) if err != nil { @@ -461,7 +477,7 @@ func (session *ClientCommandSession) writeOneSetup(setupUri string) error { rtcpConn, err := nazanet.NewUdpConnection(func(option *nazanet.UdpConnectionOption) { option.Conn = rtcpC - option.RAddr = net.JoinHostPort(session.urlCtx.Host, fmt.Sprintf("%d", rRtcpPort)) + option.RAddr = rtcpRAddr option.MaxReadPacketSize = rtprtcp.MaxRtpRtcpPacketSize }) if err != nil { @@ -537,8 +553,8 @@ func (session *ClientCommandSession) writeCmd(method, uri string, headers map[st } req := PackRequest(method, uri, headers, body) - Log.Debugf("[%s] > write %s.", session.uniqueKey, method) - //Log.Debugf("[%s] > write %s. req=%s", session.uniqueKey, method, req) + //Log.Debugf("[%s] > write %s.", session.uniqueKey, method) + Log.Debugf("[%s] > write %s. req=%s", session.uniqueKey, method, req) _, err := session.conn.Write([]byte(req)) return err } diff --git a/pkg/rtsp/client_pull_session.go b/pkg/rtsp/client_pull_session.go index a03450d..6e67d4e 100644 --- a/pkg/rtsp/client_pull_session.go +++ b/pkg/rtsp/client_pull_session.go @@ -75,7 +75,6 @@ func (session *PullSession) WithOnDescribeResponse(onDescribeResponse func()) *P } // Pull 阻塞直到和对端完成拉流前,握手部分的工作(也即收到RTSP Play response),或者发生错误 -// func (session *PullSession) Pull(rawUrl string) error { Log.Debugf("[%s] pull. url=%s", session.UniqueKey(), rawUrl) if err := session.cmdSession.Do(rawUrl); err != nil { @@ -137,13 +136,11 @@ func (session *PullSession) GetSdp() sdp.LogicContext { // --------------------------------------------------------------------------------------------------------------------- // Dispose 文档请参考: IClientSessionLifecycle interface -// func (session *PullSession) Dispose() error { return session.dispose(nil) } // WaitChan 文档请参考: IClientSessionLifecycle interface -// func (session *PullSession) WaitChan() <-chan error { return session.waitChan } diff --git a/pkg/rtsp/client_push_session.go b/pkg/rtsp/client_push_session.go index 8f9cd4e..bcac8c6 100644 --- a/pkg/rtsp/client_push_session.go +++ b/pkg/rtsp/client_push_session.go @@ -59,7 +59,6 @@ func NewPushSession(modOptions ...ModPushSessionOption) *PushSession { } // Push 阻塞直到和对端完成推流前,握手部分的工作(也即收到RTSP Record response),或者发生错误 -// func (session *PushSession) Push(rawUrl string, sdpCtx sdp.LogicContext) error { Log.Debugf("[%s] push. url=%s", session.UniqueKey(), rawUrl) session.cmdSession.InitWithSdp(sdpCtx) @@ -120,13 +119,11 @@ func (session *PushSession) WriteRtpPacket(packet rtprtcp.RtpPacket) error { // --------------------------------------------------------------------------------------------------------------------- // Dispose 文档请参考: IClientSessionLifecycle interface -// func (session *PushSession) Dispose() error { return session.dispose(nil) } // WaitChan 文档请参考: IClientSessionLifecycle interface -// func (session *PushSession) WaitChan() <-chan error { return session.waitChan } diff --git a/pkg/rtsp/server.go b/pkg/rtsp/server.go index fa0a117..a235fb9 100644 --- a/pkg/rtsp/server.go +++ b/pkg/rtsp/server.go @@ -9,6 +9,7 @@ package rtsp import ( + "crypto/tls" "net" ) @@ -81,6 +82,20 @@ func (s *Server) Listen() (err error) { return } +func (s *Server) ListenWithTLS(certFile, keyFile string) (err error) { + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + Log.Errorf("start rtsps server listen failed. certFile=%s, keyFile=%s, err=%+v", certFile, keyFile, err) + return + } + tlsConfig := &tls.Config{Certificates: []tls.Certificate{cert}} + if s.ln, err = tls.Listen("tcp", s.addr, tlsConfig); err != nil { + return + } + Log.Infof("start rtsps server listen. addr=%s", s.addr) + return +} + func (s *Server) RunLoop() error { for { conn, err := s.ln.Accept() diff --git a/pkg/rtsp/server_command_session.go b/pkg/rtsp/server_command_session.go index 6bfaf55..f0e18bb 100644 --- a/pkg/rtsp/server_command_session.go +++ b/pkg/rtsp/server_command_session.go @@ -101,7 +101,6 @@ func (session *ServerCommandSession) FeedSdp(b []byte) { // WriteInterleavedPacket // // 使用RTSP TCP命令连接,向对端发送RTP数据 -// func (session *ServerCommandSession) WriteInterleavedPacket(packet []byte, channel int) error { _, err := session.conn.Write(packInterleaved(channel, packet)) return err diff --git a/pkg/rtsp/server_sub_session.go b/pkg/rtsp/server_sub_session.go index 56c710c..9c2c76e 100644 --- a/pkg/rtsp/server_sub_session.go +++ b/pkg/rtsp/server_sub_session.go @@ -38,13 +38,11 @@ func NewSubSession(urlCtx base.UrlContext, cmdSession *ServerCommandSession) *Su } // FeedSdp 供上层调用 -// func (session *SubSession) FeedSdp(sdpCtx sdp.LogicContext) { session.cmdSession.FeedSdp(sdpCtx.RawSdp) } // InitWithSdp 供 ServerCommandSession 调用 -// func (session *SubSession) InitWithSdp(sdpCtx sdp.LogicContext) { session.baseOutSession.InitWithSdp(sdpCtx) } diff --git a/pkg/sdp/avconfig.go b/pkg/sdp/avconfig.go index e4e9b86..f738e4f 100644 --- a/pkg/sdp/avconfig.go +++ b/pkg/sdp/avconfig.go @@ -19,9 +19,10 @@ import ( ) func ParseAsc(a *AFmtPBase) ([]byte, error) { - if a.Format != base.RtpPacketTypeAac { - return nil, nazaerrors.Wrap(base.ErrSdp) - } + // AAC的这个地方除了97,还遇到过104的,暂时不要这个判断了 + //if a.Format != base.RtpPacketTypeAac { + // return nil, nazaerrors.Wrap(base.ErrSdp) + //} v, ok := a.Parameters["config"] if !ok { @@ -65,7 +66,6 @@ func ParseVpsSpsPps(a *AFmtPBase) (vps, sps, pps []byte, err error) { // // 解析AVC/H264的sps,pps // 例子见单元测试 -// func ParseSpsPps(a *AFmtPBase) (sps, pps []byte, err error) { v, ok := a.Parameters["sprop-parameter-sets"] if !ok { diff --git a/pkg/sdp/parse_raw.go b/pkg/sdp/parse_raw.go index 7777d4c..c014422 100644 --- a/pkg/sdp/parse_raw.go +++ b/pkg/sdp/parse_raw.go @@ -48,7 +48,6 @@ type AControl struct { } // ParseSdp2RawContext 例子见单元测试 -// func ParseSdp2RawContext(b []byte) (RawContext, error) { lines := strings.Split(string(b), "\r\n") ctx, err := parseSdp2RawContext(lines) diff --git a/pkg/sdp/parse_test.go b/pkg/sdp/parse_test.go index 1b14c65..68813a9 100644 --- a/pkg/sdp/parse_test.go +++ b/pkg/sdp/parse_test.go @@ -135,9 +135,9 @@ func TestParseAsc(t *testing.T) { } // TODO chef 补充assert判断 -//[]byte{0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x3f, 0x95, 0x98, 0x09} -//[]byte{0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x3f, 0xa0, 0x05, 0x02, 0x01, 0x69, 0x65, 0x95, 0x9a, 0x49, 0x32, 0xbc, 0x04, 0x04, 0x00, 0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x3c, 0x20} -//[]byte{0x44, 0x01, 0xc1, 0x72, 0xb4, 0x62, 0x40} +// []byte{0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x3f, 0x95, 0x98, 0x09} +// []byte{0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x3f, 0xa0, 0x05, 0x02, 0x01, 0x69, 0x65, 0x95, 0x9a, 0x49, 0x32, 0xbc, 0x04, 0x04, 0x00, 0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x3c, 0x20} +// []byte{0x44, 0x01, 0xc1, 0x72, 0xb4, 0x62, 0x40} func TestParseVpsSpsPps(t *testing.T) { s := "a=fmtp:96 sprop-vps=QAEMAf//AWAAAAMAkAAAAwAAAwA/ugJA; sprop-sps=QgEBAWAAAAMAkAAAAwAAAwA/oAUCAXHy5bpKTC8BAQAAAwABAAADAA8I; sprop-pps=RAHAc8GJ" f, err := ParseAFmtPBase(s) @@ -597,3 +597,66 @@ a=control:streamid=1 assert.Equal(t, nil, err) _ = ctx } + +func TestCase15(t *testing.T) { + // TODO(chef): [fix] 有多路音频的情况 202209 + //v=0 + //o=- 2266397444 2266397444 IN IP4 0.0.0.0 + //s=Media Server + //c=IN IP4 0.0.0.0 + //t=0 0 + //a=control:* + // a=packetization-supported:DH + //a=rtppayload-supported:DH + //a=range:npt=now- + // a=x-packetization-supported:IV + //a=x-rtppayload-supported:IV + //m=video 0 RTP/AVP 96 + //a=control:trackID=0 + //a=framerate:25.000000 + //a=rtpmap:96 H264/90000 + //a=fmtp:96 packetization-mode=1;profile-level-id=64103C;sprop-parameter-sets=Z2QQPKwbGqAIAA4/lmyAAAADAIAAABlHhEI1AA==,aO4xshsA + //a=recvonly + //m=audio 0 RTP/AVP 8 + //a=control:trackID=1 + //a=rtpmap:8 PCMA/8000 + //a=recvonly + //m=audio 0 RTP/AVP 8 + //a=control:trackID=2 + //a=rtpmap:8 PCMA/8000 + //a=recvonly +} + +func TestCase16(t *testing.T) { + golden := `v=0 +o=- 1667405799319376 1667405799319376 IN IP4 192.168.1.64 +s=Media Presentation +e=NONE +b=AS:5100 +t=0 0 +a=control:rtsp://192.168.1.64/ +m=video 0 RTP/AVP 96 +c=IN IP4 0.0.0.0 +b=AS:5000 +a=recvonly +a=x-dimensions:1920,1080 +a=control:rtsp://192.168.1.64/trackID=1 +a=rtpmap:96 H264/90000 +a=fmtp:96 profile-level-id=420029; packetization-mode=1; sprop-parameter-sets=Z00AKY2NQDwBE/LNwEBAUAAAcIAAFfkAQA==,aO48gA== +m=audio 0 RTP/AVP 104 +c=IN IP4 0.0.0.0 +b=AS:50 +a=recvonly +a=control:rtsp://192.168.1.64/trackID=2 +a=rtpmap:104 mpeg4-generic/16000/1 +a=fmtp:104 profile-level-id=15; streamtype=5; mode=AAC-hbr; config=1408;SizeLength=13; IndexLength=3; IndexDeltaLength=3; Profile=1; +a=Media_header:MEDIAINFO=494D4B48010300000400000101200110803E0000007D000000000000000000000000000000000000; +a=appversion:1.0 +` + golden = strings.ReplaceAll(golden, "\n", "\r\n") + ctx, err := ParseSdp2LogicContext([]byte(golden)) + var avcCtx avc.Context + avc.ParseSps(ctx.Sps, &avcCtx) + assert.Equal(t, nil, err) + _ = ctx +} diff --git a/test.sh b/test.sh index 64f4229..98c5c49 100755 --- a/test.sh +++ b/test.sh @@ -2,11 +2,11 @@ #set -x -echo '-----add_go_license-----' -if command -v add_go_license >/dev/null 2>&1; then - add_go_license -d ./ -e 191201771@qq.com -n Chef +echo '-----add_src_license-----' +if command -v add_src_license >/dev/null 2>&1; then + add_src_license -d ./ -e 191201771@qq.com -n Chef else - echo 'CHEFNOTICEME add_go_license not exist!' + echo 'CHEFNOTICEME add_src_license not exist!' fi echo '-----gofmt-----' @@ -55,7 +55,7 @@ cp ./conf/cert.pem ./conf/key.pem ./testdata/conf/ ## 执行所有pkg里的单元测试,并生成测试覆盖文件 echo "" > coverage.txt -for d in $(go list ./... | grep -v vendor | grep pkg | grep -v innertest); do +for d in $(go list ./... | grep -v vendor | grep pkg); do go test -race -coverprofile=profile.out -covermode=atomic $d if [ -f profile.out ]; then cat profile.out >> coverage.txt