librtmp源碼分析之核心實現解讀

librtmp是我們平常工作中進行推拉流開發的重要工具,官方提供的版本是基于C/C++技術棧的,但是有不少的其它高級語言技術棧也都提供了相應的包裝或移植版本。

RTMP協議非常復雜,網上又鮮有較為完整的文檔,再加上它的一些設計理念比較奇特(奇特的事件會剌激我們的反向思維,讓我們對現有認知產生否定),以至于我們完全理解它需要耗費非常多的時間和腦力。
從RTMP的實現版本也側面驗證了這點,現有的主流實現仍然遵循TLV范式(不了解的話請百度),協議層面基本拋棄了消息頭的建議,鏈路層復用只使用塊頭,塊頭之上就是負載,對指令來說負載就是AMF,對媒體來說負載就是編碼幀,清爽干凈!

本文不講解RTMP協議規范的相關知識,讀者朋友們請自行百度。本文的主要目的是配合大家一起閱讀librtmp的核心源碼,但是在開始之前建議大家有一些RTMP協議的基本認識,我希望在完成本文的閱讀之后,大家對librtmp的使用能上一個更高的層次。

下載源碼:

在閱讀源碼前,請大家到官方網址下載:http://rtmpdump.mplayerhq.hu/download
最新的是2.3版本,作者已經很多年沒有更新了,說明還是比較穩定的。

下源源碼后,解壓,文件樹結構如下,部分文件我做了說明:

.
├── ChangeLog
├── COPYING
├── librtmp   // rtmp協議的一個客戶端庫實現
│?? ├── amf.c    // AMF序列化實現
│?? ├── amf.h   // AMF序列化頭文件
│?? ├── bytes.h
│?? ├── COPYING
│?? ├── dhgroups.h
│?? ├── dh.h    // DH非對稱加密算法調用封裝
│?? ├── handshake.h    // 加密版本的握手實現
│?? ├── hashswf.c    // SWF哈希校驗相關實現
│?? ├── http.h    // HTTP請求頭文件定義
│?? ├── librtmp.3
│?? ├── librtmp.3.html
│?? ├── librtmp.pc.in
│?? ├── log.c    // 日志輸出實現
│?? ├── log.h    // 日志輸出定義
│?? ├── Makefile
│?? ├── parseurl.c    // RTMP網址解析實現
│?? ├── rtmp.c    // RTMP主邏輯實現
│?? ├── rtmp.h    // RTMP主邏輯頭文件
│?? └── rtmp_sys.h
├── Makefile
├── README
├── rtmpdump.1
├── rtmpdump.1.html
├── rtmpdump.c    // 一個很強大的FLV文件解析、拉流的例子
├── rtmpgw.8
├── rtmpgw.8.html
├── rtmpgw.c    // 一個HTTP服務代理的實現
├── rtmpsrv.c    // 一個簡單的RTMP服務器的實現的基本框架
├── rtmpsuck.c
├── thread.c    // 線程封裝
└── thread.h    // 線程封裝頭文件

編譯的話一般執行make就可以了,需要openssl依賴,生成的庫在librtmp子目錄下。

主要結構定義:

在rtmp.h中(以下除非特殊說明,均指rtmp.h或rtmp.c),定義了4種塊頭格式:

#define RTMP_PACKET_SIZE_LARGE    0    // 對應于基本塊頭的0格式
#define RTMP_PACKET_SIZE_MEDIUM   1    // 對應于基本塊頭的1格式
#define RTMP_PACKET_SIZE_SMALL    2    // 對應于基本塊頭的2格式
#define RTMP_PACKET_SIZE_MINIMUM  3    // 對應于基本塊頭的3格式

4種格式的塊頭大小:

static const int packetSize[] = { 12, 8, 4, 1 };

整個塊頭的大小是不固定的,最小為1字節(基本塊頭為格式3,且塊流ID大于1小于64時),最大為18字節(基本塊頭為格式0,且塊流ID大于319,且時間戳大于0x00ffffff時)。這里packetSize定義的是4種塊頭格式的消息塊頭大小+1字節的基本塊頭。

塊頭結構定義:

  typedef struct RTMPPacket
  {
    uint8_t m_headerType;    // 塊頭格式
    uint8_t m_packetType;    // 命令類型
    uint8_t m_hasAbsTimestamp;   // 是否絕對時間
    int m_nChannel;    // 塊流ID
    uint32_t m_nTimeStamp;     // 時間戳
    int32_t m_nInfoField2;    // 特殊字段,通常用于保存0x14號遠程調用時的流ID
    uint32_t m_nBodySize;    // 負載大小
    uint32_t m_nBytesRead;    // 當前讀取的負載大小,合包處理時使用
    RTMPChunk *m_chunk;    // 保存原始的chunk數據流
    char *m_body;    // 負載數據指針
  } RTMPPacket;

RTMPPacket非常強大,它負責處理發送和接收過程中的協議解析、分包、合包等復雜邏輯。

RTMP套接字上下文:

  typedef struct RTMPSockBuf
  {
    int sb_socket;    // 套接字
    int sb_size;    // 緩沖區可讀大小
    char *sb_start;    // 緩沖區讀取位置
    char sb_buf[RTMP_BUFFER_CACHE_SIZE];    // 套接字讀取緩沖區
    int sb_timedout;    // 超時標志
    void *sb_ssl;    // TLS上下文
  } RTMPSockBuf;

RTMP在與底層套接口通訊時,使用了這個與邏輯無關的讀緩沖。

RTMP協議層連接上下文:

  typedef struct RTMP_LNK
  {
    AVal hostname;    // 目標主機地址
    AVal sockshost;    // socks代理地址

    // 連接和推拉流涉及的一些參數信息
    AVal playpath0;     /* parsed from URL */
    AVal playpath;      /* passed in explicitly */
    AVal tcUrl;
    AVal swfUrl;
    AVal pageUrl;
    AVal app;
    AVal auth;
    AVal flashVer;
    AVal subscribepath;
    AVal token;
    AMFObject extras;
    int edepth;

    int seekTime;    // 播放流的開始時間
    int stopTime;    // 播放流的停止時間

#define RTMP_LF_AUTH    0x0001  /* using auth param */
#define RTMP_LF_LIVE    0x0002  /* stream is live */
#define RTMP_LF_SWFV    0x0004  /* do SWF verification */
#define RTMP_LF_PLST    0x0008  /* send playlist before play */
#define RTMP_LF_BUFX    0x0010  /* toggle stream on BufferEmpty msg */
#define RTMP_LF_FTCU    0x0020  /* free tcUrl on close */
    int lFlags;

    int swfAge;

    int protocol;    // 連接使用的協議
    int timeout;    // 連接超時時間

    unsigned short socksport;    // socks代理端口
    unsigned short port;    // 目標主機端口

#ifdef CRYPTO
#define RTMP_SWF_HASHLEN        32
    void *dh;                   /* for encryption */
    void *rc4keyIn;
    void *rc4keyOut;

    uint32_t SWFSize;
    uint8_t SWFHash[RTMP_SWF_HASHLEN];
    char SWFVerificationResponse[RTMP_SWF_HASHLEN+10];
#endif
  } RTMP_LNK;

RTMP_LNK包含了連接的服務器地址,以及推拉流所需的各種參數信息,一些需要在連接前進行設置。

RTMP_Read()操作的附加上下文:

  typedef struct RTMP_READ
  {
    char *buf;    // 讀取緩沖區
    char *bufpos;    // 緩沖區讀取位置
    unsigned int buflen;  // 當前緩沖區數據的長度
    uint32_t timestamp;    // 讀取的最新時間戳
    uint8_t dataType;    // 讀取到的元數據媒體類型
    uint8_t flags;    // 讀取標志集合
#define RTMP_READ_HEADER        0x01
#define RTMP_READ_RESUME        0x02
#define RTMP_READ_NO_IGNORE     0x04
#define RTMP_READ_GOTKF         0x08
#define RTMP_READ_GOTFLVK       0x10
#define RTMP_READ_SEEKING       0x20
    int8_t status;    // 當前讀取的狀態
#define RTMP_READ_COMPLETE      -3
#define RTMP_READ_ERROR -2
#define RTMP_READ_EOF   -1
#define RTMP_READ_IGNORE        0

    /* if bResume == TRUE */
    uint8_t initialFrameType;
    uint32_t nResumeTS;
    char *metaHeader;
    char *initialFrame;
    uint32_t nMetaHeaderSize;
    uint32_t nInitialFrameSize;
    uint32_t nIgnoredFrameCounter;
    uint32_t nIgnoredFlvFrameCounter;
  } RTMP_READ;

RTMP_READ結構定義了RTMP_Read()函數工作時需要的附加上下文和緩沖區。
RTMP_Read()與RTMP_ReadPacket()的主要區別是,RTMP_Read()返回的是FLV格式的流,它需要做兩層操作,首先是解RTMP協議,其次是編碼為FLV格式,而RTMP_ReadPacket()只需要執行一步。

