9. 【RTMP協(xié)議和傳輸】

RTMP協(xié)議介紹

為了替代RMTP協(xié)議姐呐,蘋果出了一個HLS協(xié)議,Adobe已經(jīng)決定不再維護RTMP協(xié)議邢锯;目前國內(nèi)大部分還是使用的RTMP協(xié)議爹梁,它在傳輸和實時性方面都要強于HLS;
作用:用于娛樂直播的或者點播
通信步驟:

  • 先基于socket進行TCP連接急侥;

  • tcp連接之后再進行握手砌滞;
    客戶端先發(fā)個c0+c1,服務器收到后往客戶端發(fā)送s0+s1+s2坏怪,客戶端最后發(fā)送個c2贝润,就結束了握手流程;


    握手流程
  • 握手完成后再確定建立rtmp連接铝宵;
    客戶端向服務端發(fā)送個連接到消息打掘;
    服務端回可變窗口控制的大小、帶寬和數(shù)據(jù)塊的大小鹏秋,以及連接成功的消息尊蚁;
    客戶端也會放一個傳輸塊的最大大小侣夷;


    連接流程
  • 創(chuàng)建RTMP流横朋,連接之后數(shù)據(jù)以stream的方式進行數(shù)據(jù)交互;


    創(chuàng)建rtmp流
  • 推流流程
    其metaData就是音視頻流的基本信息惜纸,如采樣率陈惰、采樣大小溉仑、通道數(shù)、幀率、分辨率等贮乳;


    推流
  • 播流流程


    播流流程
  • RTMP消息的結構
    消息有頭部和body組成


    消息格式

basic header:是必須要有的,占用一個字節(jié)爸黄,前兩位赢织,表示格式,后六位表示chunk stream id腺阳;
message header:中有時間戳落君、消息長度、數(shù)據(jù)類型ID亭引、流ID绎速。當因為數(shù)據(jù)量太大而被拆分成多個chunk 的時候,根據(jù)消息是否屬于同一個流焙蚓、同一個類型數(shù)據(jù)纹冤、消息長度相同、時間戳相同购公,決定是否需要這幾個字段是否需要省略萌京;
Extended timestamp:擴展時間戳,當message header中的時間戳3個字節(jié)不足以表示的時候宏浩,就需要這個擴展知残,也就是timestamp=0xFFFFFF的時候;
1.message header和Extended timestamp根據(jù)basic header頭部中的信息決定的比庄;
2.當chunk stream id為0時求妹,basic header占用2個字節(jié),多出字節(jié)用來表示更多的chunk stream id印蔗;
3.當chunk stream id為1時扒最,basic header占用4個字節(jié),多出字節(jié)用來表示更多的chunk stream id华嘹;
4.basic header中的前兩位決定message header的長度吧趣,當fmt == 00,表示message header全有耙厚;當fmt = 01强挫,消息頭部占用7個字節(jié),當fmt = 10薛躬,消息頭部只占用3個字節(jié)俯渤;當fmt = 11,沒有消息頭部型宝;

消息的類型:有三種:控制消息八匠、音視頻數(shù)據(jù)絮爷、命令消息;
set chunk size : 設置chunk包的大小梨树,默認是128字節(jié)坑夯;
abort message:當某個流結束的時候,告訴服務端就不需要接受這個流了抡四;
acknowledgement:協(xié)商從那個起始點開始確認消息柜蜈;
window acknowledgement size :設置滑動窗口的大小
set peer bandwidth :告訴對方本機的最大一次可傳輸?shù)臄?shù)據(jù)量,也就是帶寬指巡;
data message : 就是音視頻數(shù)據(jù)的元數(shù)據(jù)淑履,比如推流前的metaData,AMF0和AMF1是flash數(shù)據(jù)編碼的一種格式藻雪;
shared object message : 共享消息
command message : 命令消息

FLV協(xié)議

