音视频学习(九、再探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解析也可以看的出来。

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

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