其实这个再探推流拉流是打算放在第5节,第5节的时候有写了一点,然后发现确实写的有点困难,然后就出了第6.7.8分析h264 Nalu、YUV、FFmpeg这些基础,现在把这些基础全部补上了,就可以再次讨论推流拉流,因为前面的rtmp推流拉流只是简答的调用demo,不算是真正的推流拉流,所以这个再次往我们正常使用的方向靠近。

9.1 再探rtmp推流

这次再探rtmp推流不像以前直接调用demo了,现在要有逻辑思想,分析一些关键代码,全部代码我就不粘贴出来了,因为是丹老师写的代码,不是自己写的,不太好意思贴出来,不过主体思想还有一些关键代码分析还是可以分析分析。(目前都不涉及音频,音频等视频搞完之后在添加)

9.2 整体分析

推流代码的整体分析,目前有3个模块,RtmpPusher推流模块,H264Encode编码模块,VideoCapturer视频采集模块。

9.3 VideoCapturer分析

videoCapturer内部创建了一个线程,由这个线程推动,这个线程只要的工作就是,读取一个yuv格式的视频,读取一帧数据就把数据传送到回调函数中,让回调函数处理。

这个部分主要注意的地方是,怎么读取到一帧的数据,这个就要看我的第7篇文章,《初识YUV》里面就讲到了YUV420格式占多少字节,这里就直接说出:_width * _height * 1.5; //一帧数据的长度(一个长_width和宽_height的图片,格式为YUV420占用的字节数)

这里为什么用YUV格式的视频,是因为我们还在调试,为了简单起见,还是先用YUV格式,等到后面我们可以把这个视频采集模块替换成,录制桌面视频的也是可以的。

9.4 H264Encode分析

这个是h264编码模块,编码模块也比较简单,大体都是按照我上节的初始化,都差不多的。

主要需要注意的是,context h264编码控制块里面有一个flag标记,这个标记还是挺多的,注意的是下面的标记:

_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; //extradata拷贝 sps pps

使能这个标记就是每帧都不带sps/pps的帧标记,因为我们是推流端,为了考虑减少带宽,所以有必要把每个I帧带的sps/pps去掉,到解码端再还原回来。

还有一个是获取sps/pps:

if (avcodec_open2(_ctx, _codec, &_param) < 0)
{
printf("Failed to open encoder! \n");
}
if(_ctx->extradata)
{
printf("extradata_size:%d %d%d%d%d", _ctx->extradata_size, _ctx->extradata[0],
_ctx->extradata[1], _ctx->extradata[2], _ctx->extradata[3]);
// 第一个为sps 7
// 第二个为pps 8

uint8_t *sps = _ctx->extradata + 4; // 直接跳到数据,因为头数据是0001,表示着是一个Nalu开头
int sps_len = 0;
uint8_t *pps = NULL;
int pps_len = 0;
uint8_t *data = _ctx->extradata + 4;
for (int i = 0; i < _ctx->extradata_size - 4; ++i) //遍历找到pps的开头
{
if (0 == data[i] && 0 == data[i + 1] && 0 == data[i + 2] && 1 == data[i + 3])
{
pps = &data[i+4]; //再次找到0001,说明已经到了pps了
break;
}
}
sps_len = int(pps - sps) - 4; // 4是00 00 00 01占用的字节,计算sps_len
pps_len = _ctx->extradata_size - 4*2 - sps_len; //计算pps_len
_sps.append(sps, sps + sps_len); //添加到_sps
_pps.append(pps, pps + pps_len); //添加到_pps
}

在初始化编码完成之后,我们就可以获取相关编码的extradata数据了,因为我们用的是h264编码,所以我们获取的就是sps/pps。把获取到的sps/pps存储到_sps/_pps中。

最后一点是编码的时候,要把YUV的数据分离出来,YUV占比也在(七、初识YUV)中说过,这里就直接复制出来,