FLV是一種文件秘噪,在將視頻文件進行推流時,會先生成flv文件阔涉。所有的rtmp數(shù)據(jù)在flv中都被加了個頭部缆娃,

  1. FLV文件結構
  • 9個字節(jié)頭部:1=F、2=L 瑰排、3=V贯要、4=版本、5=類型椭住、6789=表示頭部的大小崇渗,固定是9 ;
  • 其中頭部的第5個字節(jié)中的前五位和第七位是保留位京郑,第6位表示是否有音頻tag宅广,第8位表示是否有視頻tag;
  • 后面所有的內(nèi)容就是由pre_tag_size些举、tag組成跟狱,其中pre_tag_size表示前一個tag的大小,占用4字節(jié)户魏;
  • 每個tag又由tag_header驶臊、tag_data組成,
    tag_header是對tag_data的描述包括:TT(標簽類型)是音頻還是視頻叼丑, datasize(data的長度)关翎、timesta(時間戳)、E(前面時間戳的擴展)鸠信、SID(流id)纵寝;
    tag_data分為音頻和視頻數(shù)據(jù):
    其中音頻數(shù)據(jù)audio data又由頭部和 aac data組成,頭部是音頻的采樣信息星立,aac data又由音頻配置信息和adts包裝的音頻數(shù)據(jù)爽茴,這個aac data是rtmp協(xié)議真正需要的葬凳;
    其中的視頻數(shù)據(jù)video data由頭部和AVCVideoPacket組成,頭部是表示編碼器id和編碼器類型室奏,AVCVideoPacket前面也有一個類型和時間戳沮明,AVCVideoPacket里面就是sps、pps和NAL組成窍奋;


    FLV
  1. FLV 文件分析器
    Diffindo下載
  • 編譯:在下載文件的gcc目錄下執(zhí)行 ./flv_compile_clang.sh,成功后生成gcc_flv文件夾酱畅;
  • 開始分析:FLVParser flv文件路徑 輸出文件路徑琳袄;
    使用ffmpeg根據(jù)視頻文件生成flv文件:ffmpeg -i 視頻文件 -f flv 文件路徑;

推流實踐

安裝librtmp:
1.使用brew rtmpdump 安裝librtmp
2.openssl 我使用的源碼的方式直接在文件下面執(zhí)行./Configure && make && make install

  1. 推流步驟
    1. 生成獲取FLV文件
      二進制讀取方式打開FLV文件纺酸,并且跳過flv的頭部和第一個pre_tag_size窖逗,使用fseek;
    2. 獲取FLV中的音視頻數(shù)據(jù)餐蔬,讀取到RTMPPacket中
    • tag的12個字節(jié)是tag的頭部碎紊,從頭部中讀取關鍵信息,再根據(jù)頭部信息的size獲取flv文件里面的頻數(shù)據(jù)到packet->mbody中樊诺;由于FLV是大端存儲仗考,再因為intel處理器是小端讀取,所以在頭部的信息需要將大端轉換成小端進行存儲词爬;
    • 設置rtmp頭部類型m_headerType為RTMP_PACKET_SIZE_LARGE秃嗜,用最長的消息長度方式;
    • 設置時間戳m_nTimestamp顿膨,音視頻同步使用
    • 設置數(shù)據(jù)類型m_packetType
    • 設置數(shù)據(jù)大小m_nBodySize
    1. 初始化librtmp對象: RTMP_Alloc锅锨、RTMP_Init
      設置超時時間:rtmp->Link->timeout
      設置推流地址 : RTMP_SetupURL
      設置是否是推流:RTMP_EnableWrite 設置了就是推流 未設置就是播流
      連接流媒體服務器:RTMP_Connect
      創(chuàng)建流:RTMP_ConnectStream(); 從0開始,創(chuàng)建流可能會失敗
    2. 利用librtmp傳輸
      rtmp傳輸?shù)臄?shù)據(jù)需要被包裝到RTMPPacket中恋沃,循環(huán)讀取flv文件發(fā)送必搞;
    • 初始化RTMPPacket:malloc 分配空間、RTMPPacket_Alloc分配緩沖區(qū)最大傳輸 = 64 x 1024囊咏、RTMPPacket_Reset重置緩沖區(qū)恕洲、m_hasAbsTimestmp不要絕對時間戳、m_nChannel = 0x4;
    • 從flv中讀取音視頻數(shù)據(jù)(看第二步)匆笤;
    • 判斷rtmp連接是否正常 RMTP_IsConnected
    • 發(fā)送數(shù)據(jù)RTMP_Send_Packet研侣,隊列大小等于0就好;
    • 由于服務端緩沖區(qū)有限炮捧,所以應該在每發(fā)送一段數(shù)據(jù)后庶诡,就延遲數(shù)據(jù)的播放時長后再發(fā)送下一段數(shù)據(jù),利用當前tag的時間戳減去上一個tag的時間戳 = 需要休眠的時間咆课。調用usleep后末誓,再發(fā)送數(shù)據(jù)扯俱;