0x14命令遠程過程調用隊列子項:

  typedef struct RTMP_METHOD
  {
    AVal name;    // 當前的調用過程名稱
    int num;    // 操作流水號,初使為1,自增
  } RTMP_METHOD;

處理所有RTMP操作的連接上下文:

  typedef struct RTMP
  {
    int m_inChunkSize;    // 最大接收塊大小
    int m_outChunkSize;    // 最大發送塊大小
    int m_nBWCheckCounter;    // 帶寬檢測計數器
    int m_nBytesIn;    // 接收數據計數器
    int m_nBytesInSent;    // 當前數據已回應計數器
    int m_nBufferMS;    // 當前緩沖的時間長度,以MS為單位
    int m_stream_id;    // 當前連接的流ID
    int m_mediaChannel;    // 當前連接媒體使用的塊流ID
    uint32_t m_mediaStamp;    // 當前連接媒體最新的時間戳
    uint32_t m_pauseStamp;    // 當前連接媒體暫停時的時間戳
    int m_pausing;    // 是否暫停狀態
    int m_nServerBW;    // 服務器帶寬
    int m_nClientBW;    // 客戶端帶寬
    uint8_t m_nClientBW2;    // 客戶端帶寬調節方式
    uint8_t m_bPlaying;    // 當前是否推流或連接中
    uint8_t m_bSendEncoding;    // 連接服務器時發送編碼
    uint8_t m_bSendCounter;    // 設置是否向服務器發送接收字節應答

    int m_numInvokes;    // 0x14命令遠程過程調用計數
    int m_numCalls;    // 0x14命令遠程過程請求隊列數量
    RTMP_METHOD *m_methodCalls;    // 遠程過程調用請求隊列

    RTMPPacket *m_vecChannelsIn[RTMP_CHANNELS];    // 對應塊流ID上一次接收的報文
    RTMPPacket *m_vecChannelsOut[RTMP_CHANNELS];    // 對應塊流ID上一次發送的報文
    int m_channelTimestamp[RTMP_CHANNELS];    // 對應塊流ID媒體的最新時間戳

    double m_fAudioCodecs;    // 音頻編碼器代碼
    double m_fVideoCodecs;    // 視頻編碼器代碼
    double m_fEncoding;         /* AMF0 or AMF3 */

    double m_fDuration;    // 當前媒體的時長

    int m_msgCounter;    // 使用HTTP協議發送請求的計數器
    int m_polling;    // 使用HTTP協議接收消息主體時的位置
    int m_resplen;    // 使用HTTP協議接收消息主體時的未讀消息計數
    int m_unackd;    // 使用HTTP協議處理時無響應的計數
    AVal m_clientID;    // 使用HTTP協議處理時的身份ID

    RTMP_READ m_read;    // RTMP_Read()操作的上下文
    RTMPPacket m_write;    // RTMP_Write()操作使用的可復用報文對象
    RTMPSockBuf m_sb;    // RTMP_ReadPacket()讀包操作的上下文
    RTMP_LNK Link;    // RTMP連接上下文
  } RTMP;

RTMP做為整個推拉流操作的上下文,從握手開始到關閉連接,它慣穿了整個會話的生存期。

主要函數實現:

報文操作:

RTMPPacket是librtmp收發報文的關鍵結構,基中m_body緩沖區是塊頭和負載公用的,這里有個技巧,請看代碼:

// 為報文結構分配指定負載大小的內存
int
RTMPPacket_Alloc(RTMPPacket *p, int nSize)
{
  // 這里多分配了18個字節的內存
  char *ptr = calloc(1, nSize + RTMP_MAX_HEADER_SIZE);
  if (!ptr)
    return FALSE;
  // 讓負載指向內存的第19字節
  p->m_body = ptr + RTMP_MAX_HEADER_SIZE;
  p->m_nBytesRead = 0; 
  return TRUE;
}

// 釋放負載內存
void
RTMPPacket_Free(RTMPPacket *p)
{
  if (p->m_body)
    {    
      free(p->m_body - RTMP_MAX_HEADER_SIZE);
      p->m_body = NULL;
    }    
}

// 重置除負載內存以外的其它字段
void
RTMPPacket_Reset(RTMPPacket *p)
{
  p->m_headerType = 0; 
  p->m_packetType = 0; 
  p->m_nChannel = 0; 
  p->m_nTimeStamp = 0; 
  p->m_nInfoField2 = 0; 
  p->m_hasAbsTimestamp = FALSE;
  p->m_nBodySize = 0; 
  p->m_nBytesRead = 0; 
}

RTMPPacket_Alloc()多分配18個字節的內存,其好處在于,發送報文時,頭部和負載可以序列化在一段連續的緩沖區,理想情況下只需要執行一個Write調用。

RTMP上下文的初使化操作:

librtmp提供RTMP上下文內存分配、初使化、釋放的函數,雖然也不復雜,但使用標準接口是個好習慣。

// 分配RTMP內存
RTMP *
RTMP_Alloc()
{
  return calloc(1, sizeof(RTMP));
}

// 釋放RTMP內存
void
RTMP_Free(RTMP *r)
{
  free(r);
}

// 初使化RTMP內存
void
RTMP_Init(RTMP *r)
{
#ifdef CRYPTO
  if (!RTMP_TLS_ctx)
    RTMP_TLS_Init();
#endif

  memset(r, 0, sizeof(RTMP));    // 這里將所有的內存置0
  r->m_sb.sb_socket = -1;
  r->m_inChunkSize = RTMP_DEFAULT_CHUNKSIZE;    // 默認最大接收塊限制128字節
  r->m_outChunkSize = RTMP_DEFAULT_CHUNKSIZE;    // 默認最大發送塊限制128字節
  r->m_nBufferMS = 30000;    // 默認最大時長緩沖設置,需通知服務器
  r->m_nClientBW = 2500000;    // 默認最大客戶端帶寬
  r->m_nClientBW2 = 2;    // 默認最大客戶端帶寬的調整方式
  r->m_nServerBW = 2500000;    // 默認最大服務器帶寬
  r->m_fAudioCodecs = 3191.0;
  r->m_fVideoCodecs = 252.0;
  r->Link.timeout = 30;    // 默認連接超時
  r->Link.swfAge = 30;
}
參數設置:

大多數參數設置函數必須在連接服務器即RTMP_Connect()之前調用,否則可能不會生效。

// 設置推流操作選項,這樣在RTMP_ConnectStream()操作內部,將使用推流請求代替拉流請求。
void
RTMP_EnableWrite(RTMP *r)
{
  r->Link.protocol |= RTMP_FEATURE_WRITE;
}

// 設置服務器緩存的流時間長度
void
RTMP_SetBufferMS(RTMP *r, int size)
{
  r->m_nBufferMS = size;
}

// 設置RTMP推拉流的完整地址
int RTMP_SetupURL(RTMP *r, char *url)
{
  ......
  // 解析流地址
  ret = RTMP_ParseURL(url, &r->Link.protocol, &r->Link.hostname,
        &port, &r->Link.playpath0, &r->Link.app);
  if (!ret)
    return ret;
  r->Link.port = port;
  r->Link.playpath = r->Link.playpath0;
  ......
  // 解析其他KV參數
  while (ptr) {
    *ptr++ = '\0';
    p1 = ptr;
    p2 = strchr(p1, '=');
    if (!p2)
      break;
    opt.av_val = p1;
    opt.av_len = p2 - p1;
    *p2++ = '\0';
    arg.av_val = p2;
    ptr = strchr(p2, ' ');
    if (ptr) {
      *ptr = '\0';
      arg.av_len = ptr - p2;
      /* skip repeated spaces */
      while(ptr[1] == ' ')
        *ptr++ = '\0';
    } else {
      arg.av_len = strlen(p2);
    }

    arg.av_len = p2 - arg.av_val;

    ret = RTMP_SetOpt(r, &opt, &arg);
    if (!ret)
      return ret;
  }
  ......
}
連接和握手操作:

在完成選項設置后,接下來調用RTMP_Connect()進行RTMP的握手操作。握手完成后,客戶端還需要主動發送connect遠程過程調用,這些操作都封裝在RTMP_Connect(),請看代碼:

// 對用戶開放的RTMP連接接口
int
RTMP_Connect(RTMP *r, RTMPPacket *cp)
{
  struct sockaddr_in service;
  if (!r->Link.hostname.av_len)
    return FALSE;

  memset(&service, 0, sizeof(struct sockaddr_in));
  service.sin_family = AF_INET;

  // 設置直接連接的服務器地址
  if (r->Link.socksport)
    {
      /* Connect via SOCKS */
      if (!add_addr_info(&service, &r->Link.sockshost, r->Link.socksport))
        return FALSE;
    }
  else
    {
      /* Connect directly */
      if (!add_addr_info(&service, &r->Link.hostname, r->Link.port))
        return FALSE;
    }

  // 發起網絡連接
  if (!RTMP_Connect0(r, (struct sockaddr *)&service))
    return FALSE;

  r->m_bSendCounter = TRUE;

  // 發起握手協商
  return RTMP_Connect1(r, cp);
}

