FFmpeg學習(八)RTMP與FLV協議


一:RTMP協議 

詳細解析見:https://www.jianshu.com/p/b2144f9bbe28

(一)RTMP創建流的基本流程

RTMP協議是應用層協議,是要靠底層可靠的傳輸層協議(通常是TCP)來保證信息傳輸的可靠性的。
在基於傳輸層協議的連接建立完成后RTMP協議也要客戶端和服務器通過“握手”來建立基於傳輸層鏈接之上的RTMP Connection鏈接
在Connection鏈接上會傳輸一些控制信息
,如SetChunkSize,SetACKWindowSize。
其中CreateStream命令會創建一個Stream鏈接用於傳輸具體的音視頻數據和控制這些信息傳輸的命令信息。
RTMP協議傳輸時會對數據做自己的格式化,這種格式的消息我們稱之為RTMP Message
而實際傳輸的時候為了更好地實現多路復用、分包和信息的公平性,發送端會把Message划分為帶有Message ID的Chunk
每個Chunk可能是一個單獨的Message,也可能是Message的一部分
在接受端會根據chunk中包含的data的長度,message id和message的長度把chunk還原成完整的Message,從而實現信息的收發。

(二)RTMP協議的握手:https://blog.csdn.net/m0_37599645/article/details/116033040(字段含義)

要建立一個有效的RTMP Connection鏈接,首先要“握手”:
  客戶端要向服務器發送C0,C1,C2(按序)三個chunk,
  服務器向客戶端發送S0,S1,S2(按序)三個chunk,然后才能進行有效的信息傳輸。

RTMP協議本身並沒有規定這6個Message的具體傳輸順序,但RTMP協議的實現者需要保證這幾點:

客戶端要等收到S1之后才能發送C2
客戶端要等收到S2之后才能發送其他信息(控制信息和真實音視頻等數據)
服務端要等到收到C0之后發送S1
服務端必須等到收到C1之后才能發送S2
服務端必須等到收到C2之后才能發送其他信息(控制信息和真實音視頻等數據)

實際實現中為了在保證握手的身份驗證功能的基礎上盡量減少通信的次數,一般的發送順序是如上圖中的“RTMP真實的握手”,這一點可以通過wireshark抓ffmpeg推流包進行驗證!

(三)建立RTMP連接

    1、客戶端發送命令“connect”給服務器
    2、服務器接收到“connect”命令之后,發送消息“確認窗口大小(Window Acknowledgement Size)”給客戶端,同時連接到“connect”命令中提到的應用程序
    3、服務器發送消息“設置帶寬”給客戶端
    4、客戶端接收到消息“設置帶寬”之后,發送消息“確認窗口大小”給服務器
    5、服務器發送消息“流開始”給客戶端
    6、服務器發送消息“結果”給客戶端,通知客戶端連接的狀態

(四)RTMP流中的創建

    1、客戶端發送命令“創建流”給服務器
    2、服務器接收到命令之后,發送“結果”給客戶端。

在實際中,在創建之前,如果存在該流,那么服務端需要進行釋放。然后接着客戶端會發送FCPublishStream消息給服務端。

(五)推RTMP流

1.客戶端發送publish命令給服務端
2.服務端回復響應消息給客戶端
3.客戶端發送metaData數據(后面要發送的音、視頻基本信息,如分辨率、幀率、采樣率...)給服務端
4.客戶端發送音視頻數據給服務端

其中服務端可以根據元信息,對音視頻數據進行編解碼,或者直接轉發給訂閱者。

(六)拉RTMP流

    1、客戶端發送命令“播放”給服務器
    2、服務器接收到命令之后,發送消息“設置塊大小”給客戶端
    3、服務器發送“stream begin”給客戶端,告訴客戶端 流的id
    4、播放命令成功的話,服務器發送“響應狀態”給客戶端,告訴客戶端播放成功
    5、服務器發送音視頻數據給客戶端

(七)RTMP消息格式

Chunk Stream是對傳輸RTMP Chunk的流的邏輯上的抽象,客戶端和服務器之間有關RTMP的信息都在這個流上通信。這個流上的操作也是我們關注RTMP協議的重點。

1.Message消息

