音视频学习(十、再探rtmp拉流)

本文详细分析了RTMP拉流过程,包括Rtmpplayer模块如何从服务器拉取视频流并进行数据解析,以及videoDecode模块如何解码视频数据。探讨了metadata解析、视频数据处理和回调函数的工作原理。
摘要由CSDN通过智能技术生成

昨天把推流写了,不知道看的懂的有多少,确实没有看到代码直接看文字有点难,这不是自己写的代码,所以还是只要是分析为主,不能全部粘贴出来。

10.1 再探rtmp拉流

相比推流还有视频采集模块,拉流就简单了一点,只有一个Rtmpplayer模块和vidoeDecode模块,Rtmpplayer只要负责拉流工作,vidoeDecode负责解码工作。

10.2 Rtmpplayer分析

Rtmpplayer内部启动了一个单独的线程,这个单独的线程专门拉去视频流数据,也是判断Rtmp包中是否有数据,如果有数据就取出来分析分析。

void* RtmpPlayer::readPacketThread()
{
    RTMPPacket packet = {0};

    while(!_exit_thread)
    {
        //短线重连
        if(!isConnect())
        {
            printf("短线重连 re connect");
            if(!connect(_url))      //重连失败
            {
                printf("短线重连 reConnect fail %s",_url.c_str());
                msleep(10);
                continue;
            }
        }


        RTMP_ReadPacket(_rtmp, &packet);		//rtmp读取包数据

        if(RTMPPacket_IsReady(&packet))    //判断是不是整个包都组好了
        {
            uint8_t nalu_header[4] = { 0x00, 0x00, 0x00, 0x01 };

            if(!packet.m_nBodySize)
                continue;

            if(packet.m_packetType == RTMP_PACKET_TYPE_VIDEO)
            {
                //这个就是视频数据,视频数据中,包括两个部分,一个是AVC sequence header,一个是nalu
                //这两个分支下面都做了不同的处理

                // 解析完数据再发送给解码器
                // 判断起始字节, 检测是不是spec config, 还原出sps pps等
                // 重组帧
                bool keyframe = 0x17 == packet.m_body[0] ? true : false;
                // AVC NALU : 0x01/ AVC sequence header : 0x00
                bool sequence = 0x00 == packet.m_body[1];        

                //SPS/PPS sequence
                if(sequence)
                {
                   
                }
                // Nalu frames
                else
                {
                    
                }
            }
            else if(packet.m_packetType == RTMP_PACKET_TYPE_AUDIO)
            {
                printf("rtmp audio\n");
                //目前不处理音频,以后处理
            }
            else if(packet.m_packetType == RTMP_PACKET_TYPE_INFO)               
            //一启动就能收到服务器发送过来的两条info信息
            {
                printf("rtmp info\n");
                //这个就是视频的info信息,也就是FLV中的metadata数据,metadata只要包含视频中配置信息

                //解析信息tag
                parse_script_tag(packet);
                if(video_width > 0 && video_height>0)
                {
                    FLVMetadataMsg *metadata = new FLVMetadataMsg();
                    metadata->width = video_width;//720;
                    metadata->height = video_height;//480;
                    video_callable_object_(RTMP_BODY_METADATA, metadata, false);
                }
            }
            else
            {
                printf("rtmp else\n");
                RTMP_ClientPacket(_rtmp, &packet);
            }
        }

        RTMPPacket_Free(&packet);

        memset(&packet,0,sizeof(RTMPPacket));
    }
    printf("thread exit\n");
    return NULL;

}

10.2.1 metadata数据解析

//解析脚本文件
void RtmpPlayer::parse_script_tag(RTMPPacket &packet)
{
    AMFObject obj;
    AVal val;
    AMFObjectProperty * property;
    AMFObject subObject;
	//使用rtmp库中的AMF模块进行metadata解析
    if(AMF_Decode(&obj, packet.m_body, packet.m_nBodySize, FALSE) < 0 )
    {
        printf("error AMF Decode\n");
    }

    AMF_Dump(&obj);
    printf(" amf obj %d\n",obj.o_num);
	//为什么有两个for循环,就是因为metadata数据是多个类的方式封装的
    for(int n=0; n<obj.o_num; n++)			
    {
        property = AMF_GetProp(&obj, NULL, n);  //通过父类中提取出子对象的信息
        if(property != NULL)
        {
           if(property->p_type == AMF_OBJECT)	//如果子对象也是一个类,就要再次提取
           {
                AMFProp_GetObject(property, &subObject);      //获取子类对象
                for(int m = 0; m < subObject.o_num; m++)
                {
                    property = AMF_GetProp(&subObject, NULL, m);	//这里才是再次提取
                    printf("val = %s\n",property->p_name.av_val);

                    if(property != NULL)
                    {
                        if (property->p_type == AMF_OBJECT)		//应该没有对象了
                        {

                        }
                        else if ( property->p_type == AMF_BOOLEAN )  //获取bool值
                        {
                            int bval = AMFProp_GetBoolean(property);
                            if(strncasecmp("stereo", property->p_name.av_val, property->p_name.av_len) == 0)
                            {
                                audio_channel = bval > 0 ? 2 : 1;
                                printf("parse channel %d\n", audio_channel);
                            }
                        }
                        else if (property->p_type == AMF_NUMBER)	//这里是提取值,这个比较多
                       {
                       	//包括视频的宽 高  码流  帧率等
                           double dVal = AMFProp_GetNumber(property);
                           if (strncasecmp("width", property->p_name.av_val, property->p_name.av_len) == 0)
                           {
                               video_width = (int)dVal;
                               printf("parse widht %d\n",video_width);
                           }
                           else if (strcasecmp("height", property->p_name.av_val) == 0)
                           {
                               video_height = (int)dVal;
                               printf("parse Height %d\n",video_height);
                           }
                           else if (strcasecmp("framerate", property->p_name.av_val) == 0)
                           {
                               video_frame_rate = (int)dVal;
                               printf("parse frame_rate %d\n",video_frame_rate);
                           }
                           else if (strcasecmp("videocodecid", property->p_name.av_val) == 0)
                           {
                               video_codec_id = (int)dVal;
                               printf("parse video_codec_id %d\n",video_codec_id);
                           }
                           else if (strcasecmp("audiosamplerate", property->p_name.av_val) == 0)
                           {
                               audio_sample_rate = (int)dVal;
                               printf("parse audiosamplerate %d\n",audio_sample_rate);
                           }
                           else if (strcasecmp("audiodatarate", property->p_name.av_val) == 0)
                           {
                               audio_bit_rate = (int)dVal;
                               printf("parse audiodatarate %d\n",audio_bit_rate);
                           }
                           else if (strcasecmp("audiosamplesize", property->p_name.av_val) == 0)
                           {
                               audio_sample_size = (int)dVal;
                               printf("parse audiosamplesize %d\n",audio_sample_size);
                           }
                           else if (strcasecmp("audiocodecid", property->p_name.av_val) == 0)
                           {
                               audio_codec_id = (int)dVal;
                               printf("parse audiocodecid %d\n",audio_codec_id);
                           }
                           else if (strcasecmp("filesize", property->p_name.av_val) == 0)
                           {
                               file_size = (int)dVal;
                               printf("parse filesize %d\n",file_size);
                           }
                       }
                       else if (property->p_type == AMF_STRING)
                       {
                           AMFProp_GetString(property, &val);
                       }
                    }
                }
           }
           else
           {
               AMFProp_GetString(property, &val);

               printf("val = %s\n",val.av_val);
           }

        }
    }
}

这样就能完全解析出来,要想深入研究,可以去分析源码,我这里就先不分析了,先用起来先

10.2.2 视频数据解析

//printf("rtmp video\n");

// 解析完数据再发送给解码器
// 判断起始字节, 检测是不是spec config, 还原出sps pps等
// 重组帧
bool keyframe = 0x17 == packet.m_body[0] ? true : false;
bool sequence = 0x00 == packet.m_body[1];         // AVC NALU : 0x01/ AVC sequence header : 0x00