//编码是根据我们数据进行不同的编码,这个是YUV420p
_frame->data[0] = in; // Y
_frame->data[1] = in + _data_size; // U
_frame->data[2] = in + _data_size * 5 / 4; // V

9.5 rtmpbase分析

rtmpbase就是对rtmp进行的一次封装,封装的函数如下:

bool init();
bool connect(char* url);
bool isConnect();
void close();

都是比较基础的封装,rtmpbase的封装,只要是对rtmp的基础函数进行封装

9.6 RtmpPusher分析

RtmpPusher推流器,只要任务是对rtmpbase进行封装,封装函数如下:

sendPacket()
sendMetadata()
sendH264Packet()
sendH264SequenceHeader()

RtmpPusher是把rtmpbase的函数封装成包的形式,调用这些函数就可以直接发送数据,不过RtmpPusher继承了一个Looper类,这个类就是对队列的一个封装,这个类中有单独的线程,线程就是检测队列中是否有数据,如果有数据就取出来,然后调用RtmpPusher发送函数的回调,这个类中也提供了post方法,就是往队列中填充数据。

9.7 视频采集回调函数分析

视频采集模块是一个单独的线程,我们在初始化的时候,绑定了一个采集完一帧图像之后就调用回调:

void PushWork::yuvCallback(uint8_t* yuv, int32_t size)
{
//调用这个回调函数就说明是一帧数据,也就是一个NALU
char start_code[] = {0x00, 0x00, 0x00, 0x01};

if(_need_send_video_config) //是否需要发送sps/pps
{
//下面就是把sps/pps的数据全部打包好,然后post到队列中
_need_send_video_config = false;
VideoSequenceHeaderMsg *video_config_msg = new VideoSequenceHeaderMsg(
_video_encoder->get_sps_data(), _video_encoder->get_sps_size(),
_video_encoder->get_pps_data(), _video_encoder->get_pps_size()
);
video_config_msg->_Width = _video_width;
video_config_msg->_Height = _video_height;
video_config_msg->_FrameRate = _video_bitrate;
video_config_msg->_VideoDataRate = _video_bitrate;
_rtmp_pusher->post(RTMP_BODY_VID_CONFIG, video_config_msg);
}

_video_nalu_size = VIDEO_NALU_BUF_MAX_SIZE;
if(_video_encoder->encode(yuv, _video_nalu_buf, _video_nalu_size) == 0 ) {
//把视频采集到的yuv裸数据发送到h264编码中,编码成功之后把编码后的nalu数据存储到_video_nalu_buf中
NaluStruct *nalu = new NaluStruct(_video_nalu_buf, _video_nalu_size);
nalu->type = _video_nalu_buf[0] & 0x1f;
_rtmp_pusher->post(RTMP_BODY_VID_RAW, nalu); //把nalu数据打包好,然后也post到队列中
if(_h264_fp) {
//printf("nalu type %x\n", _video_nalu_buf[4]);
//fwrite(_video_nalu_buf, _video_nalu_size, 1, _h264_fp);
//fflush(_h264_fp);
}
saveYuvFile("h264", 5, _video_nalu_buf, _video_nalu_size);
}
}

另外还有一个注意的地方,就是在编码器初始化的时候,把metadata数据准备好,然后post到队列中了。

//发送 RTMP -> FLV 格式去发送,metadata
FLVMetadataMsg *metadata = new FLVMetadataMsg();
//设置视频相关
metadata->has_video = true;
metadata->width = _video_encoder->get_width();
metadata->height = _video_encoder->get_height();
metadata->framerate = _video_encoder->get_framerate();
metadata->videodatarate = _video_encoder->get_bit_rate();
// 音频相关,目前还没有音频,所以不设置
metadata->has_audio = false;
_rtmp_pusher->post(RTMP_BODY_METADATA, metadata, false); //post到队列中

9.8 视频推流函数分析

这个其实是一个回调函数,Looper类中对队列的数据进行查询,如果有数据提取出来,然后传给回调函数处理,这个回调函数刚好是RtmpPusher里的函数:

void RtmpPusher::handle(int what, MsgBaseObj *data)
{
if(!isConnect())
{
printf("开始断线重连");
if(!connect())
{
printf("重连失败");
delete data;
return;
}
}

switch(what)
{
case RTMP_BODY_METADATA: //需要发送metadata数据
{
if(!sendMetadata((FLVMetadataMsg *)data))
{
printf("send Metadata\n"); //发送成功进来,自己发送了一次
}

break;
}
case RTMP_BODY_VID_CONFIG: //需要发送sps/pps的数据
{
VideoSequenceHeaderMsg *vid_cfg_msg = (VideoSequenceHeaderMsg*)data;
printf("RTMP_BODY_VID_CONFIG %p\n", vid_cfg_msg);
if(sendH264SequenceHeader(vid_cfg_msg) != 0)
{
printf("sendH264SequenceHeader failed\n");
}
printf("RTMP_BODY_VID_CONFIG \n");
break;
}
case RTMP_BODY_VID_RAW: //这个就是进行视频裸数据发送
{
NaluStruct *nalu = (NaluStruct *)data;
sendH264Packet((char*)nalu->data, nalu->size, (nalu->type == 0x05)?true:false, nalu->pts);
//printf("RTMP_BODY_VID_RAW \n");
delete nalu;
break;
}
default:
break;

}

}

接下来只要分析一下我们对数据的封装,也就是封装成FLV格式,因为rtmp推流是推FLV格式的,FLV格式我在之前的章节已经介绍过了,不过那时候介绍的不详细,只是大概说了,现在再次应用到数据的项目中进行分析。

发送metadata数据

//要看看是服务器解析,还是由服务器转发然后在拉流端解析,之后分析
bool RtmpPusher::sendMetadata(FLVMetadataMsg *metadata)
{
if (metadata == NULL)
{
return false;
}
char body[1024] = { 0 };

char * p = (char *)body;
p = put_byte(p, AMF_STRING);
p = put_amf_string(p, "@setDataFrame");

p = put_byte(p, AMF_STRING);
p = put_amf_string(p, "onMetaData");

p = put_byte(p, AMF_OBJECT);
p = put_amf_string(p, "copyright");
p = put_byte(p, AMF_STRING);
p = put_amf_string(p, "firehood");

if(metadata->has_video)
{
p = put_amf_string(p, "width");
p = put_amf_double(p, metadata->width);

p = put_amf_string(p, "height");
p = put_amf_double(p, metadata->height);

p = put_amf_string(p, "framerate");
p = put_amf_double(p, metadata->framerate);

p = put_amf_string(p, "videodatarate");
p = put_amf_double(p, metadata->videodatarate);

p = put_amf_string(p, "videocodecid");
p = put_amf_double(p, FLV_CODECID_H264);
}
if(metadata->has_audio)
{
p = put_amf_string(p, "audiodatarate");
p = put_amf_double(p, (double)metadata->audiodatarate);

p = put_amf_string(p, "audiosamplerate");
p = put_amf_double(p, (double)metadata->audiosamplerate);

p = put_amf_string(p, "audiosamplesize");
p = put_amf_double(p, (double)metadata->audiosamplesize);

p = put_amf_string(p, "stereo");
p = put_amf_double(p, (double)metadata->channles);

p = put_amf_string(p, "audiocodecid");
p = put_amf_double(p, (double)FLV_CODECID_AAC);
}
p = put_amf_string(p, "");
p = put_byte(p, AMF_OBJECT_END);

return sendPacket(RTMP_PACKET_TYPE_INFO, (unsigned char*)body, p - body, 0);
}

这个就是按照metadata的方式,把发送过来FLVMetadataMsg 数据进行封装成metadata数据,我这里就不分析二进制文件了,有兴趣的可以分析分析。

发送sps/pps数据