這里的Message是指滿足該協議格式的、可以切分成Chunk發送的消息,消息包含的字段如下:

  • Timestamp(時間戳):消息的時間戳(但不一定是當前時間,后面會介紹),3個字節
  • Length(長度):是指Message Payload(消息負載)即音視頻等信息的數據的長度,3個字節
  • TypeId(類型Id):消息的類型Id,1個字節
  • Message Stream ID(消息的流ID):每個消息的唯一標識,划分成Chunk和還原Chunk為Message的時候都是根據這個ID來辨識是否是同一個消息的Chunk的,4個字節,並且以小端格式存儲

2.Chunking(Message分塊)

RTMP在收發數據的時候並不是以Message為單位的,而是把Message拆分成Chunk發送,而且必須在一個Chunk發送完成之后才能開始發送下一個Chunk。
每個Chunk中帶有MessageID代表屬於哪個Message,接受端也會按照這個id來將chunk組裝成Message。
為什么RTMP要將Message拆分成不同的Chunk呢?
通過拆分,數據量較大的Message可以被拆分成較小的“Message”,這樣就可以避免優先級低的消息持續發送阻塞優先級高的數據
比如在視頻的傳輸過程中,會包括視頻幀,音頻幀和RTMP控制信息,如果持續發送音頻數據或者控制數據的話可能就會造成視頻幀的阻塞,然后就會造成看視頻時最煩人的卡頓現象。
同時對於數據量較小的Message,可以通過對Chunk Header的字段來壓縮信息,從而減少信息的傳輸量。
Chunk的默認大小是128字節,在傳輸過程中,通過一個叫做Set Chunk Size的控制信息可以設置Chunk數據量的最大值,
在發送端和接受端會各自維護一個Chunk Size,可以分別設置這個值來改變自己這一方發送的Chunk的最大大小。
大一點的Chunk減少了計算每個chunk的時間從而減少了CPU的占用率,但是它會占用更多的時間在發送上,尤其是在低帶寬的網絡情況下,很可能會阻塞后面更重要信息的傳輸。
小一點的Chunk可以減少這種阻塞問題,但小的Chunk會引入過多額外的信息(Chunk中的Header),少量多次的傳輸也可能會造成發送的間斷導致不能充分利用高帶寬的優勢,因此並不適合在高比特率的流中傳輸。
在實際發送時應對要發送的數據用不同的Chunk Size去嘗試,通過抓包分析等手段得出合適的Chunk大小,並且在傳輸過程中可以根據當前的帶寬信息和實際信息的大小動態調整Chunk的大小,從而盡量提高CPU的利用率並減少信息的阻塞機率。

3.Basic Header(基本的頭信息)

Baisc header 是1-3個字節,第一個字節的高2位表示包頭的格式低6位表示chunk stream ID

1.fmt取值決定了整個包頭header的長度(以下表現的長度均不包含Basic header的長度)

      兩位的fmt取值為 0~3,分別代表的意義如下:
      case 0:chunk Msg Header長度為11;
      case 1:chunk Msg Header長度為7;
      case 2:chunk Msg Header長度為3;
      case 3:chunk Msg Header長度為0;

我們以11個字節的完整包頭來解釋Chunk Msg Header,如圖所示

 長度是7 bytes 的chunk head,該類型不包含stream ID,該chunk的streamID和前一個chunk的stream ID是相同的,變長的消息,例如視頻流格式,在第一個新的chunk以后使用這種類型,注意其中時間戳部分是相對時間,為何上一個絕對時間之間的差值 如圖所示:

3 bytes的chunk head,該類型既不包含stream ID 也不包含消息長度,這種類型用於stream ID和前一個chunk相同,且有固定長度的信息,例如音頻流格式,在第一個新的chunk以后使用該類型。如圖所示:

 0 bytes的chunk head,這種類型的chunk從前一個chunk得到值信息,當一個單個消息拆成多個chunk時,這些chunk除了第一個以外,其他的都應該使用這種類型, 

2.chunk stream ID決定了Basic header的字節數,首先查看低六位的取值(chunk type)

包含了chunk stream ID(流通道Id)和chunk type(chunk的類型)chunk stream id一般被簡寫為CSID,用來唯一標識一個特定的流通道。

chunk type決定了Baic Header的長度Basic Header的長度可能是1,2,或3個字節,其中chunk type的長度是固定的(占低6位,注意單位是位,bit),Basic Header的長度取決於CSID的大小,在足夠存儲這兩個字段的前提下最好用盡量少的字節從而減少由於引入Header增加的數據量。
對於chunk type來說:(后6位)

  • (后6位==0)為兩個字節,chunk stream id = 64 + 第二個字節值 (64-319)
  • (后6位==1)為三個字節,chunk stream id = 第三字節*256 + 第二字節 + 64(64–65599)
  • (1<后6位<=64)為一個字節,后6bits表示塊流ID

