1小時學會:最簡單的iOS直播推流(九)flv 編碼與音視頻時間戳同步

最簡單的iOS 推流代碼拆融,視頻捕獲蠢琳,軟編碼(faac,x264)镜豹,硬編碼(aac傲须,h264),美顏趟脂,flv編碼泰讽,rtmp協(xié)議吩蔑,陸續(xù)更新代碼解析念恍,你想學的知識這里都有,愿意懂直播技術(shù)的同學快來看G剖 硼一!

源代碼:https://github.com/hardman/AWLive

前文介紹了如何獲取音視頻的aac/h264數(shù)據(jù)累澡,那么如何將數(shù)據(jù)寫入rtmp流中呢?
rtmp最初是Adobe Flash用于音視頻播放的一個實時傳輸協(xié)議般贼。而flv正是Adobe推出的一個視頻格式愧哟,因此rtmp協(xié)議支持flv視頻流奥吩。
這里可以我們把獲取的aac/h264的數(shù)據(jù),直接轉(zhuǎn)成flv格式的視頻幀蕊梧,然后按照時間戳依次發(fā)送給服務(wù)端即可霞赫。

flv格式簡介

flv總體來說是一個簡單的視頻格式,它包含2部分:header 和 body肥矢。

header是固定格式的數(shù)據(jù)端衰,表示本文件是一個flv文件。
header的長度是9個字節(jié)甘改。

header后面緊跟著body數(shù)據(jù)旅东。body是由一個一個稱為的tag數(shù)據(jù)組成。
tag其實就是一個固定格式的數(shù)據(jù)塊楼誓,構(gòu)造方式同header類似玉锌,只是叫法不同而已。

tag分為3種疟羹。script tag主守,video tag,audio tag榄融。
script tag是flv的第一個tag参淫,用于放一些視頻信息的,比如duration愧杯,width涎才,height等。script tag對于flv格式的視頻文件比較重要力九,對于rtmp來說耍铜,可以不寫入script tag。
video tag是視頻數(shù)據(jù)的封裝跌前,也就是我們獲取的h264數(shù)據(jù)基礎(chǔ)之上棕兼,增加一些flv特定的數(shù)據(jù)。
audio tag同video tag類似抵乓,是acc數(shù)據(jù)的封裝伴挚。

代碼解析

flv相關(guān)代碼在 aw_encode_flv.h和aw_encode_flv.c中。
此模塊提供了flv編碼(aac+h264)功能灾炭。

這個模塊的暴露給外部的api為2部分:

//一部分是創(chuàng)建flv的方法
//寫入header
extern void aw_write_flv_header(aw_data **flv_data);
//寫入flv tag
extern void aw_write_flv_tag(aw_data **flv_data, aw_flv_common_tag *common_tag);

//第二部分是所有tag的構(gòu)造
//script tag
extern aw_flv_script_tag *alloc_aw_flv_script_tag();
extern void free_aw_flv_script_tag(aw_flv_script_tag **);

//audio tag
extern aw_flv_audio_tag *alloc_aw_flv_audio_tag();
extern void free_aw_flv_audio_tag(aw_flv_audio_tag **);

//video tag
extern aw_flv_video_tag *alloc_aw_flv_video_tag();
extern void free_aw_flv_video_tag(aw_flv_video_tag **);

外部使用時茎芋,可根據(jù)具體數(shù)據(jù)先創(chuàng)建不同的tag,填充好各個數(shù)據(jù)蜈出,然后使用aw_write_flv_tag方法將tag寫入aw_data中田弥。
可用上述方法可以構(gòu)造出完整的flv文件。

aw_data

aw_data是為了方便文件數(shù)據(jù)的讀取/寫入和管理而創(chuàng)建的工具模塊掏缎。
此模塊已處理了大端小端差異皱蹦,能夠讓文件讀寫更加方便快捷煤杀。
相關(guān)代碼在aw_data.h / aw_data.c中眷蜈。

flv header

