分类目录归档:文档

【原创】QosFecNack传输方案C风格API说明

简介

本文档主要站在使用者角度对音视频传输库QosFuncNack(以下简称AVCom)进行说明,库内部的算法等实现细节可以参考网站上的详细文档说明。AVCom是一个封装了FEC前向纠错、QOS收端质量保证以及NACK自适应重传、帧率自适应、IDR主动请求、发送端Smooth平滑等处理的UDP传输模块。它能够在传统UDP基础上增强数据传输对于丢包、乱序、重复包等情况的抵抗力,特别适用于WIFI、3G、4G、5G等无线场景下的实时音视频传输。

当需要发送音视频时,用户只需要调用发送接口,传入音视频数据即可;当接收到来自远端的音视频数据时,模块将调用相应的回调接口送出音视频数据,接口简洁。用户无需关心FEC、QOS、NACK等实现细节。

注意:这里的音视频数据是一个广义的概念,既可以是H264码流也可以是JPEG码流,还可以是文本数据,它们对传输层来说都是一视同仁,传输层不会尝试解析,传输层所做的就是以相对可靠的质量将发送端Send的数据在接收端原样输出。

注意:本方案与KCP等ACK方案不同,它们是优化版本的“TCP”,在保障数据不丢失的前提下,提高数据的吞吐率。而本方案不做100%数据传输保障,以实时性为宗旨,转为音视频领域而生。

一种常见的使用场景:

用户已经有自己的媒体传输系统,可能是点对点模式也可能是客户端-服务器模式,可能是单工也可能是双工,这都不影响用户使用本模块。

用户只需要将本模块当做一个普通的处理“函数”。只需要在发端Create一个AVCom对象,然后调用AVCom对象的发送数据接口,该接口将对用户传入的数据进行FEC编码保护,生成的数据将通过用户Create时传入的回调接口送出,这个回调接口一般就是用户之前的网络发送函数。同样,在接收端创建一个AVCom对象,将用户外层接收到的数据原封不动的put给AVCom模块,模块进行QOS、FEC解码处理后通过用户Create时传入的回调接口送出,这个回调接口一般就是用户之前送解码的函数。NACK的处理封装到了模块的内部,对于用户外层而言不可见。

x1

注意:传入传出AVCom模块的数据一定要是“对称”的,比如用户在发送回调函数中采用RTP发送,那么接收端应该是RTP接收后的数据送AVCom,而不是将UDP接收的数据送AVCom。

注意:假设外层用户传入1MB数据,模块内部将自动拆分成适合网络传输的小块并进行FEC编码处理后发出,接收端在收到数据后自行拼接后输出,对于用户来说并未察觉数据的实际收发过程。

QosFuncNack库使用标准C++语言(非C++11)开发,为了方便C、C#等语言的调用,我们提供了一个C风格的封装接口。接口的定义位于RtpAvComSdk.h之中,下面对常用接口进行介绍。

 

日志接口

  • 日志对于问题定位非常重要,SDK提供了一组全局日志开关接口,用户只需要在应用中完成如下步骤即可激活SDK日志。(1)包含全局头文件

    #include “RtpAvComSdk.h”

    (2)系统启动时调用一次初始化

    void  RtpAvCom_Enviroment_Init(const char *  outputPath,         LOG_OUTPUT_LEVEL  outputLevel)

    参数:

    @param: outputPath:日志文件输出的目录,若目录不存在,SDK将自动创建,支持相对路径或绝对路径。注:只需要提供目录,日志文件内部自行命名。

    @param: outputLevel:日志输出的级别,只有等于或者高于该级别的日志才会输出到日志文件,日志级别定义见枚举LOG_OUTPUT_LEVEL:LOG_OUTPUT_LEVEL_DEBUG、LOG_OUTPUT_LEVEL_INFO、LOG_OUTPUT_LEVEL_WARNING、LOG_OUTPUT_LEVEL_ERROR、LOG_OUTPUT_LEVEL_ALARM、LOG_OUTPUT_LEVEL_FATAL、LOG_OUTPUT_LEVEL_NONE。当指定为LOG_OUTPUT_LEVEL_NONE时,将不会生成日志文件。

    (3)系统退出时调用一次反初始化

    void RtpAvCom_Enviroment_Free()

功能API

(1)RtpAvCom_Create接口

Create接口用于创建QOS-FEC-NACK模块,相关的控制参数即在此传入,该接口在所有接口中最为复杂,接下来将分别予以说明。

void* RtpAvCom_Create(

FEC_REDUN_METHOD  eRedunMethod,  unsigned int  unRedunRatio,  unsigned int  unFecGroupSize,

SendExternal  pfVideoSendCallBack,  SendExternal  pfAudioSendCallBack,  void *pObjectSend,

ReciveDataCallBackFunc  pfVideoRecCallBack, ReciveDataCallBackFunc pfAudioRecCallBack , void * pObjectRecv );