RTMP協議支持用戶自定義[3,65599]之間的CSID,對於CSID0,1,2由協議保留表示特殊信息。

  • 當Basic Header為1個字節時,CSID占6位,6位最多可以表示64個數,因此這種情況下CSID在[0,63]之間,其中用戶可自定義的范圍為[3,63]。chunk type的長度固定為2位,因此CSID的長度是(6=8-2)、(14=16-2)、(22=24-2)中的一個。

  • 當Basic Header為2個字節時,CSID占14位,此時協議將與chunk type所在字節的其他位都置為0,剩下的一個字節來表示CSID-64,這樣共有8個二進制位來存儲CSID,8位可以表示[0,255]共256個數,因此這種情況下CSID在[64,319],其中319=255+64。

  • 當Basic Header為3個字節時,CSID占22位,此時協議將[2,8]字節置為1,余下的16個字節表示CSID-64,這樣共有16個位來存儲CSID,16位可以表示[0,65535]共65536個數,因此這種情況下CSID在[64,65599],其中65599=65535+64,需要注意的是,Basic Header是采用小端存儲的方式,越往后的字節數量級越高,因此通過這3個字節每一位的值來計算CSID時,應該是:<第三個字節的值>x256+<第二個字節的值>+64

4.Message Header(消息的頭信息)

包含了要發送的實際信息(可能是完整的,也可能是一部分)的描述信息。Message Header的格式和長度取決於Basic Header的chunk type,共有4種不同的格式,由上面所提到的Basic Header中的fmt字段控制。其中第一種格式可以表示其他三種表示的所有數據,但由於其他三種格式是基於對之前chunk的差量化的表示,因此可以更簡潔地表示相同的數據,實際使用的時候還是應該采用盡量少的字節表示相同意義的數據。以下按照字節數從多到少的順序分別介紹這4種格式的Message Header。

Type=0:

type=0時Message Header占用11個字節,其他三種能表示的數據它都能表示,但在chunk stream的開始的第一個chunk和頭信息中的時間戳后退(即值與上一個chunk相比減小,通常在回退播放的時候會出現這種情況)的時候必須采用這種格式。
  • timestamp(時間戳):占用3個字節,因此它最多能表示到16777215=0xFFFFFF=2
    24-1, 當它的值超過這個最大值時,這三個字節都置為1,這樣實際的timestamp會轉存到Extended Timestamp字段中,接受端在判斷timestamp字段24個位都為1時就會去Extended timestamp中解析實際的時間戳。
  • message length(消息數據的長度):占用3個字節,表示實際發送的消息的數據如音頻幀、視頻幀等數據的長度,單位是字節。注意這里是Message的長度,也就是chunk屬於的Message的總數據長度,而不是chunk本身Data的數據的長度。
  • message type id(消息的類型id):占用1個字節,表示實際發送的數據的類型,如8代表音頻數據、9代表視頻數據。
  • msg stream id(消息的流id):占用4個字節,表示該chunk所在的流的ID,和Basic Header的CSID一樣,它采用小端存儲的方式,

Type=1:

type=1時Message Header占用7個字節,省去了表示msg stream id的4個字節,表示此chunk和上一次發的chunk所在的流相同,如果在發送端只和對端有一個流鏈接的時候可以盡量去采取這種格式。
  • timestamp delta:占用3個字節,注意這里和type=0時不同,存儲的是和上一個chunk的時間差。類似上面提到的timestamp,當它的值超過3個字節所能表示的最大值時,三個字節都置為1,實際的時間戳差值就會轉存到Extended Timestamp字段中,接受端在判斷timestamp delta字段24個位都為1時就會去Extended timestamp中解析時機的與上次時間戳的差值。

Type=2:

type=2時Message Header占用3個字節,相對於type=1格式又省去了表示消息長度的3個字節和表示消息類型的1個字節,表示此chunk和上一次發送的chunk所在的流、消息的長度和消息的類型都相同。余下的這三個字節表示timestamp delta,使用同type=1。

Type=3:

0字節!!!好吧,它表示這個chunk的Message Header和上一個是完全相同的,自然就不用再傳輸一遍了。當它跟在Type=0的chunk后面時,表示和前一個chunk的時間戳都是相同的。什么時候連時間戳都相同呢?就是一個Message拆分成了多個chunk,這個chunk和上一個chunk同屬於一個Message。而當它跟在Type=1或者Type=2的chunk后面時,表示和前一個chunk的時間戳的差是相同的。比如第一個chunk的Type=0,timestamp=100,第二個chunk的Type=2,timestamp delta=20,表示時間戳為100+20=120,第三個chunk的Type=3,表示timestamp delta=20,時間戳為120+20=140

5.Extended Timestamp(擴展時間戳):

上面我們提到在chunk中會有時間戳timestamp和時間戳差timestamp delta,並且它們不會同時存在,只有這兩者之一大於3個字節能表示的最大數值0xFFFFFF=16777215時,才會用這個字段來表示真正的時間戳,否則這個字段為0。擴展時間戳占4個字節,能表示的最大數值就是0xFFFFFFFF=4294967295。當擴展時間戳啟用時,timestamp字段或者timestamp delta要全置為1,表示應該去擴展時間戳字段來提取真正的時間戳或者時間戳差。注意擴展時間戳存儲的是完整值,而不是減去時間戳或者時間戳差的值。

6.Chunk Data(塊數據):

用戶層面上真正想要發送的與協議無關的數據,長度在(0,chunkSize]之間。

(八)RTMP消息類型

在RTMP的chunk流會用一些特殊的值來代表協議的控制消息,它們的Message Stream ID必須為0(代表控制流信息),CSID必須為2,Message Type ID可以為1,2,3,5,6,具體代表的消息會在下面依次說明。控制消息的接受端會忽略掉chunk中的時間戳,收到后立即生效。

1.Set Chunk Size(Message Type ID=1):設置chunk中Data字段所能承載的最大字節數,默認為128B,通信過程中可以通過發送該消息來設置chunk Size的大小(不得小於128B),而且通信雙方會各自維護一個chunkSize,兩端的chunkSize是獨立的。比如當A想向B發送一個200B的Message,但默認的chunkSize是128B,因此就要將該消息拆分為Data分別為128B和72B的兩個chunk發送,如果此時先發送一個設置chunkSize為256B的消息,再發送Data為200B的chunk,本地不再划分Message,B接受到Set Chunk Size的協議控制消息時會調整的接受的chunk的Data的大小,也不用再將兩個chunk組成為一個Message。
以下為代表Set Chunk Size消息的chunk的Data:
其中第一位必須為0,chunk Size占31個位,最大可代表2147483647=0x7FFFFFFF=2 31-1,但實際上所有大於16777215=0xFFFFFF的值都用不上,因為chunk size不能大於Message的長度,表示Message的長度字段是用3個字節表示的,最大只能為0xFFFFFF。

2.Abort Message(Message Type ID=2):當一個Message被切分為多個chunk,接受端只接收到了部分chunk時,發送該控制消息表示發送端不再傳輸同Message的chunk,接受端接收到這個消息后要丟棄這些不完整的chunk。Data數據中只需要一個CSID,表示丟棄該CSID的所有已接收到的chunk。

3.Acknowledgement(Message Type ID=3):當收到對端的消息大小等於窗口大小(Window Size)時接受端要回饋一個ACK給發送端告知對方可以繼續發送數據。窗口大小就是指收到接受端返回的ACK前最多可以發送的字節數量,返回的ACK中會帶有從發送上一個ACK后接收到的字節數。

4.Window Acknowledgement Size(Message Type ID=5):發送端在接收到接受端返回的兩個ACK間最多可以發送的字節數。 

5.Set Peer Bandwidth(Message Type ID=6):限制對端的輸出帶寬。接受端接收到該消息后會通過設置消息中的Window ACK Size來限制已發送但未接受到反饋的消息的大小來限制發送端的發送帶寬。如果消息中的Window ACK Size與上一次發送給發送端的size不同的話要回饋一個Window Acknowledgement Size的控制消息。
  • Hard(Limit Type=0):接受端應該將Window Ack Size設置為消息中的值
  • Soft(Limit Type=1):接受端可以講Window Ack Size設為消息中的值,也可以保存原來的值(前提是原來的Size小與該控制消息中的Window Ack Size)
  • Dynamic(Limit Type=2):如果上次的Set Peer Bandwidth消息中的Limit Type為0,本次也按Hard處理,否則忽略本消息,不去設置Window Ack Size。

