其實這個再探推流拉流是打算放在第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編碼)
//第二個數據代表的是是否是sequence header或者是nalu,我們現在發送的是sps/pps。說明是sequence header
body[i++] = 0x00; //AVC sequence header
//後面這三個字節,對應着就是compostitionTime。目前都爲0
body[i++] = 0x00;
body[i++] = 0x00;
body[i++] = 0x00;
剩下的後面那節,我在FLV格式解析中並沒有給出,所以這次需要添加一下:
剛剛今天不加的內容,通過這個格式再回去看代碼是不是覺得理解了:
發送視頻數據
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解析也可以看的出來。
本來是計劃這篇文件把推流拉流都介紹完,結果只能介紹了推流,沒辦法了,下次再介紹拉流。