绿色部分为传输控制参数:

* @param eRedunMethod:为FEC冗余度方法,包括固定冗余度FIX_REDUN和自适应冗余度AUTO_REDUN两种取值。自适应冗余度根据当前网络的丢包情况动态调整本端发出的冗余度,其丢包抵抗力更强,码率的波动性也更大,其最高冗余度可以达到50%,最低为30%。固定冗余度则由用户设定冗余比例。【本参数影响本端FEC编码发送,与接收无关】

* @param unRedunRatio:为FEC固定冗余度时的冗余比率,建议根据实际网络情况、成本综合考虑而定。最大值100,最小值10。自动冗余度时忽略本参数【本参数影响本端FEC编码发送,与接收无关】

* @param unFecGroupSize: 为FEC分组大小的下限,分组上限为内部自动选取。720P、1080P分辨率建议本值设置为28,对于标清、码率分辨率越低,group设置得越小,具体可技术咨询。【本参数影响本端FEC编码发送,与接收无关】

红色部分为媒体发送函数指针:

应用层媒体数据传入本模块后,经FEC编码处理,模块将调用本“媒体发送函数指针”实现媒体数据的真实发送。pObjectSend 是发送函数的形参,内部不会访问,只会在调用回调函数时透传给外层。bSendByInternal在Android JNI接口中使用,其他平台无需理会。

typedef void (*SendExternal)(int len,  unsigned char*data,  void *pObjectSend , BOOL bSendByInternal);

紫色部分为接收媒体输出函数指针:

在此传入“媒体输出函数指针”,模块内部接收数据并经过FEC解码处理后,数据经本接口送外层。

typedef void (*ReciveDataCallBackFunc)(int len,  BYTE *data,  BOOL bComplete,  BOOL bPrevTotalFrameLost,  void *pObjectRecv);

pObjectRecv 内部不会访问,只会在调用回调函数时透传给外层。

若没有丢包,本接口的输出数据以及输出频率与发送端调用Send(SendVideoData\ SendAudioData)发送的数据以及频率完全一致。当出现FEC\NACK机制恢复失败的情况时,本接口中的bComplete、bPrevTotalFrameLost标志即可通知外层。它们分别表示:

bComplete用于标识当前输出data数据是否完整,若为True表示该帧数据完整,否则可能是头部、中间或者尾部有数据丢失。

bPrevTotalFrameLost用于标识当前输出data数据与上一次输出之间是否有整帧数据丢失的情况。它可以弥补bComplete标志无法判断中间帧丢失的不足,比如发送端发送第1、2、3、4帧视频码流,接收端收到1、2、4,虽然它们单帧角度看接收完整,但因为第3帧的丢失将导致解码花屏。

外层可以借助这两个标志以及帧类型判定实现丢帧冻结机制,具体可咨询技术支持。

 

(2)RtpAvCom_Delete接口

void RtpAvCom_Delete(void* pRtp_avcom); 释放Create创建的资源

* @param pRtp_avcom:模块指针

注意:外层需要做好本接口与其他接口的互斥(send接口和put接口),就如同malloc了一块内存,在其他线程正在使用的过程中,主线程不应该无互斥保护的free掉该内存一样。为了让接收和发送并行、音频和视频并行,一般创建4把锁,分别用于本接口与音频send接口、视频send接口、音频put接口、视频put接口的互斥(其他几个低频率的统计数据获取接口可以复用视频send锁)。若仅创建一把锁,则上述各接口变成串行,影响了效率。

 

(3)音视频发送接口

BOOL RtpAvCom_SendAudioData(void* pRtp_avcom,  unsigned char *byBuf,  int nLen);

BOOL RtpAvCom_SendVideoData(void* pRtp_avcom,  unsigned char *byBuf,   int nLen);

应用层数据通过本函数传入,内部进行FEC编码处理,然后使用外部传入的“媒体发送函数指针”发往远端。

* @param pRtp_avcom 模块指针

* @param byBuf 输入数据

* @param nLen 输入数据长度

RtpAvCom_SendVideoData接口一次传入一帧不限大小的码流,内部视情况自动拆分成合适大小发送给远端,远端内部自动拼接后送外层。

* @return: 返回TRUE成功,为FALSE则失败

注意:音频数据外层需要保证单次输入的小于1000字节,一般来说音频编码后单帧码流远小于本值。

 

(4)收到音视频数据后送模块处理接口

BOOL RtpAvCom_PutAudioData(void* pRtp_avcom,  unsigned char *byBuf,  int nLen);

BOOL RtpAvCom_PutVideoData(void* pRtp_avcom,  unsigned char *byBuf,  int nLen);

外层在接收到数据后需要通过本接口送模块进行QOS-FEC解码处理,处理后的应用层数据将通过上文描述的虚函数或者ReciveDataCallBackFunc回调接口输出。

* @param pRtp_avcom 模块指针

* @param byBuf 输入数据