// 執行基礎網絡連接
int
RTMP_Connect0(RTMP *r, struct sockaddr * service)
{
  int on = 1;
  r->m_sb.sb_timedout = FALSE;
  r->m_pausing = 0;
  r->m_fDuration = 0.0;

  // 創建套接字
  r->m_sb.sb_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (r->m_sb.sb_socket != -1)
    {
      // 連接對端
      if (connect(r->m_sb.sb_socket, service, sizeof(struct sockaddr)) < 0)
        {
          int err = GetSockError();
          RTMP_Log(RTMP_LOGERROR, "%s, failed to connect socket. %d (%s)",
              __FUNCTION__, err, strerror(err));
          RTMP_Close(r);
          return FALSE;
        }
 
      // 執行Socks協商
      if (r->Link.socksport)
        {
          RTMP_Log(RTMP_LOGDEBUG, "%s ... SOCKS negotiation", __FUNCTION__);
          if (!SocksNegotiate(r))
            {
              RTMP_Log(RTMP_LOGERROR, "%s, SOCKS negotiation failed.", __FUNCTION__);
              RTMP_Close(r);
              return FALSE;
            }
        }
    }
  else
    {
      RTMP_Log(RTMP_LOGERROR, "%s, failed to create socket. Error: %d", __FUNCTION__,
          GetSockError());
      return FALSE;
    }

  // 設置接收網絡超時
  {
    SET_RCVTIMEO(tv, r->Link.timeout);
    if (setsockopt
        (r->m_sb.sb_socket, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof(tv)))
      {
        RTMP_Log(RTMP_LOGERROR, "%s, Setting socket timeout to %ds failed!",
            __FUNCTION__, r->Link.timeout);
      }
  }

  setsockopt(r->m_sb.sb_socket, IPPROTO_TCP, TCP_NODELAY, (char *) &on, sizeof(on));

  return TRUE;
}

// 繼續執行SSL或HTTP協商,以及RTMP握手
int
RTMP_Connect1(RTMP *r, RTMPPacket *cp)
{
  // SSL握手處理
  if (r->Link.protocol & RTMP_FEATURE_SSL)
    {
#if defined(CRYPTO) && !defined(NO_SSL)
      TLS_client(RTMP_TLS_ctx, r->m_sb.sb_ssl);
      TLS_setfd(r->m_sb.sb_ssl, r->m_sb.sb_socket);
      if (TLS_connect(r->m_sb.sb_ssl) < 0)
        {
          RTMP_Log(RTMP_LOGERROR, "%s, TLS_Connect failed", __FUNCTION__);
          RTMP_Close(r);
          return FALSE;
        }
#else
      RTMP_Log(RTMP_LOGERROR, "%s, no SSL/TLS support", __FUNCTION__);
      RTMP_Close(r);
      return FALSE;

#endif
    }
  // HTTP代理協商
  if (r->Link.protocol & RTMP_FEATURE_HTTP)
    {
      r->m_msgCounter = 1;
      r->m_clientID.av_val = NULL;
      r->m_clientID.av_len = 0;
      HTTP_Post(r, RTMPT_OPEN, "", 1);
      HTTP_read(r, 1);
      r->m_msgCounter = 0;
    }
  RTMP_Log(RTMP_LOGDEBUG, "%s, ... connected, handshaking", __FUNCTION__);
  // RTMP握手
  if (!HandShake(r, TRUE))
    {
      RTMP_Log(RTMP_LOGERROR, "%s, handshake failed.", __FUNCTION__);
      RTMP_Close(r);
      return FALSE;
    }
  RTMP_Log(RTMP_LOGDEBUG, "%s, handshaked", __FUNCTION__);

  // 發送第一個連接報文
  if (!SendConnectPacket(r, cp))
    {
      RTMP_Log(RTMP_LOGERROR, "%s, RTMP connect failed.", __FUNCTION__);
      RTMP_Close(r);
      return FALSE;
    }
  return TRUE;
}

// SOCKS協商處理
static int
SocksNegotiate(RTMP *r)
{
  unsigned long addr;
  struct sockaddr_in service;
  memset(&service, 0, sizeof(struct sockaddr_in));

  add_addr_info(&service, &r->Link.hostname, r->Link.port);
  addr = htonl(service.sin_addr.s_addr);

  {
    char packet[] = {
      4, 1,                     /* SOCKS 4, connect */
      (r->Link.port >> 8) & 0xFF,
      (r->Link.port) & 0xFF,
      (char)(addr >> 24) & 0xFF, (char)(addr >> 16) & 0xFF,
      (char)(addr >> 8) & 0xFF, (char)addr & 0xFF,
      0
    };                          /* NULL terminate */

    WriteN(r, packet, sizeof packet);

    if (ReadN(r, packet, 8) != 8)
      return FALSE;

    if (packet[0] == 0 && packet[1] == 90)
      {
        return TRUE;
      }
    else
      {
        RTMP_Log(RTMP_LOGERROR, "%s, SOCKS returned error code %d", packet[1]);
        return FALSE;
      }
  }
}
connect遠程調用:

在前面RTMP_Connect1()的最后一步,調用了SendConnectPacket()這個函數,實際上的用途就是發起0x14命令connect遠程調用,代碼摘錄并簡化如下:

static int
SendConnectPacket(RTMP *r, RTMPPacket *cp)
{
  RTMPPacket packet;
  char pbuf[4096], *pend = pbuf + sizeof(pbuf);
  char *enc;

  // 填寫塊頭字段
  packet.m_nChannel = 0x03;     /* control channel (invoke) */
  packet.m_headerType = RTMP_PACKET_SIZE_LARGE;
  packet.m_packetType = 0x14;   /* INVOKE */
  packet.m_nTimeStamp = 0;
  packet.m_nInfoField2 = 0;
  packet.m_hasAbsTimestamp = 0;
  packet.m_body = pbuf + RTMP_MAX_HEADER_SIZE;
  enc = packet.m_body;

  // 壓入connect命令和操作流水號
  enc = AMF_EncodeString(enc, pend, &av_connect);
  enc = AMF_EncodeNumber(enc, pend, ++r->m_numInvokes);
  *enc++ = AMF_OBJECT;
  
  // 壓入對象的各個屬性
  enc = AMF_EncodeNamedString(enc, pend, &av_app, &r->Link.app);
  if (!enc)
    return FALSE;
  if (r->Link.protocol & RTMP_FEATURE_WRITE)
    {
      enc = AMF_EncodeNamedString(enc, pend, &av_type, &av_nonprivate);
      if (!enc)
        return FALSE;
    }
  if (r->Link.flashVer.av_len)
    {
      enc = AMF_EncodeNamedString(enc, pend, &av_flashVer, &r->Link.flashVer);
      if (!enc)
        return FALSE;
    }
  ......
  // 壓入屬性結束標記
  *enc++ = 0;
  *enc++ = 0;                   /* end of object - 0x00 0x00 0x09 */
  *enc++ = AMF_OBJECT_END;
  packet.m_nBodySize = enc - packet.m_body;

  // 發送報文,并記入應答隊列
  return RTMP_SendPacket(r, &packet, TRUE);
}

然后,我們再看看RTMP_SendPacket()的實現,代碼摘錄并簡化如下(相關邏輯的解釋添加在源碼中):

