利用FFmpeg 开发音视频流(三)——将视频 YUV 格式编码成 H264

已经好几个月没有写博客了,突然来感去写,手艺也有些生疏了,确实是这几个月实在太忙了,谁也知道现在直播火了起来,很多公司也在做直播,博主也不例外呀,做起客户端都是从 0 开始,呕心沥血,好了,废话不说太多,说说今天的主题。
首先开始的时候我们插入一张雷神大大的图帮助大家理解一下我们今天的操作究竟属于那一步。

音视频格式封装层次
ps:在这里也要啰嗦一下,大家看见这个图黑白色了么?这是为啥?说起这个太感慨了,
一代雷神大大离我们而去,26岁的博士雷神大大,一个爱分享的人就这么陨落了,太可惜了。

从上图可以看出我们要做的,就是将像素层的 YUV 格式,编码出编码层的 h264数据。


前面讲到我们已经成功编译出 iOS 中可用的 ffmpeg 的库了,那么我们首先熟悉一下今天我们要用到的 ffmpeg 中的函数和结构体

AVFormatContext: 数据文件操作者,主要是用于存储音视频封装格式中包含的信息, 在工程当中占着具足轻重的地位,因为很多函数都要用到它作为参数。同时,它也是我们进行解封装的功能结构体。


AVOutputFormat: 输出的格式,包括音频封装格式、视频装格式、字幕封装格式,所有封装格式都在 AVCodecID 这个枚举类型上面了


AVStream: 一个装载着视频/音频流信息的结构体,包括音视频流的长度,元数据信息,其中 index 属性用于标识视频/音频流。


AVCodecContext: 这个结构体十分庞大,但它的主要是用于编码使用的,结构体中的的 AVCodec *codec 就是编码所采用的编码器器, 当然,这个结构体中要存入视频的基本参数,例如宽高等,存入音频的基本参数,声道,采样率等。


AVCodec:编码器,设置编码类型,像素格式,视频宽高,fps(每秒帧数), 用于编解码音视频编码层使用。


AVIOContext:用于管理输入输出结构体。例如解码的情况下,将一个视频文件中的数据先从硬盘中读入到结构体中的 buffer 中,然后送给解码器用于解码,后面我们会用到。


AVFrame: 结构体一般用于存储原始数据(即非压缩数据,例如对视频来说是YUV,RGB,对音频来说是PCM),此外还包含了一些相关的信息。比如说,解码的时候存储了宏块类型表,QP表,运动矢量表等数据。编码的时候也存储了相关的数据。因此在使用FFMPEG进行码流分析的时候,AVFrame是一个很重要的结构体。

好了,上面就是我们这次解封装用到的结构体的大概解析,那么我们就上代码,好好分析一番。


1、先取个霸气点的函数名,通过输入一个 yuv 文件路径,然后将文件数据进行编码,输出 H264文件。

yuvCodecToVideoH264(const char *input_file_name)

2、打开输入的 yuv 文件, 并设置我们 h264 文件的输出路径,

FILE *in_file = fopen(input_file, "rb");  
// 因为我们在 iOS 工程当中,所以输出路径当然要设置本机的路径了
const char* out_file = [[NSTemporaryDirectory() stringByAppendingPathComponent:@"dash.h264"] cStringUsingEncoding:NSUTF8StringEncoding];

3、获取 yuv 视频中的信息

// 注册 ffmpeg 中的所有的封装、解封装 和 协议等,当然,你也可用以下两个函数代替  
// * @see av_register_input_format()
// * @see av_register_output_format()
 av_register_all();

//  用作之后写入视频帧并编码成 h264,贯穿整个工程当中
AVFormatContext* pFormatCtx;
pFormatCtx = avformat_alloc_context();

// 通过这个函数可以获取输出文件的编码格式, 那么这里我们的 fmt 为 h264 格式(AVOutputFormat *)
fmt = av_guess_format(NULL, out_file, NULL);
pFormatCtx->oformat = fmt;

4、将输出文件中的数据读入到程序的 buffer 当中,方便之后的数据写入,也可以说缓存数据写入

// 打开文件的缓冲区输入输出,flags 标识为  AVIO_FLAG_READ_WRITE ,可读写
if (avio_open(&pFormatCtx->pb,out_file, AVIO_FLAG_READ_WRITE) < 0){
  printf("Failed to open output file! \n");
  return;
}