* @param nLen 输入数据长度

* @return: 返回TRUE成功,为FALSE则失败

 

(5)Nack重传功能使能接口

void RtpAvCom_EnableQosNackForVideo(void* pRtp_avcom,  BOOL bEnable);

模块默认关闭NACK功能,若需要启用NACK,则需要显式调用本API开启。

* @param pRtp_avcom 模块指针

* @param bEnable是否开启视频通道的NACK功能

注意:NACK是一个双向的过程,只有本端和远端均开启NACK时才会真实生效。

 

以下接口为高级接口:

(6)获取网络丢包率接口

BOOL RtpAvCom_GetVideoAudioUpDownLostRatio(void* pRtp_avcom,           float *pfVideoUpLostRatio,    float *pfVideoDownLostRatio,                             float *pfAudioUpLostRatio,   float *pfAudioDownLostRatio);

获得当前时刻的音视频上下行丢包率数据,范围[0.0, 100.0]

* @param pRtp_avcom 模块指针

* @param others音视频上下行丢包率存放变量地址

 

(7)获取网络RTT接口

BOOL RtpAvCom_GetRttMs(void* pRtp_avcom, unsigned int*punRttMs);

获得当前网络的RTT,单位毫秒。

* @param pRtp_avcom 模块指针

* @param punRttMs  RTT存放变量地址

【原创】客户端接入转发服务器 API说明

基本概念

房间:服务器上一个虚拟的概念,进入同一个房间的客户端才允许数据交互,不同房间之间的客户端并无交集。一个房间内客户端的最大数目理论不限,具体根据服务器端设置。服务器允许最大的房间数目同样依据服务端设置而定。最简单的模型是一个房间内一个发布者,一个接收者。

客户端UID:用于标识每个客户端的唯一ID,在系统中不允许有相同UID的客户端同时在线,否则新登录的客户端会将之前的客户端“顶下去”。

音视频发布者:具备上行音视频到服务器(房间)能力的客户端类型,同时也能接收来自服务器(房间)的音视频数据。

音视频接收者:只从服务器(房间)接收音视频的客户端类型,不具备上行能力,比如直播业务中的普通观众。

音视频位置:一个房间内允许同时多个音视频发布者,它们发布音视频到不同的“位置”上,接收者可选择接收某个或某些位置的音视频。目前单个房间允许32个“位置”。发布者登录后可以选择由服务器自动分配空闲的“位置”供其发布,也可以指定“位置”发布,若当前该“位置”上已有其他发布者,后者将顶替前者以保证同一位置上同时只有一个发布者。

音视频接收掩码:客户端可以根据业务需要选择接收房间内某个或某几个位置的音频或视频数据,可以通过设置自己期望接收的掩码到服务器。音频和视频使用各自独立的掩码,每个掩码32bit对应房间内的32个位置。

传输参数:本文传输参数是指视频通道的FEC上行冗余度、上行FEC Group组大小、接收Qos丢包等待时间,音频通道在内部已经根据经验数据配置好了合适的值且不对外开放。对于音视频接收者同样也可以设置FEC上行冗余度、上行FEC Group组大小,只是没有实际意义。

主动API接口:由外层应用主动发起的调用,比如登录、下线、发送音视频等。

被动API接口:称为回调接口更为贴切,比如接收到来自服务器的音视频数据、底层TCP连接状态发生错误的通知、底层要求获取服务器的IP地址。

 

主动接口

1、登录服务器

int IFace_OnlineUser(UINT unUid, UINT unRoomId, BYTE byUserType, UINT unDomainId = 1, BYTE byNetType = OPERATOR_OTHER);

参数:

@unUid,表示当前客户端使用的唯一用户ID,需要由用户自行保障唯一性。要求非0

@unRoomId,表示当前客户端进入的房间号,要求非0

@byUserType,表示客户端的类型,音视频发布者固定为USER_TYPE_AV_PROVIDE, 音视频接收者为USER_TYPE_AV_PLAYER

@unDomainId,域ID,默认为1即可,具体含义请参考SRTP-Server服务器设计相关文档。

@byNetType,客户端主动告知服务器自己的网络类型,便于域服务器快速分配优选的媒体服务器供其使用。若不提供或未知自己的网络类型,域服务器将根据客户端的出口IP查询IP地址库得到其运营商类型。

返回值:

返回0表示登录成功,返回负数则为失败,负数值为其错误码。

 

2、下线服务器

void IFace_OfflineUser();

参数:

返回值:

 

3、请求上传音视频到指定位置

int IFace_OnPosition(BYTE &byIndex);

参数:

@byIndex,大于等于0,小于服务器支持的最大位置数(具体依据服务器配置而定,比如[0,32))。特殊值:当设置为255时表示由服务器分配当前空闲的位置,分配结果将写回参数。请求成功后,将自动停止接收自己位置的音视频流(不看自己)。注意位置是从0开始编号。

返回值:

