音视频开发音视频开发audio&video

【iOS】FFmpeg编译+h264解码+yuv渲染

2016-11-02  本文已影响1789人  H_Liuju

从零开始认识视频解码和渲染,需求是用ffmpeg软解码和videoToolBox硬解码,现已完成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);
});
}
上一篇下一篇

猜你喜欢

热点阅读