代碼實戰(zhàn)

  1. 打開flv文件,跳過flv頭部和第一個pre_tag_size喇澡;
static FILE* open_flv(const char *path) {
    
    FILE *file = fopen(path, "rb");
    if (!file) {
        printf("flv文件打開失敗\n");
        return NULL;
    }
    
    //跳過flv文件的頭部 9個字節(jié)
    fseek(file, 9, SEEK_SET);
    // 跳過第一個presize
    fseek(file, 4, SEEK_CUR);

    return file;
}
  1. 初始化librtmp對象迅栅,并建立連接
static RTMP* connect_rtmp(char *rtmp_url) {
    
    RTMP *rtmp = NULL;
    int result = -1;
    rtmp = RTMP_Alloc();
    if (rtmp == NULL) {
        printf("初始化rtmp 失敗\n");
        goto __ERROR;
    }
    RTMP_Init(rtmp);
    
    rtmp->Link.timeout = 10;
    RTMP_SetupURL(rtmp, rtmp_url);
    // 確認是推流
    RTMP_EnableWrite(rtmp);
    
    result = RTMP_Connect(rtmp, NULL);
    if (result < 0) {
        printf("rtmp 連接失敗:%s\n", av_err2str(result));
        goto __ERROR;
    }
    result = RTMP_ConnectStream(rtmp, 0);
    
    if (result < 0) {
        printf("rtmp 創(chuàng)建流失斍缇痢:%s\n", av_err2str(result));
    }
    return rtmp;
__ERROR:
    if (rtmp) {
        RTMP_Close(rtmp);
        RTMP_Free(rtmp);
    }
    
    return rtmp;
}
  1. 初始化RTMPPacket對象
static RTMPPacket* init_packet() {
   
    RTMPPacket *packet = NULL;
    packet = malloc(sizeof(RTMPPacket));
    // 最大傳輸64kb
    if (RTMPPacket_Alloc(packet, 64 * 1024) < 0) {
        printf("packet緩沖區(qū)分配失敗\n");
        RTMPPacket_Free(packet);
        return NULL;
    }
    RTMPPacket_Reset(packet);
    packet->m_hasAbsTimestamp = 0;
    packet->m_nChannel = 0x4;
    return packet;
}
  1. 從flv文件中讀取tag的數(shù)據(jù)
//讀取文件數(shù)據(jù)
static int read_data_unsigned8(FILE *file,unsigned char *data) {
    
    if(fread(data, 1, 1, file) != 1) {
        return -1;
    }
    
    return 0;
}
static int read_data_unsigned24(FILE *file,uint32_t *data) {
    
    if (fread(data, 1, 3, file) != 3) {
        return -1;
    }
    // 因為FLV中是大端存儲的  又因為Intel處理器是小端存儲的  所以需要將大端轉換成小端存儲
    *data = (*data >> 16 & 0x000000FF) | (*data << 16 & 0x00FF0000) | (*data & 0x0000ff00);
    
    return 0;
}

static int read_timestamp(FILE *file, uint32_t *data) {
    
    if (fread(data, 1, 4, file) != 4) {
        return -1;
    }
    // 因為FLV中是大端存儲的  又因為Intel處理器是小端存儲的  所以需要將大端轉換成小端存儲
    // 擴展時間戳解析時放在高8位
    *data = (*data >> 16 & 0x000000FF) | (*data << 16 & 0x00FF0000) | (*data & 0x0000ff00) | (*data & 0xff000000);
    return 0;
}