extern void aw_write_flv_header(aw_data **flv_data){
    uint8_t
    f = 'F', l = 'L', v = 'V',//FLV
    version = 1,//固定值
    av_flag = 5;//5表示av沪哺,5表示只有a,1表示只有v
    uint32_t flv_header_len = 9;//header固定長度為9
    data_writer.write_uint8(flv_data, f);
    data_writer.write_uint8(flv_data, l);
    data_writer.write_uint8(flv_data, v);
    data_writer.write_uint8(flv_data, version);
    data_writer.write_uint8(flv_data, av_flag);
    data_writer.write_uint32(flv_data, flv_header_len);
    
    //first previous tag size 根據(jù)flv協(xié)議酌儒,每個tag后要寫入當前tag的size辜妓,稱為previous tag size,header后面需要寫入4字節(jié)空數(shù)據(jù)忌怎。
    data_writer.write_uint32(flv_data, 0);
}

flv body

注意
如果是要構(gòu)造flv文件籍滴,寫入header之后就可以寫入script tag了。
如果是使用rtmp協(xié)議榴啸,則無需構(gòu)造header孽惰,也無需script tag∨赣。可直接寫入 video tag和audio tag勋功。
若使用rtmp協(xié)議必須在首幀寫入AVCDecoderConfigurationRecord (包含sps pps數(shù)據(jù))和 AudioSpecificConfig,否則服務(wù)端無法正常解析音視頻數(shù)據(jù)库说。

flv的body是由一個接一個的tag構(gòu)成的狂鞋。
一個flv tag分為3部分:tag header + tag body + tag data size。

extern void aw_write_flv_tag(aw_data **flv_data, aw_flv_common_tag *common_tag){
    //寫入header
    aw_write_tag_header(flv_data, common_tag);
    //寫入body
    aw_write_tag_body(flv_data, common_tag);
    //寫入data size
    aw_write_tag_data_size(flv_data, common_tag);
}

tag header

static void aw_write_tag_header(aw_data **flv_data, aw_flv_common_tag *common_tag){
    //header 長度為固定11個字節(jié)
    //寫入tag type潜的,video:9 audio:8 script:18
    data_writer.write_uint8(flv_data, common_tag->tag_type);
    //寫入body的size(data_size為整個tag的長度)
    data_writer.write_uint24(flv_data, common_tag->data_size - 11);
    //寫入時間戳
    data_writer.write_uint24(flv_data, common_tag->timestamp);
    data_writer.write_uint8(flv_data, common_tag->timestamp_extend);
    //寫入stream id為0
    data_writer.write_uint24(flv_data, common_tag->stream_id);
}

script tag body

static void aw_write_script_tag_body(aw_data **flv_data, aw_flv_script_tag *script_tag){
    //script tag寫入規(guī)則為:類型-內(nèi)容-類型-內(nèi)容...類型-內(nèi)容
    //類型是1個字節(jié)整數(shù)骚揍,可取12種值:
    //    0 = Number type
    //    1 = Boolean type
    //    2 = String type
    //    3 = Object type
    //    4 = MovieClip type
    //    5 = Null type
    //    6 = Undefined type
    //    7 = Reference type
    //    8 = ECMA array type
    //    10 = Strict array type
    //    11 = Date type
    //    12 = Long string type
    // 比如:如果類型是字符串,那么先寫入1個字節(jié)表類型的2啰挪。另信不,寫入真正的字符串前,需要寫入2個字節(jié)的字符串長度亡呵。
    // data_writer.write_string能夠在寫入字符串前抽活,先寫入字符串長度,此函數(shù)第三個參數(shù)表示用多少字節(jié)來存儲字符串長度政己。
    // script tag 的結(jié)構(gòu)基本上是固定的酌壕,首先寫入一個字符串: onMetaData,然后寫入一個數(shù)組歇由。
    // 寫入數(shù)組需要先寫入數(shù)組編號1字節(jié):8卵牍,然后寫入數(shù)組長度4字節(jié):11。
    // 數(shù)組同OC的Dictionary類似沦泌,可寫入一個字符串+一個value糊昙。
    // 所以每個數(shù)組元素可先寫入一個字符串,然后寫入一個Number Type谢谦,再寫入具體的數(shù)值释牺。
    // 結(jié)束時需寫入3個字節(jié)的0x000009表示數(shù)組結(jié)束萝衩。
    // 下面代碼中的duration/width/filesize均遵循此規(guī)則。

    //2表示類型没咙,字符串
    data_writer.write_uint8(flv_data, 2);
    data_writer.write_string(flv_data, "onMetaData", 2);
    
    //數(shù)組類型:8
    data_writer.write_uint8(flv_data, 8);
    //數(shù)組長度:11
    data_writer.write_uint32(flv_data, 11);
    
    //寫入duration 0表示double猩谊,1表示uint8
    data_writer.write_string(flv_data, "duration", 2);
    data_writer.write_uint8(flv_data, 0);
    data_writer.write_double(flv_data, script_tag->duration);
    //寫入width
    data_writer.write_string(flv_data, "width", 2);
    data_writer.write_uint8(flv_data, 0);
    data_writer.write_double(flv_data, script_tag->width);
    ...
    ...
    ...
    //寫入file_size
    data_writer.write_string(flv_data, "filesize", 2);
    data_writer.write_uint8(flv_data, 0);
    data_writer.write_double(flv_data, script_tag->file_size);
    
    //3字節(jié)的0x9表示數(shù)組結(jié)束
    data_writer.write_uint24(flv_data, 9);
}

