通过ZLMediaKit学习RTSP协议

发布时间 2023-12-08 13:48:32作者: 泽良_小涛

分析代码的准备

1.1.编译及运行

1.下载源码

代码从git获取,如果没安装git,需要执行

sudo apt-get install git

cd /opt

#拉取项目代码

git clone https://github.com/ZLMediaKit/ZLMediaKit.git

#国内用户推荐从同步镜像网站gitee下载

git clone --depth 1 https://gitee.com/xia-chu/ZLMediaKit

cd ZLMediaKit

#不要忘了这句命令

git submodule update --init

2..编译

完整的编译需要第三方库,可以查找相关内容。这里只是简单的修改代码后进行编译的步骤:

cd /opt/ZLMediaKit

mkdir build

cd build

cmake .. -DENABLE_WEBRTC=true -DOPENSSL_ROOT_DIR=/opt/openssl -DOPENSSL_LIBRARIES=/opt/openssl/lib

cmake --build . --target MediaServer

(简单的修改文件,只执行这一步。删除build文件夹,才执行上述的全部步骤。)

3.执行

cd /opt/ZLMediaKit/release/linux/Debug

./MediaServer

1.2.推送

1.ffmpeg的推送(以自己的机器为例)

e:

cd e:\Demo\CGAvioRead\Debug

d:

cd d:\CGAvioRead\Debug

ffmpeg -re -stream_loop -1 -i d:/H264_AAC_2021-02-10_1080P.mp4 -vcodec copy -acodec copy -f rtsp -rtsp_transport tcp rtsp://192.168.0.109:554/live/Camera_00001

ffmpeg -re -stream_loop -1 -i e:/H264_AAC_2021-02-10_1080P.mp4 -vcodec copy -acodec copy -f rtsp -rtsp_transport tcp rtsp://10.10.15.30:554/live/Camera_00001

2.OBS推流

可推流rtmp协议(部分版本支持RTSP协议),操作固定,可参考网上资料。

Wirehark截图

通过ffmpeg进行推流。

2.1.RTSP

(1)推流截图

(2)拉流截图

(3)回复截图

2.2.RTCP协议截图

2.3 RTP的截图

1.推流

2.拉流

RTSP协议的处理

RTSP发起/终结流媒体,和rtmp的命令类似。RtspSession::onRecv—》HttpRequestSplitter::input--》RtspSplitter::onRecvHeader—》RtspSession::onWholeRtspPacket

1.RtspSession::onWholeRtspPacket

  1. 如何区分RTSP协议与其他协议?

在RtspSplitter::onSearchPacketTail_l中,data[0] != '$'。

(2)与处理命令函数RtmpSession::onProcessCmd相似。

(3)Parser类为rtsp/http/sip解析类。在此将收到的内容解析以后存到此类中。

2. RtspSession::sendRtspResponse

(1)参数protocol在定义中默认,protocol = "RTSP/1.0"。

(2)先处理printer << protocol << " " << res_code << "\r\n";即截图的第一行。

(3)接着对key和value进行添加。

(4)添加换行:printer << "\r\n";。

(5)如查sdp不为空,添加sdp。

(6)对组成的数据直接进行发送。

3. RtspSession::handleReq_Options

未过多处理,直接将支持的命令通过sendRtspResponse回复给推流端和拉流端。

===========================================================

4. RtspSession::handleReq_ANNOUNCE

只在推流中会出现此命令,主要是对SDP进行处理。

(1)程序的流程,要明白函数的流程:

NOTICE_EMIT—》Broadcast::PublishAuthInvoker invoker(lamdba函数)—》auto onRes(lamdba函数)。

(2)full_url 是推流的全连接。如rtsp://192.168.169.197:554/live/Camera_00001

(3)_media_info主要存储连接信息:

 {_full_url = "rtsp://192.168.169.197:554/live/Camera_00001", _schema = "rtsp", _host = "192.168.169.197", _port = 554, _vhost = "__defaultVhost__", _app = "live", 

  _streamid = "Camera_00001", _param_strs = ""}

(3)SdpParser sdpParser(parser.Content());对sdp内容进行解析。--5

(4)_sessionid是一个1从字母和数字的数组中随机取出的12位数,不能保证完全不重复。

(5)_sdp_track为2。

(6)执行if (!_push_src),创建推流源。

(7)调用sendRtspResponse回复。

(8)NOTICE_EMIT—invoker—onRes--RtspMediaSourceImp::setSdp—3.1

 5. SdpParser::load(const string &sdp)

  1. RtspSession::handleReq_ANNOUNCE-SdpParser sdpParser(parser.content())中在构造函数中。
  2. Ffmpeg推流的一个截图。