static int read_data_to_packet(FILE *file, RTMPPacket **packet) {
    
    // 數(shù)據(jù)類型
    uint8_t type;
    // tag的大小
    uint32_t data_size;
    // 時間戳
    uint32_t timestamp;

    // 流id
    uint32_t stream_id;
    
    if (read_data_unsigned8(file, &type) < 0 ||
        read_data_unsigned24(file, &data_size) < 0 ||
        read_timestamp(file, &timestamp) < 0 ||
        read_data_unsigned24(file, &stream_id) < 0) {
        
        printf("讀取flv tag 頭部信息失敹链妗!\n");
        goto __ERROR;
    }
    
    size_t read_size = fread((*packet)->m_body, 1, data_size, file);
    if (read_size != data_size) {
        
        printf("讀取flv 中的數(shù)據(jù)出錯呕屎!\n");
        goto __ERROR;
    }

    // 相當于message的頭部全開啟 占用11個字節(jié)
    (*packet)->m_headerType = RTMP_PACKET_SIZE_LARGE;
    
    (*packet)->m_packetType = type;
    (*packet)->m_nBodySize = data_size;
    (*packet)->m_nTimeStamp = timestamp;
    

    // 跳過4個字節(jié)的pre tag size
    fseek(file, 4, SEEK_CUR);
    return 0;
    
__ERROR:
    return -1;  
}
  1. 推送數(shù)據(jù)流到rtmp服務
static void push_data(FILE *file, RTMP *rtmp) {
    
    // 延遲時間戳
    useconds_t delay_timestamp = 0;
    RTMPPacket *packet = init_packet();
    packet->m_nInfoField2 = rtmp->m_stream_id;
    while (is_living) {

        if (read_data_to_packet(file, &packet) < 0) {
            printf("從flv文件中讀取信息失敗或者讀取完畢让簿!\n");
            RTMPPacket_Free(packet);

            break;
        }
        if (!RTMP_IsConnected(rtmp)) {
            printf("連接已斷開!");
            break;
        }
        
        printf("等待時間 == %d\n", (packet->m_nTimeStamp - delay_timestamp) * 1000);
        // usleep 使用的是納秒
        usleep((packet->m_nTimeStamp - delay_timestamp) * 1000);
        
        // 開始發(fā)送
        RTMP_SendPacket(rtmp, packet, 0);
       
        
        delay_timestamp = packet->m_nTimeStamp;
    }
__ERROR:
    RTMPPacket_Free(packet);
}
  1. 推流總體調用
void start_push(void) {
    
    is_living = 1;
    // 1. 打開flv文件
    char *path = "/Users/cunw/Desktop/learning/音視頻學習/音視頻文件/iphone.flv";
    FILE *file = open_flv(path);
    // 2.連接rtmp服務器 本地nginx服務
    char *url = "rtmp://localhost/live/1026238004";
    RTMP *rtmp = connect_rtmp(url);
    // 3.推送數(shù)據(jù)
    push_data(file, rtmp);

    is_living = 0;
    // 4. 釋放資源
    if (rtmp) {
        RTMP_Close(rtmp);
        RTMP_Free(rtmp);
    }

}
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末秀睛,一起剝皮案震驚了整個濱河市尔当,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蹂安,老刑警劉巖椭迎,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異田盈,居然都是意外死亡畜号,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門允瞧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來弄兜,“玉大人,你說我怎么就攤上這事瓷式√娑觯” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵贸典,是天一觀的道長视卢。 經(jīng)常有香客問我,道長廊驼,這世上最難降的妖魔是什么据过? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮妒挎,結果婚禮上绳锅,老公的妹妹穿的比我還像新娘。我一直安慰自己酝掩,他們只是感情好鳞芙,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般原朝。 火紅的嫁衣襯著肌膚如雪驯嘱。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天喳坠,我揣著相機與錄音鞠评,去河邊找鬼。 笑死壕鹉,一個胖子當著我的面吹牛剃幌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播晾浴,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼锥忿,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了怠肋?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤淹朋,失蹤者是張志新(化名)和其女友劉穎笙各,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體础芍,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡杈抢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了仑性。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片惶楼。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖诊杆,靈堂內(nèi)的尸體忽然破棺而出歼捐,到底是詐尸還是另有隱情,我是刑警寧澤晨汹,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布豹储,位于F島的核電站,受9級特大地震影響淘这,放射性物質發(fā)生泄漏剥扣。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一铝穷、第九天 我趴在偏房一處隱蔽的房頂上張望钠怯。 院中可真熱鬧,春花似錦曙聂、人聲如沸晦炊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽刽锤。三九已至镊尺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間并思,已是汗流浹背庐氮。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留宋彼,地道東北人弄砍。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像输涕,于是被迫代替她去往敵國和親音婶。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容