最近看了一些VC++音视频编解码技术实践相关的书籍,收获颇多,于是结合书籍内容与网上相关资料,将所见所得分享给大家,一起学习共同进步!本文中,首先列出音频常见的名词解释,接着讲下常见音频编解码协议,音频传输低层协议,也会介绍一个基于TCP、UDP的即时语音通讯的源码架构,并相应拓展,简单介绍音频传输高层协议如实时传输协议RTP相关的协议内容及应用场景。
1.音频常见名词解释
音频:指人耳可以听到的声音频率在20HZ~20kHz之间的声波(声音是一种横波,频率就是声波每秒震动的次数),称为音频
频率:单位时间内完成周期性变化的次数,单位命名为赫兹(工频、声频、转角频率...)
采样率:每秒从连续信号中提取并组成离散信号的采样个数,用赫兹(Hz)来表示(奈奎斯特采样定理)
采样位数:声卡在采集和播放声音文件时所使用数字声音信号的二进制位数
频道数:声音的通道的数目。常有单声道和立体声之分,单声道的声音只能使用一个喇叭发声(有的也处理成两个喇叭输出同一个声道的声音),立体声可以使两个喇叭都发声(一般左右声道有分工)
带宽:数字信号系统中,带宽用来标识通讯线路所能传送数据的能力,即在单位时间内通过网络中某一点的最高数据率,常用的单位为bps(又称为比特率---bit per second,每秒多少比特)
2.常见音频编解码协议
音频编解码协议主要有G711,G723,G729,视频编解码协议主要有H.261,H.263,本文主要介绍音频传输,因此只针对常见音频编码协议进行介绍。
G.711与G.729语音带宽的计算方法
我们知道G.711与G.729的带宽分别是80Kbps和24Kbps。这个结果那么是如何计算出来的呢。
恩奎斯特原理规定声音的采样频率是每秒8000次,每次8bit,语音数据带宽就是64Kbps(8000*8)。正常语音包是10ms成帧一次,每两帧成一个包。这样,每秒就成50个包(1000/20)。每个包的IP头是20byte,UDP头8byte,RTP头12byte,总共 40byte,即320bits(40 * 8)。50个包就是16000bits(320 * 50),或16K。就是说全部包头的带宽要求是16Kbps。加上语音数据,全部带宽就是80Kbps(64+16)。那么每个包的大小是多少呢?80Kbits/50=1600bits=200byte(1600 /8)。其中,语音数据的大小是160byte(200-40)。这是G.711。
G.729采用了压缩算法,语音数据大小是20byte(显然,比起G.711的160byte,压缩比为8:1),包头不变还是40byte,一共就是60byte。带宽要求就是60 * 8 * 50=24000bps=24Kbps。这是G.729。
由于相对数据负载,包头太大(2倍),看上去似乎头重脚轻,所以G.729在WAN的电路上往往对包头进行压缩。压缩过后的包头是4byte或2byte。带宽要求将进一步减少。
3.传输低层协议
由于音、视频的特殊性(数据量大、对时延敏感等),在传统的TCP/UDP传输协议之上,开发岀一系列的协议,来加强网络在传输音、视频等多媒体数据方面的能力。对音、视频而言,传输层协议可划分为两个层次: 高层协议和低层协议,如下图所示。在发送端,应用层的音、视频数据,首先通过传输高层协议(如RTP)的封装,然后再将其封装在传输低层协议(如UDP)中交给网络层,接收端则进行相反的操作。上下两层传输协议通过套接字联系在一起。
1.TCP
TCP是面向连接的传输控制协议,它由RFC793定义。TCP利用网络层IP协议提供的不 可靠的分组传输服务,为应用进程提供可靠的、端到端的、面向连接的字节流通信。Internet 许多著名的服务,如Telnet、FTP、HTTP等,都采用TCP作为其传输协议。TCP的主要功能概括如下:
一旦建立TCP连接,上层数据将源源不断地发送到TCP缓存中,TCP协议将数据分块, 并为每一块数据加上TCP报头,形成TCP消息段(segment),其形式如下图所示,该消息段向下将与IP报头构成IP数据报。
尽管TCP传输控制协议与网络层的IP协议,构成了当前Internet传输的主干,但是音、 视频的传输一般不采用TCP,主要原因如下:
•TCP的重传机制对于实时性要求高的音、视频数据传输来说几乎是灾难姓的,极容易 造成时延和断点。
•TCP的拥塞控制机制,在探测到有数据包丢失时,TCP会减小它的滑动窗口的大小; 而另一方面,音、视频码率是不可能突变的.
•TCP的报头比下面介绍的UDP大。而且TCP的报头不能提供时间戳和编解码等信息,
•TCP的启动需要建立连接,初始化过程需要较长的时间,从而会造成较大的启动延时, 対某些音视频应用而言,这恰恰是不期望的。
基于上述原因,TCP传输控制协议一般不直接用来传送音、视频数据本身,但是,对于 音、视频传输中的控制信息而言,TCP是最合适的。
2.UDP
UDP是无连接的用户数据报传输协议,它由RFC768标准定义…与TCP类似,UDP为 每个来自上层的数据块加上一个UDP报头,形成UDP消息段,其形式如下图所示,从图中不难看出,与TCP相比,UDP的报头要简单得多,其中校验和用于检査传输中是否出现错误;数据包长度包含图中所有5个域的字节数,
下图总结了 TCP传输控制协议和UDP用户数据报协议之间的主要不同
如图所示,对首、视频数据的传输而言(对时延敏感,但可以容忍一定的错误), UDP比TCP更为适用。尽管TCP是一个很成功的协议,但是它无法满足音、视频传输的需要。TCP的大量确认应答使音、视频数据不得不因为等待应答而放弃,造成不必要的延迟和更大范围的数据丢失。相比较而言,UDP只要网络流量足够,音、视频数据就可以源源不断的到达接收者。因此,在IP网络上传送视频数据,往往采用UDP协议,而不是TCP协议。
此外,如果为每个UDP包加上一个包含时标和序号的域,接收端再配以适当的缓冲,
就可以利用这些信息再生数据包,记录失序包,同步音、视频数据以及改善重放效果等。
4.基于TCP、UDP的即时语音通讯的源码架构
源码结构主要分为三大部分,分别是语音会话控制、音频传输、编解码及录音回放
源码结构UML图
源码结构流程图
下面针对每个模块重点进行分析,首先是语音会话控制部分,ListenSocket在程序运行时会自动监听会话控制连接端口,当有会话连接请求时,判断是否已经处于会话中,若无,建立tcp接收端控制连接实例并设置连接状态
//重载OnAccept() 函数
void CListenSocket::OnAccept(int nErrorCode)
{
//客户端地址
SOCKADDR add;
//客户端地址长度
int iLen;
iLen = sizeof(SOCKADDR);
//临时socket
CSocket soTemp;
//已经连接
if (m_sopClient->m_bConnect )
{
//用临时socket接收连接请求
soTemp.Accept (*this);
//关闭
soTemp.Close ();
//返回
return ;
}
//接受连接请求
if (!Accept(*m_sopClient,&add,&iLen))
{
//返回
return ;
}
//设置连接标记
m_sopClient->m_bConnect = TRUE;
//调用基类OnAccept()函数
CSocket::OnAccept(nErrorCode);
}
SendSocket在发起会话请求时创建控制连接实例请求对方连接,并且设置udp发送套接字单播目的ip及端口,当socket返回连接成功时发送正常会话帧请求会话,若收到同意回复,则设置录音数据允许发送标记,并且在会话建立后被动关闭进行会话处理
void CSendClient::OnConnect(int nErrorCode)
{
//获取连接结果
m_pInterFace->ConnectResult(nErrorCode);
//调用基类OnConnect()函数
CAsyncSocket::OnConnect(nErrorCode);
}
//重载OnReceive()函数
void CSendClient::OnReceive(int nErrorCode)
{
struct TalkFrame *frame;
frame = (struct TalkFrame *)m_pBuffer;
int iLen = sizeof(struct TalkFrame);
//接收缓存中所有的TalkFrame结构
while(iLen > 0)
{
//接收数据
int i = Receive (
m_pBuffer + sizeof(struct TalkFrame) - iLen,iLen);
//出错
if (i == SOCKET_ERROR )
//返回
return ;
iLen -= i;
}
//如果不是正常数据就返回
if (strcmp(frame->cFlag ,"TalkFrame") != 0)
{
return;
}
iLen = frame->iLen;
//接收缓存中所有的音频数据
while (iLen > 0)
{
//接收数据
int i = Receive (m_pBuffer + sizeof(struct TalkFrame)
+ (frame->iLen - iLen),iLen);
//出错
if (i == SOCKET_ERROR)
//返回
return ;
iLen -= i;
}
//对方地址
CString add;
//端口
UINT port;
switch (frame->iCom )
{
//正常通信帧
case TC_AGREE_TALK:
//获取连接对方地址和端口
GetPeerName (add,port);
//提示开始工作
m_pInterFace->TalkStart (add);
//允许发送数据
m_pIn->EnableSend (TRUE);
//设置允许工作标志
m_pInterFace->m_bWork = TRUE;
break;
default:
break;
}
//调用基类OnReceive()函数
CAsyncSocket::OnReceive(nErrorCode);
}
void CSendClient::OnClose(int nErrorCode)
{
//设置连接断开标志
m_bConnect = FALSE;
//关闭
m_pInterFace->BeClose ();
//调用基类OnClose()函数
CAsyncSocket::OnClose(nErrorCode);
}
ClientSocket用于接收对方连接请求建立的控制连接socket,主要用于同意,拒绝会话发起方请求以及会话建立后可进行主动关闭连接及进行被动关闭连接后续处理
void CClientSocket::OnClose(int nErrorCode)
{
//设置关闭标志
m_bConnect = FALSE;
//关闭
m_pInterface->BeClose ();
//调用基类OnClose()函数
CSocket::OnClose(nErrorCode);
}
void CClientSocket::OnReceive(int nErrorCode)
{
struct TalkFrame *frame;
frame = (struct TalkFrame *)m_pBuffer;
//接收缓存中的所有TalkFrame结构
int iLen = sizeof(struct TalkFrame);
while(iLen > 0)
{
//接收
int i = Receive (m_pBuffer +
sizeof(struct TalkFrame) - iLen,iLen);
//出错
if (i == SOCKET_ERROR )
return ;
iLen -= i;
}
//如果是非法数据就返回
if (strcmp(frame->cFlag ,"TalkFrame") != 0)
{
//返回
return;
}
//接收缓存中的所有音频数据
iLen = frame->iLen;
while (iLen > 0)
{
//接收
int i = Receive (m_pBuffer + sizeof(struct TalkFrame)
+ (frame->iLen - iLen),iLen);
//出错
if (i == SOCKET_ERROR )
return ;
//修改循环标志
iLen -= i;
}
//地址
CString add;
//端口
UINT port;
switch (frame->iCom )
{
//正常通信帧
case TC_NORMAL_TALK:
//初始化
memset(frame,0,sizeof (struct TalkFrame));
//设置数据帧标志
sprintf(frame->cFlag,"TalkFrame");
//获得与此套接字相连的地址和端口
GetPeerName (add,port);
//处于连接状态
if (m_pInterface ->IsConnect (add))
{
//设置通信帧标志及其长度
frame->iCom = TC_AGREE_TALK;
frame->iLen = 0;
//出错
if (SOCKET_ERROR == Send (m_pBuffer,sizeof(struct TalkFrame)))
{
break ;
};
//提示开始通信
m_pInterface->TalkStart (add);
//设置IP地址
m_pUdp->SetIp (add);
//允许发送
m_pIn->EnableSend (TRUE);
//允许工作
m_pInterface->m_bWork = TRUE;
break;
};
//设置通信帧标志
frame->iCom = TC_DISAGREE_TALK;
//设置通信帧长度
frame->iLen = 0;
//发送
Send (m_pBuffer,sizeof(struct TalkFrame));
//关闭
Close ();
break;
default:
break;
}
//调用基类OnReceive()函数
CSocket::OnReceive(nErrorCode);
}
接着是udp传输音频流,注重将对于实时传输udp数据报文失序及丢失的情况下如何处理进行分析
void CSortData::ReceiveData(char *pBuffer, int iLen)
{
struct Frame *m_pFrame;
m_pFrame = (struct Frame *)pBuffer;
TRACE("Rece %d %d .\n",iLen,m_pFrame->iIndex );
if (m_pFrame->iIndex < m_iLast + 1)
{
TRACE("Lost %d.\n",m_pFrame->iIndex);
}
if (m_pFrame->iIndex == m_iLast + 1)
{
unsigned __int32 iTemp;
///play //it is right next data in buffer
Play(pBuffer + sizeof(struct Frame),60);
m_iLast = m_pFrame->iIndex ;
TRACE("Receive and paly %d.\n",m_iLast);
iTemp = m_pFrame->iIndex;
int iNext;
for (iNext= 0;iNext < DELAY_BUFFER;iNext ++)
{
if (m_iFill[iNext] != iTemp + 1)
break;
iTemp = m_iFill[iNext];
}
if (iNext != 0)
{
//paly right
for (iTemp =0;iTemp < (unsigned int)iNext;iTemp ++)
{
Play(m_pBuffer[iTemp],60);
TRACE("Play %d.\n",m_iFill[iTemp]);
m_iFill[iTemp] = 0;
}
//move back
int iHead;
iHead = 0;
for (iTemp = iNext;iTemp < DELAY_BUFFER;iTemp ++)
{
if (m_iFill[iTemp] != 0)
{
m_iFill[iHead] = m_iFill[iTemp];
memcpy(m_pBuffer[iHead],m_pBuffer[iTemp],60);
m_iFill[iTemp] = 0;
iHead = 0;
}
else
{
break;
}
}
}
}
int iNow;
int iEnd;
int iMove;
for (iNow = 0;iNow < DELAY_BUFFER ;iNow++)
{
if (m_iFill[iNow] != 0)
{
//insert
if (m_iFill[iNow] > m_pFrame->iIndex)
{
for (iEnd = iNow;iEnd < DELAY_BUFFER;iEnd ++)
{
if (m_iFill[iEnd] == 0)
{
break;
}
}
for (iMove = iEnd;iMove > iNow;iMove --)
{
m_iFill[iMove] = m_iFill[iMove - 1];
memcpy(m_pBuffer[iMove],m_pBuffer[iMove - 1],60);
}
memcpy(m_pBuffer[iNow],pBuffer + sizeof(struct Frame),60);
m_iFill[iNow] = m_pFrame->iIndex;
break;
}
}
//append
else
{
memcpy(m_pBuffer[iNow],pBuffer + sizeof(struct Frame),60);
m_iFill[iNow] = m_pFrame->iIndex;
break;
}
}
//buffer full
if (m_iFill[DELAY_BUFFER - 1] != 0)
{
m_iLast = m_iFill[DELAY_BUFFER - 1];
for (iNow = 0;iNow < DELAY_BUFFER ;iNow++)
{
//paly all;
Play(m_pBuffer[iNow],60);
TRACE("Play %d.\n",m_iFill[iNow]);
m_iFill[iNow] = 0;
}
}
}
上面是管理数据,调整乱序包的顺序处理逻辑,代码中会维护一份长度为32个报文长度的缓存以及一个长度为24报文标记数组用来记录当前是否有失序报文,首先当接收到报文时,判断当前报文是否是上一次正常播放顺序的理想下一序号报文,若是则播放此帧音频,并且到标记数组中起始处顺序遍历查找是否有此报文期待的下一序号报文(存在乱序接收,之前以及接收过当前报文的下一报文),将这部分报文从缓存中读出并播放,标记数组移除前面输序部分,后面标记移前,在上述步骤之后,将当前报文插入缓存中,通过标记数组记录的缓存对应位置的报文序号,从小到大插入,最后,判断缓存是否已满,若满,则读出所有报文并解码播放,并且设置理想的下一条报文序号为缓存最后一条报文序号+1,最后一步主要是为了增强播音效果。对于音视频乱序,丢包的情况,处理的算法可以根据实际场景去设计。
此节最后讲一下音频编解码及录音回放
////录音设备返回数据
case MM_WIM_DATA:
//录音格式
WAVEHDR* pWH=(WAVEHDR*)msg.lParam;
//释放缓存
waveInUnprepareHeader((HWAVEIN)msg.wParam,pWH,sizeof(WAVEHDR));
//非法数据
if(pWH->dwBytesRecorded!=SIZE_AUDIO_FRAME)
break;
//复制录音数据
memcpy(buffer,pWH->lpData,pWH->dwBytesRecorded);
//设置时戳
pWaveIn->GetData (buffer ,pWH->dwBytesRecorded );
//为音频设备增加一个缓存,准备继续录音
waveInPrepareHeader((HWAVEIN)msg.wParam,pWH,sizeof(WAVEHDR));
waveInAddBuffer((HWAVEIN)msg.wParam,pWH,sizeof(WAVEHDR));
break;
////开启播音
//复制缓存数据
CopyMemory(p,buf,uSize);
//初始化
ZeroMemory(pwh,sizeof(WAVEHDR));
//数据长度
pwh->dwBufferLength=uSize;
//波音数据
pwh->lpData=p;
//为回放数据作好准备
m_mmr=waveOutPrepareHeader(m_hOut,pwh,sizeof(WAVEHDR));
//出错
if (m_mmr)
{
//返回
return FALSE;
}
//将数据发往播音设备
m_mmr=waveOutWrite(m_hOut,pwh,sizeof(WAVEHDR));
//播音完毕
case WOM_DONE:
//播音格式
WAVEHDR* pwh=(WAVEHDR*)msg.lParam;
//释放播音缓存
waveOutUnprepareHeader((HWAVEOUT)msg.wParam,pwh,sizeof(WAVEHDR));
//减少播音缓存数目
pWaveIn->BufferSub ();
//删除Play调用时分配的内存
delete []pwh->lpData;
//删除播音格式
delete pwh;
break;
}
录音时,在Winmm 回调MM_WIM_DATA表示有音频缓存录制完成,取出数据并交由pWaveIn实例去判断需不需要通过udp传输至对话一方,最后清空录音数据,并将其放于Winmm 录音缓存中;回放播音时,每次接收到udp音频数据解码后新建缓存数据并复制数据发往播音设备,在回调播放完毕后将此数据回收
//编码音频数据
BOOL CAudioCode::EncodeAudioData(char *pin,int len,char* pout,int* lenr)
{
//编码成功与否标记
BOOL bRet=FALSE;
//无效输入或输出
if(!pin||len!=SIZE_AUDIO_FRAME||!pout)
goto RET;
//分块进行编码
va_g729a_encoder((short*)pin,(BYTE*)pout);
va_g729a_encoder((short*)(pin+160),(BYTE*)pout+10);
va_g729a_encoder((short*)(pin+320),(BYTE*)pout+20);
va_g729a_encoder((short*)(pin+480),(BYTE*)pout+30);
va_g729a_encoder((short*)(pin+640),(BYTE*)pout+40);
va_g729a_encoder((short*)(pin+800),(BYTE*)pout+50);
//编码长度
if(lenr)
*lenr=SIZE_AUDIO_PACKED;
//编码成功标记
bRet=TRUE;
RET:
//返回
return bRet;
}
//音频解码
BOOL CAudioCode::DecodeAudioData(char *pin,int len,char* pout,int* lenr)
{
//解码成功与否标记
BOOL bRet=FALSE;
//无效输入或输出
if(!pin||len!=SIZE_AUDIO_PACKED||!pout)
goto RET;
//分块解码
va_g729a_decoder((BYTE*)pin,(short*)(pout),0);
va_g729a_decoder((BYTE*)pin+10,(short*)(pout+160),0);
va_g729a_decoder((BYTE*)pin+20,(short*)(pout+320),0);
va_g729a_decoder((BYTE*)pin+30,(short*)(pout+480),0);
va_g729a_decoder((BYTE*)pin+40,(short*)(pout+640),0);
va_g729a_decoder((BYTE*)pin+50,(short*)(pout+800),0);
//解码长度
if(lenr)
*lenr=SIZE_AUDIO_FRAME;
//设置解码成功标记
bRet=TRUE;
RET:
//返回
return bRet;
}
音频编解码主要采用了g729编码,对原始pcm流编码数据压缩比可达16:1,有效的降低通信传输带宽,降低时延。
以上便是源码的大体结构说明,项目源码地址:https://gitee.com/jaymercychen/VoiceChat
实际上对于单一简单的实时通讯场景,udp+tcp的组合大部分都可以满足,涉及到更为复杂的场景,那么就需要更为“专业”的高层协议去满足。
5.传输高层协议内容介绍及应用场景
- RTP
RTP是一种提供一对一或一对多服务的实时传输协议,它在RFC1889定义。RTP允许应用传输不同类型的实时负裁。由于RTP本身不提供任何保证传输的机制,一般它位于UDP 协议之上,依赖低层的协议来实现此功能。对音、视频数据的传输而言,RTP实时传输协议 在UDP协议的基础之上提供以下功能:
•提供时间同步信息;
•对不同媒体类型的报文的分割;
•丢失探测;
•标识内容。
下图展示了rtp协议的具体内容
1.版本号(V):2比特,用来标志使用的RTP版本。
2.填充位(P):1比特,如果该位置位,则该RTP包的尾部就包含附加的填充字节。
3.扩展位(X):1比特,如果该位置位的话,RTP固定头部后面就跟有一个扩展头部。
4,CSRC计数器(CC):4比特,含有固定头部后面跟着的CSRC的数目。
5.标记位(M):1比特,该位的解释由配置文档(Profile)来承担.
6.载荷类型(PT):7比特,标识了RTP载荷的类型。(https://blog.csdn.net/qq_40732350/article/details/88374707)
7.序列号(SN):16比特,发送方在每发送完一个RTP包后就将该域的值增加1,接收方可以由该域检测包的丢失及恢复包序列。序列号的初始值是随机的。
8.时间戳:32比特,记录了该包中数据的第一个字节的采样时刻。在一次会话开始时,时间戳初始化成一个初始值。时间戳是去除抖动和实现同步不可缺少的。
9.同步源标识符(SSRC):32比特,同步源就是指RTP包流的来源。在同一个RTP会话中不能有两个相同的SSRC值。该标识符是随机选取的 RFC1889推荐了MD5随机算法。
10.贡献源列表(CSRC List):0~15项,每项32比特,用来标志对一个RTP混合器产生的新包有贡献的所有RTP包的源。由混合器将这些有贡献的SSRC标识符插入表中。SSRC标识符都被列出来,以便接收端能正确指出交谈双方的身份。
结合代码来看rtp头部数据
/*
* RTP data header
*/
typedef struct{
#if RTP_BIG_ENDIAN
/* byte 0 */
uint8_t v : 2;//版本号
uint8_t p : 1;//填充标志,为1时标识数据尾部有无效填充
uint8_t x : 1;//扩展标志,为1时表示rtp报头后有1个扩展包
uint8_t cc : 4;//crc标识符个数
/* byte 1 */
uint8_t m : 1;//载荷标记,视频为1帧结束,音频为会话开始
uint8_t pt : 7;//有小载荷类型
#else
/* byte 0 */
uint8_t cc : 4;//crc标识符个数
uint8_t x : 1;//扩展标志,为1时表示rtp报头后有1个扩展包
uint8_t p : 1;//填充标志,为1时标识数据尾部有无效填充
uint8_t v : 2;//版本号
/* byte 1 */
uint8_t pt : 7;//有小载荷类型
uint8_t m : 1;//载荷标记,视频为1帧结束,音频为会话开始
#endif
/* bytes 2,3 */
uint16_t seq;//序列号,每帧+1,随机开始,音/视频分开
/* bytes 4-7 */
uint32_t timestamp;//时间戳,us,自增
/* bytes 8-11 */
uint32_t ssrc;//同步信号源
}RtpHeader;
这里需要注意,对于预编译宏RTP_BIG_ENDIAN包起来的区域,采用了字节bit位域的用法,每个变量实际占用不到一个字节,在任何不同系统间的通信信息都经过网络字节(大端)序进行传输,也就是说不管本机是什么模式,都要保证发送端传输的数据转换为网络序,接受端都要把网络序的数据转换为本地序,16bit和32bit的大小端转换很常见,一般不会存在什么问题,但是对于存在bit位域的情况,网络字节序转换就不起作用了,所以在各自字节序的基础下需要定义存放位域的顺序,上图中,对于大字节序,版本号为第一个变量占用2个bit,位置在第一个字节的高位处,对于小字节序系统,如果版本号还是第一个位置,那么实际存储在了第一个字节的低位处,读取就会存在问题,所以需要将版本号放置于第四个变量位置处,这样实际存储于第一个字节的高位处,读取就会一致,对于位域大小端的问题,可以参考此博客内容https://www.cnblogs.com/chencheng/archive/2012/06/19/2554081.html
接着为了加深理解,我们来看一下几种音视频格式文件是如何打包成Rtp包的
G711音频文件
struct AVFrame
{
AVFrame(uint32_t size = 0)
:buffer(new uint8_t[size + 1])
{
this->size = size;
type = 0;
timestamp = 0;
}
std::shared_ptr buffer; /* 帧数据 */
uint32_t size; /* 帧大小 */
uint8_t type; /* 帧类型 */
uint32_t timestamp; /* 时间戳 */
};
struct RtpPacket
{
RtpPacket()
: data(new uint8_t[1600])
{
type = 0;
}
std::shared_ptr data;
uint32_t size;
uint32_t timestamp;
uint8_t type;
uint8_t last;
};
bool G711ASource::HandleFrame(MediaChannelId channel_id, AVFrame frame)
{
if (frame.size > MAX_RTP_PAYLOAD_SIZE) {
return false;
}
uint8_t *frame_buf = frame.buffer.get();
uint32_t frame_size = frame.size;
RtpPacket rtpPkt;
rtpPkt.type = frame.type;
rtpPkt.timestamp = frame.timestamp;
rtpPkt.size = frame_size + 4 + RTP_HEADER_SIZE;
rtpPkt.last = 1;
memcpy(rtpPkt.data.get()+4+RTP_HEADER_SIZE, frame_buf, frame_size);
if (send_frame_callback_) {
send_frame_callback_(channel_id, rtpPkt);
}
return true;
}
AAC音频文件
struct AVFrame
{
AVFrame(uint32_t size = 0)
:buffer(new uint8_t[size + 1])
{
this->size = size;
type = 0;
timestamp = 0;
}
std::shared_ptr buffer; /* 帧数据 */
uint32_t size; /* 帧大小 */
uint8_t type; /* 帧类型 */
uint32_t timestamp; /* 时间戳 */
};
struct RtpPacket
{
RtpPacket()
: data(new uint8_t[1600])
{
type = 0;
}
std::shared_ptr data;
uint32_t size;
uint32_t timestamp;
uint8_t type;
uint8_t last;
};
bool AACSource::HandleFrame(MediaChannelId channel_id, AVFrame frame)
{
if (frame.size > (MAX_RTP_PAYLOAD_SIZE-AU_SIZE)) {
return false;
}
int adts_size = 0;
if (has_adts_) {
adts_size = ADTS_SIZE;
}
uint8_t *frame_buf = frame.buffer.get() + adts_size;
uint32_t frame_size = frame.size - adts_size;
char AU[AU_SIZE] = { 0 };
AU[0] = 0x00;
AU[1] = 0x10;
AU[2] = (frame_size & 0x1fe0) >> 5;
AU[3] = (frame_size & 0x1f) << 3;
RtpPacket rtpPkt;
rtpPkt.type = frame.type;
rtpPkt.timestamp = frame.timestamp;
rtpPkt.size = frame_size + 4 + RTP_HEADER_SIZE + AU_SIZE;
rtpPkt.last = 1;
rtpPkt.data.get()[4 + RTP_HEADER_SIZE + 0] = AU[0];
rtpPkt.data.get()[4 + RTP_HEADER_SIZE + 1] = AU[1];
rtpPkt.data.get()[4 + RTP_HEADER_SIZE + 2] = AU[2];
rtpPkt.data.get()[4 + RTP_HEADER_SIZE + 3] = AU[3];
memcpy(rtpPkt.data.get()+4+RTP_HEADER_SIZE+AU_SIZE, frame_buf, frame_size);
if (send_frame_callback_) {
send_frame_callback_(channel_id, rtpPkt);
}
return true;
}
H264视频文件
struct AVFrame
{
AVFrame(uint32_t size = 0)
:buffer(new uint8_t[size + 1])
{
this->size = size;
type = 0;
timestamp = 0;
}
std::shared_ptr buffer; /* 帧数据 */
uint32_t size; /* 帧大小 */
uint8_t type; /* 帧类型 */
uint32_t timestamp; /* 时间戳 */
};
struct RtpPacket
{
RtpPacket()
: data(new uint8_t[1600])
{
type = 0;
}
std::shared_ptr data;
uint32_t size;
uint32_t timestamp;
uint8_t type;
uint8_t last;
};
bool H264Source::HandleFrame(MediaChannelId channel_id, AVFrame frame)
{
uint8_t* frame_buf = frame.buffer.get();
uint32_t frame_size = frame.size;
if (frame.timestamp == 0) {
frame.timestamp = GetTimestamp();
}
if (frame_size <= MAX_RTP_PAYLOAD_SIZE) {
RtpPacket rtp_pkt;
rtp_pkt.type = frame.type;
rtp_pkt.timestamp = frame.timestamp;
rtp_pkt.size = frame_size + 4 + RTP_HEADER_SIZE;
rtp_pkt.last = 1;
memcpy(rtp_pkt.data.get()+4+RTP_HEADER_SIZE, frame_buf, frame_size);
if (send_frame_callback_) {
if (!send_frame_callback_(channel_id, rtp_pkt)) {
return false;
}
}
}
else {
char FU_A[2] = {0};
FU_A[0] = (frame_buf[0] & 0xE0) | 28;
FU_A[1] = 0x80 | (frame_buf[0] & 0x1f);
frame_buf += 1;
frame_size -= 1;
while (frame_size + 2 > MAX_RTP_PAYLOAD_SIZE) {
RtpPacket rtp_pkt;
rtp_pkt.type = frame.type;
rtp_pkt.timestamp = frame.timestamp;
rtp_pkt.size = 4 + RTP_HEADER_SIZE + MAX_RTP_PAYLOAD_SIZE;
rtp_pkt.last = 0;
rtp_pkt.data.get()[RTP_HEADER_SIZE+4] = FU_A[0];
rtp_pkt.data.get()[RTP_HEADER_SIZE+5] = FU_A[1];
memcpy(rtp_pkt.data.get()+4+RTP_HEADER_SIZE+2, frame_buf, MAX_RTP_PAYLOAD_SIZE-2);
if (send_frame_callback_) {
if (!send_frame_callback_(channel_id, rtp_pkt))
return false;
}
frame_buf += MAX_RTP_PAYLOAD_SIZE - 2;
frame_size -= MAX_RTP_PAYLOAD_SIZE - 2;
FU_A[1] &= ~0x80;
}
{
RtpPacket rtp_pkt;
rtp_pkt.type = frame.type;
rtp_pkt.timestamp = frame.timestamp;
rtp_pkt.size = 4 + RTP_HEADER_SIZE + 2 + frame_size;
rtp_pkt.last = 1;
FU_A[1] |= 0x40;
rtp_pkt.data.get()[RTP_HEADER_SIZE+4] = FU_A[0];
rtp_pkt.data.get()[RTP_HEADER_SIZE+5] = FU_A[1];
memcpy(rtp_pkt.data.get()+4+RTP_HEADER_SIZE+2, frame_buf, frame_size);
if (send_frame_callback_) {
if (!send_frame_callback_(channel_id, rtp_pkt)) {
return false;
}
}
}
}
return true;
}
Rtp发送代码
int RtpConnection::SendRtpOverTcp(MediaChannelId channel_id, RtpPacket pkt)
{
auto conn = rtsp_connection_.lock();
if (!conn) {
return -1;
}
uint8_t* rtpPktPtr = pkt.data.get();
rtpPktPtr[0] = '$';
rtpPktPtr[1] = (char)media_channel_info_[channel_id].rtp_channel;
rtpPktPtr[2] = (char)(((pkt.size-4)&0xFF00)>>8);
rtpPktPtr[3] = (char)((pkt.size -4)&0xFF);
conn->Send((char*)rtpPktPtr, pkt.size);
return pkt.size;
}
int RtpConnection::SendRtpOverUdp(MediaChannelId channel_id, RtpPacket pkt)
{
//media_channel_info_[channel_id].octetCount += pktSize;
//media_channel_info_[channel_id].packetCount += 1;
int ret = sendto(rtpfd_[channel_id], (const char*)pkt.data.get()+4, pkt.size-4, 0,
(struct sockaddr *)&(peer_rtp_addr_[channel_id]),
sizeof(struct sockaddr_in));
if(ret < 0) {
Teardown();
return -1;
}
return ret;
}
下面我们来看一下RTP协议的具体应用实操及报文形式,首先使用vlc作为rtp推流拉流的工具,在两台主机上分别开启vlc,接收端开启端口监听,并将拉取的音频流使用自带的编解码器及音频播放接口实时播放音频流,发送端则将本地存储的音频通过rtp协议进行推流。
1.接收端监听rtp协议端口
2.发送端推流
3.接收端播放音频流
接着我们通过抓包工具来看一下报文格式,由于rtp是基于udp传输的,我们在网卡过滤条件中添加协议及端口限制以便更好定位到rtp报文。
可以看到,实际上rtp是承载在udp之上的的,接着我们来看看每个udp报文的内容
首先除去以太网帧的14个字节(去除“前导码”和“帧校验序列” 因为wireshark把这2个都给过滤了)、ip头部20个字节,udp头部8个字节,报文总长度1370字节,最后得到udp Data部分1328个字节,这一部分其实就是rtp的内容
接着我们把报文解码为rtp协议格式
可以看到报文已经被解码为RTP / MPEG transport stream协议格式,我们来看看具体的报文内容
协议内容里udp之下的就是RTP协议,圆框圈出的即时rtp头部内容,分别对应着协议版本号,padding flag,extension flag,CSRC count,Marker bit,payload type,sequence number,timestamp,synchronization source,rtp头部下方data即是MPEG2-TS协议包,对于MPEG2-TS协议,编解码会分别打包拆包对应的TS包,每个TS包长度为188个字节(图中圈出的数据即是ts数据包),以太网数据帧的最大长度为1500个字节,刚才对udp包解析已经得到udp数据部分为1328个字节,除去rtp头部12个字节,剩余1316个字节,所以大部分通过rtp传输ts数据的实时传输程序(类似ffmpeg),每个rtp协议部分都会存放7个ts包
rtp、rtcp源码demo地址:https://gitee.com/jaymercychen/SimpleRtspServer
本文参考链接:
https://blog.csdn.net/bytxl/article/details/50400987
https://blog.csdn.net/petershina/article/details/8307163
https://blog.csdn.net/g0415shenw/article/details/79825524
https://www.jianshu.com/p/b5ca697535bd
https://www.jianshu.com/p/8a811d64aaa0