【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文件夹(里边有包含两个文件夹includelib),拖入工程中,设置路径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对视频文件进行解码的大致流程:

  1. 注册所有容器格式和CODEC: av_register_all()
  2. 打开文件: av_open_input_file()
  3. 从文件中提取流信息: av_find_stream_info()
  4. 穷举所有的流,查找其中种类为CODEC_TYPE_VIDEO
  5. 查找对应的解码器: avcodec_find_decoder()
  6. 打开编解码器: avcodec_open()
  7. 为解码帧分配内存: avcodec_alloc_frame()
  8. 不停地从码流中提取中帧数据: av_read_frame()
  9. 判断帧的类型,对于视频帧调用: avcodec_decode_video()
  10. 解码完后,释放解码器: avcodec_close()
  11. 关闭输入文件: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);
});
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容