从零开始学习音视频编程技术(46)接收rtp流解析并解码播放
时间:01月27日 人气:...


到目前为止,播放rtp流,我们一直都是采用vlc播放器来播放。现在,时机已到,是时候实现自己的rtp流播放器了,说干就干。


认真研究过之前发布rtp流代码的小伙伴就会发现,推送rtp的时候我们只使用了2种包,那就是:单个NAL单元包和RTP分片单元的FU-A版本,

至于什么是单个NAL单元包和FU-A,这里不做解释了,忘记的童鞋请点击下方的连接先去看看:

rtp协议解析地址:http://blog.yundiantech.com/?log=blog&id=40


哦了,现在开始写代码了。


第一步当然是socket实现接收udp包了,废话不多说直接上代码:


一、windows下实现Udp接收数据

1.初始化网络

BOOL InitWinsock()
{
    int Error;
    WORD VersionRequested;
    WSADATA WsaData;
    VersionRequested=MAKEWORD(2,2);
    Error=WSAStartup(VersionRequested,&WsaData); //启动WinSock2
    if(Error!=0)
    {
        return FALSE;
    }
    else
    {
        if(LOBYTE(WsaData.wVersion)!=2||HIBYTE(WsaData.wHighVersion)!=2)
        {
            WSACleanup();
            return FALSE;
        }
    }
    return TRUE;
}


