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<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。携带时间戳,在接收端会依据此时间戳整型上交应用。
  • TsbPdDelayPeerTsbPdDelay: Timestamp based Delay,一个用于发送端,一个用于接收端。含义是从发送端发送出去的时间戳开始计算,到接收端递交给应用最大时延, 默认是120ms。接收端在会等待delay时间到后才将数据递交给应用。这是因为接收端做整型的时候需要考虑时间波动的问题,引入此延迟可以获得一个缓冲。注意这个值需要考虑RTT抖动,重传等因素。
  • TLPktDrop: 是否允许发送侧丢包。
  • SndDropDelay: 发送侧丢包所做的时延判定。如果缓存在发送队列中的数据时间跨度大于max(SndDropDelay+PeerTsbPdDelay(120ms)+20ms, 1000ms)值则考虑丢包。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容