更多見:https://www.jianshu.com/p/b2144f9bbe28

二:FLV協議

FLV的字節序為大端序,在做協議解析的時候一定要注意。

詳見:https://blog.csdn.net/luzubodfgs/article/details/78155117

(一)FLV header

FLV header由如下字段組成

其中前4字節中,前三個字節內容固定是FLV,第4字節為版本信息

第5個字節,用來表示是音視頻tag

最后4個字節內容固定是9(對FLV版本1來說)

(二)FLV file body

FLV file body很有規律,由一系列的TagSize(前一個tag的大小)和Tag組成,其中:

  1. PreviousTagSize0 總是為0;
  2. tag 由tag header、tag body組成;
  3. 對FLV版本1,tag header固定為11個字節,因此,PreviousTagSize(除第1個)的值為 11 + 前一個tag 的 tag body的大小;

 

(三)FLV tags

FLV tag由 tag header + tag body組成。

tag header如下,總共占據11個字節:

(四)Audio tags

定義如下:

(五)Vedio tags

三:實戰RTMP推流FLV

FLV解析器:https://sourceforge.net/projects/flvformatanalysis/

(一)安裝rtmp庫

http://rtmpdump.mplayerhq.hu/

cd rtmpdump
make
make install

(二)編程思路

1.推流具體步驟

2.librtmp的基本用法

(三)代碼實現

#include <stdio.h>
#include <librtmp/rtmp.h>

static FILE* open_flv(char* flv_name){
    FILE* fp = fopen(flv_name,"rb");
    if(!fp){
        printf("Failed to open flv:%s\n", flv_name);
        return NULL;
    }
    
    fseek(fp,9,SEEK_SET);    //跳過9字節的FLV header
    fseek(fp,4,SEEK_CUR);    //跳過4字節的preTagSize

    return fp;    //可以上面直接跳過13字節
}

static RTMP* connect_rtmp_server(char* rtmp_addr){
    //1.創建RTMP對象
    RTMP* rtmp = RTMP_Alloc();
    if(!rtmp){
        printf("Fail to alloc RTMP object!\n");
        goto __ERROR;
    }
    //2.進行初始化
    RTMP_Init(rtmp);

    //3.設置RTMP服務器地址,以及連接超時時間
    rtmp->Link.timeout = 10;
    RTMP_SetupURL(rtmp,rtmp_addr);

    //4.建立連接
    if(!RTMP_Connect(rtmp,NULL)){
        printf("Fail to Connect RTMP Server!\n");
        goto __ERROR;
    }

    //5.設置屬性,進行推流
    RTMP_EnableWrite(rtmp);    //如果不設置,則默認為拉流

    //6.創建流
    RTMP_ConnectStream(rtmp,0);    //從0時刻開始

    return rtmp;

__ERROR:
    if(rtmp){
        RTMP_Close(rtmp);
        RTMP_Free(rtmp);
    }

    return NULL;
}

static RTMPPacket* alloc_packet(){
    RTMPPacket* packet = NULL;
    packet = (RTMPPacket*)malloc(sizeof(RTMPPacket));
    if(!packet){
        printf("No memory,Failed to alloc RTMPPacket!\n");
        return NULL;
    }
    if(!RTMPPacket_Alloc(packet,64*1024)){    //64K
        printf("No memory,Failed to alloc RTMPPacket buffer!\n");
        return NULL;
    }    
    RTMPPacket_Reset(packet);    //清零內部


    packet->m_hasAbsTimestamp = 0;    //不使用絕對時間戳
    packet->m_nChannel = 0x4;        //0x04,channel is chunk msg id,it headerType 后6bit,對於a/v data是固定數據04

    return packet;
}

static int read_u8(FILE* fp,unsigned int *u8){
    *u8 = 0;
    if(fread(u8,1,1,fp)!=1)
        return -1;
    return 0;
}

//注意:FLV數據格式為大端存儲
//本機在ubuntu下為小端(可以寫程序測試)
//所以要進行轉換
static int read_u24(FILE* fp,unsigned int *u24){
    unsigned int temp=0;
    if(fread(&temp,1,3,fp)!=3)
        return -1;
    *u24 = ((temp>>16)&0xFF)|((temp<<16)&0xFF0000)|(temp&0xFF00);
    return 0;
}