//SPS/PPS sequence   昨天分析可以看出,第二个字节为0代表是sps/pps
if(sequence)
{
    uint32_t offset = 10;       //偏移量=10 sps  
    //可以看到昨天推流的数据,10刚好就是 sps nums   body[i++] = 0xE1;           //&0x1f
    uint32_t sps_num = packet.m_body[offset++] & 0x1f;    //需要 & 0x1f 才能算出真正的长度
    if(sps_num > 0)
    {
        _sps_vector.clear();    // 先清空原来的缓存
    }
    for (int i = 0; i < sps_num; i++)				//保存sps数据
    {
        uint8_t len0 = packet.m_body[offset];
        uint8_t len1 = packet.m_body[offset + 1];
        uint32_t sps_len = ((len0 << 8) | len1);
        offset += 2;
        // Write sps data
        std::string sps;
        sps.append(nalu_header, nalu_header + 4); // 存储 start code
        sps.append(packet.m_body + offset, packet.m_body + offset + sps_len);
        _sps_vector.push_back(sps);
        offset += sps_len;                  //sps data
    }

	//后面就是保存pps的数据了
    uint32_t pps_num = packet.m_body[offset++] & 0x1f;
    if(pps_num > 0)
    {
        _pps_vector.clear();    // 先清空原来的缓存
    }
    for (int i = 0; i < pps_num; i++)
    {
        uint8_t len0 = packet.m_body[offset];
        uint8_t len1 = packet.m_body[offset + 1];
        uint32_t pps_len = ((len0 << 8) | len1);
        offset += 2;
        // Write pps data
        std::string pps;
        pps.append(nalu_header, nalu_header + 4); // 存储 start code
        pps.append(packet.m_body + offset, packet.m_body + offset + pps_len);
        _pps_vector.push_back(pps);
        offset += pps_len;
    }
    VideoSequenceHeaderMsg * vid_config_msg = new VideoSequenceHeaderMsg(
                                (uint8_t *)_sps_vector[0].c_str(),
                                _sps_vector[0].size(),
                                (uint8_t *)_pps_vector[0].c_str(),
                                _pps_vector[0].size()
                                );
    //调用回调
    video_callable_object_(RTMP_BODY_VID_CONFIG, vid_config_msg, false);
}
// Nalu frames
else
{
    //nalu 数据就比较简单了,直接跳到长度位置,提取4个字节的数据代表nalu数据长度
    uint32_t offset = 5;
    uint8_t len0 = packet.m_body[offset];
    uint8_t len1 = packet.m_body[offset + 1];
    uint8_t len2 = packet.m_body[offset + 2];
    uint8_t len3 = packet.m_body[offset + 3];
    uint32_t data_len = ((len0 << 24) | (len1 << 16) | (len2 << 8) | len3);
    offset += 4;

    NaluStruct * nalu = new NaluStruct(data_len + 4);
    memcpy(nalu->data, nalu_header, 4);
    memcpy(nalu->data + 4, packet.m_body + offset, data_len);
    if(_video_pre_pts == -1){
        _video_pre_pts= packet.m_nTimeStamp;
        if(!packet.m_hasAbsTimestamp) {
            printf("no init video pts\n");
        }
    }
    else {
        if(packet.m_hasAbsTimestamp)
            _video_pre_pts= packet.m_nTimeStamp;
        else
            _video_pre_pts += packet.m_nTimeStamp;
    }
    nalu->pts = _video_pre_pts;
    video_callable_object_(RTMP_BODY_VID_RAW, nalu, false);
    offset += data_len;
}

不难发现我们采集到数据之后,都会调用一个video_callable函数,这是在初始化的时候绑定的,接下分析一下video_callable函数。

10.2.3 视频回调函数分析

这次回调函数比较简单,回调函数收到Rtmpplayer收到的数据,直接post到队列中。

10.3 videoDecodeLoop分析

videoDecodeLoop模块是视频解码模块,在初始化中,初始化了一个h264decoder模块,还有初始胡一个Looper循环队列,这个循环队列内部带线程,线程只要作用就是检测队列中是否有数据,如果有数据,提取数据,传给回调函数处理,也就是videoDecodeLoop::handle函数。

10.3.1 videoDecodeLoop::handle分析

这个函数是取到队列中的数据,然后调用的:

void videoDecodeLoop::handle(int what, MsgBaseObj *data)
{
    if(what == RTMP_BODY_METADATA)				//接收到metadata数据,进行播放器的初始化
    {
        if(!_video_out_sdl)
        {
            _video_out_sdl = new VideoOutSDL();
            if(!_video_out_sdl)
            {
                printf("new VideoOutSDL() failed\n");
                return;
            }
            Properties vid_out_properties;
            FLVMetadataMsg *metadata = (FLVMetadataMsg *)data;
            vid_out_properties.SetProperty("video_width", metadata->width);
            vid_out_properties.SetProperty("video_height",  metadata->height);
            vid_out_properties.SetProperty("win_x", 1000);
            vid_out_properties.SetProperty("win_title", "pull video display");
            delete metadata;
            if(_video_out_sdl->init(vid_out_properties) != 0)
            {
                printf("video_out_sdl Init failed\n");
                return;
            }
        }
    }
    else if(what == RTMP_BODY_VID_CONFIG)			//把接收到的sps/pps送到解码器中解码
    {
        VideoSequenceHeaderMsg *vid_config = (VideoSequenceHeaderMsg *)data;

        // 把sps送给解码器
        _yuv_buf_size = YUV_BUF_MAX_SIZE;
        _h264decoder->decode(vid_config->_sps, vid_config->_sps_size,
                                        _yuv_buf, _yuv_buf_size);
        // 把pps送给解码器
        _yuv_buf_size = YUV_BUF_MAX_SIZE;
        _h264decoder->decode(vid_config->_pps, vid_config->_pps_size,
                                        _yuv_buf, _yuv_buf_size);
        delete vid_config;
    }
    else							//最后就是发送nalu数据到解压器中解码
    {
        NaluStruct *nalu = (NaluStruct *)data;
        _yuv_buf_size = YUV_BUF_MAX_SIZE;
        if(_h264decoder->decode(nalu->data, nalu->size,
                                        _yuv_buf, _yuv_buf_size) == 0)
        {
            callable_object_(_yuv_buf, _yuv_buf_size);
        }
        delete nalu;     // 要手动释放资源
    }
}

这个都比较简单,接下来重点的还是h264解码器。

10.3.2 h264decoder分析

h264decode解码初始化跟demo的解压代码差不多,所以这里最主要还是要看解码函数:

int h264decoder::decode(uint8_t *in, int32_t in_len, uint8_t *out, int32_t &out_len)
{
   int got_picture=0;

   AVPacket pkt;
   av_init_packet(&pkt);
   pkt.data = in;
   pkt.size = in_len;
   
   int readed = avcodec_decode_video2(_ctx, _frame, &got_picture, &pkt);

   //Si hay picture_
   if (got_picture && readed>0)
   {
       if(_ctx->width==0 || _ctx->height==0)
       {
           printf("-Wrong dimmensions [%d,%d]\n", _ctx->width, _ctx->height);
           return 0;
       }
       int width = _frame->width;       //720
       int height = _frame->height;     //480

       out_len = _frame->width * _frame->height * 1.5;    //图片的长度,这是h264输入,然后YUV数据出来
       /*memcpy(out, _frame->data[0], _frame->width * _frame->height);        //Y
       memcpy(out + _frame->width * _frame->height, _frame->data[1],        //U
               (_frame->width * _frame->height) /4);
       memcpy(out + (_frame->width * _ctx->height) + (_frame->width * _ctx->height) /4,
              _frame->data[2],
               (_frame->width * _frame->height) /4);   */                   //V

	//解码出来的数据是对齐的,所以需要这样子复制,如果是上面那样复制不行的
       for(int j=0; j<height; j++)
       {
           memcpy(out + j*width, _frame->data[0] + j * _frame->linesize[0], width);    
           //768   64字节对齐
       }
       out += width * height;
       for(int j=0; j<height/2; j++)
       {
           memcpy(out + j*width/2, _frame->data[1] + j * _frame->linesize[1], width/2);
       }
       out += width * height/2/2;
       for(int j=0; j<height/2; j++)
       {
           memcpy(out + j*width/2, _frame->data[2] + j * _frame->linesize[2], width/2);
       }

		//下面是保存成YUV格式的,保存成YUV格式也是对齐保存
       static FILE *dump_yuv = NULL;
       if(!dump_yuv)
       {
           dump_yuv = fopen("h264_dump_320x240.yuv", "wb");
           if(!dump_yuv)
           {
               printf("fopen h264_dump.yuv failed");
           }
       }
       if(dump_yuv)
       {//ffplay -ar 48000 -ac 2 -f s16le -i aac_dump.pcm
           //AVFrame存储YUV420P对齐分析
           //https://blog.csdn.net/dancing_night/article/details/80830920?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
           for(int j=0; j<height; j++)
               fwrite(_frame->data[0] + j * _frame->linesize[0], 1, width, dump_yuv);
           for(int j=0; j<height/2; j++)
               fwrite(_frame->data[1] + j * _frame->linesize[1], 1, width/2, dump_yuv);
           for(int j=0; j<height/2; j++)
               fwrite(_frame->data[2] + j * _frame->linesize[2], 1, width/2, dump_yuv);

           fflush(dump_yuv);
       }
       return 0;
   }
   out_len = 0;
   return 0;
}

优化过后的推流拉流就先讲到这里,下一节是要音频数据添加上来,现在为了简单处理都只是使用了视频数据。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值