目标:
RTMP是Adobe 公司为 Flash 播放器和服务器之间音视频数据传输开发的私有协议,因为出现的比较早,所以RTMP协议已经成为国内直播领域尤其是CDN之间推流的标准协议。
Adobe在2017年宣布到2020年底将不再支持Flash,所以很多系统平台的浏览器也都不再支持RTMP协议,如果流媒体服务器只支持RTMP协议,则最新的浏览器就无法通过无插件的方式从服务器获取媒体流,所以SRS服务器有个很重要的工作就是针对音视频数据的转码。
例如,HLS(HTTP Live Streaming)是一个被各种流媒体播放客户端广泛支持的标准协议,SRS流媒体服务器将RTMP推流端发送的音视频数据,转换为满足HLS协议要求的m3u8文件和ts文件,最终,浏览器通过HTTP协议从服务器获取m3u8文件和ts文件并实现本地播放。
本章将深入学习SRS4.0源代码中Origin模式下RTMP协议的解封装处理逻辑,为理解后续的各种转码逻辑打下基础。
内容:
SRS如果工作在Edge模式下,则属于纯转发,此时不会做HLS(ts、m3u8文件)转码处理,只有工作在Origin模式下的SRS服务器才会执行HLS(ts、m3u8文件)转码处理。所以从Origin模式的入口函数开始分析:
srs_error_t SrsOriginHub::on_video(SrsSharedPtrMessage* shared_video, bool is_sequence_header)
srs_error_t SrsOriginHub::on_audio(SrsSharedPtrMessage* shared_audio)
- 1、对于RTMP协议,视频数据总是以FLV格式封装成Message中的payload部分,所以,首先分析SRS代码中针对视频FLV格式报文的解析处理:
srs_error_t SrsOriginHub::on_video(SrsSharedPtrMessage* shared_video, bool is_sequence_header)
{
// 因为RTMP客户端发送的SPS+PPS报文,可能存在格式不统一,导致SRS解析失败
// 实际上SPS+PPS是编码器生成用于指导解码器解码的信息,对于流媒体服务器其实意义不大
// 更多的时候是服务器缓存后直接转发给播放器客户端
// 所以,SRS作者在配置文件中增加了一个配置项,用于禁用SPS解析,防止不必要的失败导致无法推流
if (is_sequence_header) {
format->avc_parse_sps = _srs_config->get_parse_sps(req->vhost);
}
// 我们知道,对于RTMP协议,视频数据总是以FLV格式封装成Message中的payload部分
// 所以SrsFormat::on_video()函数其实就是对FLV封装格式的H264报文进行格式化解析,后面详细分析
if ((err = format->on_video(msg)) != srs_success) {
return srs_error_wrap(err, "format consume video");
}
// 这里是为了过滤掉不支持的视频格式,例如H263
if (!format->vcodec) { return err; }
if (format->is_avc_sequence_header()) {
// 这里主要是为了把视频帧的SPS+PPS信息打印出来
}
// 这里的判断,是为了保证先收到SPS+PPS后,再处理普通视频帧
if (format->vcodec && !format->vcodec->is_avc_codec_ok()) { return err; }
// HLS协议转码及其错误处理
if ((err = hls->on_video(msg, format)) != srs_success) {
......
}
// DASH、DVR、HDS、FORWARDER处理,转码处理大同小异,暂不分析
dash->on_video(msg, format);
dvr->on_video(msg, format);
hds->on_video(msg);
forwarder->on_video(msg); // copy to all forwarders.
}
上面的函数基本包括了SRS源站在进行HLS转码之前针对Message的逻辑处理,其中的难点主要是SrsFormat::on_video()函数针对Message的解析处理。其实,如果知道了在RTMP协议中,音视频数据总是以FLV格式经行封装,就不难理解此函数内部的处理逻辑。
这里我们先回顾一下H264视频帧的FLV封装格式:
FLV VideoTagHeader 后续数据类型描述符 是SPS+PPS时,这里3字节为全零
+---------------------+ +---------------+ +------------------+
|Frame Type | CodecID | + | AVCPacketType | + | CompositionTime |
+---------------------+ +---------------+ +------------------+
4 bits 4 bits 1 byte 3 byte
视频帧类型 编码器ID 0:SPS+PPS;1:NALU cts字段,用于计算pts和dts
所以,SrsFormat::on_video()处理逻辑是:
- 读取一个字节的FLV videoTag,并根据高4bit判断帧类型,低4bit判断是否是H264编码类型
- 创建SrsVideoCodecConfig对象用于保存处理SPS+PPS
- 使用SrsFormat::video_avc_demux()处理H264视频帧
srs_error_t SrsFormat::on_video(int64_t timestamp, char* data, int size)
{
SrsBuffer* buffer = new SrsBuffer(data, size);
// 读取一个字节的FLV videoTag,并根据低4bit判断是否是H264编码类型,不是则直接返回
int8_t frame_type = buffer->read_1bytes();
SrsVideoCodecId codec_id = (SrsVideoCodecId)(frame_type & 0x0f);
if (codec_id != SrsVideoCodecIdAVC) { return err; }
// 创建处理SPS+PPS的SrsVideoCodecConfig对象,创建SrsVideoFrame对象
if (!vcodec) { vcodec = new SrsVideoCodecConfig(); }
if (!video) { video = new SrsVideoFrame(); }
// 初始化SrsVideoCodecConfig对象
if ((err = video->initialize(vcodec)) != srs_success) {
return srs_error_wrap(err, "init video");
}
// buffer回退一个字节,再调用video_avc_demux()函数重新解析FLV报文
buffer->skip(-1 * buffer->pos());
return video_avc_demux(buffer, timestamp);
}
上面的函数只是对报文做简单的H264类型判断,接下来SrsFormat::video_avc_demux()函数解析FLV封装信息
srs_error_t SrsFormat::video_avc_demux(SrsBuffer* stream, int64_t timestamp)
{
// 读取一个字节的FLV videoTag,并判断低4bit的编码类型和高4bit帧类型,不满足预期则直接返回
int8_t frame_type = stream->read_1bytes();
SrsVideoCodecId codec_id = (SrsVideoCodecId)(frame_type & 0x0f);
frame_type = (frame_type >> 4) & 0x0f;
// 如果帧类型是信息帧则直接丢弃不处理
video->frame_type = (SrsVideoAvcFrameType)frame_type;
if (video->frame_type == SrsVideoAvcFrameTypeVideoInfoFrame) { return err; }
if (codec_id != SrsVideoCodecIdAVC) { return err; }
// FLV videoTag后面,必须还有4个字节(AVCPacketType+CompositionTime),否则先返回,暂不处理
if (!stream->require(4)) { return err; }
// 获取1字节AVCPacketType和3字节CompositionTime(即cts)
int8_t avc_packet_type = stream->read_1bytes();
int32_t composition_time = stream->read_3bytes();
// 所谓cts其实是用于计算pts的,即 pts = dts + cts
// avc_packet_type=0表示报文是SPS+PPS,avc_packet_type=1表示报文是普通NALU
video->dts = timestamp;
video->cts = composition_time;
video->avc_packet_type = (SrsVideoAvcFrameTrait)avc_packet_type;
// 解析完FLV封装后,使用类成员 raw 和 nb_raw 记录纯数据和纯数据的长度
raw = stream->data() + stream->pos();
nb_raw = stream->size() - stream->pos();
// 使用avc_demux_sps_pps()函数处理SPS+PPS报文,video_nalu_demux()函数处理普通NALU报文
if (avc_packet_type == SrsVideoAvcFrameTraitSequenceHeader) {
avc_demux_sps_pps(stream);
} else if (avc_packet_type == SrsVideoAvcFrameTraitNALU){
video_nalu_demux(stream);
}
}
因为SPS+PPS是编码器自动生成用于指导解码器正确解码的信息,对于流媒体服务器更多的是存储和转发,所以暂时可以不用深入了解SrsFormat::avc_demux_sps_pps()函数的处理细节。
SrsFormat::video_nalu_demux()函数用于处理普通NALU报文,此函数的重点是,根据AnnexB或IBMF格式解析NALU
srs_error_t SrsFormat::video_nalu_demux(SrsBuffer* stream)
{
// 第一个报文进来,先进入此分支猜测NALU报文封装格式
if (vcodec->payload_format == SrsAvcPayloadFormatGuess)
{
if ((err = avc_demux_annexb_format(stream)) != srs_success) {
......
vcodec->payload_format = SrsAvcPayloadFormatIbmf;
} else {
vcodec->payload_format = SrsAvcPayloadFormatAnnexb;
}
}
else if (vcodec->payload_format == SrsAvcPayloadFormatIbmf)
{
if ((err = avc_demux_ibmf_format(stream)) != srs_success) {
......
}
}
else {
if ((err = avc_demux_annexb_format(stream)) != srs_success) {
......
}
}
return err;
}
如下,不管是哪种封装格式的NALU,最终都是调用SrsVideoFrame::add_sample()函数保存视频数据
srs_error_t SrsFormat::avc_demux_annexb_format(SrsBuffer* stream)
{
while (!stream->empty()) {
srs_avc_startswith_annexb(stream, &nb_start_code);
......
video->add_sample(p, (int)(pp - p));
}
}
srs_error_t SrsFormat::avc_demux_ibmf_format(SrsBuffer* stream)
{
for (int i = 0; i < PictureLength;) {
......
video->add_sample(stream->data() + stream->pos(), NALUnitLength);
}
}
- 2、接下来分析SRS针对RTMP音频报文FLV封装格式的解析处理:
srs_error_t SrsOriginHub::on_audio(SrsSharedPtrMessage* shared_audio)
{
// 使用SrsFormat::on_audio()解析音频报文,如果报文为无效类型,则直接返回
if ((err = format->on_audio(msg)) != srs_success) { }
if (!format->acodec) { return err; }
if (format->is_aac_sequence_header()) {
// 这里主要是为了将aac sequence header信息打印出来
}
// HLS协议转码及其错误处理
if ((err = hls->on_video(msg, format)) != srs_success) {
}
// DASH、DVR、HDS、FORWARDER处理,本章暂不分析
}
在了解SrsFormat::on_audio()函数之前,我们先回顾一下AAC音频帧的FLV封装格式:
FLV AudioTagHeader
++++++++++++++++++++++++++++++++++++++++++++++++++ +++++++++++++++
|SoundFormat | SoundRate | SoundSize | SoundType | OR + |AACPacketType|
++++++++++++++++++++++++++++++++++++++++++++++++++ +++++++++++++++
4bits 2bits 1bit 1bit 1 byte
编码格式 采样率 采样精度8或16位 声道数(单/双) 0:AAC sequence header;1:AAC音频数据
所以,SrsFormat::on_audio()处理逻辑是:
- 读取一个字节的FLV audioTag,并根据高4bit判断音频编码类型,暂时只支持AAC和MP3
- 创建SrsAudioCodecConfig对象用于保存处理AAC sequence header
- 根据音频编码类型,调用audio_aac_demux()或audio_mp3_demux()处理实际的音频数据。
srs_error_t SrsFormat::on_audio(int64_t timestamp, char* data, int size)
{
SrsBuffer* buffer = new SrsBuffer(data, size);
// 读取一个字节的FLV audioTag,并根据低4bit判断是否是AAC或MP3编码类型,不是则直接返回
uint8_t v = buffer->read_1bytes();
SrsAudioCodecId codec = (SrsAudioCodecId)((v >> 4) & 0x0f);
if (codec != SrsAudioCodecIdMP3 && codec != SrsAudioCodecIdAAC) { return err; }
// 创建处理AAC sequence header的SrsAudioCodecConfig对象,创建SrsAudioFrame对象
if (!acodec) { acodec = new SrsAudioCodecConfig(); }
if (!audio) { audio = new SrsAudioFrame(); }
if ((err = audio->initialize(acodec)) != srs_success) {
return srs_error_wrap(err, "init audio");
}
// buffer回退一个字节,再重新解析FLV报文
// 如果是MP3报文,调用audio_mp3_demux()函数,如果是AAC,则调用audio_aac_demux()函数
buffer->skip(-1 * buffer->pos());
if (codec == SrsAudioCodecIdMP3) { return audio_mp3_demux(buffer, timestamp); }
return audio_aac_demux(buffer, timestamp);
}
AAC音频处理:
srs_error_t SrsFormat::audio_aac_demux(SrsBuffer* stream, int64_t timestamp)
{
audio->cts = 0;
audio->dts = timestamp;
// 读取一个字节的FLV AudioTagHeader,并解析,
// 并用解析的结果初始化SrsFormat::SrsAudioCodecConfig对象
int8_t sound_format = stream->read_1bytes();
int8_t sound_type = sound_format & 0x01;
int8_t sound_size = (sound_format >> 1) & 0x01;
int8_t sound_rate = (sound_format >> 2) & 0x03;
sound_format = (sound_format >> 4) & 0x0f;
SrsAudioCodecId codec_id = (SrsAudioCodecId)sound_format;
acodec->id = codec_id;
acodec->sound_type = (SrsAudioChannels)sound_type;
acodec->sound_rate = (SrsAudioSampleRate)sound_rate;
acodec->sound_size = (SrsAudioSampleBits)sound_size;
// 再读取一个字节的AACPacketType
SrsAudioAacFrameTrait aac_packet_type = (SrsAudioAacFrameTrait)stream->read_1bytes();
audio->aac_packet_type = (SrsAudioAacFrameTrait)aac_packet_type;
raw = stream->data() + stream->pos();
nb_raw = stream->size() - stream->pos();
// 根据报文类型分别处理AAC sequence header 和 AAC纯数据
if (aac_packet_type == SrsAudioAacFrameTraitSequenceHeader)
{
acodec->aac_extra_data = std::vector<char>(copy_stream_from, copy_stream_from + aac_extra_size);
audio_aac_sequence_header_demux(&acodec->aac_extra_data[0], aac_extra_size);
}
else if (aac_packet_type == SrsAudioAacFrameTraitRawData)
{
if (!acodec->is_aac_codec_ok()) { return err; }
audio->add_sample(stream->data() + stream->pos(), stream->size() - stream->pos())
}
}
MP3音频处理:
srs_error_t SrsFormat::audio_mp3_demux(SrsBuffer* stream, int64_t timestamp)
{
// 处理逻辑大同小异,不再单独分析
// mp3 payload.
if ((err = audio->add_sample(data, size)) != srs_success) {
return srs_error_wrap(err, "add audio frame");
}
}
画重点: 上面所有针对音视频RTMP报文的解析处理都是为了去除FLV封装,得到纯H264和AAC数据后,最终,通过SrsFrame::add_sample()对纯的音视频数据进行保存,具体代码如下:
srs_error_t SrsFrame::add_sample(char* bytes, int size)
{
srs_error_t err = srs_success;
if (nb_samples >= SrsMaxNbSamples) {
return srs_error_new(ERROR_HLS_DECODE_ERROR, "Frame samples overflow");
}
SrsSample* sample = &samples[nb_samples++];
sample->bytes = bytes;
sample->size = size;
sample->bframe = false;
return err;
}
所以,每帧音视频数据,最终都是通过SrsFrame::add_sample()临时保存在SrsFrame对象中。
- 3、Origin模式下的SRS服务器收到推流客户端publish和unpublish消息时,通知各种协议对象开始或结束转码逻辑
srs_error_t SrsOriginHub::on_publish()
{
create_forwarders(); // 创建forwarder转发对象
encoder->on_publish(req); // 外部ffmpeg编码工具
hls->on_publish(); // 启动HLS协议转码
dash->on_publish(); // 启动DASH
dvr->on_publish(req); // 启动DVR
hds->on_publish(req); // 启动HDS
ng_exec->on_publish(req); // 启动外部工具进程
}
void SrsOriginHub::on_unpublish()
{
is_active = false;
destroy_forwarders(); // 释放forwarder转发对象
encoder->on_unpublish(); //
hls->on_unpublish(); //
dash->on_unpublish(); //
dvr->on_unpublish(); //
hds->on_unpublish(); //
ng_exec->on_unpublish();
}
srs_error_t SrsHls::on_publish()
{
// 如果配置文件中HLS未使能,则直接返回
if (!_srs_config->get_hls_enabled(req->vhost)) { return err; }
controller->on_publish(req); // 通知SrsHlsController为HLS转码做准备
// 如果配置文件为true,则后续处理中直接将FLV时间戳转换为TS DTS
hls_dts_directly = _srs_config->get_vhost_hls_dts_directly(req->vhost);
}
srs_error_t SrsHlsController::on_publish(SrsRequest* req)
{
muxer->on_publish(req); // 在SrsHlsMuxer内部启动一个异步协程,排队处理一些耗时、阻塞的事情
// 为后续生成ts和m3u8文件准备一些基本配置信息,包括:文件名、文件路径、文件时长
// 具体参数信息:
// win=60000ms, frag=10000ms, prefix=, path=./objs/nginx/html, m3u8=[app]/[stream].m3u8, ts=[app]/[stream]-[seq].ts,
// aof=2.00, floor=0, clean=1, waitk=1, dispose=0ms, dts_directly=1
muxer->update_config(req, entry_prefix, path, m3u8_file, ts_file, hls_fragment,
hls_window, ts_floor, hls_aof_ratio, cleanup,
wait_keyframe,hls_keys,hls_fragments_per_key,
hls_key_file, hls_key_file_path, hls_key_url);
muxer->segment_open();
}
总结:
上面分析了RTMP协议针中FLV封装格式的音视频报文的解析逻辑,下一章将分析HLS转码的处理逻辑。