//发送视频配置信息
int RtmpPusher::sendH264SequenceHeader(VideoSequenceHeaderMsg *seq_header)
{
if(!seq_header) {
return -1;
}

char body[1024] = {0};

int i = 0;
body[i++] = 0x17; //1: keyframe 7: AVC(h264)
body[i++] = 0x00; //AVC sequence header

body[i++] = 0x00;
body[i++] = 0x00;
body[i++] = 0x00;

//后面的就要继续分析FLV
// AVCDecoderConfigurationRecord.
body[i++] = 0x01; // configurationVersion
body[i++] = seq_header->_sps[1]; // AVCProfileIndication
body[i++] = seq_header->_sps[2]; // profile_compatibility
body[i++] = seq_header->_sps[3]; // AVCLevelIndication
body[i++] = 0xff; // lengthSizeMinusOne

// sps nums
body[i++] = 0xE1; //&0x1f
// sps data length
body[i++] = seq_header->_sps_size >> 8;
body[i++] = seq_header->_sps_size & 0xff;
// sps data
memcpy(&body[i], seq_header->_sps, seq_header->_sps_size);
i = i + seq_header->_sps_size;

// pps nums
body[i++] = 0x01; //&0x1f
// pps data length
body[i++] = seq_header->_pps_size >> 8;
body[i++] = seq_header->_pps_size & 0xff;
// sps data
memcpy(&body[i], seq_header->_pps, seq_header->_pps_size);
i = i + seq_header->_pps_size;
//printf("sendH264SequenceHeader %d\n", i);
return sendPacket(RTMP_PACKET_TYPE_VIDEO, (unsigned char*)body, i, 0);
}

这次发送的sps/pps数据,相对于FLV格式来说都是视频数据,所以发送的类型都是RTMP_PACKET_TYPE_VIDEO,但是我们怎么知道发送的是sps/pps数据而不是视频数据,这个就需要好好看看我之前那篇FLV格式解析了,解析的步骤,还是写在代码中把,这样好看一点。

body[i++] = 0x17;  //1: keyframe 7: AVC(h264)
//看到下图,是不是瞬间就明白第一个数据为什么填0x17了,是因为1代表关键帧,7代表AVC(h264编码)

音视频学习(九、再探rtmp推流)_i++

//第二个数据代表的是是否是sequence header或者是nalu,我们现在发送的是sps/pps。说明是sequence header
body[i++] = 0x00; //AVC sequence header

//后面这三个字节,对应着就是compostitionTime。目前都为0
body[i++] = 0x00;
body[i++] = 0x00;
body[i++] = 0x00;

音视频学习(九、再探rtmp推流)_数据_02

剩下的后面那节,我在FLV格式解析中并没有给出,所以这次需要添加一下:

音视频学习(九、再探rtmp推流)_ide_03

刚刚今天不加的内容,通过这个格式再回去看代码是不是觉得理解了:

音视频学习(九、再探rtmp推流)_数据_04

发送视频数据

bool RtmpPusher::sendH264Packet(char *data,int size, bool is_keyframe, unsigned int timestamp)
{
if (data == NULL && size<11)
{
return false;
}

unsigned char *body = new unsigned char[size+9];

int i = 0;
//就是封装成flv的格式,只不过是都没有头信息
if(is_keyframe)
{
body[i++] = 0x17; //1:Iframe 7:AVC
}
else
{
body[i++] = 0x27; // 2: Pframe 7:AVC
}

body[i++] = 0x01; //AVC NALU
body[i++] = 0x00; //CompositionTime 3个字节
body[i++] = 0x00;
body[i++] = 0x00;

//malu size
body[i++] = size >> 24;
body[i++] = size >> 16;
body[i++] = size >> 8;
body[i++] = size & 0xff;

// Nalu data 深拷贝好像有3 4次了吧
memcpy(&body[i], data, size);

int ret = sendPacket(RTMP_PACKET_TYPE_VIDEO, body, i + size, timestamp);
delete body;
return ret;
}

这个发送视频数据就正常了很多,我这里就不多做分析了,对着上面的FLV解析也可以看的出来。

本来是计划这篇文件把推流拉流都介绍完,结果只能介绍了推流,没办法了,下次再介绍拉流。