video tag body

static void aw_write_video_tag_body(aw_data **flv_data, aw_flv_video_tag *video_tag){
    // video tag body 結(jié)構(gòu)是這樣的:
    // frame_type(4bit) + codec_id(4bit) + h264_package_type(8bit) + h264_composition_time(24bit) + video_tag_data(many bits)
    // frame_type 表示是否關(guān)鍵幀,關(guān)鍵幀為1祭刚,非關(guān)鍵幀為2(當然還有更多取值牌捷,請參考[flv協(xié)議](https://wuyuans.com/img/2012/08/video_file_format_spec_v10.rar)
    // codec_id 表示視頻協(xié)議:h264是7 h263是2。
    // h264_package_type表示視頻幀數(shù)據(jù)的類型涡驮,2種取值:sequence header(也就是前面說的 sps pps 數(shù)據(jù)暗甥,rtmp要求首幀發(fā)送此數(shù)據(jù),也稱為AVCDecoderConfigurationRecord)捉捅,另一種為nalu撤防,正常的h264視頻幀。
    // h264_compsition_time:cts是pts與dts的差值棒口,flv中的timestamp表示的應(yīng)該是pts寄月。如果h264數(shù)據(jù)中不包含B幀,那么此數(shù)據(jù)可傳0陌凳。
    // video_tag_data 即純264數(shù)據(jù)剥懒。

    uint8_t video_header = 0;
    video_header |= video_tag->frame_type << 4 & 0xf0;
    video_header |= video_tag->codec_id;
    data_writer.write_uint8(flv_data, video_header);
    
    if (video_tag->codec_id == aw_flv_v_codec_id_H264) {
        data_writer.write_uint8(flv_data, video_tag->h264_package_type);
        data_writer.write_uint24(flv_data, video_tag->h264_composition_time);
    }
    
    switch (video_tag->h264_package_type) {
        case aw_flv_v_h264_packet_type_seq_header: {
            data_writer.write_bytes(flv_data, video_tag->config_record_data->data, video_tag->config_record_data->size);
            break;
        }
        case aw_flv_v_h264_packet_type_nalu: {
            data_writer.write_bytes(flv_data, video_tag->frame_data->data, video_tag->frame_data->size);
            break;
        }
        case aw_flv_v_h264_packet_type_end_of_seq: {
            //nothing
            break;
        }
    }
}

audio tag body