返回0表示请求发布成功,返回负数则为失败,负数值为其错误码。

 

4、请求从位置上下来

void IFace_OffPosition();

参数:

返回值:

对于当前在位置上的音视频发布者,OffPosition将从位置上下来并停止上行音视频数据,但它仍然保持在线状态并能接收其他位置的音视频流。当IFace_OfflineUser调用时将自动从位置上下来,而无需调用IFace_OffPosition(当然Offline前调用OffPosition也无妨)。请求成功后,将自动恢复接收自己之前位置的音视频流(可能有其他客户端加入该位置)。

 

5、上传视频数据

void IFace_SendVideoStreamData(BYTE* buf, UINT unLen, UINT unDts = 0);

向请求的位置发送视频码流,一次传入带H264起始码的一帧码流。

参数:

@buf,码流存放区。

@unLen,码流长度。

@unDts,可以使用外层时间戳,也可以(建议)由模块内部自行管理时间戳。内部将使用1KHZ时基(毫秒)生成时间戳。

返回值:

 

6、上传音频数据

void IFace_SendAudioStreamData(BYTE* buf, UINT unLen, UINT unDts = 0);

向请求的位置发送音频码流,一次传一帧ADTS码流。

参数:

@buf,码流存放区。

@unLen,码流长度。

@unDts,可以使用外层时间戳,也可以(建议)由模块内部自行管理时间戳。内部将使用1KHZ时基(毫秒)生成时间戳。

返回值:

 

7、设置音视频下行掩码

void IFace_SetAvTransStatus (UINT unAudioMask, UINT unVideoMask);

通过设置音视频下行掩码,可以选择从服务器接收哪几个位置的音视频数据。每一个bit对应一个位置,最低位对应0号位置,最高位对应31号位置。比如希望接收某个index位置的音视频时,可以设置为:

UINT unMask = 0x1 << (index);

unAudioDownChannelsMask |= unMask;

unVideoDownChannelsMask |= unMask;

当系统停止接收某个index位置的音视频时,可以设置为:

UINT unMask = 0x1 << (index);

unMask = ~unMask;

unAudioDownChannelsMask &= unMask;

unVideoDownChannelsMask &= unMask;

参数:

@unAudioMask,控制音频接收的掩码。

@unVideoMask,控制视频接收的掩码。

返回值:

 

8、设置音视频传输参数

void IFace_SetTransParams(UINT unQosDropDelay, FEC_REDUN_METHOD_TYPE eRedunMethod, UINT unRedunRatio, UINT unFecGroupSize);

参数:

@unQosDropDelay,本客户端接收码流时的QOS丢包等待时间,可设置为80。

@eRedunMethod,为上行FEC冗余度方法,建议设置为FIX_REDUN。

@unRedunRatio,固定冗余度时对应的上行冗余比率,建议设置为30。

@unFecGroupSize,为上行FEC分组大小,720P分辨率,建议设置为22,1080P分辨率可设置为28,低分辨率低码率场合建议设置得比较小,比如12

返回值:

注意:本函数需在IFace_OnlineUser之前调用,本API的使用若有疑问,请联系技术支持获得帮助。

 

被动接口

被动接口均使用CSDTermCmdIFace类虚函数方式提供,用户需要使用一个类继承CSDTermCmdIFace类并实现该虚函数。

1、请求获取服务器IP地址

virtual void GetDomianSerIp(char *strIp)

virtual void GetDomianSerIpSecond(char *strIp);

参数:

@strIp,在本函数内向该字符串填入服务器IP地址。

返回值:

服务器可能为双IP的情况,所以我们预留了两个接口,若服务器为单IP,则无需实现GetDomianSerIpSecond函数。

 

2、收到服务器下发的视频数据

virtual void OnRemoteVideo(BYTE byIndex, BYTE* data, UINT unLen, UINT unPTS, VideoFrameInfo tFrameInfo);

参数:

@byIndex,当前码流属于哪一个“位置”。

@data,指向接收的码流帧存放区域

@unLen,接收码流帧的长度

@unPTS,当前码流帧的时间戳

@tFrameInfo,当前码流的详细描述,其定义如下:

typedef struct VideoFrameInfo

{

UINT unWidth;

UINT unHeight;

BOOL bPacketLost;

BOOL bKeyFrame;

}VideoFrameInfo;

模块内部会解析SPS获得当前码流的宽和高告知外层(可能外层根本不需要这个)。另外两个BOOL变量是用于外层实现丢帧冻结机制时的参考。bPacketLost表示当前帧是否接收完整,若网络丢包且FEC未能恢复时该标志将置位。bKeyFrame表示当前帧是否为IDR关键帧。如果使用这两个标志实现丢帧冻结可以联系技术支持获得帮助。需要说明的是,当没有丢包发生时,本函数的输出应与IFace_SendVideoStreamData函数的输入完全一致。

返回值:

 

3、收到服务器下发的音频数据