https://img2022.cnblogs.com/blog/2525040/202207/2525040-20220719140449688-817977819.png(3)经过SdpParser解析以后,存到std::vector<SdpTrack::Ptr> _sdp_track中,打印变量为:

6. RtspSession::handleReq_Setup

(1) setup要接收两次,指明流媒体的传输方式 。

(2) parser.fullUrl(),如:tsp://10.10.15.30:554/live/Camera_00001/streamid=0,getTrackIndexByControlUrl是通过对比的方式得到SdpTrack的序号。

(3)strTransport,如:"RTP/AVP/TCP;unicast;interleaved=0-1;mode=record",所以rtpType = Rtsp::RTP_TCP;

(4)经解析后key_values的值为:["interleaved"] = "0-1", ["mode"] = "record", ["RTP/AVP/TCP"] = "", ["unicast"] = ""。

(5)解析出interleaved_rtp,interleaved_rtcp的值。

7. RtspSession::handleReq_RECORD

(1) _content_base,如:rtsp://10.10.15.30:554/live/Camera_00001

(2)track->getControlUrl,如:url=rtsp://10.10.15.30:554/live/Camera_00001/streamid=0,

(3)rtp_info.pop_back()去掉后的“,”

+=========================================================

8.RtspSession::handleReq_Describe

与RtspSession::handleReq_ANNOUNCE相似,拉流时发生。

  1. authorization(授权):值为空。
  2. strong_self->emitOnPlay();--9

9. RtspSession::emitOnPlay

(1)无需rtsp专属认证, 那么继续url通用鉴权认证(on_play)

(2) strong_self->onAuthSuccess();--10

10. RtspSession::onAuthSuccess()

(1) MediaSource::findAsync—11

(2)回调函数,对发现的MediaSource进行处理。-12

11. MediaSource::findAsync—》findAsync_l

(1)find_l找到源,对源进行回调处理

(2)find_l--13

12.回调函数的处理

(1)strong_self->_sdp_track的值如下图所示:

(2)为每个_sdp_track创建一个RtcpContextForSend。

(3)为拉流创建_sessionid。

(4)为拉流指定src.

(5) 为每个_sdp_track赋值。

(6)通过sendRtspResponse回复。

13. MediaSource::Ptr find_l

通过这行代码:MediaSource::for_each_media([&](const MediaSource::Ptr &src) { ret = std::move(const_cast<MediaSource::Ptr &>(src)); }, schema, vhost, app, id);找到MediaSource。

=========================================================+

14. RtspSession::handleReq_Play

(1)得到Play源,auto play_src = _play_src.lock();

(2)得到回复信息。

(3) _play_reader->setReadCB-- strong_self->sendRtpPacket,读取和发送。

(4) _RingReader—setDetachCB.

3.1.TRACK的创建

1. RtspMediaSourceImp::setSdp

(1)_demuxer->loadSdp(strSdp);--2

(2)RtspMediaSource::setSdp(strSdp);

不需要编解码: RtspMediaSource::setSdp(strSdp)—对sdp进行解析—注册事件(MediaSource::regist())

2. RtspDemuxer::loadSdp

(1) makeVideoTrack(track);--3

(2) makeAudioTrack(track);--3

(3)注册事件addTrackCompleted,track完成。

3. makeVideoTrack和makeAudioTrack

两者执行的类似,在Factory::getTrackBySdp函数中得到track。

(1)//生成Track对象 H264Track—4

(2) //生成Track对象 AACTrack—5

(3)生成H264RtpDecoder对象和生成AACRtpDecoder对象

只是创建了一个frame对象。

(4)_video_rtp_decoder->addDelegate--FrameWriterInterface* addDelegate

设置rtp解码器代理,生成的frame写入该Track

(5)addTrack--11

4. H264Track的创建

(1)得到sps、pps的frame信息。

(2)H264Track::H264Track--6

5. AACTrack的创建

(1)AACTrack::AACTrack(const string &aac_cfg)

(2)aac_cfg为两个字节,如:0x14, 0x08。

(3)onReady()--9

===========================================================

6. H264Track::H264Track

(1)得到sps、pps的字符信息。

(2)onReady—7

7. H264Track::onReady

getAVCInfo—8

8. getAVCInfo --H264.cpp

(1)h264DecSeqParameterSet-- SPSParser.c

得到SPS的信息

(2)h264GetWidthHeight:得到视频的宽和高。

(3)h264GeFramerate:iVideoFps,得到frame的rate。

9. AACTrack::onReady()

parseAacConfig(_cfg, _sampleRate, _channel);--10

10. parseAacConfig—ACC.cpp

执行else部分,得到samplerate: 16000和channels:1。

11. Demuxer::addTrack

(1)_sink为空。

(2)_listener不为空。

(3)MediaSink该类的作用是等待Track ready()返回true也就是就绪后再通知派生类进行下一步的操作,目的是输入Frame前由Track截取处理下,以便获取有效的信息(譬如sps pps aa_cfg)。

(4)通过代理进入MultiMediaSourceMuxer::onTrackFrame。

RTP协议的处理

RTP传输流媒体数据。

1. RtspSession::onRecv

(1)input—》HttpRequestSplitter::input—2

(2)上面函数的调用通过继承关系:

class RtspSession: public TcpSession, public RtspSplitter, public RtpReceiver , public MediaSourceEvent

{}

class RtspSplitter : public HttpRequestSplitter

{}

2. HttpRequestSplitter::input

(1)onRecvHeader-- RtspSplitter::onRecvHeader--3

(2)onRecvContent-- RtspSplitter::onRecvContent

3. RtspSplitter::onRecvHeader

onRtpPacket(data,len);-- RtspSession::onRtpPacket--4

4. RtspSession::onRtpPacket

通过data[1],即第二个字节(channel)为奇数和偶数来区别rtp和rtcp的处理。

  1. handleOneRtp—5

(uint8_t *) data + RtpPacket::kRtpTcpHeaderSize:传入参数时将RTSP去掉,前4个字节。

  1. onRtcpPacket—五

5. RtpMultiReceiver::handleOneRtp--RtpReceiver.h

(1)没有对数据进行改变,交由track处理。

(2)RtpTrackImp _track-- RtpTrackImp : public RtpTrack

(3)_track[index].inputRtp-- RtpTrack::inputRtp--6

6. RtpTrack::inputRtp

(1) 重新将数据封装成一个rtp包。

(2) onBeforeRtpSorted—五

(3)调用sortPacket排序 -- sortPacket(SEQ seq, T packet)(RtpReceiver.h)--7

(4) rtpMaxSize:10

(5)//设置ntp时间戳

rtp->ntp_stamp = _ntp_stamp.getNtpStamp(rtp->getStamp(), sample_rate);

7. PacketSortor—sortPacket(RtpReceiver.h)

(1) // 收到下一个seq

output(seq, std::move(packet));--8

(2) // 清空连续包列表

flushPacket();--9

8. PacketSortor—output

(1) _cb(seq, std::move(packet));

(2) track.setOnSorted([this, index](RtpPacket::Ptr rtp) {

onRtpSorted(std::move(rtp), index);

}-- class RtspSession : public RtpReceiver

(3) RtspSession::onRtpSorted--10

9. flushPacket

做一些清理工作,最终还是调用8。

10. RtspSession::onRtpSorted

_push_src->onWrite-- RtspMediaSourceImp::onWrite –开始分转码与未转码情况。

4.1未转码情况

与RTMP未转情况类似。

1. RtspMediaSourceImp::onWrite

RtspMediaSource::onWrite—2

2.RtspMediaSource::onWrite

主要代码:PacketCache<RtpPacket>::inputPacket。--3

3.PacketCache—inputPacket

(1) 追加数据到最后_cache->emplace_back(std::move(pkt));

(2)flush();--》onFlush

4. RtspMediaSource—onFlush

_ring->write

5. RingBuffer—write

_storage->write(std::move(in), is_key);

6. _RingStorage—write

_data_cache.back().emplace_back写入环形缓存数据。

4.2转码情况—解码

1.RtspMediaSourceImp::onWrite

key_pos = _demuxer->inputRtp(rtp)—2

2.RtspDemuxer::inputRtp

(1)_video_rtp_decoder->inputRtp—4.2.1--1

(2)_audio_rtp_decoder->inputRtp—4.2.2--1

3. RtspSession::handleReq_ANNOUNCE

NoticeCenter::Instance().emitEvent—invoker—onRes--RtspMediaSourceImp::setSdp

4.2.1视频

先要明白_video_rtp_decoder如何得到。

1. H264RtpDecoder::inputRtp--H264Rtp.cpp

decodeRtp(rtp)—2

2.H264RtpDecoder::decodeRtp

根据frame[0]的后5位将rtp分为三类:

(1)24:STAP-A,aggregation packet

unpackStapA—3

(2)28: FU-A, Fragmentation unit

mergeFu—4

(3)一个packet只有一个frame

singleFrame--5

3. H264RtpDecoder::unpackStapA

(1)对于这样的数据包:类型(1个字节)+frame 1 len(2个字节)+frame 1 data(前述两个字节表示的长度) +frame2 len(2个字节)+frame 2 data(前述两个字节表示的长度)+…..

(2)这种情况一般是sps、pps的合集。

(3)分拆完成以后singleFrame—5。

4. H264RtpDecoder::mergeFu

(1)如果是一个frame的开始,加上rpt over rtsp的4字节包头,组织FU indicator和 Fu header两个字节。

(2)两个字节的type是一样的。

(3)合成一个frame以后调用outputFrame(rtp, _frame);--6

5. H264RtpDecoder::singleFrame

(1)加上rpt over rtsp的4字节包头

(2)调用outputFrame(rtp, _frame);--6

6. H264RtpDecoder::outputFrame

(1)_dts的生成。

(2)RtpCodec::inputFrame(frame);--7

7. FrameDispatcher : : inputFrame

(1)RtspDemuxer::makeVideoTrack和RtmpDemuxer::makeAudioTrack中添加代理。

(2)最终执行H264Track::inputFrame。

8. H264Track::inputFrame

(1) inputFrame_l—9

对于b、p、IDR frme直接调用

(2)splitH264

/非I/B/P帧情况下,split一下,防止多个帧粘合在一起。然后调用inputFrame_l—9

9. H264Track::inputFrame_l

在这里会添加pps,sps信息(添加完pps,sps信息的h264数据就能直接用ffmpeg播放了),然后将数据发出去给各个代理。同时进入MultiMediaSourceMuxer::onTrackFrame函数的_ring->write部分。

4.2.2音频

1. AACRtpDecoder::inputRtp

(1)一个rtp包中包含多个frame。

(2) 首2字节表示Au-Header的个数,单位bit,所以除以16得到Au-Header个数。

(3) 之后的2字节是AU_HEADER,其中高13位表示一帧AAC负载的字节长度,低3位无用。

(4) 拆分每个frame.—flushData--2

(5)计算每个frame时间戳。

2. AACRtpDecoder::flushData()

(1) 插入adts头

(2)有adts头则插入adts头—3

3. FrameDispatcher : : inputFrame

(1)RtspDemuxer::makeVideoTrack和RtmpDemuxer::makeAudioTrack中添加代理。

(2)最终执行AACTrack::inputFrame。--4

4. AACTrack::inputFrame

(1) if (frame_len == (int)frame->size()) {

return inputFrame_l(frame);--5

}

(2)已经拆分过了,没调试到拆分的情况。

5. AACTrack::inputFrame_l

(1)makeAacConfig:根据7个字节的adts头生成aac config

(2)onReady:根据aac config生成_sampleRate, _channel。

(3)AudioTrack::inputFrame(frame)—》FrameDispatcher—inputFrame

根据代理进行分发。同时进入MultiMediaSourceMuxer::onTrackFrame函数的_ring->write部分。

4.3转码情况—编码

需要其他的协议,或不设置直接代理。

1. MultiMediaSourceMuxer::onTrackFrame--3

FrameDispatcher::inputFrame(Frame.h)--》FrameWriterInterfaceHelper::inputFrame(Frame.cpp:241)--》MediaSink::addTrack(设置的代理)—2

2. MediaSink::addTrack

RtspDemuxer::makeVideoTrack—》 Demuxer::addTrack—》 RtspMediaSourceImp::addTrack

—> MediaSink::addTrack

3. RtspMediaSourceMuxer—inputFrame

(1)第一个if没有执行。

(2) RtspMuxer::inputFrame—4

4. RtspMuxer::inputFrame

encoder->inputFrame根据编码器的不同,分为音频和视频。

4.3.1视频

1. H264RtpEncoder::inputFrame

(1)这里的frame是带00 00 00 01的。

(2) sps、pps直接取出。

(3)执行的是非低延迟模式。

(4)inputFrame_l—2

2. H264RtpEncoder::inputFrame_l

(1)insertConfigFrame--保证每一个关键帧前都有SPS与PPS

(2)packRtp—3

3. H264RtpEncoder::packRtp

(1)packRtpSmallFrame—>4

采用STAP-A/Single NAL unit packet per H.264 模式

(2) packRtpFu?5

4. H264RtpEncoder::packRtpSmallFrame

packRtpStapA--6

5. H264RtpEncoder::packRtpFu

(1)把每一帧分成多个rtp包。每个rtp包的长度为:1386

(2)将一个frame按1386进行拆分。

(3)创建一个没有有效负载的RTP包: makeRtp—>7

(4)FU-A 第1个字节末尾5bit为nalu type,固定为28(FU-A)

(5)FU-A 第2个字节为H264_TYPE。

https://img-blog.csdn.net/20180517164131977?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2dvX3N0cg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70

6.packRtpStapA

(1)创建一个没有有效负载的RTP包: makeRtp—>7

(2)在rt4 个字节的rtsp over tcp+12个字节rtp头+1个字节的类型+2个字节的长度

(3)RtpCodec::inputRtp(rtp, gop_pos);--》8

7. RtpInfo::makeRtp

(1) 4 个字节的rtsp over tcp 头。

(2)12个字节rtp头

(3)有效负载

8. RtpRing—inputRtp

写入到环形缓冲里。

4.3.2音频

1. AACRtpEncoder::inputFrame

(1) frame->prefixSize()为7,即ADTS的长度。

(2)max_size:为584和h264的不同。

(3)if语句执行剩余的长度不够max_size。

(4)执行过makeAACRtp以后的格式为:

4 个字节的rtsp over tcp 头+12个字节rtp头+1个字节(0)+1个字节(16)+2个字节(包含有长度)+有效负载。

(5)makeAACRtp—》2

2. AACRtpEncoder::makeAACRtp

RtpCodec::inputRtp—》3

3.RtpRing—inputRtp

写入到环形缓冲里。

RTCP协议的处理

RTCP对RTP进行控制,同步。

5.1接收

1. RtspSession::onRtpPacket

通道号为奇数时,调用onRtcpPacket—1

2. RtspSession::onRtcpPacket

(1)RtcpHeader::loadFromBytes—》RtcpHeader::net2Host? RtcpSR::net2Host得到ReportItem

(2)收到RTCP包的处理。设置rtp时间戳与ntp时间戳的对应关系。

(3)设置_rtcp_context[track_idx]->onRtcp(rtcp)的一些参数。

5.2发送

1.有两种情况发送

(1)RtspSession::onBeforeRtpSorted:和推送端的发送。--2

(2)RtspSession::sendRtpPacket:拉流端的发送。--2

2. RtspSession::updateRtcpContext

(1). send rtcp every 5 second

(2).发送SR rtcp包—》3

(3).发送RR rtcp包—》4

(4).发送SDEC—》5

每次发送完rtcp以后,要发送SDEC。

3. RtcpContextForSend::createRtcpSR

SR如下图所示

  1. 创建SR包,只赋值了RtcpHeader部分(网络字节序),ReportItem对象个数为0。—6
  2. 接下的代码是对rtcp的sender info部分赋值。
  3. 没有发送report block部分,即ReportItem。

4. RtcpContextForRecv::createRtcpRR

(1)创建RR包,RtcpRR::create(1)—》7

5. RtcpSdes::create

  1. 有一个SdesChunk,SdesType的值在哪里赋?
  2. setupHeader对header赋值。

6. RtcpSR::create

参数为0的情况下,调用setupHeader对rtcp的header部分进行赋值。参数为0,说明没有report block 。

7. RtcpRR::create

(1)参数为1,说明有一个report block(ReportItem)。

(2) 调用setupHeader,对header赋值。

六、部分细节的解释

6.1 SDP

1介绍

sdp,英文全称Session Description Protocol,会话描述协议,对应RFC2327,RTSP协议中使用sdp进行媒体信息的描述。语音通话SIP协议、监控安防GB28181国标、webRtc都用到了sdp。

sdp的目的就是在媒体会话中,传递媒体流信息,允许会话描述的接收者去参与会话,定义了会话描述的统一格式!

sdp信息由多行"<type>=<value>"组成,其中<type>是一个字符串,<value>是一个字符串,type表示类型,value的格式视type而定,整个协议区分大小写,"="两侧不允许有空格!

sdp会话描述包含一个会话级描述(session_level_description)和多个媒体级描述(media_level description)组成!会话级描述的作用域是整个会话,其位置从"v="行开始到第一个媒体描述为止;媒体级描述是对单个的媒体流进行描述,如传输过程中的视频流信息,从m=开始到下一个媒体描述为止。

会话级描述主要包含以下字段

https://img-blog.csdnimg.cn/img_convert/d6a2042d618ca371ee74b2a3942eaa95.png

媒体级描述主要包含以下字段

https://img-blog.csdnimg.cn/img_convert/0a7efef5dbee0227d643a91a01b9366c.png

2各字段描述 

version(必选)

格式:  v=<version> 

描述: 表示sdp的版本号,不包含次版本号

https://img-blog.csdnimg.cn/img_convert/d5cff77f223658874442f626fc78f312.png

origin(必选)

格式:o=<username> <sessionid> <version> <network type> <address type> <address>

描述:o=选项对会话的发起者进行了描述;

<username>:是用户的登录名, 如果主机不支持<username>,则用"-"代替,<username> 不能包含空格;

<session id>:是一个数字串,在整个会话中,必须是唯一的,建议使用个NTP 时间戳;

<version>: 该会话公告的版本,供公告代理服务器检测同一会话的如果多个公告哪个是最新公告,基本要求是会话数据修改后该版本值递增,建议使用NTP时间戳

<networktype>: 网络类型,一般为"IN",表示internet

<addresstype>: 地址类型,一般为IP4

<adress>:地址

Session Name(必选)

格式:s=

会话名称,在整个会话中有且只有1个"s="

Connection Data(可选)

格式: c=<networktype> <address type> <connection address>描述:表示媒体连接信息;一个会话级描述中必须有"c="或者在每个媒体级描述中有一个"c="选项,也可能在会话级描述和媒体级描述中都有"c="选项;

network type表示网络类型,一般为IN,表示internet;

address type,地址类型,一般为IP4;

connection address,地址,可能为域名或ip地址两种形式

https://img-blog.csdnimg.cn/img_convert/84c8a40a56be8f4f805eaa88acab55d5.png

Bandwidth(可选)

格式: b=<modifier>:<bandwidth-value>

描述:该选项描述了建议的带宽,单位 kbs/s,可选,modifier包括两种类型,CT和AS,CT表示总带宽,AS表示单个媒体带宽的最大值;bandwidth-value表示具体的带宽。

Times(必选)

格式:t=<start time> <stop time>

描述:t字段描述了会话的开始时间和结束时间,<start time> <stop time>为NTP时间,单位是秒;如果<stop time>为0表示过了<start time>之后,会话一直持续;当<start time> 和<stop time>都为0的时候,表示持久会话;建议两个值不设为0,如果设为0,不知道开始时间和结束时间,增大了调度的难度

来看一个实际的抓包文件:

start time和stop time均为0,表示一个持久的会话。

email(可选)

格式:e=<email address>

描述:用来描述邮件地址

phone number(可选)

格式:p=<phone number>

描述:比较简单,用来描述电话号码

URI(可选)

格式:u=<uri>

描述:类似于url的一个值,这里不过多介绍了

a=(*) (可选)

格式 :a=<*>

描述:表示一个会话级别或媒体级别下的0个或多个属性

会话级别中有一个属性a,a=control:rtsp://192.17.1.63:554,表示新增的属性的类型为control,值为rtsp://192.17.1.63:554

media information(必选)

格式:m=<media> <port> <transport type> <fmt list>

描述:

<media>表示媒体类型

有"audio","video","application","data"(不向用户显示的数据),"control"(描述额外的控制通道);

<port>表示媒体流发往传输层的端口,对于RTP,偶数端口用来传输数据,奇数端口用来传输RTCP;

<transport>表示传输协议,与"c="一行相关联,一般用RTP/AVP表示,即 Realtime Transport Protocol using the Audio/Video profile over udp,即我们常说的RTP over udp;

<fmt list>表示媒体格式,分为静态绑定和动态绑定

静态绑定:媒体编码方式与RTP负载类型有确定的一一对应关系,如: m=audio 0 RTP/AVP 8

动态绑定:媒体编码方式没有完全确定,需要使用rtpmap进行进一步的说明: 如:

m=video 0 RTP/AVP 96

a=rtpmap:96 H264/90000

rtpmap(可选)

格式:a=rtpmap:<payload typee> <encoding name>/<clock rate>

描述:

payload type表示动态负载类型,如 98表示h264

encoding name表示编码名称,如H.264

clock rate表示时钟频率,如90000

fmtp

定义指定格式的附加参数

a=fmtp:<payload type> <format specific parameters>

<payload type>:负载类型

<format specific parameters>:具体参数.

3实际举例

v=0

o=- 1586545639954157 1586545639954157 IN IP4 192.17.1.63

s=Media Presentation

e=NONE

b=AS:5100

t=0 0

a=control:rtsp://192.17.1.63:554/

m=video 0 RTP/AVP 96

c=IN IP4 0.0.0.0

b=AS:5000

a=recvonly

a=x-dimensions:1920,1080

a=control:rtsp://192.17.1.63:554/trackID=1

a=rtpmap:96 H264/90000

a=fmtp:96 profile-level-id=420029; packetization-mode=1; sprop-parameter-sets=Z01AKI2NQDwBE/LgLcBAQFAAAD6AAAw1DoYACYFAABfXgu8uNDAATAoAAL68F3lwoA==,aO44gA==

m=audio 0 RTP/AVP 8

c=IN IP4 0.0.0.0

b=AS:50

a=recvonlya=control:rtsp://192.17.1.63:554/trackID=2

a=rtpmap:8 PCMA/8000

a=Media_header:MEDIAINFO=494D4B48010300000400000111710110401F000000FA000000000000000000000000000000000000;

a=appversion:1.0

v=0

o=34020000001320000010 0 0 IN IP4 192.17.1.202

s=Play

c=IN IP4 192.17.1.202

t=0 0

m=video 5500 RTP/AVP 96 97 98

a=rtpmap:96 PS/90000

a=rtpmap:97 MPEG4/90000

a=rtpmap:98 H264/90000

a=recvonly

6.2.rtp包

6.2.1 RTP Header格式 https://img2020.cnblogs.com/blog/2293816/202112/2293816-20211228112407029-1154329894.png

V: 2bits,表示版本号,

P: 1bit,表示是否支持填充,置为1的时候,表示在packet的末尾进行填充,方便一些针对固定长度算法的封装

X: 1bit, 表示是否支持Rtp头扩展,置为1的时候,RtpHeader之后会跟1个header extension

CC(CSRC count): 4bits,表示头部之后contributing sources identifiers的个数

M: 1bit; 该值为1时,表示该数据包是一帧数据的最后一个数据包。

PT: 7bits,表示传输的多媒体类型(标识了RTP载荷的类型)。常用的的96表示h264,97 表示ACC。

sequence number:16bits(2字节),表示RTP包序号 。

timestamp:32bits(4字节),表示时间戳, 必须使用90 kHz 时钟频率

SSRC:32bits(4字节),用于标识同步信源,参加同一视频会议的两个同步信源不能有相同的SSRC.

CSRC:特约信源标识符,每个CSRC占用4个字节,可以有0~15个。每个CSRC标识了包含在该RTP报文有效载荷中的所有特约信源

6.2.2 RtpHeader的定义

Rtp包都包含什么,可看class RtpHeader的定义。

class RtpHeader {

……

// 版本号,固定为2

uint32_t version : 2;

// padding

uint32_t padding : 1;

// 扩展

uint32_t ext : 1;

// csrc

uint32_t csrc : 4;

// mark

uint32_t mark : 1;

// 负载类型

uint32_t pt : 7;

// 序列号

uint32_t seq : 16;

// 时间戳

uint32_t stamp;

// ssrc

uint32_t ssrc;

// 负载,如果有csrc和ext,前面为 4 * csrc + (4 + 4 * ext_len)

uint8_t payload;

6.3 adts头

参见:https://www.jianshu.com/p/af0165f923e9

1.ADTS全称是(Audio Data Transport Stream),是AAC的一种十分常见的传输格式。

2. 一般是在AAC ES流前添加7个字节的ADTS header。也就是说你可以吧ADTS这个头看作是AAC的frameheader。

3.ADTS内容及结构

ADTS 头中相对有用的信息:采样率、声道数、帧长度。一般情况下ADTS的头信息都是7个字节,分为2部分:
adts_fixed_header();

在这里插入图片描述

固定头信息中的数据每一帧都相同,而可变头信息则在帧与帧之间可变。

syncword :同步头代表着1个ADTS帧的开始,所有bit置1,即 0xFFF

ID:MPEG标识符,0标识MPEG-4,1标识MPEG-2

Layer: 直接置00

protection_absent:表示是否误码校验。1 no CRC , 0 has CRC

profile:AAC 编码级别, 0: Main Profile, 1:LC(最常用), 2: SSR, 3: reserved.

sampling_frequency_index:采样率标识

Private bit:直接置0,解码时忽略这个参数

channel_configuration: 声道数标识

original_copy: 直接置0,解码时忽略这个参数

home:直接置0,解码时忽略这个参数

采样率和通道数标识表:

https://img-blog.csdnimg.cn/ca09218ff448486ba39189b58bb38838.png#pic_center

adts_variable_header();

在这里插入图片描述

copyright_identification_bit: 直接置0,解码时忽略这个参数(版权—识别)

copyright_identification_start: 直接置0,解码时忽略这个参数

aac_frame_lenght: 当前音频帧的字节数,编码元数据字节数 + 文件头字节数(0 == protection_absent ? 7: 9)

adts_buffer_fullness: 当设置为0x7FF时表示时可变码率

number_of_raw_data_blocks_in_frames: 当前音频包里面包含的音频编码帧数, 置为 aac_nums - 1, 即只有一帧音频时置0

七、相关的C++语言

7.1 mutable 关键字

1. const 关键字用于类的成员函数,成为常成员函数,即:不允许在常成员函数的内部 (实现里) 修改数据成员的值。

2.mutable 关键字用于类的成员变量,即:允许在常成员函数的内部 (实现里) 修改成员变量的值。

3.例子:

(1)void A::get_a() const

{

a = 9; //a 被修饰为mutable类型,a在常成员函数里,能修改a的值(能修改该数据成员)

}

(2)mutable int a; //a 被 mutable 修饰

7.2 C++:匿名函数(Lambda函数)

参考: https://blog.csdn.net/asdasdde/article/details/116268964

1. 语法形式:

auto func = [capture] (params) opt -> ret { func_body; };

其中 func 是可以当作 lambda 表达式的名字,作为一个函数使用,capture 是捕获列表,params 是参数表,opt: 不需要可以省略。 (mutable: 可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)exception: 指定函数抛出的异常,如抛出整数类型的异常,可以使用 throw ()), ret 是返回值类型,func_body 是函数体。

捕获列表:

[] 不捕捉任何变量

[&] 捕获外部作用域中所有变量,并作为引用在函数体内使用 (按引用捕获)

[=] 捕获外部作用域所有变量,在函数内内有个副本使用,拷贝的副本在匿名函数体内部是只读的

[=,&foo] 按值捕获外部作用域中所有变量,并按照引用捕获外部变量 foo

[bar] 按值捕获 bar 变量,同时不捕获其他变量

[&bar] 按引用捕获 bar 变量,同时不捕获其他变量

[this] 捕获当前类中的 this 指针,让 lambda 表达式拥有和当前类成员函数同样的访问权限,如果已经使用了 & 或者 =, 默认添加此选项。

2.例子

(1)例1

#include <iostream>

#include <functional>

using namespace std;

class Test

{

public:

void output(int x, int y)

{

//auto x1 = [] {return m_number; }; // error

auto x2 = [=] {return m_number + x + y; }; // ok

auto x3 = [&] {return m_number + x + y; }; // ok

auto x4 = [this] {return m_number; }; // ok

//auto x5 = [this] {return m_number + x + y; }; // error

auto x6 = [this, x, y] {return m_number + x + y; }; // ok

auto x7 = [this] {return m_number++; }; // ok

}

int m_number = 100;

};

(2)例2

int main(void)

{

int a = 10, b = 20;

// auto f1 = [] {return a; }; // error

auto f2 = [&] {return a++; }; // ok

auto f3 = [=] {return a; }; // ok

// auto f4 = [=] {return a++; }; // error

// auto f5 = [a] {return a + b; }; // error

auto f6 = [a, &b] {return a + (b++); }; // ok

auto f7 = [=, &b] {return a + (b++); }; // ok

return 0;

}

(3)例3

int a = 0;

auto f1 = [=] {return a++; }; // error, 按值捕获外部变量, a是只读的

auto f2 = [=]()mutable {return a++; }; // ok

7.3 auto的用法

参考:https://blog.csdn.net/yl_puyu/article/details/88646962

  1. 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&。
  2. 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
  3. auto不能作为函数参数。
  4. auto不能直接用来声明数组。
  5. 对象属性不能用 auto 关键字定义。
  6. 不能用于定义非静态成员变量

class Base {

public :

static int x;

};

auto Base::x = 123;

7.4 using的三种用法

https://blog.csdn.net/shift_wwx/article/details/78742459

1. 命令空间的using声明

(1)using namespace std;

(2)using std::cin;

2. 在子类中引用基类成员

(1)私有成员不能用。

(2)using Base::value;

(3)using::test1只是声明,不需要形参指定,所以test1的两个重载版本在子类中都可使用。test1在基类中是函数,有不同的参数。

3. 使用using起别名

(1)typedef std::vector<int> intvec;

using intvec = std::vector<int>; //这两个写法是等价的

(2)typedef void (*FP) (int, const std::string&);

using FP = void (*) (int, const std::string&);

7.5 std::move

详见:https://www.cnblogs.com/shadow-lr/p/Introduce_Std-move.html

  1. std::move作用主要可以将一个左值转换成右值引用,从而可以调用C++11右值引用的拷贝构造函数

(2)// 例2:std::vector和std::string的实际例子

int main() {

std::string str1 = "aacasxs";

std::vector<std::string> vec;

vec.push_back(str1); // 传统方法,copy vec.pu

sh_back(std::move(str1)); // 调用移动语义的push_back方法,避免拷贝,str1会失去原有值,变成空字符串

vec.emplace_back(std::move(str1)); // emplace_back效果相同,str1会失去原有值

vec.emplace_back("axcsddcas"); // 当然可以直接接右值

}