/****************************************************************************************** Copyright(C), 2019-2021, 个人。 文件名:main.c 作者:Wind 版本:V1.0 日期:2020.4.1 文件描述: 对纯视频H264码流的RTP传输的实现。 其它说明: 当前文件仅用于个人学习。 历史修改记录: 1. 2020-4-1:Wind 创建。 2. 2021-6-19:Wind 修改格式以向实验室开放。 ******************************************************************************************/ //+------------------------------------------------------------------------------------------+ //| RTP测试说明 //+------------------------------------------------------------------------------------------+ //| 测试使用VLC播放器进行,首先建立文件输入以下内容并保存为.sdp文件: //| m=video 1118 RTP/AVP 96 //| a=rtpmap:96 H264 //| a=framerate:30 //| c=IN IP4 169.254.134.37 //| 其中端口和RTP发送端地址需要根据实际设置修改,使用VLC打开此.sdp文件即可进行播放。 //| 注:如果VLC播放器提示SDP文件格式不正确,可更换VLC版本再试。 //| 注:播放前可能需首先关闭防火墙。 //+------------------------------------------------------------------------------------------+ //| 头文件包含 //+------------------------------------------------------------------------------------------+ /*|*/#include /*|*/#include /*|*/#include /*|*/#include /*|*/#include /*|*/ /*|*/#include "L_RTP.h" //+------------------------------------------------------------------------------------------+ //| 函数名称:L_RTP_CreateSession //| 功能描述:创建一个RTP会话,主要是打开对客户端的Socket端口。 //| 参数说明:L_STRUCT_RTPSESSION结构体指针 //| 返回值说明:成功返回0,失败返回-1。 //| 备注:初始化中,执行如下操作: //| 1. 检测结构体是否已经初始化过 //| 2. 打开一个数据报套接字端口 //| 3. 按需设置socket属性 //| 4. 获取本机IP //| 5. 置结构体内已初始化标志位 //| 以上任意一步失败则回滚操作并退出。 //+------------------------------------------------------------------------------------------+ int L_RTP_CreateSession(L_STRUCT_RTPSESSION* RTPSession) { //**************************************** //这里的判断寄期望于编译器初始化结构体的时候 //将此变量赋值为0,或者调用函数在设置参数时将 //此标志位置0。 //**************************************** if(RTPSession->flag_inited) { printf("The Struct has been inited!\n"); goto QUIT; } //对于没有初始化的结构体,某些变量需要初始化 RTPSession->s32Sock = -1; //描述符无效 RTPSession->pData = NULL; //数据指针为空 RTPSession->DataLength = 0; //数据长度为0 RTPSession->DataType = RTP_NONE;//荷载类型无效 RTPSession->SequenceNum = 0; //RTP包序列号 //**************************************** //关于Unix中套接字使用的协议族,创建套接字时 //采用PF_x,设置套接字时采用AF_x,这两种形式 //的差别并不大,甚至可以混用。 //**************************************** if((RTPSession->s32Sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) { printf("Open socket failed!\n"); goto QUIT; } //如果地址为255.x.x.x则设置套接字允许广播 if(0xFF000000 == (RTPSession->u32DestIP&0xFF000000)) { int s32Broadcast = 1; if(-1 == setsockopt(RTPSession->s32Sock, SOL_SOCKET, SO_BROADCAST, (char *)&s32Broadcast, sizeof(s32Broadcast))) { printf("Set socket failed!\n"); goto QUIT; } } //填充目的端地址 RTPSession->stDestAddr.sin_family = AF_INET; RTPSession->stDestAddr.sin_port = htons(RTPSession->DestPort); RTPSession->stDestAddr.sin_addr.s_addr = RTPSession->u32DestIP; bzero(&(RTPSession->stDestAddr.sin_zero), 8); //**************************************** //获取本机网络设备名 //此处只尝试获取了eth0(有线网络)和wlan0 //(无线网络),但是并非所有标号都是这两种, //在多网卡情况下或者使用特殊的网卡,默认名称 //会随之更换,程序中也应作出修改。 //**************************************** strcpy(RTPSession->stIfreq.ifr_name, "eth0"); if(ioctl(RTPSession->s32Sock, SIOCGIFADDR, &(RTPSession->stIfreq)) < 0) { printf("Get eth0 IP failed!\n"); strcpy(RTPSession->stIfreq.ifr_name, "wlan0"); if(ioctl(RTPSession->s32Sock, SIOCGIFADDR, &(RTPSession->stIfreq)) < 0) { printf("Get wlan0 IP failed!\n"); goto QUIT; } } //**************************************** //计算本机IP的网络字节序并保存 //RTP是一个单向的发送协议,因此对于发送端而言 //不需要指定源端口号,如果需要设置可以在此处 //初始化的时候使用bind函数对Socket进行定位。 //在客户端接收数据的时候也不需要发送端的端口号, //客户端监听的是目的端口号。 //**************************************** RTPSession->u32SrcIP = htonl(((struct sockaddr_in *)(&RTPSession->stIfreq.ifr_addr))->sin_addr.s_addr); RTPSession->flag_inited = 1;//置标志位,表示该结构指代的RTP会话已经初始化成功 return 0; QUIT: //Socket描述符不为-1说明已经打开,故异常退出需要关闭 if(RTPSession->s32Sock >= 0) { close(RTPSession->s32Sock); } return -1; } //+------------------------------------------------------------------------------------------+ //| 函数名称:L_RTP_DestorySession //| 功能描述:销毁一个RTP会话,主要是销毁对客户端的Socket端口。 //| 参数说明:L_STRUCT_RTPSESSION结构体指针 //| 返回值说明:成功返回0,失败返回-1。 //| 备注: //+------------------------------------------------------------------------------------------+ int L_RTP_DestorySession(L_STRUCT_RTPSESSION* RTPSession) { if(!RTPSession->flag_inited) { printf("The Struct has NOT been inited!\n"); goto QUIT; } close(RTPSession->s32Sock); RTPSession->flag_inited = 0; RTPSession->pData = NULL; RTPSession->DataLength = 0; RTPSession->DataType = RTP_NONE; return 0; QUIT: return -1; } //+------------------------------------------------------------------------------------------+ //| 函数名称:L_RTP_Send_H264NALU //| 功能描述:发送H264数据流。 //| 参数说明:L_STRUCT_RTPSESSION结构体指针 //| 返回值说明:成功返回0,失败返回-1。 //| 备注:用此函数发送的数据流不应包含起始码。 //+------------------------------------------------------------------------------------------+ static int L_RTP_Send_H264NALU(L_STRUCT_RTPSESSION* RTPSession) { int ret = 0; unsigned char NALUByte; L_STRUCT_RTPHEADER *pRTPHeader; unsigned char *pRTPSendBuf; //**************************************** //RTP首部是12个字节,通过对结构体的强制类型转换 //将RTP首部的存放位置与申请的发送缓冲区共享。 //**************************************** pRTPSendBuf = (unsigned char *)calloc(RTP_H264_PACKAGE_LENGTH+60, sizeof(unsigned char)); if(pRTPSendBuf == NULL) { printf("Calloc failed!\n"); ret = -1; goto QUIT; } pRTPHeader = (L_STRUCT_RTPHEADER *)pRTPSendBuf; //初始化RTP首部公共部分 pRTPHeader->u7Payload = RTP_HEAD_PAYLOAD_H264; pRTPHeader->u2Version = 2; pRTPHeader->u1Marker = 0; pRTPHeader->u32SSrc = RTPSession->u32SrcIP; //**************************************** //时间戳计算 //这里计算使用的是编码器生成H264码流的时候 //码流包中携带的时间戳信息,为无符号64位整型值, //单位是us。 //RTP码流中视频时间戳是基于90KHz的,故其单位为 //1/90000,RTP首部中的时间戳要换算成该单位下 //的变量,方法是(以下计算忽略了变量范围): //RTPTimeStamp=H264TimeStamp*90000/1000000。 //**************************************** pRTPHeader->u32TimeStamp = htonl((unsigned long)(RTPSession->TimeStamp*9/100)); //**************************************** //由于RTP的定义时依照大端模式进行的,所以 //所有有关RTP协议部分的参数都需要转换成 //对应格式,这是NALU头需要单独提取的原因。 //**************************************** //提取NALU头 NALUByte = *(RTPSession->pData); RTPSession->pData++; RTPSession->DataLength--; //**************************************** //分片处理,对分片的说明见分片长度宏定义注释 //分片和不分片,对NALU头的处理方式是不同的。 //在不分片的情况下,一个RTP包格式是这样的: //RTP首部(12Bytes)+NALU(1Byte)+数据 //在分片的时候,分片前端是这样的: //RTP首部(12Bytes)+FUA指示(1Byte) //+FUA头(1Byte)+数据+... //这时,NALU头会被分为两个部分分别存放在 //FUA指示字节和FUA头中。 //**************************************** if(RTPSession->DataLength <= RTP_H264_PACKAGE_LENGTH) { L_STRUCT_RTP_NALUHEADER *pRTPNALUHeader; pRTPHeader->u1Marker = 1; pRTPHeader->u16SeqNum = htons(RTPSession->SequenceNum++); //将NALU头写到RTP首部之后(占用1Byte) pRTPNALUHeader = (L_STRUCT_RTP_NALUHEADER *)(pRTPSendBuf+12); pRTPNALUHeader->u1F = (NALUByte & 0x80) >> 7; pRTPNALUHeader->u2Nri = (NALUByte & 0x60) >> 5; pRTPNALUHeader->u5Type = NALUByte & 0x1f; //拷贝数据流到缓冲区 memcpy(pRTPSendBuf+13, RTPSession->pData, RTPSession->DataLength); //发送数据到目的地址 if(sendto(RTPSession->s32Sock, pRTPSendBuf, RTPSession->DataLength+13, 0, (struct sockaddr *)&RTPSession->stDestAddr, sizeof(RTPSession->stDestAddr)) < 0) { printf("Socket send failed!\n"); ret = -1; goto QUIT; } } else { L_STRUCT_RTP_FUA_INDICATOR *pRTPFUAIndicator; L_STRUCT_RTP_FUA_HEADER *pRTPFUAHeader; int tmp_is_first = 1;//用于指示分批发送的数据是否为第一批 int tmp_sendlength = 0;//用于记录每次发送的数据长度 //填充FUA指示字节 pRTPFUAIndicator = (L_STRUCT_RTP_FUA_INDICATOR *)(pRTPSendBuf+12); pRTPFUAIndicator->u1F = (NALUByte & 0x80) >> 7; pRTPFUAIndicator->u2Nri = (NALUByte & 0x60) >> 5; pRTPFUAIndicator->u5Type = 28; //填充FUA头固定部分 pRTPFUAHeader = (L_STRUCT_RTP_FUA_HEADER *)(pRTPSendBuf+13); pRTPFUAHeader->u1R = 0; pRTPFUAHeader->u5Type = NALUByte & 0x1f; //分批发送数据 while(RTPSession->DataLength>0) { //配置每包RTP首部 pRTPHeader->u16SeqNum = htons(RTPSession->SequenceNum++); pRTPHeader->u1Marker = (RTPSession->DataLength <= RTP_H264_PACKAGE_LENGTH)?1:0; //配置FU头 pRTPFUAHeader->u1E = (RTPSession->DataLength<=RTP_H264_PACKAGE_LENGTH)?1:0; if(tmp_is_first == 1) { pRTPFUAHeader->u1S = 1; tmp_is_first = 0; } else { pRTPFUAHeader->u1S = 0; } //计算每次数据提取长度并存入缓冲 tmp_sendlength = (RTPSession->DataLength <= RTP_H264_PACKAGE_LENGTH)?RTPSession->DataLength:RTP_H264_PACKAGE_LENGTH; memcpy(pRTPSendBuf+14, RTPSession->pData, tmp_sendlength); //计算发送数据长度并发送数据 tmp_sendlength += 14; if(sendto(RTPSession->s32Sock, pRTPSendBuf, tmp_sendlength, 0, (struct sockaddr *)&RTPSession->stDestAddr, sizeof(RTPSession->stDestAddr)) < 0) { printf("Socket send failed!\n"); ret = -1; goto QUIT; } RTPSession->pData += RTP_H264_PACKAGE_LENGTH; RTPSession->DataLength -= RTP_H264_PACKAGE_LENGTH; } //发送完成后复位数据指针和长度变量 RTPSession->pData = NULL; RTPSession->DataLength = 0; } QUIT: if(pRTPSendBuf != NULL) { free((void *)pRTPSendBuf); } return ret; } //+------------------------------------------------------------------------------------------+ //| 函数名称:L_RTP_Send //| 功能描述:通过RTP发送数据。 //| 参数说明:L_STRUCT_RTPSESSION结构体指针 //| 返回值说明:成功返回0,失败返回-1。 //| 备注:对于测试使用的海思平台编码的H264流,发送需要采取多包模式,每包一发,这是本程序的去 //| 起始码部分的编写方式决定的。 //+------------------------------------------------------------------------------------------+ int L_RTP_Send(L_STRUCT_RTPSESSION* RTPSession) { if(RTPSession->DataType == RTP_NONE || RTPSession->pData == NULL || RTPSession->DataLength == 0) { printf("Structure is not effectively populated!\n"); goto QUIT; } switch(RTPSession->DataType) { case RTP_H264: //**************************************** //首先去掉数据流的起始码(00 00 00 01) //H264起始码占有4个字节,所以对于有起始码的数据 //只需要使其指针指向的位置后移4位。 //对应的数据长度减少4即可。 // //然而需要注意的是,有可能不是所有的编码器都是 //4个字节的起始码。 //**************************************** RTPSession->pData = RTPSession->pData + 4; RTPSession->DataLength = RTPSession->DataLength - 4; case RTP_H264NALU: L_RTP_Send_H264NALU(RTPSession); break; case RTP_NONE: default: printf("There is no corresponding operation function for this type!\n"); goto QUIT; } return 0; QUIT: return -1; }