virtual void OnRemoteAudio(BYTE byIndex, BYTE* data, UINT unLen, UINT unPTS, AudioFrameInfo tFrameInfo);

参数:

@byIndex,当前码流属于哪一个“位置”。

@data,指向接收的码流帧存放区域

@unLen,接收码流帧的长度

@unPTS,当前码流帧的时间戳

@tFrameInfo,当前码流的详细描述,其定义如下:

typedef struct AudioFrameInfo

{

UINT unCodecType;

UINT unSampleRate;

UINT unChannelNum;

}AudioFrameInfo;

音频帧为ADTS格式,其每个包头部均附带了采样率、通道数、编码格式等信息。我们暂时未做解析,tFrameInfo内容不可参考。

返回值:

 

4、底层与服务器之间的TCP连接断开

virtual void OnSystemExit(UINT unUid, UINT unExitCause);

参数:

@unUid,当前登录使用的UID。

@unExitCause,当前与服务器的TCP连接已经断开,告知应用层的错误码

客户端登录服务器成功后将建立与服务器之间的TCP长连接,当因为网络等因素导致TCP连接断开时,内部将自动尝试重连。当相同账号在其他位置登录导致本客户端被“顶”下去时,将不会尝试重连,并通过本接口告知上层。

返回值:

 

 

一种简单场景下的使用示意

下面演示一种纯发送端(不接收)的API调用情况,其他场景的API使用可以以此为参考或咨询技术支持。

1、new一个CSDTerminal类对象

2、使用一个现有类继承自CSDTermCmdIFace类,并实现该类的几个虚函数。一个典型的实现为:

// 获取主域服务器的IP地址1

void CTerminalTestDlg::GetDomianSerIp(char *strIp)

{

sprintf(strIp, “13.5.14.9”);  //服务器的IP地址

}

 

// 获取主域服务器的IP地址2

void CTerminalTestDlg::GetDomianSerIpSecond(char *strIp)

{

//服务器为单IP类型时,直接为空

}

 

//来自底层的退出反馈(比如与服务器之间TCP连接断开时,将通过本接口反馈应用层)

void CTerminalTestDlg::OnSystemExit(UINT unUid, UINT unExitCause)

{

//通知上层

}

 

// 收到服务器发来的视频

void CTerminalTestDlg::OnRemoteVideo(BYTE byIndex, BYTE* data, UINT unLen, UINT unPTS, VideoFrameInfo tFrameInfo)

{

//纯发端无需处理本函数,直接为空

}

 

// 收到服务器发来的音频

void CTerminalTestDlg::OnRemoteAudio(BYTE byIndex, BYTE* data, UINT unLen, UINT unPTS, AudioFrameInfo tFrameInfo)

{

//纯发端无需处理本函数,直接为空

}

 

3、调用一次 IFace_SetTransParams 设置发端上行传输FEC参数,推荐调用为:

IFace_SetTransParams(0, FIX_REDUN, 30, 28);

参数说明:接收时QOS丢包延时为0(发端没有接收,直接设置0),使用固定冗余度方式FIX_REDUN,使用30%的上行冗余,使用Group大小为28

4、调用一次 IFace_OnlineUser 登录服务器,举例调用为:

IFace_OnlineUser(unUserId, unRoomId, USER_TYPE_AV_PROVIDE);

5、调用一次 IFace_OnPosition 设置上传音视频到房间的哪个位置,举例调用为:

IFace_OnPosition(0);

6、调用一次 IFace_SetAvTransStatus 设置发端从服务器接收音视频的情况,推荐调用为:

IFace_SetAvTransStatus(0, 0);

参数说明:第一个参数表示从服务器接收的音频掩码,第二个参数表示接收的视频掩码;二者均为32位整数,每个bit对应一个音视频位置,比如最低bit位对应0号位置,若希望接收该位置的视频或音频则设置该bit为1。因为发端只发不收,所以我们直接设置音频视频接收掩码都为0,表示什么也不接收。

7、N次调用IFace_SendVideoStreamData、IFace_SendAudioStreamData向服务器发送音视频数据

8、 结束前调用一次 IFace_OfflineUser (内部将自动OffPosition)

9、 delete回收CSDTerminal类对象

【原创】FEC/QOS中的NACK方案

 

一、FEC-QOS配合NACK的优势

ACK是大家比较熟悉的传输层保障措施,在类TCP的UDP传输方案中(UDT、KCP等)中通过优化ACK发送时机、更短的超时重传时间等措施来获得更高的数据吞吐率,但ACK并不适合对实时要求极高的直播互动领域。

NACK与ACK不同,它是在没有收到数据包时向远端请求重传,因而更加适合实时通讯。通过与FEC的结合,只对FEC无法恢复的数据包请求NACK重传,能够尽量的降低重传发起概率,降低重传带来的副作用。

二、NACK在系统中的位置

NACK可以放置在原来的FEC-QOS传输层之外,作为上层应用层,这种实现方式NACK将FEC-QOS看做普通的UDP传输,二者并无紧密结合,其优势是可以与成熟的NACK方案无缝衔接。我们知道任何NACK方案都必将引入延时抖动,因为接收端在发起重传请求后,需要等待发送端重新发出的数据,在“重传等待时间”内不对外输出数据。而QOS阶段里为解决UDP乱序包的问题也引入了一个“丢包等待时间”,当遇到包序号不连续时,将等待这一时间,若仍未收到所需的包则认定丢包,不再等待。如果将这两个时间合二为一,可以尽量的降低系统时延和抖动,毕竟我们需要的是一个高实时性的NACK传输方案。我们将NACK的发起和等待放置在QOS之中,入下图所示:

xx1

图1 在QOS中实现NACK发起

当QOS检测到序号不连续时,可能是发生丢包或者是乱序,此时QOS将通过FEC解码模块分析当前疑似丢包是否将导致FEC无法恢复。此时将产生三种分析结果:

A、当前丢包即使丢了也不影响FEC恢复,比如当前丢失的包为一个或者多个冗余包,且该冗余包所在的group内的媒体包均已接收,或者借助已接收的冗余包足够恢复。

B、当前丢包不能确定是否影响FEC恢复,需要接收更多的包才能确定。比如丢包发生在group的中段且丢的数量小于冗余包总数。

C、当前丢包将导致FEC确定无法恢复,比如同一个group内丢失的包数大于冗余包总数。

对于情况A,QOS将直接不予等待,将后续接收的包直接交与FEC。对于情况B,QOS将进入“丢包等待时间”,以期收到乱序的包。对于情况C,QOS将发起NACK重传并进入等待,这个等待时间即是“丢包等待时间”又是“重传等待时间”,在等待期内不管是该乱序包到达或者重传包到达,都能满足FEC的恢复条件。

在介绍了NACK的发起条件后,我们来关注“重传等待时间”的取值问题。若设置固定的重传等待时间将很难满足各类网络情况。时间过小将导致重传包尚未到达,QOS已结束等待并输出后续包,后续即便再收到重传包也将直接丢弃。重传包也可能因网络原因丢包,若“重传等待时间”过大,将导致更大的延时和抖动。为了提高自适应能力,系统通过实时获取当前网络的UDP通讯RTT时间来作为“重传等待时间”的参考,计算出合理的值。

三、信令通道与媒体通道分离模式(NO Mux Mode)

该模式下,我们使用独立的一个UDP信令通道用来传输NACK请求,而不是复用媒体通道。这样做的主要考虑是:

A、更好的兼容性,对于不支持NACK的节点,只需要忽略信令通道的内容即可与NACK节点互通。

三、信令共用媒体通道模式(Mux Mode)

该模式下,我们复用媒体通道来传输NACK请求,类似于RTCP Mux模式,而不是复用媒体通道。这样做的主要考虑是:

A、简化用户集成难度,NACK对于用户而言不可见,用户只需处理媒体通道。

B、节省NACK信令所需的端口资源

四、实现流程

方案的实现流程如下图所示:

xx2

图2 NACK的整体流程

对于传输层模块,它是全双工的,为演示方便我们只列出单向的情况,另外一个方向也是完全一致的。在FEC编码之后,所有的发出的UDP(RTP)包均会被存入一个环形缓存区中,当收到远端NACK请求时,将在下一个媒体包传输时触发重传动作,后者将在环形缓存区中检索需要重传的包并在媒体通道上发出。我们没有新增内部线程去执行重传动作,而是借助原本的媒体包发送行为来触发,这样可以简化设计提高稳定性。

检索过程中我们进行了数据包的合法性校验和时间戳有效性校验,当发现当前时间距离数据包初次发送时间的间隔已经较大时,将放弃重传,因为此时远端极有可能已经退出等待,没有必要再浪费带宽。检索使用的是RTP头中的序号字段,需要考虑序号达到最大值时的跳变动作。

【原创】FEC/QOS 音视频实测DEMO

FEC/QOS 音视频实测DEMO将以实际音视频作为测试对象,真实反映本方案给多媒体系统抗丢包能力带来的提升。DEMO界面如下图1所示:

图1   FEC/QOS 音视频实测DEMO界面

本DEMO支持如下特性:

  • 使用Direct进行摄像头、麦克风的采集和输出
  • 使用ffmpeg进行高效图像缩放等前置filter
  • 视频H264 HighProfile编码、解码
  • 音频AAC-LC、AAC-LD、AAC-ELD编码、解码(三种标准可选,44.1KHZ 16bit 2通道立体声)
  • 音视频RTP传输(带FEC\QOS功能)
  • 人为丢包测试功能
  • 实时统计输出线路丢包延时情况

DEMO的内部框架如下图所示:

图2   DEMO的内部框架

在视频采集缩放线程与视频编码线程之间,我们采用高效的双队列机制(队列元素为指针,进出队列无数据拷贝),如果视频编码性能非常充足的情况下,我们也可以将二者合并为一个线程。编码线程与网络发送线程分离,避免网络拥塞影响编码线程(这个对于UDP来说不成立,但对于RTMP这类TCP系统来说,网络收发线程与音视频编解码线程的分离是必须的,因为网络的抖动将影响音视频处理环节。站在系统设计的角度,我们为UDP和TCP统一使用上述架构)。

在音视频发送模块内,我们都配有定时握手包发送线程,这个线程与音视频使用相同的发送通道(端口),仅在包头上予以区别。它的作用非常重要,主要包括两个方面:为NAT穿越提供保障,当我们的客户端(内网IP)向服务器(公网IP)发送数据时,链路路由器会为该通讯链路映射“端口”,这样服务器(公网IP)向该客户端发送数据时,只需将收到的数据包的IP地址和端口翻转作为发送目标IP和端口便能向其发送数据。路由器在收到服务器的数据包时,检查本地存在对应的映射“端口”,予以放行,否则将丢弃这一数据包(这是基于安全的考虑,外网向内网发送数据不得不防)。值得注意的是,路由器上“端口”是有时效性的,超过一定时间即会失效,为了保证服务器能持续有效的向客户端发送数据,客户端必须以心跳包的方式向服务器发送数据以保持“端口”的有效性(客户端根据业务情况不一定向服务器持续发送数据包,可能只作为接收者)。以上是对C/S模式下NAT的简要描述,P2P模式等其他情况请参考专门的资料。定时握手包的另一个作用是传输自定义的信道统计数据,这个类似于RTCP协议,接收方统计出下行丢包率后可以通过本握手包告知发送方,以便通知对方调整发送甚至编码策略。

音频的处理流程与视频类似,因为音频编码耗时非常低,我们一般将音频采集与编码放到一个线程内进行。音频的输出与视频不同,因为它需要按固定的输出频率工作,驱动将发起定时输出线程,我们只需要在该线程内向指定内存存入指定数量的PCM数据即可。(输出频率、存放数量由音频输出通道数、采样率、采样点字节数的配置而定)

若本地IP与远端IP设置一样,则进入本地回环模式,此时将不存在任何网络丢包,我们可以通过设置手动丢包来模拟测试。若本地IP与远端IP不同,且不属于同一网段,我们可以使用开源的WANEM来进行模拟丢包、延时、抖动、重复包等情况,这一方式后面我们将专门予以介绍。

图3    随机4%丢包,不使用FEC时的情况

当使用4%随机丢包时,若关闭发端FEC功能,接收端视频将出现经常性花屏,声音出现丢失断续。若使用20%冗余度,视频花屏概率将大幅降低(不会完全消除,因为丢包是随机的,短时间内可能出现连续大量丢包的情况,超过20%冗余度的无失真抗丢包率是16.67%即会出现花屏)

若使用按间隔丢包,每6个包丢一个(丢包率将恒定为16.67%),此时选择20%冗余度可以实现无失真恢复,视频流畅无花屏,音质良好无断续。

图4    每6个包丢1个包(16.67%丢包),20%冗余度无失真恢复

 

【原创】FEC/QOS 数据测试型DEMO——–QOS功能验证

本DEMO的另一项测试功能是针对QOS来的,通过分析收发包延时结果,我们可以进一步了解丢包对系统延时的影响。在发送的媒体包时,我们在其负载中加入了系统当前时间,通过记录回环后的媒体包的接收时间(以应用层接收为准,即经过了QOS、FEC解码处理后)来计算该媒体包的来回总时长。

首先,我们选择不丢包,其他参数配置情况:冗余度20%(Group中10个媒体包配2个冗余包),两个媒体包之间的发送间隔为60ms,得到的媒体包“收发延时”情况如下图1所示。

图1 不丢包时,媒体包收发延时情况

因为媒体包为本地回环且无丢包发生,QOS、FEC均未做任何处理,延时非常低。可见与缓存一段时间数据再排序的QOS算法相比,本方案的QOS算法在没有丢包时将不会引入任何延时。

当我们选择每10个包丢弃一个时,时延情况如下图2所示:

图2 每10个包丢弃1个时,媒体包收发时延情况

下面我们来解释这一现象。每10个包丢弃1包时的情况如下图3所示:

图3  每10个包丢弃1个的情况示意

在QOS阶段,0号媒体包丢失,接下来收到的1号媒体包不能直接输出给后续FEC环节,需要等待最多200ms(QOS丢包延时设置为200ms),若仍然未收到0号媒体包则直接输出1号媒体包以及收到的后续媒体包。同样,当收到1号冗余包时也需要等待200ms方能输出。