int
RTMP_SendPacket(RTMP *r, RTMPPacket *packet, int queue)
{
  // 取出對應塊流ID上一次發送的報文
  const RTMPPacket *prevPacket = r->m_vecChannelsOut[packet->m_nChannel];
  uint32_t last = 0;
  int nSize;
  int hSize, cSize;
  char *header, *hptr, *hend, hbuf[RTMP_MAX_HEADER_SIZE], c;
  ......
  // 嘗試對非LARGE報文進行字段壓縮
  if (prevPacket && packet->m_headerType != RTMP_PACKET_SIZE_LARGE)
    {
      // MEDIUM報文可以嘗試壓縮為SMALL報文
      if (prevPacket->m_nBodySize == packet->m_nBodySize
          && prevPacket->m_packetType == packet->m_packetType
          && packet->m_headerType == RTMP_PACKET_SIZE_MEDIUM)
        packet->m_headerType = RTMP_PACKET_SIZE_SMALL;

      // MALL報文可以嘗試壓縮為MINIMUM報文
      if (prevPacket->m_nTimeStamp == packet->m_nTimeStamp
          && packet->m_headerType == RTMP_PACKET_SIZE_SMALL)
        packet->m_headerType = RTMP_PACKET_SIZE_MINIMUM;

      last = prevPacket->m_nTimeStamp;
    }
  ......
  // 根據壓縮后的報文類型預設報頭大小
  nSize = packetSize[packet->m_headerType];
  hSize = nSize; cSize = 0;
  t = packet->m_nTimeStamp - last;
  // 預設報頭的緩沖區
  if (packet->m_body)
    {
      header = packet->m_body - nSize;
      hend = packet->m_body;
    }
  else
    {
      header = hbuf + 6;
      hend = hbuf + sizeof(hbuf);
    }

  // 計算基本頭的擴充大小
  if (packet->m_nChannel > 319)
    cSize = 2;
  else if (packet->m_nChannel > 63)
    cSize = 1;
  if (cSize)
    {
      header -= cSize;
      hSize += cSize;
    }
  // 根據時間戳計算是否需要擴充頭大小
  if (nSize > 1 && t >= 0xffffff)
    {
      header -= 4;
      hSize += 4;
    }

  // 向緩沖區壓入基本頭
  hptr = header;
  c = packet->m_headerType << 6;
  switch (cSize)
    {
    case 0:
      c |= packet->m_nChannel;
      break;
    case 1:
      break;
    case 2:
      c |= 1;
      break;
    }
  *hptr++ = c;
  if (cSize)
    {
      int tmp = packet->m_nChannel - 64;
      *hptr++ = tmp & 0xff;
      if (cSize == 2)
        *hptr++ = tmp >> 8;
    }

  // 向緩沖區壓入時間戳
  if (nSize > 1)
    {
      hptr = AMF_EncodeInt24(hptr, hend, t > 0xffffff ? 0xffffff : t);
    }
  // 向緩沖區壓入負載大小和報文類型
  if (nSize > 4)
    {
      hptr = AMF_EncodeInt24(hptr, hend, packet->m_nBodySize);
      *hptr++ = packet->m_packetType;
    }
  // 向緩沖區壓入流ID
  if (nSize > 8)
    hptr += EncodeInt32LE(hptr, packet->m_nInfoField2);
  // 向緩沖區壓入擴展時間戳
  if (nSize > 1 && t >= 0xffffff)
    hptr = AMF_EncodeInt32(hptr, hend, t);

  nSize = packet->m_nBodySize;
  buffer = packet->m_body;
  nChunkSize = r->m_outChunkSize;
  // 當數據未發送完成時
  while (nSize + hSize)
    {
      int wrote;
      // 一次發送的最大負載限制為塊大小
      if (nSize < nChunkSize)
        nChunkSize = nSize;
      // 發送一個塊
      wrote = WriteN(r, header, nChunkSize + hSize);
      if (!wrote)
         return FALSE;
      // 可能有分塊,只有部分負載發送成功
      nSize -= nChunkSize;
      buffer += nChunkSize;
      hSize = 0;
      // 若只有部分負載發送成功,則需繼續構造塊再次發送
      if (nSize > 0)
        {
          // 只需要構造3號類型的塊頭
          header = buffer - 1;
          hSize = 1;
          if (cSize)
            {
              header -= cSize;
              hSize += cSize;
            }
          *header = (0xc0 | c);
          if (cSize)
            {
              int tmp = packet->m_nChannel - 64;
              header[1] = tmp & 0xff;
              if (cSize == 2)
                header[2] = tmp >> 8;
            }
        }
    }
  // 如果是0x14遠程調用,則需要解出調用名稱,加入等待響應的隊列中
  if (packet->m_packetType == 0x14)
    {
      AVal method;
      char *ptr;
      ptr = packet->m_body + 1;
      AMF_DecodeString(ptr, &method);
      if (queue) {
        int txn;
        ptr += 3 + method.av_len;
        txn = (int)AMF_DecodeNumber(ptr);
        AV_queue(&r->m_methodCalls, &r->m_numCalls, &method, txn);
      }
    }
  
  // 記錄這個塊流ID剛剛發送的報文,但是應忽略負載
  if (!r->m_vecChannelsOut[packet->m_nChannel])
    r->m_vecChannelsOut[packet->m_nChannel] = malloc(sizeof(RTMPPacket));
  memcpy(r->m_vecChannelsOut[packet->m_nChannel], packet, sizeof(RTMPPacket));
  return TRUE;
}
connect遠程調用響應_result:

connect報文發出后,這時客戶端會陷入等待狀態,必須接收到服務器的_result響應才能執行后繼的流程。librtmp庫將等待響應的過程封裝了,對使用者展現一個RTMP_ConnectStream()函數調用,代碼如下:

int
RTMP_ConnectStream(RTMP *r, int seekTime)
{
  RTMPPacket packet = { 0 };

  // 設置起始時間定位
  if (seekTime > 0)
    r->Link.seekTime = seekTime;

  r->m_mediaChannel = 0;

  // 循環讀取報文并等待完成推流或拉流的交互準備工作
  while (!r->m_bPlaying && RTMP_IsConnected(r) && RTMP_ReadPacket(r, &packet))
    {
      // 報文可讀
      if (RTMPPacket_IsReady(&packet))
        {
          if (!packet.m_nBodySize)
            continue;

          // 在所有的交互操作準備好之前,過濾非法的音視頻報文
          if ((packet.m_packetType == RTMP_PACKET_TYPE_AUDIO) ||
              (packet.m_packetType == RTMP_PACKET_TYPE_VIDEO) ||
              (packet.m_packetType == RTMP_PACKET_TYPE_INFO))
            {
              RTMP_Log(RTMP_LOGWARNING, "Received FLV packet before play()! Ignoring.");
              RTMPPacket_Free(&packet);
              continue;
            }

          // 進行準備工作期間的報文的分派處理
          RTMP_ClientPacket(r, &packet);
          RTMPPacket_Free(&packet);
        }
    }

  // 返回是否準備好推拉流
  return r->m_bPlaying;
}

在RTMP_ConnectStream()處理交互準備的過程中,有兩個重要函數:RTMP_ReadPacket()負責接收報文,RTMP_ClientPacket()負責邏輯的分派處理。先看RTMP_ReadPacket()代碼:

