音視頻學習(九、再探rtmp推流)

其實這個再探推流拉流是打算放在第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解析也可以看的出來。

本來是計劃這篇文件把推流拉流都介紹完,結果只能介紹了推流,沒辦法了,下次再介紹拉流。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章