5、创建流媒体数据,规范流媒体的编码格式,设置视频流的 fps

AVStream* video_st;
// 通过媒体文件控制者获取输出文件的流媒体数据,这里 AVCodec * 写 0 , 默认会为我们计算出合适的编码格式
video_st = avformat_new_stream(pFormatCtx, 0);

// 设置 25 帧每秒 ,也就是 fps 为 25
video_st->time_base.num = 1;
video_st->time_base.den = 25;

if (video_st==NULL){
  return ;
}

6、为输出文件设置编码所需要的参数和格式

// 用户存储编码所需的参数格式等等
AVCodecContext* pCodecCtx;

// 从媒体流中获取到编码结构体,他们是一一对应的关系,一个 AVStream 对应一个  AVCodecContext
 pCodecCtx = video_st->codec;
   
// 设置编码器的 id,每一个编码器都对应着自己的 id,例如 h264 的编码 id 就是 AV_CODEC_ID_H264
pCodecCtx->codec_id = fmt->video_codec;

// 设置编码类型为 视频编码
pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;

// 设置像素格式为 yuv 格式
pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;

// 设置视频的宽高
pCodecCtx->width = 480;
pCodecCtx->height = 720;

// 设置比特率,每秒传输多少比特数 bit,比特率越高,传送速度越快,也可以称作码率,
// 视频中的比特是指由模拟信号转换为数字信号后,单位时间内的二进制数据量。
pCodecCtx->bit_rate = 400000;

// 设置图像组层的大小。
// 图像组层是在 MPEG 编码器中存在的概念,图像组包 若干幅图像, 组头包 起始码、GOP 标志等,如视频磁带记录器时间、控制码、B 帧处理码等;
pCodecCtx->gop_size=250;

// 设置 25 帧每秒 ,也就是 fps 为 25
pCodecCtx->time_base.num = 1;
pCodecCtx->time_base.den = 25;

//设置 H264 中相关的参数
//pCodecCtx->me_range = 16;
//pCodecCtx->max_qdiff = 4;
//pCodecCtx->qcompress = 0.6;
pCodecCtx->qmin = 10;
pCodecCtx->qmax = 51;

// 设置 B 帧最大的数量,B帧为视频图片空间的前后预测帧, B 帧相对于 I、P 帧来说,压缩率比较大,也就是说相同码率的情况下,
// 越多 B 帧的视频,越清晰,现在很多打视频网站的高清视频,就是采用多编码 B 帧去提高清晰度,
// 但同时对于编解码的复杂度比较高,比较消耗性能与时间
pCodecCtx->max_b_frames=3;