//注意:FLV數據格式為大端存儲
//本機在ubuntu下為小端(可以寫程序測試)
//所以要進行轉換
static int read_u32(FILE* fp,unsigned int *u32){
    unsigned int temp=0;
    if(fread(&temp,1,4,fp)!=4)
        return -1;
    *u32 = ((temp>>24)&0xFF)|((temp<<24)&0xFF000000)|((temp<<8)&0xFF0000)|((temp>>8)&0xFF00);
    return 0;
}

//讀取時間
static int read_ts(FILE* fp,unsigned int *ts){
    unsigned int temp=0;
    if(fread(&temp,1,4,fp)!=4)
        return -1;
    //前面3字節不需要去取擴展時間戳,后面直接加上
    *ts = ((temp>>16)&0xFF)|((temp<<16)&0xFF0000)|(temp&0xFF00)|(temp&0xFF000000);
    return 0;
}


static int read_data(FILE* fp,RTMPPacket** packet){
    //FLV tag由 tag header + tag body組成。
    //先讀取header 第一個字節為Tag Type, 8audio 9video 18script
    //2-4字節為Tag body
    //5-7為timestamp,相對時間
    //8字節為時間戳擴展字段(即上面3字節不行,則變為4字節)
    //9-11字節為Stream ID
    int ret=-1;
    size_t datasize=0;
    unsigned int tt,tag_data_size,ts,streamid,tag_pre_size=0;
    if(read_u8(fp,&tt)||read_u24(fp,&tag_data_size)||read_ts(fp,&ts)||read_u24(fp,&streamid))
        return ret;

    datasize = fread((*packet)->m_body,1,tag_data_size,fp);
    printf("read tag header from flv,(tt=%10d;tag_data_size=%10d;ts=%10d;streamid=%10d;datasize=%zu;",tt, tag_data_size,ts,streamid,datasize);

    if(datasize!=tag_data_size){
        printf("Failed to read tag body from flv,(datasize=%zu:tds=%d)\n", datasize,tag_data_size);
        return ret;
    }
    (*packet)->m_headerType = RTMP_PACKET_SIZE_LARGE;    //fmt=0,chunk msg header 為 11字節
    (*packet)->m_nTimeStamp = ts;    //設置時間戳
    (*packet)->m_packetType = tt;    //設置類型
    (*packet)->m_nBodySize = tag_data_size;    //設置body大小

    //最后讀取一下pre tag size
    if(read_u32(fp,&tag_pre_size))
        return ret;
    printf("tag_pre_size:%d);\n",tag_pre_size);
    ret = 0;
    return ret;
}

static void send_data(FILE* fp,RTMP* rtmp){
    //不斷從FLV中讀取數據,構建為RTMPPacket對象,推流到服務端
    RTMPPacket* packet = alloc_packet();
    packet->m_nInfoField2 = rtmp->m_stream_id;
    int n=100,pre_ts=0,diff=0;

    while(1){
        //2.從flv文件讀取數據
        if(read_data(fp,&packet)==-1){
            printf("read all data\n");
            break;
        }

        //3.判斷RTMP連接是否正確
        if(!RTMP_IsConnected(rtmp)){
            printf("DisConnect....\n");
            break;
        }
        //----實現間隔時間,不要一次性推流所有數據,不然可能使得部分數據被丟棄(應該使用線程+隊列+定時器處理)
        diff = packet->m_nTimeStamp - pre_ts;
        if(diff<0)
            diff = 0;

        usleep(diff*1000);
        pre_ts = packet->m_nTimeStamp;

        //4.發送數據
        RTMP_SendPacket(rtmp,packet,0); //0表示不設置隊列大小    
    }

    //5.release object

    return;
}    

void publish_stream(){
    char* flv_name = "/home/ld/FFmpeg/develop/charpter_0/video/out2.flv";
    char* rtmp_addr = "rtmp://localhost/mytv/room01";
    //1.讀取flv文件
    FILE* fp = open_flv(flv_name);
    //2.連接RTMP服務器
    RTMP* rtmp = connect_rtmp_server(rtmp_addr);
    //3.推流
    send_data(fp,rtmp);

    return;
}

int main(int argc,char* argv[]){
    publish_stream();
    return 0;
}
View Code
gcc 03RTMP.c -o rtmp -lrtmp

(四)FFmpeg學習(七)流媒體服務器搭建

這里使用了nginx,當然srs一樣可以

(五)測試

ffplay rtmp://localhost/mytv/room01
./rtmp

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM