Merge branch 'master' into feat/hls_subsession

pull/245/head
thewind296 2 years ago
commit 86c8e88d6c

@ -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类型等参数

@ -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

@ -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 {

@ -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流进行分析参见另外一个demoanalyseflv。 这个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
}

@ -17,7 +17,6 @@ import (
)
// StreamExist 检查远端rtmp流是否能正常拉取
//
func StreamExist(url string) error {
const (
timeoutMs = 10000

@ -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()
}

@ -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
}

@ -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,

@ -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,

@ -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

@ -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

@ -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=

@ -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 {

@ -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) {

@ -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 {

@ -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)

@ -54,11 +54,11 @@ func TryParsePps(payload []byte) error {
return nil
}
// TryParseSeqHeader 尝试解析SeqHeader所有字段实验中请勿直接使用该函数
//
// @param <payload> 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)

@ -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)))
}
// ---------------------------------------------------------------------------------------------------------------------

@ -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
}

@ -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")
}

@ -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
}

@ -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)

@ -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
}

@ -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 {

@ -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

@ -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...))
}

@ -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

@ -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)

@ -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"`

@ -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

@ -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:])
}

@ -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

@ -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 (

@ -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

@ -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 (

@ -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)
)

@ -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
}

@ -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)
}
}

@ -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
}

@ -64,7 +64,6 @@ const (
)
// IterateNaluAvcc 遍历Avcc格式的nalu流
//
func IterateNaluAvcc(nals []byte, handler func(nal []byte)) error {
return avc.IterateNaluAvcc(nals, handler)
}

@ -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中得到VPSSPSPPS内存块
// 从HVCC格式的Seq Header中得到VPSSPSPPS内存块
//
// @param <payload> 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))

@ -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 {

@ -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如果不为nilTS流将回调给上层
//
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

@ -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]
}

@ -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)
})
}
}

@ -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 不会再使用这块 <tag> 数据。
//
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()
}

@ -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 // 整体的基础时间戳。每轮最后更新

@ -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 {

@ -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

@ -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

@ -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
}

@ -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
}

@ -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 {

@ -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())

@ -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 {

@ -16,7 +16,6 @@ import (
)
// startRecordFlvIfNeeded 必要时开启flv录制
//
func (group *Group) startRecordFlvIfNeeded(nowUnix int64) {
if !group.config.RecordConfig.EnableFlv {
return

@ -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

@ -16,7 +16,6 @@ import (
)
// startRecordMpegtsIfNeeded 必要时开启ts录制
//
func (group *Group) startRecordMpegtsIfNeeded(nowUnix int64) {
if !group.config.RecordConfig.EnableMpegts {
return

@ -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 {

@ -64,7 +64,6 @@ func (group *Group) initRelayPushByConfig() {
}
// startPushIfNeeded 必要时进行replay push转推
//
func (group *Group) startPushIfNeeded() {
// push转推功能没开
if !group.pushEnable {

@ -20,7 +20,6 @@ type ModConfigGroupCreator func(appName, streamName string, baseConfig *Config)
//
// 封装管理Group的容器
// 管理流标识appNamestreamName与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作为流唯一标识比如rtmphttpflvhttpts
// 有的协议不需要appName只使用streamName作为流唯一标识比如rtsp
// - 有的协议需要结合appName和streamName作为流唯一标识比如rtmphttpflvhttpts
// - 有的协议不需要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加起来才是全量

@ -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 {

@ -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"),

@ -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()
}

@ -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()

@ -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也使用这个函数打包

@ -41,7 +41,6 @@ import (
// --------------
// CRC32 [32b] ****
// ----------------------------------------
//
type Pmt struct {
tid uint8
ssi uint8

@ -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:

@ -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,

@ -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)

@ -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)

@ -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)
}

@ -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)
}

@ -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))

@ -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

@ -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

@ -28,7 +28,6 @@ func RtmpMsg2FlvTag(msg base.RtmpMsg) *httpflv.Tag {
// -------------------------------------------------------------------------------------------------------------------
// LazyRtmpMsg2FlvTag 在必要时,有且仅有一次做转换操作
//
type LazyRtmpMsg2FlvTag struct {
msg base.RtmpMsg
//tagWithSdf []byte

@ -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
}

@ -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)

@ -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

@ -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

@ -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")
}
}
}

@ -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 返回的内存块由内部申请,不依赖参数<message>内存块
// Message2Chunks
//
// @return 返回的内存块由内部申请,不依赖参数<message>内存块
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]
}

@ -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()
}

@ -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()
}

@ -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()
}

@ -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 {

@ -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

@ -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)

@ -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()

@ -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时间戳

@ -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

@ -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
}

@ -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 {

@ -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

@ -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])

@ -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()
}

@ -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) {

@ -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 {

@ -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 目前支持AVCHEVC和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:

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save