1小时学会:最简单的iOS直播推流(九)flv 编码与音视频时间戳同步

最简单的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介绍(完结)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容