【iOS】FFmpeg编译+h264解码+yuv渲染
从零开始认识视频解码和渲染,需求是用ffmpeg软解码和videoToolBox硬解码,现已完成ffmpeg方面,从开始的无从下手,到现在能够较好的利用它,总算不辜负好几天来的折腾。如有不当的地方,欢迎批评指正,谢谢。废话不多说,上笔记。
一、编译
- 1、首先,你需要下载
下载gas-preprocessor
下载FFmpeg-iOS-build-script - 2、如果你安装了多个xcode,可以先修改下默认路径,否则后边设置了路径还是会报#import "avformat.h"错误
sudo xcode-select -s /Applications/Xcode.app
- 3、将 gas-preprocessor文件夹内的gas-preprocessor.pl文件拷贝到/usr/sbin/目录下,并修改/usr/sbin/gas-preprocessor.pl的文件权限为可执行权限
chmod 777 /usr/sbin/gas-preprocessor.pl
- 4、通过终端进入FFmpeg-iOS-build-script文件夹,开始编译
编译所有的版本arm64、armv7、x86_64的静态库(如只需编译部分版本的可在指令后添加特定版本)
./build-ffmpeg.sh
- 5、以上算是编译完成了,此时你需要在运行后的脚本文件夹FFmpeg-iOS-build-script里找到下载编译后的FFmpeg-iOS文件夹(里边有包含两个文件夹include和lib),拖入工程中,设置路径TARGETS→Build Settings→Search Paths→Library Search Paths 设置为
"$(SRCROOT)/你的工程名/FFmpeg-iOS/lib"
(这里还有个小技巧,之前无论这样设置都行不通,谷歌许久尝试了多种设置方法都行不通,最后直接把库放在头文件下,行得通了!如果你也这样不妨试试我的办法O(∩_∩)O~) - 6、另外还需要在工程中加上一些一些库:libiconv.dylib、libbz.dylib、libz.dylib,到这里算是结束了。
- 7、最后,在头文件加上
#import "avformat.h
试试看是不是已经不会报错说找不到文件了,那么可以开始利用ffmpeg咯。
二、h264解码
ffmpeg对视频文件进行解码的大致流程:
- 注册所有容器格式和CODEC: av_register_all()
- 打开文件: av_open_input_file()
- 从文件中提取流信息: av_find_stream_info()
- 穷举所有的流,查找其中种类为CODEC_TYPE_VIDEO
- 查找对应的解码器: avcodec_find_decoder()
- 打开编解码器: avcodec_open()
- 为解码帧分配内存: avcodec_alloc_frame()
- 不停地从码流中提取中帧数据: av_read_frame()
- 判断帧的类型,对于视频帧调用: avcodec_decode_video()
- 解码完后,释放解码器: avcodec_close()
- 关闭输入文件:av_close_input_file()
刚接触的时候看到这些流程真不知如何下手,找到的一些Demo也都是.mm文件里用C++编程的,理解起来还是觉得颇有难度,后来干脆直接把这流程放在代码里,逐个突破,居然得到了意向不到的效果,后来再看网上教程,就开始明白解码也就这点流程这点函数。
这里简单梳理下大致流程:
(1)av_register_all() 这个函数来注册所有的组件,初始化只需要一次,可以用GCD的方式使其执行一次;
(2)avformat_open_input打开文件;
(3)当文件成功打开(所以需要判断文件是否成功打开),用** avformat_find_stream_info从文件中提取流信息;
(4)当成功提取了流信息(同理需要判断流信息是否提取成功),就需要做一个遍历在多个数据流中找到视频流;
(5)当找到视频流(需要判断是否找到视频流),需要 avcodec_find_decoder查找视频流中相对应的解码器;
(6)如果找到了解码器(加判断),则用 avcodec_open2打开解码器;
(7)打开解码器之后用 av_frame_alloc为解码帧分配内存;
(8)接下来要做一个循环, av_read_frame是从流中读取一帧的数据到Packet中;
(9)当已经读取到数据(加判断),则h264文件解码中最重要的函数登场, avcodec_decode_video2开始解码,它会将AVPacket(是个结构体,里面装的是h.264)转换为AVFream(里面装的是yuv数据),此时的yuv数据渲染后便是我们肉眼看到的视频数据;
(10)当(8)中的 av_read_frame**读不到数据,结束循环。
上代码:
// [1]注册所支持的所有的文件(容器)格式及其对应的CODEC av_register_all()
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
av_register_all();
});
// [2]打开文件 avformat_open_input()
pFormatContext = avformat_alloc_context();
NSString *fileName = [[NSBundle mainBundle] pathForResource:@"zjd.h264" ofType:nil];
if (fileName == nil)
{
NSLog(@"Couldn't open file:%@",fileName);
return;
}
if (avformat_open_input(&pFormatContext, [fileName cStringUsingEncoding:NSASCIIStringEncoding], NULL, NULL) != 0)//[1]函数调用成功之后处理过的AVFormatContext结构体;[2]打开的视音频流的URL;[3]强制指定AVFormatContext中AVInputFormat的。这个参数一般情况下可以设置为NULL,这样FFmpeg可以自动检测AVInputFormat;[4]附加的一些选项,一般情况下可以设置为NULL。)
{
NSLog(@"无法打开文件");
return;
}
以上是对本地H264文件的解码,当然如果你要对网络实时传输的h264视频码流进行解码,需要初始化网络环境,同时修改路径,以RTSP为例
avformat_network_init();
...(其余部分不变)
in_filename = [[NSString stringWithFormat:@"rtsp://192.168.100.1/video/h264"] cStringUsingEncoding:NSASCIIStringEncoding];
// [3]从文件中提取流信息 avformat_find_stream_info()
if (avformat_find_stream_info(pFormatContext, NULL) < 0) {
NSLog(@"无法提取流信息");
return;
}
// [4]在多个数据流中找到视频流 video stream(类型为MEDIA_TYPE_VIDEO)
int videoStream = -1;
for (int i = 0; i < pFormatContext -> nb_streams; i++)
{
if (pFormatContext -> streams[i] -> codec -> codec_type == AVMEDIA_TYPE_VIDEO)
{
videoStream = i;
}
}
if (videoStream == -1) {
NSLog(@"Didn't find a video stream.");
return;
}
// [5]查找video stream 相对应的解码器 avcodec_find_decoder(获取视频解码器上下文和解码器)
pCodecCtx = pFormatContext->streams[videoStream]->codec;
AVCodec *pCodec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (pCodec == NULL) {
NSLog(@"pVideoCodec not found.");
return NO;
}
// [6]打开解码器 avcodec_open2()
avcodec_open2(pCodecCtx, pCodec, NULL);
// [7]为解码帧分配内存 av_frame_alloc()
pFrame = av_frame_alloc();
// [8]从流中读取读取数据到Packet中 av_read_frame()
AVPacket packet;
av_init_packet(&packet);
if (av_read_frame(pFormatContext, &packet) >= 0)
{//已经读取到了数据
// [9]对video 帧进行解码,调用 avcodec_decode_video2()
avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);//作用是解码一帧视频数据。输入一个压缩编码的结构体AVPacket,输出一个解码后的结构体AVFrame
av_free_packet(&packet);
}
if (frameFinished)
{
//能够跳进此方法说明已经解码成功
}
渲染
介绍一个很棒的OpenGL直接渲染YUV的代码,在解码成功的函数里只要初始化后调用外部接口,可用
//初始化
OpenGLView20 *glView = [[OpenGLView20 alloc] initWithFrame:frame];
//设置视频原始尺寸
[glView setVideoSize:352 height:288];
//渲染yuv
[glView displayYUV420pData:yuvBuffer width:352height:288;
这里需要注意的是,我们的视频文件解码后的AVFrame的data不能直接渲染,因为AVFrame里的数据是分散的,displayYUV420pData这里指明了数据格式为YUV420P,所以我们必须copy到一个连续的缓冲区,然后再进行渲染。
像这样:
char *buf = (char *)malloc(_pFrame->width * _pFrame->height * 3 / 2);
AVPicture *pict;
int w, h, i;
char *y, *u, *v;
pict = (AVPicture *)_pFrame;//这里的frame就是解码出来的AVFrame
w = _pFrame->width;
h = _pFrame->height;
y = buf;
u = y + w * h;
v = u + w * h / 4;
for (i=0; i<h; i++)
memcpy(y + w * i, pict->data[0] + pict->linesize[0] * i, w);
for (i=0; i<h/2; i++)
memcpy(u + w / 2 * i, pict->data[1] + pict->linesize[1] * i, w / 2);
for (i=0; i<h/2; i++)
memcpy(v + w / 2 * i, pict->data[2] + pict->linesize[2] * i, w / 2);
if (buf == NULL) {
return;
}else {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(1);
[myview displayYUV420pData:buf width:_pFrame -> width height:_pFrame ->height];
free(buf);
});
}