static void aw_write_audio_tag_body(aw_data **flv_data, aw_flv_audio_tag *audio_tag){
    // audio tag body的結(jié)構(gòu)是這樣的:
    // sound_format(4bit) + sound_rate(sample_rate)(2bit) + sound_size(sample_size)(1bit) + sound_type(1bit) + aac_packet_type(8bit) + aac_data(many bits)
    // sound_format 表示聲音格式,2表示mp3合敦,10表示aac初橘,一般是aac
    // sound_rate 采樣率,表示1秒鐘采集多少個樣本充岛,可選4個值保檐,0表示5.5kHZ,1表示11kHZ崔梗,2表示22kHZ夜只,3表示44kHZ,一般是3蒜魄。
    // sound_size 采樣尺寸扔亥,單個樣本的size。2個選擇谈为,0表示8bit旅挤,1表示16bit。
    // 直觀上看伞鲫,采樣率和采樣尺寸應(yīng)該和質(zhì)量有一定關(guān)系粘茄。采樣率高,采樣尺寸大效果應(yīng)該會好,但是生成的數(shù)據(jù)量也大柒瓣。
    // sound_type 表示聲音類型儒搭,0表示單聲道,1表示立體聲芙贫。(立體聲有2條聲道)搂鲫。
    // aac_packet_type表示aac數(shù)據(jù)類型,有2種選擇:0表示sequence header屹培,即 必須首幀發(fā)送的數(shù)據(jù)(AudioSpecificConfig)默穴,1表示正常的aac數(shù)據(jù)怔檩。

    uint8_t audio_header = 0;
    audio_header |= audio_tag->sound_format << 4 & 0xf0;
    audio_header |= audio_tag->sound_rate << 2 & 0xc;
    audio_header |= audio_tag->sound_size << 1 & 0x2;
    audio_header |= audio_tag->sound_type & 0x1;
    data_writer.write_uint8(flv_data, audio_header);
    
    if (audio_tag->sound_format == aw_flv_a_codec_id_AAC) {
        data_writer.write_uint8(flv_data, audio_tag->aac_packet_type);
    }
    switch (audio_tag->aac_packet_type) {
        case aw_flv_a_aac_package_type_aac_sequence_header: {
            data_writer.write_bytes(flv_data, audio_tag->config_record_data->data, audio_tag->config_record_data->size);
            break;
        }
        case aw_flv_a_aac_package_type_aac_raw: {
            data_writer.write_bytes(flv_data, audio_tag->frame_data->data, audio_tag->frame_data->size);
            break;
        }
    }
}

tag data size

根據(jù)flv協(xié)議褪秀,每個flv tag結(jié)束時,需要寫入此tag的全部長度:header+body的長度薛训,header長度固定為11字節(jié)媒吗,而body的長度可通過上面構(gòu)造body時寫入的數(shù)據(jù)進行計算。

static void aw_write_tag_data_size(aw_data **flv_data, aw_flv_common_tag *common_tag){
    data_writer.write_uint32(flv_data, common_tag->data_size);
}

上面的data_size由外部使用此模塊的函數(shù)乙埃,在創(chuàng)建tag時計算出來的闸英。
可以看aw_sw_faac_encoder.c中的aw_encoder_create_audio_tag方法:

extern aw_flv_audio_tag *aw_encoder_create_audio_tag(int8_t *aac_data, long len, uint32_t timeStamp, aw_faac_config *faac_cfg){
    aw_flv_audio_tag *audio_tag = aw_sw_encoder_create_flv_audio_tag(faac_cfg);
    ...
    ...
    //此處計算的data_size長度為 11(tag header size) + body header size(即下面的header_size,表示body中除去aac data的部分) + aac data size
    audio_tag->common_tag.data_size = audio_tag->frame_data->size + 11 + audio_tag->common_tag.header_size;
    return audio_tag;
}

這是本項目的處理方式介袜。當然data size也可以在寫入header和body時甫何,同步計算出來。

flv時間戳

flv的tag中有2個字段表示時間戳遇伞,一個是 timestamp(pts)辙喂,一個是Composition Time(cts)。
pts表示展示時間戳鸠珠,表示這一幀什么時候展示巍耗。
說cts之前有必要介紹一下dts,dts表示解碼時間戳渐排。
我們知道h264中有3種視頻幀炬太,I幀,P幀驯耻,B幀亲族。
I和P幀不必說。
因為B幀的存在可缚,可能會令后面的視頻幀先于前面的視頻幀解析霎迫,這樣就需要在視頻幀信息中保存dts。
flv中的cts可以做這件事情城看,cts = pts - dts女气。

另一個問題是,rtmp中的flv時間戳有一個規(guī)則就是测柠,音頻+視頻幀須按照pts遞增順序發(fā)送炼鞠。
因為音頻和視頻有各自的幀率缘滥,每個音視頻幀可計算出各自的時間戳。
由于音頻和視頻在不同的線程中編碼谒主,編碼后的音視頻會合并到相同的線程中發(fā)送朝扼。
因為編碼速度等各種原因,編碼后的數(shù)據(jù)合并到相同線程時霎肯,可能并不是按照時間戳升序排列的擎颖。

