通过ZLMediaKit学习RTMP协议
分析代码的准备
1.1编译及运行
1.编译
完整的编译需要第三方库,可以查找相关内容,这里只是简单的修改代码后进行编译的步骤:
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文件夹,才执行上述的全部步骤。)
2.执行
cd /opt/ZLMediaKit/release/linux/Debug
./MediaServer
1.2推送
1.ffmpeg的推送(以自己的机器为例)
e:
cd e:\Demo\CGAvioRead\Debug
ffmpeg -re -stream_loop -1 -i d:/H264_AAC_2021-02-10_1080P.mp4 -vcodec copy -acodec copy -f flv -y rtmp://10.10.15.30:1935/live/Camera_00002
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
ffmpeg -f dshow -i video="Lenovo EasyCamera" -vcodec libx264 -r 25 -preset:v ultrafast -tune:v zerolatency -f flv rtmp://10.10.15.30:1935/live/Camera_00002
2.OBS推流
可推流rtmp协议,操作固定,可参考网上资料。
1.3Wirehark截图
下面是ffmpeg推流的示例截图。
1.推流的截图
握手部分只有数据。消息命令部分和数据部分有RTMP header 和RTMP body 。
2.拉流的截图
3.video的截图
- FFmpeg不是这种情况。
- 拉流的数据在推流时并没有找到。
4. audio的截图
(1)推流时
(2)拉流时
- 拉流和推流并不是一一对应。
- 推流时有flv tag header,拉流时没有。
- FFmpeg不是这种情况。
二、RTMP协议总述
- 握手协议。
通过截图可以看出握手的顺序为:
- 客户端向服务端发送:C0+C1
- 服务端向客户端发送:S0+S1+S2
- 客户端向服务端发送:C2
其状态机可以简述为:
未初始化—》版本发送—》确认发送—》握手完成。
具体可参考官方文档。
- 消息命令。
和rtsp协议中的rtsp相似。
- 数据的处理。
- RTMP协议推流,RTMP协议拉流。相当于一个转发的功能。
- RTMP协议推流,其它协议拉流。RTMP部分要进行解封装和解码的处理。
- 其它协议推流,RTMP协议拉流。RTMP部分要进行封装和编码的处理。
- 下面对各部分分别进行描述。
握手协议的处理
RtmpSession::onRecv函数前面执行的函数不再描述,可以在gb下通过bt查看。下面描述中如果不特别说明,将顺序执行。
1. RtmpSession::onRecv
onParseRtmp
2.RtmpProtocol::onParseRtmp
input(data, size)
3. HttpRequestSplitter::input
(1) RtmpSession🡪 RtmpProtocol🡪 HttpRequestSplitter
(2) (index = onSearchPacketTail(ptr,_remain_data_size)) != nullptr)—》4
(3)_content_len = onRecvHeader(header_ptr, header_size)在握手协议时,没有rtmp header,所以这个函数没有具体的处理内容。
4. char *RtmpProtocol::onSearchPacketTail
(1)RtmpProtocol🡪 HttpRequestSplitter
(2)RtmpProtocol的构造函数:
_next_step_func = [this](const char *data, size_t len) {
return handle_C0C1(data, len);
};
(3)此函数:
//移动拷贝提高性能
auto next_step_func(std::move(_next_step_func));
//执行下一步
auto ret = next_step_func(data, len);
if (!_next_step_func) {
//为设置下一步,恢复之
next_step_func.swap(_next_step_func);
}
return ret;
(4)因此执行:handle_C0C1
5. RtmpProtocol::handle_C0C1
(1)if (len < 1 + C1_HANDSHARK_SIZE)
C0为1个字节。
C1为1536个字节。
(2)if (data[0] != HANDSHAKE_PLAINTEXT)
对C0的判断,为0x03
(3)if (memcmp(data + 5, "\x00\x00\x00\x00", 4) == 0)
- 条件成立为简单:obs推流、
- 条件不成立为复杂:vlc拉流、ffmpeg为复杂
handle_C1_complex(data);--7
6. RtmpProtocol::handle_C1_simple
(1)RtmpSession –onSendRawData
(2)S0与C0相同,一个字节的版本号。
(3)S1:4个字节的时间+4个字节的0+1528个随机数,由RtmpHandshake::RtmpHandshake及调用的函数实现。
(4)S2:包含C1。
(5)通过_next_step_func函数设置新的处理函数,再次执行1. RtmpSession::onRecv—》2.RtmpProtocol::onParseRtmp—》3.HttpRequestSplitter::input—》4. char *RtmpProtocol::onSearchPacketTail—》handle_C2
(6)random(1528 bytes):本字段包含对端发送过来的随机数据(对C2来说是S1,对S2来说是C1)。
7. RtmpProtocol::handle_C1_complex
(1)c1s1 schema0
time: 4bytes
version: 4bytes
key: 764bytes
digest: 764bytes
(2) schema1与 schema0中 的key与digest相反。
(3) 从get_C1_digest中可得到764bytes 个digest字节的结构
offset: 4bytes
random-data: (offset)bytes
digest-data: 32bytes
random-data: (764-4-offset-32)bytes
(4)得到digest和验证digest。
(5)调用send_complex_S0S1S2发送S0S1S2。
8.RtmpProtocol::send_complex_S0S1S2
(1)//发送S0
char handshake_head = HANDSHAKE_PLAINTEXT;//0x03\
(2) //发送S1
RtmpHandshake s1(0);//时间为0
memcpy(s1.zero, "\x04\x05\x00\x01", 4);为版本号
FFMPEG推流时schemeType为1。
get_C1_digest函数得到digest在类中的位置。
764bytes digest结构
offset: 4bytes
random-data: (offset)bytes
digest-data: 32bytes
random-data: (764-4-offset-32)bytes
通过openssl_HMACsha256生成32位的digest。
合成s1发送。
(3)//发送S2
前8位是随机生成的。
key是根据c1的digest生成。
digest根据key生成。
(4)等待C2
通过_next_step_func 函数,再次执行1. RtmpSession::onRecv—》2.RtmpProtocol::onParseRtmp—》3.HttpRequestSplitter::input—》4. char *RtmpProtocol::onSearchPacketTail—》handle_C2。
(5)random-data和digest-data都应来自对应的数据(对C2来说是S1,对S2来说是C1)。
9. RtmpProtocol::handle_C2
(1)只是对字节进行了判断。
(2)通过_next_step_func 函数,再次执行1. RtmpSession::onRecv—》2.RtmpProtocol::onParseRtmp—》3.HttpRequestSplitter::input—》4. char *RtmpProtocol::onSearchPacketTail—》handle_rtmp。
(3)调用了handle_rtmp对大于1536的消息进行处理。
至此,握手结束,进入命令模式。命令模式和数据都是分块的方式传输。
四、控制、聚合、数据消息的处理
RT M P保留消息类型ID(rtmp header 中的type id)在1-7之内的消息为协议控制消息。这些消息包含RTM P块流协议和RT M P协议本身要使用的信息。ID为1和2用于RT M P块流协议。ID在3-6之内用于RTM P本身。ID 7的消息用于边缘服务与源服务器。
协议控制消息必须有消息流ID 0和块流ID 2,并且有最高的发送优先级。
每个协议控制消息都有固定大小的负载。
1.RtmpProtocol::handle_rtmp
(1) 可由RtmpProtocol::handle_C2进入。
(2)通过_next_step_func 函数,再次执行3.1. RtmpSession::onRecv—》3.2.RtmpProtocol::onParseRtmp—》3.3.HttpRequestSplitter::input—》3.4. char *RtmpProtocol::onSearchPacketTail—》handle_rtmp。
(3)可以处理有多个RtmpHeader组成的data。RtmpHeader即为块头。
(4) 最终将chunk(长消息可以拆分成多个chunk传输) 组装成RtmpPacket传给handle_chunk处理—》2
(5)根据header->fmt得到header_len。
(6)chunk_id涉及多个字节,所以由switch处理。chunk_id表示chunk stream id,表示级别,似乎在程序中没有多大作用。
(7)_map_chunk_data在程序中没有实际意义,因为找不到存值的地方。
(8)switch (header_len)没有break,处理消息被分包的情况。其中type_id表示数据的类型,如AMF0、AMF3等。
(9)接下就是合成RtmpPacket的过程。
(10)通过chunk_data.time_stamp = time_stamp + (chunk_data.is_abs_stamp ? 0 : chunk_data.time_stamp);可以看出chunk header 中的time_stamp只是一个增量值 。
2.RtmpProtocol::handle_chunk
(1)处理一些特殊类型的命令消息,其余的交给onRtmpChunk—》RtmpSession::onRtmpChunk—》3
(2)MSG_ACK:Acknowledgement确认信息,只是判断RTMP body长度不能小于4,未进行过多处理。(type id :3)
(3)MSG_SET_CHUNK: chunk_size的长度,4个字节,在RtmpProtocol::handle_rtmp
函数中合成RtmpPacket时使用。
块大小的值被承载为4字节大小的负载。块大小有默认的值,但是如果发送者希望改变这个值,则用本消息通知对等端。例如,一个客户端想要发送131字节的数据,而块大小是默认的128字节。那么,客户端发送的消息要分成两个块发送。客户端可以选择改变块大小为131字节,这样消息就不用被分割为两个块。客户端必须向服务端发送本消息来通知对方块大小改为131字节。
最大块大小为65536字节。服务端向客户端通讯的块大小与客户端向服务端通讯的块大小互相独立。
(4)MSG_USER_CONTROL:User Control Messages。Body的前2个字节是事件的类型,后面的具体的内容,如CONTROL_STREAM_BEGIN是在前两个字节中取,stream_index是在其后的4个字节中取。
(5)MSG_WIN_SIZE:Window Acknowledgement Size。客户端或服务端发送本消息来通知对方发送确认(致谢)消息的窗口大小。例如,服务端希望每当发送的字节数等于窗口大小时从客户端收到确认(致谢)。服务端在成功处理了客户端的连接请求后向客户端更新窗口大小。
这个值应在32 * 1024U-1280 * 1024U之间。客户端收到大于这个字节的数据,要向服务器发送ACK消息。
(6) MSG_SET_PEER_BW: Set Peer Bandwidth。4个字节的字节的大小,1个字节的类型。
客户端或服务端发送本消息更新对等端的输出带宽。输出带宽值与窗口大小值相同。如果对等端在本消息中收到的值与窗口大小不相同,则发回确认(致谢)窗口大小消息。
发送者可以在限制类型字段把消息标记为硬(o)、软(1)或者动态(2)。如果是硬限制对等端必须按提供的带宽发送数据。如果是软限制,对等端可以灵活决定带宽,发送端可以限制带宽。如果是动态限制,带宽既可以是硬限制也可以是软限制。
(7)MSG_AGGREGATE:Aggregate Message。聚合消息是含有一个消息列表的一种消息。消息类型值22,保留用于聚合消息。
3. RtmpSession::onRtmpChunk
根据分为三种类型处理:
- MSG_CMD、MSG_CMD3由onProcessCmd处理—》五
- MSG_DATA、MSG_DATA3为数据信息。--》4
- MSG_AUDIO、MSG_VIDEO由—》六。
4.数据消息处理
客户端或服务端通过本消息向对方发送元数据和用户数据。元数据包括数据的创建时间、时长、主题等细节。消息类型为18的用AMF0编码,消息类型为15的用AMF3编码。
由setMetaData一步步解析MetaData内容,和rtsp中的SDP有点类似。
5. RtmpSession::setMetaData
(1) 来自于 if (type == "@setDataFrame") {
setMetaData(dec);
(2)解析的内容主要存入_push_metadata(AMFValue)变量中。
(3)在第一次接收到音视频消息时,调用 _push_src->setMetaData(_push_metadata ? _push_metadata : TitleMeta().getMetadata());--》6(和这个命令没有太大的关系,是接收音视时用到的)
6. RtmpMediaSourceImp::setMetaData
(1)解析AMFValue中的各个值。
(2)通过RtmpMediaSource::setMetaData(metadata);
7.RtmpMediaSource::setMetaData
没有进入if (_ring)这行代码。只取了对应的值。
五、命令消息的处理
1. RtmpSession::onCmd_connect
(一)文档解释及截图了
(1)命令结构
(2)wireshark截图
(二)文档所述流程
命令执行过程中的消息流是:
(1)客户端发送连接命令到服务端,请求与一个服务应用实例建立连接。
(2)接收到连接命令后,服务端发送”窗口确认(致谢)消息“到客户端。服务端同时连接到连接命令中提到的应用。
(3)服务端发送”设置带宽”协议消息到客户端。
(4)在处理完”设置带宽”消息后,客户端发送”窗口确认(致谢)大小”消息到服务端。
(5)服务端发送用户控制消息中的流开始消息到客户端。
(6)服务端发送结果命令消息通知客户端连接状态。该命令指定传输ID(对于连接命令总是1)。同时还指定一些属性,例如,Flash media server版本(字符串),能力(数字),以及其他的连接信息,例如,层(字符串),代码(字符串),描述(字符串),对象编码(数字)等。程序的执行有所不同,理解有待提高。
(二)程序对应实现的方式
(1)发送了三条消息
///////////set chunk size////////////////
sendChunkSize(60000);
////////////window Acknowledgement size/////
sendAcknowledgementSize(5000000);
///////////set peerBandwidth////////////////
sendPeerBandwidth(5000000);
但set chunk size不在文档的要求内。
(2)对客户端发来的属性,服务器取出:
- tc_url,如:rtmp://10.10.15.30:1935/live。
- app
(3) 回复了version、status多为一些固定值。
(4)发送onBWDone,值为0.0。
2. RtmpSession::onCmd_createStream
直接回复。流编号设为1 。
3. RtmpSession::onCmd_publish
- on_res是主体。
- _push_src为空,创建RtmpMediaSourceImp。
- 回复onStatus。
4. RtmpSession::onCmd_deleteStream
- RtmpMediaSourceImp清空,_push_src = nullptr;。
- 回复onStatus。与RtmpSession::onCmd_publish的值有所不同。
5. RtmpSession::onCmd_play、RtmpSession::onCmd_play2
(1)onCmd_play文档中的解释及流程图
客户端发送本命令到服务端播放一个流。使用本命令多次也可以创建一个播放列表。如果想创建一个可以在不同的直播流或录制流间切换的动态播放列表,可以多次使用播放命令,并且将重设设为假。相反,如果想立即播放一个流。清楚队列中正在等待的其它流,将重设设为真。
执行命令的消息流:
- 客户端从服端接收到流创建成功消息,发送播放命令到服务端。
- 接收到播放命令后,服务端发送协议消息设置块大小。
- 服务端发送另一个协议消息(用户控制消息),并且在消息中指定事件”streamisrecorded”和流ID。消息承载的头2个字,为事件类型,后4个字节为流ID。
- 服务端发送事件”streambegin”的协议消息(用户控制),告知客户端流ID.
- 服务端发送响应状态命令消息NetStream.Play.Start&NetStream.Play.reset,如果客户端发送的播放命令成功的话。只有当客户端发送的播放命令设置了reset命令的条件下,服务端才发送NetStream.Play.reset消息。如果要发送的流没有找的话,服务端发送NetStream.PIay.StreamNotFound消息。在此之后服务端发送客户端要播放的音频和视频数据。
(2)onCmd_play2文档中的解释及流程图
和播放命令不同,播放2命令可以切换到不同的码率,而不用改变已经播放的内容的时间线。服务端对播放2命令可以请求的多个码率维护多个文件。
(3)两者都用了RtmpSession::doPlay—》11
6. RtmpSession::onCmd_play2
调试执行的为RtmpSession::onCmd_play。
NoticeCenter::Instance().emitEvent—> Broadcast::AuthInvoker invoker🡪
RtmpSession::doPlayResponse🡪12
if (!flag)不执行
7. RtmpSession::onCmd_seek
(1)sendStatus回复状态。
(2)strong_src->seekTo
(3)通过继承关系调用MediaSource::seekTo
8. RtmpSession::onCmd_pause
与RtmpSession::onCmd_seek相似
9. RtmpSession::onCmd_playCtrl
与RtmpSession::onCmd_seek相似,控制播放速度的。VCL播放器不会执行到这里
===========================================================
10. RtmpProtocol::sendRtmp
(1)通过RtmpSession : public toolkit::Session, public RtmpProtocol继承关系,调用这个函数,然后调用同名函数。
(2)onSendRawData(std::move(buffer_header));//发送rtmp头
(3) onSendRawData发送真正的数据。
11. RtmpSession::doPlay
(1)RtmpSession::onCmd_publish类似,
(2)strong_self->doPlayResponse(err, [token](bool) {})—》调用RtmpSession::doPlayResponse
12. RtmpSession::doPlayResponse
strong_self->sendPlayResponse("", rtmp_src);--》RtmpSession::sendPlayResponse
13. RtmpSession::sendPlayResponse
主要回复一列消息:
(1)stream begin
(2) NetStream.Play.Reset
(3) NetStream.Play.Start
(4) sendResponse// RtmpSampleAccess(true, true)
(5) sendResponse// onStatus(NetStream.Data.Start)
(6) sendStatus// onStatus(NetStream.Play.PublishNotify)
(7) sendResponse(MSG_DATA, invoke.data());//发送onMetaData数据
(8) 发送config frame数据
// config frame
src->getConfigFrame([&](const RtmpPacket::Ptr &pkt) {
onSendMedia(pkt);
});
在RtmpPacket::isConfigFrame判断。
(9) _ring_reader->setReadCB—》 strong_self->onSendMedia(rtmp);发送存入的环形数据。
_ring_reader->setReadCB([weak_self](const Rtm)pMediaSource::RingDataType &pkt)—》strong_self->onSendMedia(rtmp)
14. RtmpSession::onSendMedia🡪 RtmpProtocol::sendRtmp
(1)通过RtmpSession : public toolkit::Session, public RtmpProtocol继承关系,调用RtmpProtocol::sendRtmp函数,然后调用同名函数。
(2)onSendRawData(std::move(buffer_header));//发送rtmp头
(3) onSendRawData发送真正的数据。
(4)sendAcknowledgement发送确认数据。
(5) 发送命令信息时,fmt为00。fmt = 3的几行代码没有用,命令消息较短,不用拆。
15. RtmpSession.h🡪onSendRawData
调用send发送数据。
六、音、视频消息的处理
不需要编解码
1.RtmpMediaSourceImp::onWrite
(1) 由四、3 RtmpSession::onRtmpChunk函数而来。
(2) RtmpMediaSource::onWrite(std::move(pkt))。
2. RtmpMediaSource::onWrite
主要代码:PacketCache<RtmpPacket>::inputPacket(stamp, is_video, std::move(pkt), key);
3.PacketCache—inputPacket
(1)追加数据到最后_cache->emplace_back(std::move(pkt));
(2)flush();--》onFlush
4. RtmpMediaSource—onFlush
5. RingBuffer—write
_storage->write(std::move(in), is_key);
6. _RingStorage—write
_data_cache.back().emplace_back写入环形缓存数据。
和五—13 --(9)接上。都为RingType类型。
需要解码
1.RtmpMediaSourceImp::onWrite
(1) 由四、3 RtmpSession::onRtmpChunk函数而来。
(2) _demuxer->inputRtmp(pkt);
2. RtmpDemuxer::inputRtmp
分成音频和视频分别处理
6.2.1视频
1. RtmpDemuxer::inputRtmp
(1)parseVideoRtmpPacket:第一次执行时,会执行这个函数。--》2
(2)Factory::getTrackByCodecId--》3
(3)makeVideoTrack—》4
(4)addTrackCompleted();--》5
上面的都没执行,已经在RtmpMediaSourceImp::setMetaData执行过了。
(5)_video_rtmp_decoder->inputRtmp—》6
得到编码方式,为解复用RtmpPacet准备。
2. parseVideoRtmpPacket
(1)首先用一个字节表示视频数据的header,高4位表示视频帧类型,低4位表示codecID。之后是压缩后的视频数据(压缩后的数据是使用FLV的标准进行封装的)。
(2)视频帧类型:当前主要用的无非就是1和2,即H264的关键帧和非关键帧,其他类型当下几乎见不到了。
(3)对于codecID,其实我们主要关注AVC即可,它代表的是H264编码。
(4)data[1]即紧接着的第二位,表示包的类型。
enum class RtmpH264PacketType : uint8_t {
h264_config_header = 0, // AVC or HEVC sequence header(sps/pps)
h264_nalu = 1, // AVC or HEVC NALU
h264_end_seq = 2, // AVC or HEVC end of sequence (lower level NALU sequence ender is not REQUIRED or supported)
};
(5)用obs推流观察会更明显一点。
- Factory::getTrackByCodecId
在此返回一个H264Track的视频通道。
- RtmpDemuxer::makeVideoTrack
- bit_rate:2560000
- H264RtmpEncoder对象以便解码rtmp
- _video_rtmp_decoder->addDelegate-- FrameWriterInterface* addDelegate(FrameWriterInterface::Ptr delegate)
当调用inputFrame时,会调用bool H264Track::inputFrame
(4) addTrack(_video_track);--7
5. Demuxer::addTrackCompleted
_listener->addTrackCompleted();--》RtmpMediaSourceImp::addTrackCompleted—》muxer->addTrackCompleted()—》MediaSink::addTrackCompleted()—》MediaSink::setMaxTrackCount—》MediaSink::checkTrackIfReady—》12
6. H264RtmpDecoder::inputRtmp
(1)RtmpDemuxer::makeVideoTrack中创建_video_rtmp_decoder = Factory::getRtmpCodecByTrack(_video_track, false);
(2)getH264Config—17
(3) onGetH264--18
7. Demuxer::addTrack
执行的为:if (!_sink) :
_listener->addTrack(track)-- RtmpMediaSourceImp::addTrack
8. RtmpMediaSourceImp::addTrack
(1) _muxer->addTrack(track)--9
(2) track->addDelegate(_muxer)--10
9. MediaSink::addTrack
(1) _track_ready_callback中添加的回调函数在MediaSink::checkTrackIfReady中调用。
(2)onTrackReady --MultiMediaSourceMuxer::onTrackReady
各个***MediaSourceMuxe—》addTrack。
(3)在解封装时不会执行RtmpMuxer::addTrack。
10.track->addDelegate
- track未就绪,缓存frame
- track就绪,则执行onTrackFrame🡪 MultiMediaSourceMuxer::onTrackFrame--11
11. MultiMediaSourceMuxer::onTrackFrame
(1)如果是rtmp推流,RtmpMediaSourceMuxer输入将不成立。是为了不重复生成rtmp协议。
(2)通过_ring->write写frame。
12. MediaSink::checkTrackIfReady
(1)执行GET_CONFIG(uint32_t, kMaxWaitReadyMS, General::kWaitTrackReadyMS)
kMaxWaitReadyMS :10000
(2)--》MediaSink::emitAllTrackReady🡪13
13. MediaSink::emitAllTrackReady
(1)MediaSink::inputFrame一次性把之前的帧输出
MediaSink::inputFrame(frame);--》MediaSink::emitAllTrackReady—》MediaSink::checkTrackIfReady—》MediaSink::inputFrame
(2)onAllTrackReady_l();--MediaSink::onAllTrackReady_l—》MultiMediaSourceMuxer::onAllTrackReady()🡪14
14. MultiMediaSourceMuxer::onAllTrackReady
(1) _rtmp为空,不考虑这个条件
(2)listener->onAllTrackReady();--RtmpMediaSourceImp::onAllTrackReady --15
(3)createGopCacheIfNeed();--16
(4)_stamp[TrackAudio].syncTo(_stamp[TrackVideo]):音频时间戳同步于视频,因为音频时间戳被修改后不影响播放
15.RtmpMediaSourceImp::onAllTrackReady()
只执行了这条语句。
16. MultiMediaSourceMuxer::createGopCacheIfNeed()
执行到strong_self->onReaderChanged(*src, strong_self->totalReaderCount()这一句。🡪
MediaSourceEventInterceptor::onReaderChanged--MediaSourceEvent::onReaderChanged(sender, size);-- MediaSourceEvent::onReaderChanged 处理是否有人观看。
===========================================================
17. bool getH264Config
RTMP body第一个字节是表明帧的类型(关键帧)。 对于sps和pps帧,是一次传过来的:
(1)1字节以后的10个字节不知道是什么意思。
(2)接着的两个字节是sps的长度,一般为28(0x1c).
(3) 接着的一个字节不知是什么意思
(4)接着的两个字节为pps的长度,一般为4.
(5) 用obs推流时,还有一个关键帧,长度为FF FF,不知什么原因。
18. H264RtmpDecoder::onGetH264
(1) 加dts、pts时间。
(2)添加264头
(3)RtmpCodec::inputFrame(_h264frame); 写入环形缓存—19。
19. FrameDispatcher : public FrameWriterInterface—inputFrame
(1) doStatistics(frame);对frame进行统计。
(2) _delegates的由来
FrameWriterInterface* addDelegate《--RtmpDemuxer::makeVideoTrack(RtmpDemuxer::makeAudioTrack)《--RtmpDemuxer::inputRtmp(RtmpDemuxer::loadMetaData)🡪1
(3)音频分发到AACTrack::inputFrame。
(4)视频H264Track::inputFrame。
20. H264Track::inputFrame
(1)H264_TYPE:后5个字节表示frame的类型。
(2)splitH264:需要拆分的帧—21
(3)H264Track::inputFrame_l--22
21. splitH264
有多个frame组成的拆开,通过回调函数有调用H264Track::inputFrame_l
22. H264Track::inputFrame_l
(1)insertConfigFrame判断是否是I帧, 并且如果是,那判断前面是否插入过config帧, 如果插入过就不插入了。
(2)onReady读取sps 、pps中的信息。具体不再分析,留作sps、pps时再作分析。
(3)VideoTrack::inputFrame---》 FrameDispatcher:: inputFrame(仿佛双回到19了)—》23
23. MediaSink::inputFrame
(1)FrameDispatcher:: inputFrame—》pr.second->inputFrame(frame)
(2) auto ret = it->second.first->inputFrame(frame);-- >H264Track::inputFrame
(3) checkTrackIfReady();🡪12-16
6.2.2音频
1.来源
(1)RtmpSession::onRtmpChunk--_push_src->setMetaData
!_demuxer->loadMetaData
RtmpDemuxer::loadMetaData—》2
(2)RtmpDemuxer::inputRtmp-- _audio_rtmp_decoder->inputRtmp—》4
2. RtmpDemuxer::loadMetaData
(1)在MetaData中各种参数。
(2)调用makeAudioTrack。传入的参数值: audiosamplerate🡪 16000, channels🡪1, sample_bit🡪16, bit_rate🡪 31744。🡪3
3. RtmpDemuxer::makeAudioTrack
(1) _audio_track: AACTrack
(2) _audio_rtmp_decoder: AACRtmpEncoder—AACRtmpDecoder
(3) //设置rtmp解码器代理,生成的frame写入该Track
_video_rtmp_decoder->addDelegate(_video_track);
FrameDispatcher🡪 FrameWriterInterface* addDelegate(FrameWriterInterface::Ptr delegate)
- addTrack(_audio_track);🡪 Demuxer::addTrack--5
4.AACRtmpDecoder::inputRtmp
(1)pkt->isConfigFrame()—6
(2)getConfig—7
(3)onGetAAC --8
5.同视频的7-16,主要完成对track的判断。在MultiMediaSourceMuxer::onTrackFrame
_ring->write写入到了环形数据。
6.RtmpPacket::isConfigFrame()
通过下图可知
第0个字节分别表示Format、Sample rate、Sample size和Channels,第1个字节表示RtmpAACPacketType,其定义如下:
enum class RtmpAACPacketType : uint8_t {
aac_config_header = 0, // AAC sequence header
aac_raw = 1, // AAC raw
};
当第一个字节为00时,执行—7
7. getConfig
得到从第2个字节开始的数据。
8. AACRtmpDecoder::onGetAAC
(1)传入的数据为rtmp body从第二个字节开始。
(2)dumpAacConfig--生成adts头—9
(3)frame带adts头及负载
(4)RtmpCodec::inputFrame—10
- Adts的头为7个字节
- Frame为audio data的值加上adts头即rtmp body去掉第一个值。
9. dumpAacConfig
执行的为else部分,填充下列这个结构,具体不再更深入分析。
struct mpeg4_aac_t
{
uint8_t profile; // 0-NULL, 1-AAC Main, 2-AAC LC, 2-AAC SSR, 3-AAC LTP
uint8_t sampling_frequency_index; // 0-96000, 1-88200, 2-64000, 3-48000, 4-44100, 5-32000, 6-24000, 7-22050, 8-16000, 9-12000, 10-11025, 11-8000, 12-7350, 13/14-reserved, 15-frequency is written explictly
uint8_t channel_configuration; // 0-AOT, 1-1channel,front-center, 2-2channels, front-left/right, 3-3channels: front center/left/right, 4-4channels: front-center/left/right, back-center, 5-5channels: front center/left/right, back-left/right, 6-6channels: front center/left/right, back left/right LFE-channel, 7-8channels
uint32_t extension_frequency; // play frequency(AAC-HE v1/v2 sbr/ps)
uint32_t sampling_frequency; // codec frequency, valid only in decode
uint8_t channels; // valid only in decode
int sbr; // sbr flag, valid only in decode
int ps; // ps flag, valid only in decode
uint8_t pce[64];
int npce; // pce bytes
};
10. FrameDispatcher—inputFrame
和视频的19类似,进入到AACTrack::inputFrame—11
11. AACTrack::inputFrame
(1)进入AACTrack::inputFrame_l--12
if (frame_len == (int)frame->size()) {
return inputFrame_l(frame);
}
(2)不会有sub inputFrame_l.h
(3) 时的frame长,是从adts的3、4、5字节中取的,和rtmp比较是去掉rtmp body前两个字节(control+类型)+7.
12. AACTrack::inputFrame_l
(1) _cfg取的为接着的两个字节。(11,90)
(2) AACTrack::onReady得到sampleRate, channel。
(3) AudioTrack::inputFrame(frame)🡪 FrameDispatcher—inputFrame🡪和视频相同(19)。
6.2.3流程图
6.3需要编码
FrameDispatcher-inputFrame:-》FrameWriterInterfaceHelper-inputFrame(回调)--》MediaSink::addTrack(track->addDelegate)--》MultiMediaSourceMuxer::onTrackFrame
1. MultiMediaSourceMuxer::onTrackFrame
ret = _rtmp->inputFrame(frame) ? true : ret;
2. RtmpMediaSourceMuxer::inputFrame
RtmpMuxer::inputFrame(frame)
3.RtmpMuxer::inputFrame(frame)
可分视频和音频
(1) Encoder—H264RtmpEncoder
encoder->inputFrame(frame)
(2)编码器分视频和音频。
6.3.1视频
1. H264RtmpEncoder::inputFrame
(1)frame->prefixSize() :4。
(2)H264_TYPE:取后5位。
(3)switch (type)没有执行。
(4)_rtmp_packet->buffer[0]:编码类型和帧类型的组合。
(5)_rtmp_packet->buffer[1]:表示包的类型
(6)_rtmp_packet->buffer[2]- _rtmp_packet->buffer[4]存cts,cts表示pts – dts之差。
(7) RtmpCodec::inputRtmp(_rtmp_packet);--2
2. RtmpRing—inputRtmp
(1) 通过RtmpCodec : public RtmpRing这个继承得来的。
(2) _ring->write(rtmp, rtmp->isVideoKeyFrame());--3
3. RingBuffer—write
(1·)if和for都可以执行。
(2)_delegate->onWriteRtmp--》MediaSource::onWrite—》PacketCache<RtmpPacket>::inputPacke—》PacketCache:inputPacket—》cache->emplace_back(std::move(pkt));追加到数据背后。
(3)for 语句_ RingReaderDispatcher—write:有拉流时执行。
(4)_storage->write写入环形数据。_RingStorage:write。
(5)三个写入有什么区别,还未完全懂得。
6.3.2音频
1. AACRtmpEncoder::inputFrame
(1)些时的_aac_cfg不为空。如调试时为0x14、0x08。
(2)RtmpCodec::inputRtmp(_rtmp_packet);就同视频了。
七、部分细节的解析
7.1 RTMP协议中部分名词的解释
- Payload:包含于一个数据包中的数据,例如音频和视频数据。
- Packet: 由一个固定头和有效载荷数据构成数据包。
- Port:端口。
- Transport address: 传输地址,如一个IP地址。
- Message stream: 消息流,通信中消息流通的一个逻辑通道。
- Message stream ID: 消息ID,每个消息有一个关联的 ID,使用 ID 可以识别出流通中的消息流。
- Chunk: 块,一段消息。消息在网络发送之前被拆分成很多小的chunk。
- Chunk stream: 块流。通信中允许块流向一个特定方向的逻辑通道。块流可以从客户端流向服务器,也可以从服务器流向客户端。
- Chunk stream ID: 每个块有一个关联的 ID,使用 ID 可以识别出流通中的块流。
- Multiplexing: 合成,将独立的音频/视频数据合成为一个连续的音频/视频流的加工,这样可以同时发送几个视频和音频。
- DeMultiplexing: 差分或者分解,Multiplexing 的逆向处理,将交叉的音频和视频数据还原成原始音频和视频数据的格式。
- Remote Procedure Call: RPC远程调用。
- Metadata:元数据,关于数据的一个描述。一个电影的 metadata 包括电影标题、持续时间、创建时间等等。
- Application Instance: 应用实例,服务器上应用的实例,客户端可以连接这个实例并发送连接请求。
- Action Message Format: AMF,动作消息格式协议。一个用于序列化 ActionScript 对象图的紧凑的二进制格式,类似于json这样的自描述数据结构,只不过AMF是二进制的,json是文本格式的。AMF 有两个版本:AMF0 [AMF0] 和 AMF3 [AMF3]。
7.2字节序、对齐和时间格式
所有整数都是以网络字节序来表示的(big-endian)。
在没有特殊说明的情况下,RTMP 中的数据都是字节对齐的。如果有填充的话,填充字节应该用 0。
RTMP 中的时间戳是用一个整数来表示的,代表相对于一个起始时间的毫秒数。通常每个流的时间戳都从 0 开始,但这不是必须的,只要通讯双方使用统一的起始时间就可以了。要注意的是,跨流的时间同步(不同主机之间)需要额外的机制来实现。
由于时间戳的长度只有 32 位,所以只能在 50 天内循环(49 天 17 小时 2 分钟 47.296 秒)。而流是可以不断运行的,可能多年才会结束。所以 RTMP 应用在处理时间戳是应该使用连续的数字算法,并且应该支持回环处理。例如:一个应用可以假设所有相邻的时间戳间隔不超过 2^31-1 毫秒,在此基础上,10000 在 4000000000 之后,3000000000 在 4000000000 之前。
时间戳增量也是以毫秒为单位的无符号整数。时间戳增量可能会是 24 位长度也可能是 32 位长度。
7.3块
RTMP协议会把一个大的数据,切分成小的chunk来发送,例如把NALU结构拆成小的chunk发送。标准文档里面说,拆成小的chunk可以不阻塞一些小的数据传输,例如声音,或者控制指令。
chunk 分片 的好处之一,把大数据分成小分,然后小数据就能在中间插进去。大数据不完整,就不能处理,小数据完整可以优先处理。
rtmp的协议的数据包,总的来讲分为两大部分,一部分是Rtmp Header(Chunk header),另一部分为Rtmp Body。
每个块包含一个头和数据体。块头包含三个部分:
- Basic Header
fmt(2)+cs id。Fmt(format)决定了Message Header的长度,由4种格式组成。cs _id(chunk stream id,表示消息的级别)可由:6(位)表示2-63;6+8(位)表示64-319,6(位)为0,8(位)为cs id -64即cs id=第二字节+64;6+8+8(位)表示64-65599,6(位)为1,8(位)+8(位)为cs id -64,即cs id -64。
- Message Header
除第一个字节外的,最完整的有11个字节,有4种类型。
- 时间戳三个字节。
- BodySize字段,表示RTMP Body所包含数据包的大小,,除去RTMP Header部分,后面的数据部分长。
- Type ID字段表示消息类型ID。比如此处0x14表示以AMF0编码(还有AMF3编码,Adobe定义的编码方式)。另外还有如0x04表示用户控制消息,0x05表示Window Acknowledgement Size,0x06表示Set Peer Bandwith等等,就不一一列举了。
- 还有最后一个Stream ID,Stream ID通常用以完成某些特定的工作,如使用ID为0的Stream来完成客户端和服务器的连接和控制,使用ID为1的Stream来完成视频流的控制和播放等工作。
(1)00(11个字节)
如图所示。在一个块流的开始和时间戳返回的时候必须有这种块。如果时间戳大于或等于16777215 (16进制0x00ffffff),该值必须为16777215,并且扩展时间戳必须出现。否则该值就是整个的时间戳。
(2)01(7个字节)
和图示相比较,少了4个字节的stream id,块的消息流ID与先前的块相同。具有可变大小消息的流,在第一个消息之后的每个消息的第一个块应该使用这个格式。
(3)10(3个字节)
只包含timestamp。 既不包含流ID也不包消息长度。本块使用的流ID和消息长度与先前的块相同。具有固定大小消息的流,在第一个消息之后的每个消息的第一个块应该使用这个格式。(从文档中6.2的第一个示例可以看出第二个消息使用这个方式)。
(4)11(0个字节)
类型3的块没有头。流ID、消息长度、时间戳都不出现。这种类型的块使用与先前块相同的数据。当一个消息被分成多个块,除了第一块以外,所有的块都应使用这种类型。
从文档中可以看出,时间戳一般为一个增量。实际应用中(2)7个字节比较少用,一
般由固定长度的消息(如音频)和 消息长的消息 (如视频)两类。
- 固定长度(多个消息):11字节+4个字节+n*0
- 长消息(1个消息拆分):11字节(长度为整个消息的长度)+n*0
八、C++语言的一些简单解析
只作简单的解析,具体的描述可以参考相关连接
8.1 =default和=delete
(1)default构造函数和析构函数采用默认的。
(2)delete禁止这个函数的实现。
具体:https://blog.csdn.net/weixin_38339025/article/details/89161324
8.2 function使用的例子
(1)#include <functional>
(2)std::function<const char * (const char *data, size_t len)> _next_step_func;
next_step_func = [this](const char *data, size_t len) {
return handle_C0C1(data, len);
(3)const char *RtmpProtocol::onSearchPacketTail(const char *data,size_t len){
//移动拷贝提高性能
auto next_step_func(std::move(_next_step_func));
//执行下一步
auto ret = next_step_func(data, len);
if (!_next_step_func) {
//为设置下一步,恢复之
next_step_func.swap(_next_step_func);
}
return ret;
}
(4)这是一个执行不同函数的例子。
Function的介绍:https://blog.csdn.net/ThorKing01/article/details/120911438
8.3C++中的dynamic_cast和dynamic_pointer_cast
dynamic_cast:
将一个基类对象指针(或引用)cast到继承类指针,dynamic_cast会根据基类指针是否真正指向继承类指针来做相应处理。
dynamic_pointer_cast:
用在智能指针时。
详细介绍:https://blog.csdn.net/jiayizhenzhenyijia/article/details/98209529
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!