在FEC阶段,当收到1~9号媒体包时因发现存在丢包而不直接输出给上层应用,直到收到1号冗余包时具备了FEC恢复的条件(收到媒体包数+收到冗余包数>=GROUP媒体包数),恢复0号媒体包并输出0~9号媒体包给上层应用。这样计算来0号媒体包被恢复输出总计需要800ms以上(60ms收到1号媒体包+8*60ms收到2~9号媒体包+2*60ms收到1号冗余包+1号冗余包在QOS环节等待200ms)。

当18号媒体包的丢失,19号媒体包会在QOS环节等待200ms,此时间段内会陆续收到冗余包0和冗余包1,200ms后仍然未等到18号媒体包的到来则直接输出到FEC恢复环节实现恢复。这样计算来18号媒体包被恢复输出总计需要约200ms。

由上例可见,当GROUP比较大时,若丢包发生在GROUP的前部,QOS环节的处理将影响后续包的时延指标。同时,前部丢失的媒体包不得不等待较长时间,直至尾部的冗余包到来方能恢复,造成延时进一步增大。下面我们比较GROUP SIZE为5时的情况。

图4  GroupSize为5时,每10个包丢1个的情况

可见采用较小的FEC GROUP可以改善丢包延时情况。

【原创】FEC/QOS 数据测试型DEMO——–FEC功能验证

本方案为C++开发,提供PC、Android(JNI)、IOS跨平台的支持,为了方便测试,在PC下开发了一个简易测试DEMO如下图1所示,该DEMO主要用于验证功能,能直观的展示所有收发包数据。

 

图1  FEC/QOS 数据测试型DEMO界面

测试工具为点对点工作模式,可在两台PC上各自运行(同时也支持单机模式,只需将收发IP地址均设置为本地IP即可),以实现双方之间RTP(FEC+QOS)通讯。软件收发自定义的测试包数据,提供了模拟丢包功能,支持按固定间隔丢包或者按随机比率丢包;支持设置FEC冗余度或者选择冗余度自适应,支持设置QOS丢包等待时延等参数。

测试工具内部默认使用10个媒体包外加冗余度(数量由选择的冗余度决定)作为一个GROUP,当选择冗余度20%时,一个GROUP由10个媒体包附加2个冗余包组成。下图是Wireshark的观察情况,10个媒体包后面紧接着2个冗余包。

图2  Wireshark截包观察GROUP组成

需要说明的是:程序主动丢包是在UDP发送层进行,所以即可能丢媒体包也可能丢

冗余包。下面我们以20%冗余度为例说明系统对各类丢包率的抵抗能力。

当选择每10个包丢1个包时(丢包率10%),一个GROUP中最多只会丢弃1个包,20%的冗余度足够抵抗这一丢包率,测试结果也验证了这一结论,接收到的所有媒体包序号均保持连续,丢包率从10%降为0%,实验情况如上图1所示。

当选择每5个包丢弃1个包时(丢包率20%),丢包情况如下所示:

图3  每5个包丢弃1包时的情况

对于第一个GROUP,一共丢弃了三个包,包括0号媒体包、5号媒体包、0号冗余包。因为接收的媒体包数为8个加接收的冗余包数1个,总数小于总媒体包数(10个),因此接收端FEC无法恢复。对于第二个GROUP,只丢失了两个媒体包,可以正常恢复。下图4的实验结果也说明了推断的正确性,0号媒体包、5号媒体包丢失,13号、18号媒体包被成功恢复,系统丢包率从20%降低到10%左右。

图4  经过FEC恢复后的数据包情况

当选择每4个包丢弃1个包时(丢包率25%),一个GROUP中将丢弃3个数据包,必然不满足FEC恢复条件,无法恢复,此时将原样输出收到的数据包,入下图5所示:

图5  丢包率过高无法恢复的情况

需要说明的是:丢包是针对传输层数据包(媒体包和冗余包)进行的,DEMO软件中只显示媒体包部分,没有显示冗余包。“下行丢包率”参数是统计的传输层数据包的丢失率,而“下行丢包率(经过丢包恢复)”是统计的应用层媒体包的丢失率,所以会出现后者比前者还高的情况。

当系统丢包率达到FEC特定冗余度的理论极限时,丢包将无法恢复,此时唯有加大冗余度才能实现恢复功能。实际应用时,应该对线路的网络丢包情况做一定统计分析,以确定最佳的冗余度方案,这个往往是比较困难和繁琐的,为此我们推出的自动冗余度方案,从一定程度上解决了这个问题。

若定义“无失真抗丢包能力”为可百分百无失真恢复丢失的媒体包的能力。下表是各冗余度下,传输层“无失真抗丢包能力”的估算情况:

是不是10个冗余包配2个冗余包时,信道丢包率只要低于16.67%就肯定可以无失真恢复呢?这个不一定,因为丢包的分布情况是随机的,比如100个包里丢10个包,10个包是连续丢还是随机均匀丢对FEC恢复来说是完全不同的。FEC并不能解决所有丢包问题,但可以显著提升系统的抗丢包能力。