从零开始学习音视频编程技术(43) H264打包发布RTP流
时间:05月23日 人气:...


上一节我们讲解了RTP协议的结构,现在就来实践一下吧,将一个h264文件打包成rtp数据流发送出去。


我们采用udp协议来发送rtp包,首先先了解下windows下udp发送数据的流程。linux下也是类似,具体请自行百度。

一、windows下socket编程之使用udp发送数据

1.初始化套接字

1)引用头文件:

#include <winsock2.h>


这里需要注意的一点是,当用到windows上别的api需要引入windows.h的时候,windows.h要放在winsockt2.h的后面,如下所示:

#include <winsock2.h>    
#include <windows.h>


切记!!否则会出现各种莫名其妙的报错。

2)调用WSAStartup启动套接字,代码如下:

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;
}

3)初始化套接字:

SOCKET    socket1;
socket1=socket(AF_INET,SOCK_DGRAM,0); //申请UDP套接字

2.发送数据

发送方式有send和sendto:

int send( SOCKET s, const char FAR *buf, int len, int flags );

int sendto(int sockfd, const void *msg,int len unsigned int flags, const struct sockaddr *to, int tolen);

sendto就是在发送的时候指定目标Ip和端口,每次发送都要指定,麻烦,因此我们使用send。

send步骤是先connect绑定目标地址:

    struct sockaddr_in server;    
    int len =sizeof(server);

    server.sin_family=AF_INET;
    server.sin_port=htons(DEST_PORT);
    server.sin_addr.s_addr=inet_addr(DEST_IP);
    connect(socket1, (const sockaddr *)&server, len) ;

然后每次发送只需要调用send:

send( socket1, sendbuf, bytes, 0 );


二、将h264打包成rtp并发送

从上节将的rtp协议解析中可以得知,只需要将nalu数据加上特定的头部信息,即可得到rtp数据包。废话不多说,直接上代码:

void RtpSender::sendAnNalu(NALU_t *n, int framerate)
{

    char* nalu_payload;
    char sendbuf[1500] = {0};

    int	bytes=0;

    if (framerate <= 0)
    {
        framerate = 15;
    }

    timestamp_increse=(unsigned int)(90000.0 / framerate);

//    printf("start send ...
");

    memset(sendbuf,0,1500);//清空sendbuf;此时会将上次的时间戳清空,因此需要ts_current来保存上次的时间戳值
//rtp固定包头,为12字节,该句将sendbuf[0]的地址赋给rtp_hdr,以后对rtp_hdr的写入操作将直接写入sendbuf。
    rtp_hdr =(RTP_FIXED_HEADER*)&sendbuf[0];
    //设置RTP HEADER,
    rtp_hdr->payload     = H264;  //负载类型号,
    rtp_hdr->version     = 2;  //版本号,此版本固定为2
    rtp_hdr->marker    = 0;   //标志位,由具体协议规定其值。
    rtp_hdr->ssrc        = htonl(10);    //随机指定为10,并且在本RTP会话中全局唯一

//	当一个NALU小于1400字节的时候,采用一个单RTP包发送
    if(n->len<=1400)
    {
        //设置rtp M 位;
        rtp_hdr->marker=1;
        rtp_hdr->seq_no     = htons(seq_num ++); //序列号,每发送一个RTP包增1
        //设置NALU HEADER,并将这个HEADER填入sendbuf[12]
        nalu_hdr =(NALU_HEADER*)&sendbuf[12]; //将sendbuf[12]的地址赋给nalu_hdr,之后对nalu_hdr的写入就将写入sendbuf中;
        nalu_hdr->F=n->forbidden_bit;
        nalu_hdr->NRI=n->nal_reference_idc>>5;//有效数据在n->nal_reference_idc的第6,7位,需要右移5位才能将其值赋给nalu_hdr->NRI。
        nalu_hdr->TYPE=n->nal_unit_type;

        nalu_payload=&sendbuf[13];//同理将sendbuf[13]赋给nalu_payload
        memcpy(nalu_payload,n->buf+1,n->len-1);//去掉nalu头的nalu剩余内容写入sendbuf[13]开始的字符串。

        ts_current=ts_current+timestamp_increse;
        rtp_hdr->timestamp=htonl(ts_current);
        bytes=n->len + 12 ;//获得sendbuf的长度,为nalu的长度(包含NALU头但除去起始前缀)加上rtp_header的固定长度12字节
        send( socket1, sendbuf, bytes, 0 );//发送rtp包
    //	Sleep(100);

    }
    else if(n->len>1400)
    {
        //得到该nalu需要用多少长度为1400字节的RTP包来发送
        int k=0,l=0;
        k=n->len/1400;//需要k个1400字节的RTP包
        l=n->len00;//最后一个RTP包的需要装载的字节数
        int t=0;//用于指示当前发送的是第几个分片RTP包
        ts_current=ts_current+timestamp_increse;
        rtp_hdr->timestamp=htonl(ts_current);
        while(t<=k)
        {
            rtp_hdr->seq_no = htons(seq_num ++); //序列号,每发送一个RTP包增1
            if(!t)//发送一个需要分片的NALU的第一个分片,置FU HEADER的S位
            {
                //设置rtp M 位;
                rtp_hdr->marker=0;
                //设置FU INDICATOR,并将这个HEADER填入sendbuf[12]
                fu_ind =(FU_INDICATOR*)&sendbuf[12]; //将sendbuf[12]的地址赋给fu_ind,之后对fu_ind的写入就将写入sendbuf中;
                fu_ind->F=n->forbidden_bit;
                fu_ind->NRI=n->nal_reference_idc>>5;
                fu_ind->TYPE=28;

                //设置FU HEADER,并将这个HEADER填入sendbuf[13]
                fu_hdr =(FU_HEADER*)&sendbuf[13];
                fu_hdr->E=0;
                fu_hdr->R=0;
                fu_hdr->S=1;
                fu_hdr->TYPE=n->nal_unit_type;


                nalu_payload=&sendbuf[14];//同理将sendbuf[14]赋给nalu_payload
                memcpy(nalu_payload,n->buf+1,1400);//去掉NALU头

                bytes=1400+14;//获得sendbuf的长度,为nalu的长度(除去起始前缀和NALU头)加上rtp_header,fu_ind,fu_hdr的固定长度14字节
                send( socket1, sendbuf, bytes, 0 );//发送rtp包
                t++;

            }
            //发送一个需要分片的NALU的非第一个分片,清零FU HEADER的S位,如果该分片是该NALU的最后一个分片,置FU HEADER的E位
            else if(k==t)//发送的是最后一个分片,注意最后一个分片的长度可能超过1400字节(当l>1386时)。
            {

                //设置rtp M 位;当前传输的是最后一个分片时该位置1
                rtp_hdr->marker=1;
                //设置FU INDICATOR,并将这个HEADER填入sendbuf[12]
                fu_ind =(FU_INDICATOR*)&sendbuf[12]; //将sendbuf[12]的地址赋给fu_ind,之后对fu_ind的写入就将写入sendbuf中;
                fu_ind->F=n->forbidden_bit;
                fu_ind->NRI=n->nal_reference_idc>>5;
                fu_ind->TYPE=28;

                //设置FU HEADER,并将这个HEADER填入sendbuf[13]
                fu_hdr =(FU_HEADER*)&sendbuf[13];
                fu_hdr->R=0;
                fu_hdr->S=0;
                fu_hdr->TYPE=n->nal_unit_type;
                fu_hdr->E=1;

                nalu_payload=&sendbuf[14];//同理将sendbuf[14]的地址赋给nalu_payload
                memcpy(nalu_payload,n->buf+t*1400+1,l-1);//将nalu最后剩余的l-1(去掉了一个字节的NALU头)字节内容写入sendbuf[14]开始的字符串。
                bytes=l-1+14;//获得sendbuf的长度,为剩余nalu的长度l-1加上rtp_header,FU_INDICATOR,FU_HEADER三个包头共14字节
                send( socket1, sendbuf, bytes, 0 );//发送rtp包
                t++;
            //	Sleep(100);
            }
            else if(t<k&&0!=t)
            {
                //设置rtp M 位;
                rtp_hdr->marker=0;
                //设置FU INDICATOR,并将这个HEADER填入sendbuf[12]
                fu_ind =(FU_INDICATOR*)&sendbuf[12]; //将sendbuf[12]的地址赋给fu_ind,之后对fu_ind的写入就将写入sendbuf中;
                fu_ind->F=n->forbidden_bit;
                fu_ind->NRI=n->nal_reference_idc>>5;
                fu_ind->TYPE=28;

                //设置FU HEADER,并将这个HEADER填入sendbuf[13]
                fu_hdr =(FU_HEADER*)&sendbuf[13];
                //fu_hdr->E=0;
                fu_hdr->R=0;
                fu_hdr->S=0;
                fu_hdr->E=0;
                fu_hdr->TYPE=n->nal_unit_type;

                nalu_payload=&sendbuf[14];//同理将sendbuf[14]的地址赋给nalu_payload
                memcpy(nalu_payload,n->buf+t*1400+1,1400);//去掉起始前缀的nalu剩余内容写入sendbuf[14]开始的字符串。
                bytes=1400+14;//获得sendbuf的长度,为nalu的长度(除去原NALU头)加上rtp_header,fu_ind,fu_hdr的固定长度14字节
                send( socket1, sendbuf, bytes, 0 );//发送rtp包
                t++;
            }
        }
    }

//    printf("send finished
");

}

这里需要注意的是,上面的nalu中是不包含起始码的。



前面播放H264文件的例子中我们已经实现了读取H264文件,并从中获取到一个个的NALU。

不懂得如何获取nalu的请自行查看前面的文章。



总结一下,将h264文件打包成rtp流发送的流程大概就是:

1.从h264文件中读取出Nalu

2.将nalu打包成rtp

3.调用socket的send函数发送。


至此我们的功能就完成了。


至于如何播放我们发送出去的rtp流,请查看下节内容。


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

H264测试文件下载地址:https://download.csdn.net/download/qq214517703/10422777


======bug修复 Begin =======

2018-06-09更新:

1.上面的代码有一个bug:

当一个nalu大于1400且大小刚好是1400倍数的时候,发送有问题。

修复后的完整工程下载地址:

下载地址:https://download.csdn.net/download/qq214517703/10466742

======  End =======


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

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