SRT如何做到实时传输
SRT 工作模式
SRT 工作模式有实时(LIVE)模式和文件(FILE)模式两种。使用FILE模式还存在BUFFER API和MESSAGE API两种发送接口。
实时模式
实时模式用于传送实时多媒体流。
实时模式下,数据分片(默认是1316 = 7 * 188,188是单个MPEG TS大小)在一定的速率控制下发出,并且在接收端按照发送端发送的时间间隔重新组织好。
默认情况下,接收端重组会有一定的时延,默认为120ms。
文件模式
实时模式具有一定的速率控制,而文件模式则是尽力而为的传送方式。
Buffer API
Buffer API和我们平常使用的TCP socket接口类似,只要有足够的缓存能够存下这些数据,接口就会将这些数据交付到SRT协议栈。接收端也会尽力而为的接收数据。
Message API
Message API的特点是数据是存在边界的。也就是说这不是一个“流式”的接口,而是类似于UDP的存在报文边界的接口。当没有足够的缓存存下整个消息时,消息数据不会被发送到SRT协议栈。当整个消息没有接收完毕时,接收接口也不会将消息交付上去。
编程接口
通过设置socket option选项来设置工作模式和编程接口模式。
工作模式设置
使用SRTO_TRANSTYP
选项来设置工作模式:
SRTT_LIVE
: Live模式。此模式为默认的模式,用于实时流传输。SRTT_FILE:
File模式。File模式是“最快速”的数据传输方式,它在交付的时候没有速率控制和整型。
消息接口设置
使用SRTO_MESSAGEAPI
来设置消息接口格式:
true
: 使用Message模式。消息模式意味着数据是有边界的。在Live模式下默认使用该模式。false
:使用Buffer模式。Buffer模式意味着只要有数据能交付则会尽力交付。在File模式下使用该模式。
生存时间
SRT允许丢弃那些已经明确无法按照目标时间要求送达的数据报文。
编程接口
数据结构
SRT_MSGCTRL
参数:
SRT_MSGCTRL
结构体可以设置发送/接受数据的属性,其中就包含数据的生存时间msgttl
-
msgttl
: 输入型参数。消息最大生存时间,超时仍然未正确送达则将被丢弃。-1表示永不超时。只在发送端有意义。 -
inorder
: 输入型参数。设置为true表示需要将乱序报文严格排序后再提交给应用。只在发送端有意义。 -
srctime
: 在发送端为输入型参数,在接收端为输出型参数。在发送报文中打上时间戳,0表示当前时间。 -
pktseq
: 报文序列号,只在接收端有意义。 -
msgno
: 输出参数。SRT协议栈给此消息打上的消息列号。
srt_sendmsg2
接口与srt_recvmsg2
接口支持SRT_MSGCTRL参数用于控制数据包的属性。
使用方式
发送接口:
int srt_send(SRTSOCKET s, const char* buf, int len);
int srt_sendmsg(SRTSOCKET s, const char* buf, int len, int msgttl, bool inorder, uint64_t srctime);
int srt_sendmsg2(SRTSOCKET s, const char* buf, int len, SRT_MSGCTRL* msgctrl);
接收接口:
int srt_recv(SRTSOCKET s, char* buf, int len);
int srt_recvmsg(SRTSOCKET s, char* buf, int len);
int srt_recvmsg2(SRTSOCKET s, char* buf, int len, SRT_MSGCTRL* msgctrl);
发送示例:
nb = srt_sendmsg(u, buf, nb, -1, true);
nb = srt_send(u, buf, nb);
SRT_MSGCTL mc = srt_msgctl_default;
nb = srt_sendmsg2(u, buf, nb, &mc);
接收示例:
nb = srt_recvmsg(u, buf, nb);
nb = srt_recv(u, buf, nb);
SRT_MSGCTL mc = srt_msgctl_default;
nb = srt_recvmsg2(u, buf, nb, &mc);
实际上srt_send
、srt_sendmsg
最终都是通过调用srt_sendmsg2
接口实现发送。
/// Request UDT to send out a data block "data" with size of "len".
/// @param data [in] The address of the application data to be sent.
/// @param len [in] The size of the data block.
/// @return Actual size of data sent.
SRT_ATR_NODISCARD int send(const char* data, int len)
{
return sendmsg(data, len, -1, false, 0);
}
/// send a message of a memory block "data" with size of "len".
/// @param data [out] data received.
/// @param len [in] The desired size of data to be received.
/// @param ttl [in] the time-to-live of the message.
/// @param inorder [in] if the message should be delivered in order.
/// @param srctime [in] Time when the data were ready to send.
/// @return Actual size of data sent.
int CUDT::sendmsg(const char *data, int len, int msttl, bool inorder, uint64_t srctime)
{
SRT_MSGCTRL mctrl = srt_msgctrl_default;
mctrl.msgttl = msttl;
mctrl.inorder = inorder;
mctrl.srctime = srctime;
return this->sendmsg2(data, len, Ref(mctrl));
}
/// Receive a message to buffer "data".
/// @param data [out] data received.
/// @param len [in] size of the buffer.
/// @return Actual size of data received.
SRT_ATR_NODISCARD int sendmsg2(const char* data, int len, ref_t<SRT_MSGCTRL> m);
srt_sendmsg2
函数流程如下:
st=>start: sendmsg2
e=>end
exception=>end: Throw exception
checkArgs=>operation: Check Transmit Parameters for Live/File Mode
conArgs=>condition: Check Result
st->checkArgs->conArgs
checkBuff=>operation: Check Buffer Enough for Message API
conBuff=>condition: Check Result
conArgs(yes)->checkBuff
conArgs(no)->exception
checkNeedDrop=>operation: Check Need Drop
checkBuff->conBuff
conBuff(yes)->checkNeedDrop
conBuff(no)->exception
sendBlock=>operation: Block Send if no enough buffer and in block mode
checkNeedDrop->sendBlock
addToSendBuf=>operation: Add to UDT Send Buffer with TTL
sendBlock->addToSendBuf
updateSendSocket=>operation: Update current socket to send socket list
addToSendBuf->updateSendSocket->e
数据最终调用CSndBuffer::addbuffer接口添加到CsndBuff中,并设置了该数据block的TTL。
CsndBuffer类具有一个worker线程用于将已添加的数据发送出网络,将buffer数据读取并发送的函数为CsndBuffer::readData
,它会判断当前TTL是否已经超时,决定是否将该数据打包发送至网络。
其调用流程如下:
worker=>start: CsndQueue::worker
pop=>operation: CSndUList::pop
pack=>operation: CUDT::packData
packLost=>operation: CUDT::packLostData
readData=>subroutine: CSndBuffer::readData
worker->pop->pack->packLost->readData
readData函数代码如下:
int CSndBuffer::readData(char** data, const int offset, int32_t& msgno_bitset, uint64_t& srctime, int& msglen)
{
CGuard bufferguard(m_BufLock);
Block* p = m_pFirstBlock;
// XXX Suboptimal procedure to keep the blocks identifiable
// by sequence number. Consider using some circular buffer.
for (int i = 0; i < offset; ++ i)
p = p->m_pNext;
// Check if the block that is the next candidate to send (m_pCurrBlock pointing) is stale.
// If so, then inform the caller that it should first take care of the whole
// message (all blocks with that message id). Shift the m_pCurrBlock pointer
// to the position past the last of them. Then return -1 and set the
// msgno_bitset return reference to the message id that should be dropped as
// a whole.
// After taking care of that, the caller should immediately call this function again,
// this time possibly in order to find the real data to be sent.
// if found block is stale
// (This is for messages that have declared TTL - messages that fail to be sent
// before the TTL defined time comes, will be dropped).
if ((p->m_iTTL >= 0) && ((CTimer::getTime() - p->m_ullOriginTime_us) / 1000 > (uint64_t)p->m_iTTL))
{
int32_t msgno = p->getMsgSeq();
msglen = 1;
p = p->m_pNext;
bool move = false;
while (msgno == p->getMsgSeq())
{
if (p == m_pCurrBlock)
move = true;
p = p->m_pNext;
if (move)
m_pCurrBlock = p;
msglen ++;
}
HLOGC(dlog.Debug, log << "CSndBuffer::readData: due to TTL exceeded, " << msglen << " messages to drop, up to " << msgno);
// If readData returns -1, then msgno_bitset is understood as a Message ID to drop.
// This means that in this case it should be written by the message sequence value only
// (not the whole 4-byte bitset written at PH_MSGNO).
msgno_bitset = msgno;
return -1;
}
*data = p->m_pcData;
int readlen = p->m_iLength;
// XXX Here the value predicted to be applied to PH_MSGNO field is extracted.
// As this function is predicted to extract the data to send as a rexmited packet,
// the packet must be in the form ready to send - so, in case of encryption,
// encrypted, and with all ENC flags already set. So, the first call to send
// the packet originally (the other overload of this function) must set these
// flags.
msgno_bitset = p->m_iMsgNoBitset;
srctime =
p->m_ullSourceTime_us ? p->m_ullSourceTime_us :
p->m_ullOriginTime_us;
HLOGC(dlog.Debug, log << CONID() << "CSndBuffer: extracting packet size=" << readlen << " to send [REXMIT]");
return readlen;
}
从上面的分析可知,报文生存时间检查存在两个阶段,一个是sendmsg2接口中通过checkNeedDrop
函数检查已缓存数据的时间跨度(最新添加的数据与最旧添加的数据时间差)是否超过阈值,一个是在发送线程中检查数据的TTL。
Live模式的实时性
SRT默认的模式是Live模式,也可以使用setsockopt的方式设置为Live模式。
Live/File模式的实质是一系列属性配置,设置为Live模式的属性配置表为:
case SRTT_LIVE:
// Default live options:
// - tsbpd: on
// - latency: 120ms
// - linger: off
// - congctl: live
// - extraction method: message (reading call extracts one message)
m_bOPT_TsbPd = true;
m_iOPT_TsbPdDelay = SRT_LIVE_DEF_LATENCY_MS;
m_iOPT_PeerTsbPdDelay = 0;
m_bOPT_TLPktDrop = true;
m_iOPT_SndDropDelay = 0;
m_bMessageAPI = true;
m_bRcvNakReport = true;
m_zOPT_ExpPayloadSize = SRT_LIVE_DEF_PLSIZE;
m_Linger.l_onoff = 0;
m_Linger.l_linger = 0;
m_CongCtl.select("live");
这里涉及到了几个参数:
TsbPd
- Timestamp-Based Packet Delivery Mode。携带时间戳,在接收端会依据此时间戳整型上交应用。TsbPdDelay
、PeerTsbPdDelay
: Timestamp based Delay,一个用于发送端,一个用于接收端。含义是从发送端发送出去的时间戳开始计算,到接收端递交给应用最大时延, 默认是120ms。接收端在会等待delay时间到后才将数据递交给应用。这是因为接收端做整型的时候需要考虑时间波动的问题,引入此延迟可以获得一个缓冲。注意这个值需要考虑RTT抖动,重传等因素。TLPktDrop
: 是否允许发送侧丢包。SndDropDelay
: 发送侧丢包所做的时延判定。如果缓存在发送队列中的数据时间跨度大于max(SndDropDelay+PeerTsbPdDelay(120ms)+20ms, 1000ms)值则考虑丢包。