// 可选设置
AVDictionary *param = 0;
//H.264
if(pCodecCtx->codec_id == AV_CODEC_ID_H264) {
// 通过--preset的参数调节编码速度和质量的平衡。
av_dict_set(&param, "preset", "slow", 0);

// 通过--tune的参数值指定片子的类型,是和视觉优化的参数,或有特别的情况。
// zerolatency: 零延迟,用在需要非常低的延迟的情况下,比如电视电话会议的编码
av_dict_set(&param, "tune", "zerolatency", 0);

顺便说一下h264 当中有片组的概念,其中编码片分为5种,I 片、P 片、B 片、SP 片和 SI 片。

ES 码流是 MPEG 码流中的基本流,由视频压缩编码后的视频基 码流(Video ES)和音频压缩编码后的音频基 码流(Audio ES)组成。
以下顺带一张 ES 码流的结构图片,作为记录学习之用


ES 码流结构

ES 码流采用图像序列(PS)、图像组(GOP)、图像(P)、片(slice)、宏块(MB)、块(B)六层结构。

(1)图像序列层,图像序列包括若干 GOP,序列头包 起始码和序列参数,如档次、级别、彩色图像格式、帧场选择等等;
(2)图像组层,图像组包 若干幅图像,组头包 起始码、GOP 标志等,如视频磁带记录器时间、控制码、B 帧处理码等;
(3)图像层,一幅图像包 若干片,头信息中有起始码、P 标志,如时间、参考帧号、图像类型、MV、分级等;
(4)片层,片是最小的同步单位,包 若干宏块,片头中有起始码、片地址、量化步长等;
(5)宏块层,宏块由 4 个 8×8 亮度块和 2 个色度块组成,宏块头包括宏块地址、宏块类型、运动矢量等。

7、printf(输出) 一些关于输出格式的详细数据,例如时间,比特率,数据流,容器,元数据,辅助数据,编码,时间戳等等

av_dump_format(pFormatCtx, 0, out_file, 1);

8、设置编码器

// 通过 codec_id 找到对应的编码器
pCodec = avcodec_find_encoder(pCodecCtx->codec_id);
if (!pCodec){
  printf("Can not find encoder! \n");
  return;
}

// 打开编码器,并设置参数 param
if (avcodec_open2(pCodecCtx, pCodec,&param) < 0){
  printf("Failed to open encoder! \n");
  return;
}

9、设置原始数据 AVFrame

C
AVFrame *pFrame = av_frame_alloc();

// 通过像素格式(这里为 YUV)获取图片的真实大小,例如将 480 * 720 转换成 int 类型
int picture_size = avpicture_get_size(pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height);

// 将 picture_size 转换成字节数据,byte
unsigned char *picture_buf = (uint8_t *)av_malloc(picture_size);

// 设置原始数据 AVFrame 的每一个frame 的图片大小,AVFrame 这里存储着 YUV 非压缩数据
avpicture_fill((AVPicture *)pFrame, picture_buf, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height);


10、准备写入数据之前,当然要先写编码的头部了

// 编写 h264 封装格式的文件头部,基本上每种编码都有着自己的格式的头部,想看具体实现的同学可以看看 h264 的具体实现
int ret = avformat_write_header(pFormatCtx,NULL);
if (ret < 0) {
printf("write header is failed");
return;
}


这里顺便记录一下, h264 原始码流,又称为原始码流,都是由一个一个的 NALU 组成的,结构体如下

enum nal_unit_type_e
{
NAL_UNKNOWN = 0, // 未使用
NAL_SLICE = 1, // 不分区、非 IDR 图像的片
NAL_SLICE_DPA = 2, // 片分区 A
NAL_SLICE_DPB = 3, // 片分区 B
NAL_SLICE_DPC = 4, // 片分区 C
NAL_SLICE_IDR = 5, /* ref_idc != 0 / // 序列参数集
NAL_SEI = 6, /
ref_idc == 0 / // 图像参数集
NAL_SPS = 7, // 分界符
NAL_PPS = 8, // 序列结束
NAL_AUD = 9, // 码流结束
NAL_FILLER = 12, // 填充
/
ref_idc == 0 for 6,9,10,11,12 */
};
enum nal_priority_e // 优先级
{
NAL_PRIORITY_DISPOSABLE = 0,
NAL_PRIORITY_LOW = 1,
NAL_PRIORITY_HIGH = 2,
NAL_PRIORITY_HIGHEST = 3,
};

typedef struct
{
int startcodeprefix_len; //! 4 for parameter sets and first slice in picture, 3 for everything else (suggested)
unsigned len; //! Length of the NAL unit (Excluding the start code, which does not belong to the NALU)
unsigned max_size; //! Nal Unit Buffer size
int forbidden_bit; //! should be always FALSE
int nal_reference_idc; //! NALU_PRIORITY_xxxx
int nal_unit_type; //! NALU_TYPE_xxxx
char *buf; //! contains the first byte followed by the EBSP
} NALU_t;

11、创建编码后的数据 AVPacket 结构体来存储 AVFrame 编码后生成的数据

AVCodec* pCodec;
av_new_packet(&pkt,picture_size);

>其实从这里看出 AVPacket 跟 AVFrame 的关系如下
编码前:AVFrame
编码后:AVPacket

12、写入 yuv 数据到 AVFrame 结构体中

// 设置 yuv 数据中 y 图的宽高
int y_size = pCodecCtx->width * pCodecCtx->height;

for (int i=0; i<framenum; i++){
//Read raw YUV data
if (fread(picture_buf, 1, y_size3/2, in_file) <= 0){
printf("Failed to read raw data! \n");
return ;
}else if(feof(in_file)){
break;
}
pFrame->data[0] = picture_buf; // Y
pFrame->data[1] = picture_buf+ y_size; // U
pFrame->data[2] = picture_buf+ y_size
5/4; // V
//PTS
//pFrame->pts=i;
// 设置这一帧的显示时间
pFrame->pts=i(video_st->time_base.den)/((video_st->time_base.num)25);
int got_picture=0;
// 利用编码器进行编码,将 pFrame 编码后的数据传入 pkt 中
int ret = avcodec_encode_video2(pCodecCtx, &pkt,pFrame, &got_picture);
if(ret < 0){
printf("Failed to encode! \n");
return ;
}

// 编码成功后写入 AVPacket 到 输入输出数据操作着 pFormatCtx 中,当然,记得释放内存
if (got_picture==1){
printf("Succeed to encode frame: %5d\tsize:%5d\n",framecnt,pkt.size);
framecnt++;
pkt.stream_index = video_st->index;
ret = av_write_frame(pFormatCtx, &pkt);
av_free_packet(&pkt);
}
}


13、flush 编码

int flush_encoder(AVFormatContext *fmt_ctx,unsigned int stream_index){
int ret;
int got_frame;
AVPacket enc_pkt;

// 确认如果
if (!(fmt_ctx->streams[stream_index]->codec->codec->capabilities &
      CODEC_CAP_DELAY))
    return 0;
while (1) {
    enc_pkt.data = NULL;
    enc_pkt.size = 0;
    av_init_packet(&enc_pkt);
    ret = avcodec_encode_video2 (fmt_ctx->streams[stream_index]->codec, &enc_pkt,
                                 NULL, &got_frame);
    av_frame_free(NULL);
    if (ret < 0)
        break;
    if (!got_frame){
        ret=0;
        break;
    }
    printf("Flush Encoder: Succeed to encode 1 frame!\tsize:%5d\n",enc_pkt.size);
    /* mux encoded frame */
    ret = av_write_frame(fmt_ctx, &enc_pkt);
    if (ret < 0)
        break;
}
return ret;

}

int ret2 = flush_encoder(pFormatCtx,0);
if (ret2 < 0) {
printf("Flushing encoder failed\n");
return;
}


14、我们上面写完了编码头、编码数据,当然也要写入编码的尾部表示结束了啦,这样才是一个完整的编码格式嘛

// 写入数据流尾部到输出文件当中,并释放文件的私有数据
av_write_trailer(pFormatCtx);


15、释放我们之前创建的内存

if (video_st){
// 关闭编码器
avcodec_close(video_st->codec);
// 释放 AVFrame
av_free(pFrame);
// 释放图片 buf,就是 free() 函数,硬要改名字,当然这是跟适应编译环境有关系的
av_free(picture_buf);
}

// 关闭输入数据的缓存
avio_close(pFormatCtx->pb);
// 释放 AVFromatContext 结构体
avformat_free_context(pFormatCtx);

// 关闭输入文件
fclose(in_file);


----

好了,写到这里,我们首先要做的就是利用就把下面这个 .yuv 文件放到工程当中,如下图
![工程图片](http://upload-images.jianshu.io/upload_images/1073278-c6900f0bd6e3de7b.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

然后在 `- (void)viewDidLoad `方法中使用如下代码

const char *input_file = [[[NSBundle mainBundle] pathForResource:@"FFmpegTest" ofType:@"yuv"] cStringUsingEncoding:NSUTF8StringEncoding];

yuvCodecToVideoH264(input_file);


然后运行,瞬间, 利用同步推打开我们工程的系统,看到我们就得到我们想要的东西了

![沙盒文件结构](http://upload-images.jianshu.io/upload_images/1073278-cfb80f2771674440.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


----

有些小伙伴可能在编译的时候遇到错误,那是因为函数当中一些用到的工程库并没有链接到工程中,可以在工程的 General->Linked Frameworks and Libraries 检查如下图


![Linked Frameworks and Libraries](http://upload-images.jianshu.io/upload_images/1073278-a2b14540eab949a1.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)



好了,先写这么多了,写这篇博客比较赶,当中或许有许多地方还没有经过细心雕琢,而且可能还存在一些错别字,容我再找我时间好好再打磨一下,哈哈。
还有的就是,谢谢大家一路的支持,让我继续有动力写下去。


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

推荐阅读更多精彩内容