int
RTMP_ReadPacket(RTMP *r, RTMPPacket *packet)
{
  uint8_t hbuf[RTMP_MAX_HEADER_SIZE] = { 0 };
  char *header = (char *)hbuf;
  int nSize, hSize, nToRead, nChunk;
  int didAlloc = FALSE;

  // 讀取基本塊頭的首個字節
  if (ReadN(r, (char *)hbuf, 1) == 0)
    {
      RTMP_Log(RTMP_LOGERROR, "%s, failed to read RTMP packet header", __FUNCTION__);
      return FALSE;
    }

  // 解析基本塊頭的首個字節,取得報頭類型和塊流ID
  packet->m_headerType = (hbuf[0] & 0xc0) >> 6;
  packet->m_nChannel = (hbuf[0] & 0x3f);
  header++;
  if (packet->m_nChannel == 0)
    {
      // 2字節基本頭,繼續讀取1個字節
      if (ReadN(r, (char *)&hbuf[1], 1) != 1)
        {
          RTMP_Log(RTMP_LOGERROR, "%s, failed to read RTMP packet header 2nd byte",
              __FUNCTION__);
          return FALSE;
        }
      packet->m_nChannel = hbuf[1];
      packet->m_nChannel += 64;
      header++;
    }
  else if (packet->m_nChannel == 1)
    {
      // 3字節基本頭,繼續讀取2個字節
      int tmp;
      if (ReadN(r, (char *)&hbuf[1], 2) != 2)
        {
          RTMP_Log(RTMP_LOGERROR, "%s, failed to read RTMP packet header 3nd byte",
              __FUNCTION__);
          return FALSE;
        }
      tmp = (hbuf[2] << 8) + hbuf[1];
      packet->m_nChannel = tmp + 64;
      RTMP_Log(RTMP_LOGDEBUG, "%s, m_nChannel: %0x", __FUNCTION__, packet->m_nChannel);
      header += 2;
    }

  // 根據報頭類型取得塊頭長度
  nSize = packetSize[packet->m_headerType];

  // 如果是標準大頭,設置時間戳為絕對的
  if (nSize == RTMP_LARGE_HEADER_SIZE)
    packet->m_hasAbsTimestamp = TRUE;
  // 如果非標準大頭,首次嘗試拷貝上一次的報頭
  else if (nSize < RTMP_LARGE_HEADER_SIZE)
    {
      // 這里的拷貝操作有可能取得上次的分塊報文,然后繼續后續塊的接收合并工作
      if (r->m_vecChannelsIn[packet->m_nChannel])
        memcpy(packet, r->m_vecChannelsIn[packet->m_nChannel],
               sizeof(RTMPPacket));
    }

  // 計算消息塊頭(主體塊頭)大小
  nSize--;

  // 讀取消息塊頭
  if (nSize > 0 && ReadN(r, header, nSize) != nSize)
    {
      RTMP_Log(RTMP_LOGERROR, "%s, failed to read RTMP packet header. type: %x",
          __FUNCTION__, (unsigned int)hbuf[0]);
      return FALSE;
    }
  // 計算基本塊頭+消息塊頭的大小
  hSize = nSize + (header - (char *)hbuf);

  if (nSize >= 3)
    {
      // 解析時間戳
      packet->m_nTimeStamp = AMF_DecodeInt24(header);

      if (nSize >= 6)
        {
          // 解析負載長度
          packet->m_nBodySize = AMF_DecodeInt24(header + 3);
          packet->m_nBytesRead = 0;
          RTMPPacket_Free(packet);

          if (nSize > 6)
            {
              // 解析包類型
              packet->m_packetType = header[6];
              // 解析流ID
              if (nSize == 11)
                packet->m_nInfoField2 = DecodeInt32LE(header + 7);
            }
        }
      // 讀取擴展時間戳并解析
      if (packet->m_nTimeStamp == 0xffffff)
        {
          if (ReadN(r, header + nSize, 4) != 4)
            {
              RTMP_Log(RTMP_LOGERROR, "%s, failed to read extended timestamp",
                  __FUNCTION__);
              return FALSE;
            }
          packet->m_nTimeStamp = AMF_DecodeInt32(header + nSize);
          hSize += 4;
        }
    }

  // 負載非0,需要分配內存,或第一個分塊的初使化工作
  if (packet->m_nBodySize > 0 && packet->m_body == NULL)
    {
      if (!RTMPPacket_Alloc(packet, packet->m_nBodySize))
        {
          RTMP_Log(RTMP_LOGDEBUG, "%s, failed to allocate packet", __FUNCTION__);
          return FALSE;
        }
      didAlloc = TRUE;
      packet->m_headerType = (hbuf[0] & 0xc0) >> 6;
    }

  // 準備讀取的數據和塊大小
  nToRead = packet->m_nBodySize - packet->m_nBytesRead;
  nChunk = r->m_inChunkSize;
  if (nToRead < nChunk)
    nChunk = nToRead;

  // 如果packet->m_chunk非空,拷貝當前塊的相關信息
  if (packet->m_chunk)
    {
      packet->m_chunk->c_headerSize = hSize;
      memcpy(packet->m_chunk->c_header, hbuf, hSize);
      packet->m_chunk->c_chunk = packet->m_body + packet->m_nBytesRead;
      packet->m_chunk->c_chunkSize = nChunk;
    }

  // 讀取負載到緩沖區中
  if (ReadN(r, packet->m_body + packet->m_nBytesRead, nChunk) != nChunk)
    {
      RTMP_Log(RTMP_LOGERROR, "%s, failed to read RTMP packet body. len: %lu",
          __FUNCTION__, packet->m_nBodySize);
      return FALSE;
    }

  RTMP_LogHexString(RTMP_LOGDEBUG2, (uint8_t *)packet->m_body + packet->m_nBytesRead, nChunk);

  packet->m_nBytesRead += nChunk;

  // 保存當前塊流ID最新的報文,與RTMP_SendPacket()不同的是,負載部分也被保存了,以應對不完整的分塊報文
  if (!r->m_vecChannelsIn[packet->m_nChannel])
    r->m_vecChannelsIn[packet->m_nChannel] = malloc(sizeof(RTMPPacket));
  memcpy(r->m_vecChannelsIn[packet->m_nChannel], packet, sizeof(RTMPPacket));

  // 若報文負載接收完整
  if (RTMPPacket_IsReady(packet))
    {
      // 處理增量時間戳
      if (!packet->m_hasAbsTimestamp)
        packet->m_nTimeStamp += r->m_channelTimestamp[packet->m_nChannel]; 

      // 保存當前塊流ID的時間戳
      r->m_channelTimestamp[packet->m_nChannel] = packet->m_nTimeStamp;

      // 清理上下文中當前塊流ID最新的報文的負載信息
      r->m_vecChannelsIn[packet->m_nChannel]->m_body = NULL;
      r->m_vecChannelsIn[packet->m_nChannel]->m_nBytesRead = 0;
      r->m_vecChannelsIn[packet->m_nChannel]->m_hasAbsTimestamp = FALSE;
    }
  else
    {
      // 若報文不完整,不將分片負載向上拋給應用,以免引起使用誤解
      packet->m_body = NULL;
    }

  return TRUE;
}

再看RTMP_ClientPacket()源碼:

int
RTMP_ClientPacket(RTMP *r, RTMPPacket *packet)
{
  // 根據命令類型進行分派處理
  int bHasMediaPacket = 0;
  switch (packet->m_packetType)
    {
    case 0x01:
      // 更新接收處理時的塊限制
      HandleChangeChunkSize(r, packet);
      break;

    case 0x03:
      // 對端反饋的已讀大小
      RTMP_Log(RTMP_LOGDEBUG, "%s, received: bytes read report", __FUNCTION__);
      break;

    case 0x04:
      // 處理對端發送的控制報文
      HandleCtrl(r, packet);
      break;

    case 0x05:
      // 處理對端發送的應答窗口大小,這里由服務器發送,即告之客戶端收到對應大小的數據后應發送反饋
      HandleServerBW(r, packet);
      break;

    case 0x06:
      // 處理對端發送的設置發送帶寬大小,這里由服務器發送,即設置客戶端的發送帶寬
      HandleClientBW(r, packet);
      break;

    case 0x08:
      // 處理音頻數據
      HandleAudio(r, packet);
      bHasMediaPacket = 1;
      if (!r->m_mediaChannel)
        r->m_mediaChannel = packet->m_nChannel;
      if (!r->m_pausing)
        r->m_mediaStamp = packet->m_nTimeStamp;
      break;

    case 0x09:
      // 處理視頻數據
      HandleVideo(r, packet);
      bHasMediaPacket = 1;
      if (!r->m_mediaChannel)
        r->m_mediaChannel = packet->m_nChannel;
      if (!r->m_pausing)
        r->m_mediaStamp = packet->m_nTimeStamp;
      break;

    case 0x12:
      // 處理媒體元數據
      if (HandleMetadata(r, packet->m_body, packet->m_nBodySize))
        bHasMediaPacket = 1;
      break;

    case 0x14:
      // 處理遠程調用
      if (HandleInvoke(r, packet->m_body, packet->m_nBodySize) == 1)
        bHasMediaPacket = 2;
      break;
    }

  // 返回值為1表示推拉流正在正作中,為2表示已經停止
  return bHasMediaPacket;
}

在RTMP_ConnectStream()中,RTMP_ReadPacket()接收的報文,交給RTMP_ClientPacket()進行分派。connect遠程調用發出后的_result響應,也屬于0x14命令,我們需要繼續解析HandleInvoke()這個函數,代碼如下:

static int
HandleInvoke(RTMP *r, const char *body, unsigned int nBodySize)
{
  AMFObject obj;
  AVal method;
  int txn;
  int ret = 0, nRes;

  // 確保響應報文是0x14的命令字
  if (body[0] != 0x02)
    {
      RTMP_Log(RTMP_LOGWARNING, "%s, Sanity failed. no string method in invoke packet",
          __FUNCTION__);
      return 0;
    }

  // 將各參數以無名稱的對象屬性方式進行解析
  nRes = AMF_Decode(&obj, body, nBodySize, FALSE);
  if (nRes < 0)
    {
      RTMP_Log(RTMP_LOGERROR, "%s, error decoding invoke packet", __FUNCTION__);
      return 0;
    }

  AMF_Dump(&obj);

  // 獲取過程名稱和流水號
  AMFProp_GetString(AMF_GetProp(&obj, NULL, 0), &method);
  txn = (int)AMFProp_GetNumber(AMF_GetProp(&obj, NULL, 1));
  RTMP_Log(RTMP_LOGDEBUG, "%s, server invoking <%s>", __FUNCTION__, method.av_val);

  // 過程名稱為_result
  if (AVMATCH(&method, &av__result))
    {
      AVal methodInvoked = {0};
      int i;

      // 刪除請求隊列中的流水項
      for (i=0; i<r->m_numCalls; i++) {
        if (r->m_methodCalls[i].num == txn) {
          methodInvoked = r->m_methodCalls[i].name;
          AV_erase(r->m_methodCalls, &r->m_numCalls, i, FALSE);
          break;
        }
      }
      if (!methodInvoked.av_val) {
        RTMP_Log(RTMP_LOGDEBUG, "%s, received result id %d without matching request",
          __FUNCTION__, txn);
        goto leave;
      }

      RTMP_Log(RTMP_LOGDEBUG, "%s, received result for method call <%s>", __FUNCTION__,
          methodInvoked.av_val);

      // 找到了連接請求,確認是連接響應
      if (AVMATCH(&methodInvoked, &av_connect))
        {
          // 客戶端推流
          if (r->Link.protocol & RTMP_FEATURE_WRITE)
            {
              // 通知服務器釋放流通道和清理推流資源
              SendReleaseStream(r);
              SendFCPublish(r);
            }
          // 客戶端拉流
          else
            {
              // 設置服務器的應答窗口大小
              RTMP_SendServerBW(r);
              RTMP_SendCtrl(r, 3, 0, 300);
            }
          // 發送創建流通道請求
          RTMP_SendCreateStream(r);
        }
      // 找到了創建流請求,確認是創建流的響應
      else if (AVMATCH(&methodInvoked, &av_createStream))
        {
          // 從響應中取流ID
          r->m_stream_id = (int)AMFProp_GetNumber(AMF_GetProp(&obj, NULL, 3));

          // 客戶端推流
          if (r->Link.protocol & RTMP_FEATURE_WRITE)
            {
              // 發送推流點
              SendPublish(r);
            }
          // 客戶端拉流
          else
            {
              // 發送拉流點
              SendPlay(r);
              // 發送拉流緩沖時長
              RTMP_SendCtrl(r, 3, r->m_stream_id, r->m_nBufferMS);
            }
        }
      // 找到了推流和拉流請求,確認是它們的響應
      else if (AVMATCH(&methodInvoked, &av_play) ||
        AVMATCH(&methodInvoked, &av_publish))
        {
          // 標識已經進入流狀態
          r->m_bPlaying = TRUE;
        }
      free(methodInvoked.av_val);
    }
  // 過程名稱為ping
  else if (AVMATCH(&method, &av_ping))
    {
      // 發送pong響應
      SendPong(r, txn);
    }
  // 過程名稱為_error
  else if (AVMATCH(&method, &av__error))
    {
      RTMP_Log(RTMP_LOGERROR, "rtmp server sent error");
    }
  // 過程名稱為close
  else if (AVMATCH(&method, &av_close))
    {
      RTMP_Log(RTMP_LOGERROR, "rtmp server requested close");
      RTMP_Close(r);
    }
  // 過程名稱為onStatus
  else if (AVMATCH(&method, &av_onStatus))
    {
      // 獲取返回對象及其主要屬性
      AMFObject obj2;
      AVal code, level;
      AMFProp_GetObject(AMF_GetProp(&obj, NULL, 3), &obj2);
      AMFProp_GetString(AMF_GetProp(&obj2, &av_code, -1), &code);
      AMFProp_GetString(AMF_GetProp(&obj2, &av_level, -1), &level);

      RTMP_Log(RTMP_LOGDEBUG, "%s, onStatus: %s", __FUNCTION__, code.av_val);

      // 出錯返回
      if (AVMATCH(&code, &av_NetStream_Failed)
          || AVMATCH(&code, &av_NetStream_Play_Failed)
          || AVMATCH(&code, &av_NetStream_Play_StreamNotFound)
          || AVMATCH(&code, &av_NetConnection_Connect_InvalidApp))
        {
          r->m_stream_id = -1;
          RTMP_Close(r);
          RTMP_Log(RTMP_LOGERROR, "Closing connection: %s", code.av_val);
        }

      // 啟動拉流成功
      else if (AVMATCH(&code, &av_NetStream_Play_Start))
        {
          int i;
          r->m_bPlaying = TRUE;
          for (i = 0; i < r->m_numCalls; i++)
            {
              if (AVMATCH(&r->m_methodCalls[i].name, &av_play))
                {
                  AV_erase(r->m_methodCalls, &r->m_numCalls, i, TRUE);
                  break;
                }
            }
        }

      // 啟動推流成功
      else if (AVMATCH(&code, &av_NetStream_Publish_Start))
        {
          int i;
          r->m_bPlaying = TRUE;
          for (i = 0; i < r->m_numCalls; i++)
            {
              if (AVMATCH(&r->m_methodCalls[i].name, &av_publish))
                {
                  AV_erase(r->m_methodCalls, &r->m_numCalls, i, TRUE);
                  break;
                }
            }
        }

      // 通知流完成或結束
      else if (AVMATCH(&code, &av_NetStream_Play_Complete)
          || AVMATCH(&code, &av_NetStream_Play_Stop)
          || AVMATCH(&code, &av_NetStream_Play_UnpublishNotify))
        {
          RTMP_Close(r);
          ret = 1;
        }

      // 通知流暫停
      else if (AVMATCH(&code, &av_NetStream_Pause_Notify))
        {
          if (r->m_pausing == 1 || r->m_pausing == 2)
          {
            RTMP_SendPause(r, FALSE, r->m_pauseStamp);
            r->m_pausing = 3;
          }
        }
    }
leave:
  AMF_Reset(&obj);
  return ret;
}

上面的注釋顯示,在HandleInvoke()函數的處理分支中,有涉及connect調用的_result處理。在收到_result響應后,又根據推流或拉流的標志,繼續后續的流程。
HandleInvoke()函數非常強大,除了對各種_result進行處理外,它還支持onStatus操作,后續的createStream響應,publish和play推拉流的狀態反饋,都會集中在這里處理。

connect遠程調用的其它交互:

connect和_result交互是必須的,事實上,在_result返回之前,服務器還可以先返回一些可選的設置報文,例如:

服務器設置客戶端的應答窗口大小:

