SRT实时传输

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_sendsrt_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 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。携带时间戳,在接收端会依据此时间戳整型上交应用。
  • TsbPdDelayPeerTsbPdDelay: Timestamp based Delay,一个用于发送端,一个用于接收端。含义是从发送端发送出去的时间戳开始计算,到接收端递交给应用最大时延, 默认是120ms。接收端在会等待delay时间到后才将数据递交给应用。这是因为接收端做整型的时候需要考虑时间波动的问题,引入此延迟可以获得一个缓冲。注意这个值需要考虑RTT抖动,重传等因素。
  • TLPktDrop: 是否允许发送侧丢包。
  • SndDropDelay: 发送侧丢包所做的时延判定。如果缓存在发送队列中的数据时间跨度大于max(SndDropDelay+PeerTsbPdDelay(120ms)+20ms, 1000ms)值则考虑丢包。

你可能感兴趣的:(SRT实时传输)