為了保證排序,有2種辦法解決此問題:

  1. 將數(shù)據(jù)緩存起來观游,每次發(fā)送前都保證發(fā)送的是最早的數(shù)據(jù)幀搂捧。
  2. 以音頻(或視頻)為主,一旦遇到視頻(或音頻)幀時間戳小于已經(jīng)發(fā)送的時間戳懂缕,則調(diào)整視頻(或音頻)幀時間戳允跑。

推流時保存發(fā)送的flv文件

根據(jù)本文介紹,我們可以把發(fā)送到rtmp服務(wù)器的數(shù)據(jù)保存到本地flv文件搪柑。
可以修改aw_streamer.c文件聋丝。

  1. 當調(diào)用aw_streamer_open_rtmp_context時創(chuàng)建aw_data,并寫入flv header和flv script tag工碾。
  2. 調(diào)用aw_streamer_send_video_data和aw_streamer_send_audio_data時弱睦,將video tag和audio tag寫入aw_data中。
  3. 當調(diào)用aw_streamer_close_rtmp_context時渊额,將aw_data寫入到本地文件况木,保存成flv格式,然后釋放aw_data端圈。

至此焦读,flv編碼介紹完畢。

文章列表

  1. 1小時學會:最簡單的iOS直播推流(一)項目介紹
  2. 1小時學會:最簡單的iOS直播推流(二)代碼架構(gòu)概述
  3. 1小時學會:最簡單的iOS直播推流(三)使用系統(tǒng)接口捕獲音視頻
  4. 1小時學會:最簡單的iOS直播推流(四)如何使用GPUImage舱权,如何美顏
  5. 1小時學會:最簡單的iOS直播推流(五)yuv矗晃、pcm數(shù)據(jù)的介紹和獲取
  6. 1小時學會:最簡單的iOS直播推流(六)h264、aac宴倍、flv介紹
  7. 1小時學會:最簡單的iOS直播推流(七)h264/aac 硬編碼
  8. 1小時學會:最簡單的iOS直播推流(八)h264/aac 軟編碼
  9. 1小時學會:最簡單的iOS直播推流(九)flv 編碼與音視頻時間戳同步
  10. 1小時學會:最簡單的iOS直播推流(十)librtmp使用介紹
  11. 1小時學會:最簡單的iOS直播推流(十一)sps&pps和AudioSpecificConfig介紹(完結(jié))
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末张症,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子鸵贬,更是在濱河造成了極大的恐慌俗他,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阔逼,死亡現(xiàn)場離奇詭異兆衅,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進店門羡亩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來摩疑,“玉大人,你說我怎么就攤上這事畏铆±状” “怎么了?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵辞居,是天一觀的道長楷怒。 經(jīng)常有香客問我,道長瓦灶,這世上最難降的妖魔是什么鸠删? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮倚搬,結(jié)果婚禮上冶共,老公的妹妹穿的比我還像新娘。我一直安慰自己每界,他們只是感情好,可當我...
    茶點故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布家卖。 她就那樣靜靜地躺著眨层,像睡著了一般。 火紅的嫁衣襯著肌膚如雪上荡。 梳的紋絲不亂的頭發(fā)上趴樱,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天,我揣著相機與錄音酪捡,去河邊找鬼叁征。 笑死,一個胖子當著我的面吹牛逛薇,可吹牛的內(nèi)容都是我干的捺疼。 我是一名探鬼主播,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼永罚,長吁一口氣:“原來是場噩夢啊……” “哼啤呼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起呢袱,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤官扣,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后羞福,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體惕蹄,經(jīng)...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了卖陵。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片恋昼。...
    茶點故事閱讀 40,615評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖赶促,靈堂內(nèi)的尸體忽然破棺而出液肌,到底是詐尸還是另有隱情,我是刑警寧澤鸥滨,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布嗦哆,位于F島的核電站,受9級特大地震影響婿滓,放射性物質(zhì)發(fā)生泄漏老速。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一凸主、第九天 我趴在偏房一處隱蔽的房頂上張望橘券。 院中可真熱鬧,春花似錦卿吐、人聲如沸旁舰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽箭窜。三九已至,卻和暖如春衍腥,著一層夾襖步出監(jiān)牢的瞬間磺樱,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工婆咸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留竹捉,地道東北人。 一個月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓尚骄,卻偏偏與公主長得像块差,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子乖仇,可洞房花燭夜當晚...
    茶點故事閱讀 45,630評論 2 359

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