2.开始监听端口

    SOCKET sockServer = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);    
    
    if (sockServer == INVALID_SOCKET)
    {
        printf("Failed socket() 
");
        return;
    }

    SOCKADDR_IN addr_Server; //服务器的地址等信息
    addr_Server.sin_family = AF_INET;
    addr_Server.sin_port = htons(LOCAL_PORT);
    addr_Server.sin_addr.S_un.S_addr = INADDR_ANY;
    if (bind(sockServer, (SOCKADDR*)&addr_Server, sizeof(addr_Server)) == SOCKET_ERROR)
    {
        //服务器与本地地址绑定
        printf("Failed socket() %d 
", WSAGetLastError());
        return;
    }

    SOCKADDR_IN addr_Clt;

    fprintf(stderr,"UPD servese is bind success and do_echo now
");


3.读取数据

while(1)
{
    int fromlen = sizeof(SOCKADDR);        
    char mesg[MAXLINE] = {0};
    int rtpSize = recvfrom(sockServer, mesg, MAXLINE, 0, (SOCKADDR*)&addr_Clt, &fromlen);
    
    ///处理接收到的数据
    ...
}


通过udp收到的数据,就是所谓的rtp包了,现在只需要解析它就好了。


二、RTP数据解析

前面已经讲解了rtp协议,现在解析也不难了,

解析rtp代码如下:

unsigned char * rtp_unpackage(char *bufIn, int len, char *bufOut, int *outLen, int *gotFrame)
{
    int bufLen = 0;

    unsigned char recvbuf[1500] = {0};
    RTPpacket_t *p = NULL;
    RTP_HEADER * rtp_hdr = NULL;
    NALU_HEADER * nalu_hdr = NULL;
    NALU_t * n  = NULL;
    FU_INDICATOR	*fu_ind = NULL;
    FU_HEADER*fu_hdr= NULL;
    int total_bytes = 0;                 //当前包传出的数据
    static int total_recved = 0;         //一共传输的数据

    memcpy(recvbuf,bufIn, len);          //复制rtp包
    printf("包长度+ rtp头:   = %d
",len);

//////////////////////////////////////////////////////////////////////////
//begin rtp_payload and rtp_header

    p = (RTPpacket_t*)&recvbuf[0];
    if ((p = (RTPpacket_t*)malloc (sizeof (RTPpacket_t)))== NULL)
    {
        printf ("RTPpacket_t MMEMORY ERROR
");
    }
    if ((p->payload = (unsigned char * )malloc (MAXDATASIZE))== NULL)
    {
        printf ("RTPpacket_t payload MMEMORY ERROR
");
    }

 //   if ((rtp_hdr = (RTP_HEADER * )malloc(sizeof(RTP_HEADER))) == NULL)
  //  {
    //    printf("RTP_HEADER MEMORY ERROR
");
   // }

    rtp_hdr =(RTP_HEADER*)&recvbuf[0];
    printf("版本号 : %d
",rtp_hdr->version);
    p->v  = rtp_hdr->version;
    p->p  = rtp_hdr->padding;
    p->x  = rtp_hdr->extension;
    p->cc = rtp_hdr->csrc_len;
    printf("标志位 : %d
",rtp_hdr->marker);
    p->m = rtp_hdr->marker;
    printf("负载类型:%d
",rtp_hdr->payloadtype);
    p->pt = rtp_hdr->payloadtype;
    printf("包号   : %d 
",rtp_hdr->seq_no);
    p->seq = rtp_hdr->seq_no;
    printf("时间戳 : %d
",rtp_hdr->timestamp);
    p->timestamp = rtp_hdr->timestamp;
    printf("帧号   : %d
",rtp_hdr->ssrc);
    p->ssrc = rtp_hdr->ssrc;

//end rtp_payload and rtp_header
//////////////////////////////////////////////////////////////////////////
//begin nal_hdr
    if (!(n = AllocNALU()))          //为结构体nalu_t及其成员buf分配空间。返回值为指向nalu_t存储空间的指针
    {
        printf("NALU_t MMEMORY ERROR
");
    }
    if ((nalu_hdr = (NALU_HEADER * )malloc(sizeof(NALU_HEADER))) == NULL)
    {
        printf("NALU_HEADER MEMORY ERROR
");
    }

    nalu_hdr =(NALU_HEADER*)&recvbuf[12];                        //网络传输过来的字节序 ,当存入内存还是和文档描述的相反,只要匹配网络字节序和文档描述即可传输正确。
    printf("forbidden_zero_bit: %d
",nalu_hdr->F);              //网络传输中的方式为:F->NRI->TYPE.. 内存中存储方式为 TYPE->NRI->F (和nal头匹配)。
    n->forbidden_bit= nalu_hdr->F << 7;                          //内存中的字节序。
    printf("nal_reference_idc:  %d
",nalu_hdr->NRI);
    n->nal_reference_idc = nalu_hdr->NRI << 5;
    printf("nal 负载类型:       %d
",nalu_hdr->TYPE);
    n->nal_unit_type = nalu_hdr->TYPE;

//end nal_hdr
//////////////////////////////////////////////////////////////////////////
//开始解包
    if ( nalu_hdr->TYPE  == 0)
    {
        printf("这个包有错误,0无定义
");
    }
    else if ( nalu_hdr->TYPE >0 &&  nalu_hdr->TYPE < 24)  //单包
    {
        printf("当前包为单包
");

        bufOut[bufLen++] = 0X00;
        bufOut[bufLen++] = 0X00;
        bufOut[bufLen++] = 0X00;
        bufOut[bufLen++] = 0X01;

        total_bytes +=4;
        memcpy(p->payload,&recvbuf[13],len-13);
        p->paylen = len-13;
        //fwrite(nalu_hdr,1,1,poutfile);
        memcpy(bufOut+bufLen,nalu_hdr,1);
        bufLen += 1;

        total_bytes += 1;
        //fwrite_number = fwrite(p->payload,1,p->paylen,poutfile);
        memcpy(bufOut+bufLen,p->payload,p->paylen);
        bufLen += p->paylen;

        total_bytes = p->paylen;
        printf("包长度 + nal= %d
",total_bytes);

        *gotFrame = 1;
    }
    else if ( nalu_hdr->TYPE == 24)                    //STAP-A   单一时间的组合包
    {
        printf("当前包为STAP-A
");
    }
    else if ( nalu_hdr->TYPE == 25)                    //STAP-B   单一时间的组合包
    {
        printf("当前包为STAP-B
");
    }
    else if (nalu_hdr->TYPE == 26)                     //MTAP16   多个时间的组合包
    {
        printf("当前包为MTAP16
");
    }
    else if ( nalu_hdr->TYPE == 27)                    //MTAP24   多个时间的组合包
    {
        printf("当前包为MTAP24
");
    }
    else if ( nalu_hdr->TYPE == 28)                    //FU-A分片包,解码顺序和传输顺序相同
    {
        if ((fu_ind = ( FU_INDICATOR	*)malloc(sizeof(FU_INDICATOR))) == NULL)
        {
            printf("FU_INDICATOR MEMORY ERROR
");
        }
        if ((fu_hdr = (FU_HEADER*)malloc(sizeof(FU_HEADER))) == NULL)
        {
            printf("FU_HEADER MEMORY ERROR
");
        }

        fu_ind=(FU_INDICATOR*)&recvbuf[12];
        printf("FU_INDICATOR->F     :%d
",fu_ind->F);
        n->forbidden_bit = fu_ind->F << 7;
        printf("FU_INDICATOR->NRI   :%d
",fu_ind->NRI);
        n->nal_reference_idc = fu_ind->NRI << 5;
        printf("FU_INDICATOR->TYPE  :%d
",fu_ind->TYPE);
        n->nal_unit_type = fu_ind->TYPE;

        fu_hdr=(FU_HEADER*)&recvbuf[13];
        printf("FU_HEADER->S        :%d
",fu_hdr->S);
        printf("FU_HEADER->E        :%d
",fu_hdr->E);
        printf("FU_HEADER->R        :%d
",fu_hdr->R);
        printf("FU_HEADER->TYPE     :%d
",fu_hdr->TYPE);
        n->nal_unit_type = fu_hdr->TYPE;               //应用的是FU_HEADER的TYPE

        if (rtp_hdr->marker == 1)                      //分片包最后一个包
        {
            printf("当前包为FU-A分片包最后一个包
");
            memcpy(p->payload,&recvbuf[14],len - 14);
            p->paylen = len - 14;
            //fwrite_number = fwrite(p->payload,1,p->paylen,poutfile);
            memcpy(bufOut+bufLen,p->payload,p->paylen);
            bufLen += p->paylen;

            total_bytes = p->paylen;
            printf("包长度 + FU = %d
",total_bytes);

            *gotFrame = 1;
        }
        else if (rtp_hdr->marker == 0)                 //分片包 但不是最后一个包
        {
            if (fu_hdr->S == 1)                        //分片的第一个包
            {
                unsigned char F;
                unsigned char NRI;
                unsigned char TYPE;
                unsigned char nh;
                printf("当前包为FU-A分片包第一个包
");

                ///添加4字节起始码
                bufOut[bufLen++] = 0X00;
                bufOut[bufLen++] = 0X00;
                bufOut[bufLen++] = 0X00;
                bufOut[bufLen++] = 0X01;

                total_bytes += 4;

                F = fu_ind->F << 7;
                NRI = fu_ind->NRI << 5;
                TYPE = fu_hdr->TYPE;    //应用的是FU_HEADER的TYPE
                nh = F | NRI | TYPE;

                memcpy(bufOut+bufLen,&nh,1);
                bufLen+=1;

                total_bytes +=1;
                memcpy(p->payload,&recvbuf[14],len - 14);
                p->paylen = len - 14;

                memcpy(bufOut+bufLen,p->payload,p->paylen);
                bufLen += p->paylen;

                total_bytes = p->paylen;
                printf("包长度 + FU_First = %d
",total_bytes);
            }
            else                                      //如果不是第一个包
            {
                printf("当前包为FU-A分片包
");
                memcpy(p->payload,&recvbuf[14],len - 14);
                p->paylen= len - 14;

                memcpy(bufOut+bufLen,p->payload,p->paylen);
                bufLen += p->paylen;

                total_bytes = p->paylen;
                printf("包长度 + FU = %d
",total_bytes);
            }
        }
    }
    else if ( nalu_hdr->TYPE == 29)                //FU-B分片包,解码顺序和传输顺序相同
    {
        if (rtp_hdr->marker == 1)                  //分片包最后一个包
        {
            printf("当前包为FU-B分片包最后一个包
");

        }
        else if (rtp_hdr->marker == 0)             //分片包 但不是最后一个包
        {
            printf("当前包为FU-B分片包
");
        }
    }
    else
    {
        printf("这个包有错误,30-31 没有定义
");
    }
    total_recved += total_bytes;
    printf("total_recved = %d
",total_recved);
    memset(recvbuf,0,1500);
    free (p->payload);
    free (p);
    FreeNALU(n);

    *outLen = bufLen;

    fprintf(stderr,"%d %d
",bufLen,total_bytes);

//结束解包
//////////////////////////////////////////////////////////////////////////

}


注:这里仅仅是处理了我们使用的2种rtp包:单个NAL单元包和FU-A


这里还有2点需要注意:

1.如果收到的包是单个nal元(通过类型是24判断),那么解析出来的nalu就是一帧数据,可以直接丢给解码器解码。

2.如果是分片包(通过类型是28判断),那么解析出来的nalu就不是完整的一帧图像,需要缓存起来,直到收到分片包的最后一个包(通过rtp_hdr->marker ==1判断),才算是获取到完整的一帧,方可丢给解码器解码。


这个实现代码如下:

    
    uint8_t *h264Buffer = (uint8_t*)malloc(1024*1024);    
    int h264BufferSize = 0;

    while(1)
    {
        int fromlen = sizeof(SOCKADDR);
        char mesg[MAXLINE] = {0};
        int rtpSize = recvfrom(sockServer, mesg, MAXLINE, 0, (SOCKADDR*)&addr_Clt, &fromlen);

        int outSize = 0;
        int gotFrame = 0;

        char naluBuffer[10240] = {0};
        rtp_unpackage(mesg, rtpSize, naluBuffer, &outSize, &gotFrame);

        memcpy(h264Buffer+h264BufferSize, naluBuffer, outSize);

        h264BufferSize += outSize;

        ///收到了完整的一帧数据
        if (gotFrame)
        {
//           fprintf(stderr, "got one frame size=%d
",h264BufferSize);

           uint8_t *bufferRGB;
           int width;
           int height;

           int ret = mH264Decorder->decodeH264(h264Buffer, h264BufferSize, bufferRGB, width, height);

           //把这个RGB数据 放入QIMage
           QImage image = QImage((uchar *)bufferRGB, width, height, QImage::Format_RGB32);

           //然后传给主线程显示
           emit sig_GetOneFrame(image.copy());

           h264BufferSize = 0;

        }
    }


三、解码h264并显示

至于怎么解码以及怎么显示这里就不说了,不懂的朋友,请查看以前的例子:http://blog.yundiantech.com/?log=blog&id=39



哦了,到这里我们的rtp播放器也算是写好了,配合上次的推流端就可以实现屏幕共享的功能了。


当然,这还只是个雏形,在播放器上看到的图像,有时候会出现严重的花屏现象,这是因为数据包丢失了,获取顺序乱了引起的,udp协议的丢包和乱序问题真的是让人头疼。不过别担心,我们依然有办法解决他,后续我们会引入一个rtp库(jrtplib)来实现rtp数据的收发,可以有效的解决花屏问题。



完整工程下载地址:https://download.csdn.net/download/qq214517703/10940413

学习音视频技术欢迎访问 http://blog.yundiantech.com  

音视频技术交流讨论欢迎加 QQ群 121376426