int
RTMP_ClientPacket(RTMP *r, RTMPPacket *packet)
{
    ......
  switch (packet->m_packetType)
    {
    ......
    case 0x05:
      /* server bw */
      HandleServerBW(r, packet);
      break;
    ......
}

static void
HandleServerBW(RTMP *r, const RTMPPacket *packet)
{
  r->m_nServerBW = AMF_DecodeInt32(packet->m_body);
  RTMP_Log(RTMP_LOGDEBUG, "%s: server BW = %d", __FUNCTION__, r->m_nServerBW);
}

服務器設置客戶端的發送帶寬大小:

int
RTMP_ClientPacket(RTMP *r, RTMPPacket *packet)
{
    ......
  switch (packet->m_packetType)
    {
    ......
    case 0x06:
      /* client bw */
      HandleClientBW(r, packet);
      break;
    ......
}

static void
HandleClientBW(RTMP *r, const RTMPPacket *packet)
{
  r->m_nClientBW = AMF_DecodeInt32(packet->m_body);
  if (packet->m_nBodySize > 4)
    r->m_nClientBW2 = packet->m_body[4];
  else
    r->m_nClientBW2 = -1;
  RTMP_Log(RTMP_LOGDEBUG, "%s: client BW = %d %d", __FUNCTION__, r->m_nClientBW,
      r->m_nClientBW2);
}

服務器設置客戶端的接收塊大小:

int
RTMP_ClientPacket(RTMP *r, RTMPPacket *packet)
{
    ......
  switch (packet->m_packetType)
    {
    case 0x01:
      /* chunk size */
      HandleChangeChunkSize(r, packet);
      break;
    ......
}

static void
HandleChangeChunkSize(RTMP *r, const RTMPPacket *packet)
{
  if (packet->m_nBodySize >= 4)
    {
      r->m_inChunkSize = AMF_DecodeInt32(packet->m_body);
      RTMP_Log(RTMP_LOGDEBUG, "%s, received: chunk size change to %d", __FUNCTION__,
          r->m_inChunkSize);
    }
}
createStream遠程調用:

在成功處理connect的_result響應之后,即表示服務器接收了客戶端的第一步的地址請求,接下來客戶端需要根據推流或拉流的場景,發起后續的請求了,精簡HandleInvoke()后的代碼如下:

static int
HandleInvoke(RTMP *r, const char *body, unsigned int nBodySize)
{
  if (AVMATCH(&method, &av__result))
    {
      ......
      if (AVMATCH(&methodInvoked, &av_connect))
        {
          // 判斷推流標志是否設置
          if (r->Link.protocol & RTMP_FEATURE_WRITE)
            {
              // 推流準備(必須)
              SendReleaseStream(r);
              SendFCPublish(r);
            }
          else
            {
              // 拉流準備(可選)
              RTMP_SendServerBW(r);
              RTMP_SendCtrl(r, 3, 0, 300);
            }
          
          // 創建流通道
          RTMP_SendCreateStream(r);
        }
    }
      ......
}

上面的代碼和注釋顯示,無論是推流還是拉流,都需要創建流通道,但是在之前,有一些不同的額外操作要設置。

推流的額外操作:

推流之前,需要先釋放掉當前的推流點的資源,并且準備好新的推流點。

// 發送釋放推流點請求
static int
SendReleaseStream(RTMP *r)
{
  RTMPPacket packet;
  char pbuf[1024], *pend = pbuf + sizeof(pbuf);
  char *enc;

  packet.m_nChannel = 0x03;     /* control channel (invoke) */
  packet.m_headerType = RTMP_PACKET_SIZE_MEDIUM;
  packet.m_packetType = 0x14;   /* INVOKE */
  packet.m_nTimeStamp = 0;
  packet.m_nInfoField2 = 0;
  packet.m_hasAbsTimestamp = 0;
  packet.m_body = pbuf + RTMP_MAX_HEADER_SIZE;

  // 壓入遠程過程調用的參數,尤其是推流點
 enc = packet.m_body;
  enc = AMF_EncodeString(enc, pend, &av_releaseStream);
  enc = AMF_EncodeNumber(enc, pend, ++r->m_numInvokes);
  *enc++ = AMF_NULL;
  enc = AMF_EncodeString(enc, pend, &r->Link.playpath);    // 推流點
  if (!enc)
    return FALSE;

  packet.m_nBodySize = enc - packet.m_body;

  return RTMP_SendPacket(r, &packet, FALSE);
}

// 發送準備推流點請求
static int
SendFCPublish(RTMP *r)
{
  RTMPPacket packet;
  char pbuf[1024], *pend = pbuf + sizeof(pbuf);
  char *enc;

  packet.m_nChannel = 0x03;     /* control channel (invoke) */
  packet.m_headerType = RTMP_PACKET_SIZE_MEDIUM;
  packet.m_packetType = 0x14;   /* INVOKE */
  packet.m_nTimeStamp = 0;
  packet.m_nInfoField2 = 0;
  packet.m_hasAbsTimestamp = 0;
  packet.m_body = pbuf + RTMP_MAX_HEADER_SIZE;

  // 壓入遠程過程調用的參數,尤其是推流點
  enc = packet.m_body;
  enc = AMF_EncodeString(enc, pend, &av_FCPublish);
  enc = AMF_EncodeNumber(enc, pend, ++r->m_numInvokes);
  *enc++ = AMF_NULL;
  enc = AMF_EncodeString(enc, pend, &r->Link.playpath);    // 推流點
  if (!enc)
    return FALSE;

  packet.m_nBodySize = enc - packet.m_body;

  return RTMP_SendPacket(r, &packet, FALSE);
}
拉流的額外操作:

通知服務器收到指定大小的客戶端數據后,需要發送應答。拉流時客戶端基本不發送流量,個人感覺這步有些多余。

int
RTMP_SendServerBW(RTMP *r)
{
  RTMPPacket packet;
  char pbuf[256], *pend = pbuf + sizeof(pbuf);

  packet.m_nChannel = 0x02;     /* control channel (invoke) */
  packet.m_headerType = RTMP_PACKET_SIZE_LARGE;
  packet.m_packetType = 0x05;   /* Server BW */
  packet.m_nTimeStamp = 0;
  packet.m_nInfoField2 = 0;
  packet.m_hasAbsTimestamp = 0;
  packet.m_body = pbuf + RTMP_MAX_HEADER_SIZE;

  packet.m_nBodySize = 4;

  // 壓入4字節帶寬
  AMF_EncodeInt32(packet.m_body, pend, r->m_nServerBW);
  return RTMP_SendPacket(r, &packet, FALSE);
}

// 控制報文的含義比較多,具體也需要查看手冊,對應拉流來說,主要是設置服務器的緩存時間。
int
RTMP_SendCtrl(RTMP *r, short nType, unsigned int nObject, unsigned int nTime)
{
  RTMPPacket packet;
  char pbuf[256], *pend = pbuf + sizeof(pbuf);
  int nSize;
  char *buf;

  packet.m_nChannel = 0x02;     /* control channel (ping) */
  packet.m_headerType = RTMP_PACKET_SIZE_MEDIUM;
  packet.m_packetType = 0x04;   /* ctrl */
  packet.m_nTimeStamp = 0;      /* RTMP_GetTime(); */
  packet.m_nInfoField2 = 0;
  packet.m_hasAbsTimestamp = 0;
  packet.m_body = pbuf + RTMP_MAX_HEADER_SIZE;

  switch(nType) {
  case 0x03: nSize = 10; break; /* buffer time */
  case 0x1A: nSize = 3; break;  /* SWF verify request */
  case 0x1B: nSize = 44; break; /* SWF verify response */
  default: nSize = 6; break;
  }

  packet.m_nBodySize = nSize;

  buf = packet.m_body;
  buf = AMF_EncodeInt16(buf, pend, nType);

  if (nType == 0x1B)
    {
    ......
    }
  else if (nType == 0x1A)
    {
    ......
   }
  else
    {
      if (nSize > 2)
        buf = AMF_EncodeInt32(buf, pend, nObject);

      if (nSize > 6)
        buf = AMF_EncodeInt32(buf, pend, nTime);
    }

  return RTMP_SendPacket(r, &packet, FALSE);
}
創建流通道操作:
// 發送創建流請求
int
RTMP_SendCreateStream(RTMP *r)
{
  RTMPPacket packet;
  char pbuf[256], *pend = pbuf + sizeof(pbuf);
  char *enc;

  packet.m_nChannel = 0x03;     /* control channel (invoke) */
  packet.m_headerType = RTMP_PACKET_SIZE_MEDIUM;
  packet.m_packetType = 0x14;   /* INVOKE */
  packet.m_nTimeStamp = 0;
  packet.m_nInfoField2 = 0;
  packet.m_hasAbsTimestamp = 0;
  packet.m_body = pbuf + RTMP_MAX_HEADER_SIZE;

  // 壓入遠程過程調用的參數
  enc = packet.m_body;
  enc = AMF_EncodeString(enc, pend, &av_createStream);
  enc = AMF_EncodeNumber(enc, pend, ++r->m_numInvokes);
  *enc++ = AMF_NULL;            /* NULL */

  packet.m_nBodySize = enc - packet.m_body;

  return RTMP_SendPacket(r, &packet, TRUE);
}
createStream遠程調用響應:

服務器收到createStream請求后,若無錯誤,則返回流ID,有了流ID客戶端就可以進行后續的推流或拉流了,邏輯如下:

static int
HandleInvoke(RTMP *r, const char *body, unsigned int nBodySize)
{
  if (AVMATCH(&method, &av__result))
    {
      ......
      else if (AVMATCH(&methodInvoked, &av_createStream))
        {
          // 保存流ID
          r->m_stream_id = (int)AMFProp_GetNumber(AMF_GetProp(&obj, NULL, 3));

          if (r->Link.protocol & RTMP_FEATURE_WRITE)
            {
              // 開始推流
              SendPublish(r);
            }
          else
            {
              // 開始拉流
              SendPlay(r);
              // 控制該流的緩沖時長
              RTMP_SendCtrl(r, 3, r->m_stream_id, r->m_nBufferMS);
            }
        }
      ......
    }
}

上面的代碼和注釋顯示,推流和拉流,分別進行不同的邏輯處理。

推流處理publish調用:

對推流來說,客戶端需要請求服務器將流ID和推送點進行關聯。請求代碼如下:

static int
SendPublish(RTMP *r)
{
  RTMPPacket packet;
  char pbuf[1024], *pend = pbuf + sizeof(pbuf);
  char *enc;

  packet.m_nChannel = 0x04;     /* source channel (invoke) */
  packet.m_headerType = RTMP_PACKET_SIZE_LARGE;
  packet.m_packetType = 0x14;   /* INVOKE */
  packet.m_nTimeStamp = 0;
  packet.m_nInfoField2 = r->m_stream_id;    // 指定流ID
  packet.m_hasAbsTimestamp = 0;
  packet.m_body = pbuf + RTMP_MAX_HEADER_SIZE;

  enc = packet.m_body;
  enc = AMF_EncodeString(enc, pend, &av_publish);
  enc = AMF_EncodeNumber(enc, pend, ++r->m_numInvokes);
  *enc++ = AMF_NULL;
  enc = AMF_EncodeString(enc, pend, &r->Link.playpath);    // 指定推送點
  if (!enc)
    return FALSE;

  /* FIXME: should we choose live based on Link.lFlags & RTMP_LF_LIVE? */
  enc = AMF_EncodeString(enc, pend, &av_live);
  if (!enc)
    return FALSE;

  packet.m_nBodySize = enc - packet.m_body;

  return RTMP_SendPacket(r, &packet, TRUE);    // 需要反饋
}
拉流處理play調用:

對拉流來說,客戶端需要請求服務器將流ID和播放點進行關聯。請求代碼如下:

static int
SendPlay(RTMP *r)
{
  RTMPPacket packet;
  char pbuf[1024], *pend = pbuf + sizeof(pbuf);
  char *enc;

  packet.m_nChannel = 0x08;     /* we make 8 our stream channel */
  packet.m_headerType = RTMP_PACKET_SIZE_LARGE;
  packet.m_packetType = 0x14;   /* INVOKE */
  packet.m_nTimeStamp = 0;
  packet.m_nInfoField2 = r->m_stream_id;        /*0x01000000; */    // 指定流ID
  packet.m_hasAbsTimestamp = 0;
  packet.m_body = pbuf + RTMP_MAX_HEADER_SIZE;

  enc = packet.m_body;
  enc = AMF_EncodeString(enc, pend, &av_play);
  enc = AMF_EncodeNumber(enc, pend, ++r->m_numInvokes);
  *enc++ = AMF_NULL;

  RTMP_Log(RTMP_LOGDEBUG, "%s, seekTime=%d, stopTime=%d, sending play: %s",
      __FUNCTION__, r->Link.seekTime, r->Link.stopTime,
      r->Link.playpath.av_val);
  enc = AMF_EncodeString(enc, pend, &r->Link.playpath);    // 指定推送點
  if (!enc)
    return FALSE;

  // 指定開始時間
  /* Optional parameters start and len.
   *
   * start: -2, -1, 0, positive number
   *  -2: looks for a live stream, then a recorded stream,
   *      if not found any open a live stream
   *  -1: plays a live stream
   * >=0: plays a recorded streams from 'start' milliseconds
   */
  if (r->Link.lFlags & RTMP_LF_LIVE)
    enc = AMF_EncodeNumber(enc, pend, -1000.0);
  else
    {
      if (r->Link.seekTime > 0.0)
        enc = AMF_EncodeNumber(enc, pend, r->Link.seekTime);    /* resume from here */
      else
        enc = AMF_EncodeNumber(enc, pend, 0.0); /*-2000.0);*/ /* recorded as default, -2000.0 is not reliable since that freezes the
 player if the stream is not found */
    }
  if (!enc)
    return FALSE;

  // 指點播放時長
  /* len: -1, 0, positive number
   *  -1: plays live or recorded stream to the end (default)
   *   0: plays a frame 'start' ms away from the beginning
   *  >0: plays a live or recoded stream for 'len' milliseconds
   */
  /*enc += EncodeNumber(enc, -1.0); */ /* len */
  if (r->Link.stopTime)
    {
      enc = AMF_EncodeNumber(enc, pend, r->Link.stopTime - r->Link.seekTime);
      if (!enc)
        return FALSE;
    }

  packet.m_nBodySize = enc - packet.m_body;

  return RTMP_SendPacket(r, &packet, TRUE);    // 需要反饋
}
publish或play狀態反饋:

在前面完成publish或play過程調用后,客戶端需要等待響應或結果反饋。繼續看HandleInvoke()函數精簡后的相關代碼:

static int
HandleInvoke(RTMP *r, const char *body, unsigned int nBodySize)
{
  ......
  else if (AVMATCH(&method, &av_onStatus))
    {
      AMFObject obj2;
      AVal code, level;
      AMFProp_GetObject(AMF_GetProp(&obj, NULL, 3), &obj2);
      AMFProp_GetString(AMF_GetProp(&obj2, &av_code, -1), &code);
      AMFProp_GetString(AMF_GetProp(&obj2, &av_level, -1), &level);

      RTMP_Log(RTMP_LOGDEBUG, "%s, onStatus: %s", __FUNCTION__, code.av_val);

      // 判斷是否出錯,出錯需中止流程
      if (AVMATCH(&code, &av_NetStream_Failed)
          || AVMATCH(&code, &av_NetStream_Play_Failed)
          || AVMATCH(&code, &av_NetStream_Play_StreamNotFound)
          || AVMATCH(&code, &av_NetConnection_Connect_InvalidApp))
        {
          r->m_stream_id = -1;
          RTMP_Close(r);
          RTMP_Log(RTMP_LOGERROR, "Closing connection: %s", code.av_val);
        }
      ......
      // 啟動推流成功
      else if (AVMATCH(&code, &av_NetStream_Publish_Start))
        {
          int i;
          r->m_bPlaying = TRUE;    // 設置可以推流標志
          for (i = 0; i < r->m_numCalls; i++)
            {
              if (AVMATCH(&r->m_methodCalls[i].name, &av_publish))
                {
                  AV_erase(r->m_methodCalls, &r->m_numCalls, i, TRUE);
                  break;
                }
            }
        }
      // 啟動拉流成功
      else if (AVMATCH(&code, &av_NetStream_Play_Start))
        {
          int i;
          r->m_bPlaying = TRUE;    // 設置可以拉流標志
          for (i = 0; i < r->m_numCalls; i++)
            {
              if (AVMATCH(&r->m_methodCalls[i].name, &av_play))
                {
                  AV_erase(r->m_methodCalls, &r->m_numCalls, i, TRUE);
                  break;
                }
            }
        }
      ......
    }
  ......
}

讓我們回到RTMP_ConnectStream()函數,我們留意其中的m_bPlaying標志,由于此時標志為TRUE,循環條件不成立,函數退出。簡化代碼如下:

int
RTMP_ConnectStream(RTMP *r, int seekTime)
{
  ......
  while (!r->m_bPlaying && RTMP_IsConnected(r) && RTMP_ReadPacket(r, &packet))
    {
        ......
    }
  return r->m_bPlaying;    // 返回推流或拉流準許的狀態
}

當RTMP_ConnectStream()返回TRUE的時候,librtmp庫的使用者,就可以在自己的代碼層次,進行后續的推流或拉流操作了。

推流操作代碼節選:
    // 初使化RTMP報文
    RTMPPacket packet;
    RTMPPacket_Reset(&packet);
    packet.m_body = NULL;
    packet.m_chunk = NULL;

    packet.m_nInfoField2 = pRTMP->m_stream_id;

    uint32_t starttime = RTMP_GetTime();

    while (true)
    {
        // 讀取TAG頭

        uint8_t type = 0;
        if (!ReadU8(&type, pFile))
            break;

        uint32_t datalen = 0;
        if (!ReadU24(&datalen, pFile))
            break;

        uint32_t timestamp = 0;
        if (!ReadTime(&timestamp, pFile))
            break;

        uint32_t streamid = 0;
        if (!ReadU24(&streamid, pFile))
            break;

/*
        // 跳過0x12 Script
        if (type != 0x08 && type != 0x09)
        {
            fseek(pFile, datalen + 4, SEEK_CUR);
            continue;
        }
*/

        RTMPPacket_Alloc(&packet, datalen);

        if (fread(packet.m_body, 1, datalen, pFile) != datalen)
            break;

        // 組織報文并發送
        packet.m_headerType = RTMP_PACKET_SIZE_LARGE;
        packet.m_packetType = type;
        packet.m_hasAbsTimestamp = 0;
        packet.m_nChannel = 6;
        packet.m_nTimeStamp = timestamp;
        packet.m_nBodySize = datalen;

        if (!RTMP_SendPacket(pRTMP, &packet, 0))
        {
            printf("Send Error! \n");
            break;
        }

        printf("send type:[%d] timestamp:[%d] datasize:[%d] \n", type, timestamp, datalen);

        // 跳過PreTag
        uint32_t pretagsize = 0;
        if (!ReadU32(&pretagsize, pFile))
            break;

        // 延時,避免發送太快
        uint32_t timeago = (RTMP_GetTime() - starttime);
        if (timestamp > 1000 && timeago < timestamp - 1000)
        {
            printf("sleep...\n");
            usleep(100000);
        }

        RTMPPacket_Free(&packet);
    }

上面這段代碼片斷演示了使用librtmp推流,讀取FLV文件,并向上推流的例子。

拉流操作代碼節選:
    bool bSaveMP3 = true;
    FILE* pFile = fopen(bSaveMP3 ? "testrtmp.mp3" : "testrtmp.flv", "wb");

    while (RTMP_IsConnected(pRTMP))
    {
        if (bSaveMP3)
        {
            RTMPPacket packet;
            RTMPPacket_Reset(&packet);
            packet.m_body = NULL;
            packet.m_chunk = NULL;
            b = RTMP_ReadPacket(pRTMP, &packet);
            if (!b)
                break;

            if (!RTMPPacket_IsReady(&packet))
                continue;

            printf("\t headerType:[%d] \n", packet.m_headerType);
            printf("\t packetType:[%d] \n", packet.m_packetType);
            printf("\t hasAbsTimestamp:[%d] \n", packet.m_hasAbsTimestamp);
            printf("\t nChannel:[%d] \n", packet.m_nChannel);
            printf("\t nTimeStamp:[%d] \n", packet.m_nTimeStamp);
            printf("\t nInfoField2:[%d] \n", packet.m_nInfoField2);
            printf("\t nBodySize:[%d] \n", packet.m_nBodySize);
            printf("\t nBytesRead:[%d] \n", packet.m_nBytesRead);

            if (packet.m_packetType == 0x08)
            {
                fwrite(packet.m_body + 1, 1, packet.m_nBodySize - 1, pFile);
            }

            RTMPPacket_Free(&packet);
        }
        else
        {
            char sBuf[4096] = {0};
            int bytes = RTMP_Read(pRTMP, sBuf, sizeof(sBuf));
            printf("RTMP_Read() ret:[%d] \n", bytes);

            if (bytes <= 0)
                break;

            fwrite(sBuf, 1, bytes, pFile);
        }
    }

上面這段代碼片斷演示了使用librtmp拉流,并將音頻保存為MP3,或者將音視頻保存為FLV的例子。

小結:

源代碼解析到這一步,我想應該大部分的關鍵流程都帶過一遍了,但是簽于我的時間和水平的關系,很多細節目前都還是淺嘗輒止,后面若有機會再慢慢補上吧,希望朋友們理解。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容