簡單的iOS直播推流——flv 編碼與音視訊時間戳同步

hard_man發表於2018-01-19

最簡單的iOS 推流程式碼,視訊捕獲,軟編碼(faac,x264),硬編碼(aac,h264),美顏,flv編碼,rtmp協議,陸續更新程式碼解析,你想學的知識這裡都有,願意懂直播技術的同學快來看!!

原始碼:https://github.com/hardman/AWLive

前文介紹瞭如何獲取音視訊的aac/h264資料,那麼如何將資料寫入rtmp流中呢? rtmp最初是Adobe Flash用於音視訊播放的一個實時傳輸協議。而flv正是Adobe推出的一個視訊格式,因此rtmp協議支援flv視訊流。 這裡可以我們把獲取的aac/h264的資料,直接轉成flv格式的視訊幀,然後按照時間戳依次傳送給服務端即可。

#flv格式簡介

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

header是固定格式的資料,表示本檔案是一個flv檔案。 header的長度是9個位元組。

header後面緊跟著body資料。body是由一個一個稱為的tag資料組成。 tag其實就是一個固定格式的資料塊,構造方式同header類似,只是叫法不同而已。

tag分為3種。script tag,video tag,audio tag。 script tag是flv的第一個tag,用於放一些視訊資訊的,比如duration,width,height等。script tag對於flv格式的視訊檔案比較重要,對於rtmp來說,可以不寫入script tag。 video tag是視訊資料的封裝,也就是我們獲取的h264資料基礎之上,增加一些flv特定的資料。 audio tag同video tag類似,是acc資料的封裝。

#程式碼解析

flv相關程式碼在 aw_encode_flv.h和aw_encode_flv.c中。 此模組提供了flv編碼(aac+h264)功能。

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

//一部分是建立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的構造
//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 **);
複製程式碼

外部使用時,可根據具體資料先建立不同的tag,填充好各個資料,然後使用aw_write_flv_tag方法將tag寫入aw_data中。 可用上述方法可以構造出完整的flv檔案。

##aw_data aw_data是為了方便檔案資料的讀取/寫入和管理而建立的工具模組。 此模組已處理了大端小端差異,能夠讓檔案讀寫更加方便快捷。 相關程式碼在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 根據flv協議,每個tag後要寫入當前tag的size,稱為previous tag size,header後面需要寫入4位元組空資料。
    data_writer.write_uint32(flv_data, 0);
}
複製程式碼

##flv body

注意 如果是要構造flv檔案,寫入header之後就可以寫入script tag了。 如果是使用rtmp協議,則無需構造header,也無需script tag。可直接寫入 video tag和audio tag。 若使用rtmp協議必須在首幀寫入AVCDecoderConfigurationRecord (包含sps pps資料)和 AudioSpecificConfig,否則服務端無法正常解析音視訊資料。

flv的body是由一個接一個的tag構成的。 一個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個位元組
    //寫入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寫入規則為:型別-內容-型別-內容...型別-內容
    //型別是1個位元組整數,可取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個位元組表型別的2。另,寫入真正的字串前,需要寫入2個位元組的字串長度。
    // data_writer.write_string能夠在寫入字串前,先寫入字串長度,此函式第三個參數列示用多少位元組來儲存字串長度。
    // script tag 的結構基本上是固定的,首先寫入一個字串: onMetaData,然後寫入一個陣列。
    // 寫入陣列需要先寫入陣列編號1位元組:8,然後寫入陣列長度4位元組:11。
    // 陣列同OC的Dictionary類似,可寫入一個字串+一個value。
    // 所以每個陣列元素可先寫入一個字串,然後寫入一個Number Type,再寫入具體的數值。
    // 結束時需寫入3個位元組的0x000009表示陣列結束。
    // 下面程式碼中的duration/width/filesize均遵循此規則。

    //2表示型別,字串
    data_writer.write_uint8(flv_data, 2);
    data_writer.write_string(flv_data, "onMetaData", 2);
    
    //陣列型別:8
    data_writer.write_uint8(flv_data, 8);
    //陣列長度: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位元組的0x9表示陣列結束
    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 結構是這樣的:
    // frame_type(4bit) + codec_id(4bit) + h264_package_type(8bit) + h264_composition_time(24bit) + video_tag_data(many bits)
    // frame_type 表示是否關鍵幀,關鍵幀為1,非關鍵幀為2(當然還有更多取值,請參考[flv協議](https://wuyuans.com/img/2012/08/video_file_format_spec_v10.rar)
    // codec_id 表示視訊協議:h264是7 h263是2。
    // h264_package_type表示視訊幀資料的型別,2種取值:sequence header(也就是前面說的 sps pps 資料,rtmp要求首幀傳送此資料,也稱為AVCDecoderConfigurationRecord),另一種為nalu,正常的h264視訊幀。
    // h264_compsition_time:cts是pts與dts的差值,flv中的timestamp表示的應該是pts。如果h264資料中不包含B幀,那麼此資料可傳0。
    // video_tag_data 即純264資料。

    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的結構是這樣的:
    // 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。
    // 直觀上看,取樣率和取樣尺寸應該和質量有一定關係。取樣率高,取樣尺寸大效果應該會好,但是生成的資料量也大。
    // sound_type 表示聲音型別,0表示單聲道,1表示立體聲。(立體聲有2條聲道)。
    // aac_packet_type表示aac資料型別,有2種選擇:0表示sequence header,即 必須首幀傳送的資料(AudioSpecificConfig),1表示正常的aac資料。

    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

根據flv協議,每個flv tag結束時,需要寫入此tag的全部長度:header+body的長度,header長度固定為11位元組,而body的長度可通過上面構造body時寫入的資料進行計算。

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由外部使用此模組的函式,在建立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時間戳有一個規則就是,音訊+視訊幀須按照pts遞增順序傳送。 因為音訊和視訊有各自的幀率,每個音視訊幀可計算出各自的時間戳。 由於音訊和視訊在不同的執行緒中編碼,編碼後的音視訊會合併到相同的執行緒中傳送。 因為編碼速度等各種原因,編碼後的資料合併到相同執行緒時,可能並不是按照時間戳升序排列的。

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

  1. 將資料快取起來,每次傳送前都保證傳送的是最早的資料幀。
  2. 以音訊(或視訊)為主,一旦遇到視訊(或音訊)幀時間戳小於已經傳送的時間戳,則調整視訊(或音訊)幀時間戳。

##推流時儲存傳送的flv檔案 根據本文介紹,我們可以把傳送到rtmp伺服器的資料儲存到本地flv檔案。 可以修改aw_streamer.c檔案。

  1. 當呼叫aw_streamer_open_rtmp_context時建立aw_data,並寫入flv header和flv script tag。
  2. 呼叫aw_streamer_send_video_data和aw_streamer_send_audio_data時,將video tag和audio tag寫入aw_data中。
  3. 當呼叫aw_streamer_close_rtmp_context時,將aw_data寫入到本地檔案,儲存成flv格式,然後釋放aw_data。

至此,flv編碼介紹完畢。

文章列表

  1. 1小時學會:最簡單的iOS直播推流(一)專案介紹
  2. 1小時學會:最簡單的iOS直播推流(二)程式碼架構概述
  3. 1小時學會:最簡單的iOS直播推流(三)使用系統介面捕獲音視訊
  4. 1小時學會:最簡單的iOS直播推流(四)如何使用GPUImage,如何美顏
  5. 1小時學會:最簡單的iOS直播推流(五)yuv、pcm資料的介紹和獲取
  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介紹(完結)

相關文章