FFmpeg 解码本地 H.264

最近学习 FFmpeg,自己写了一个小 Demo 解码一个 H.264 裸流数据
FFmpeg 的编译导入等工作我们不再赘述,下面直接进入正题

导入库文件

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

声明一些我们需要用到的全局变量

@interface LLQH264Decoder()<UIAlertViewDelegate>
{
    AVFormatContext *pFormatCtx;  //解码上下文,贯穿整个解码过程
    int i,videoIndex;
    AVCodecContext *pCodecCtx;  //解码器上下文,存储解码器、解码信息等
    AVCodec *pCodec;  //解码器
    AVFrame *pFrame, *pFrameYUV;  //存储每一帧的原始数据
    uint8_t *out_Buffer;
    AVPacket *packet;  //存储每一帧的解码后数据
    int ret,got_picture;
    struct SwsContext *img_convert_ctx;
    int frame_cnt;
}

准备工作已经做完了,下面开始重点

解码部分

我们通过传入一个 H.264 文件的路径,来读取这个文件

- (void)setupFFMPEGwithPath:(NSString *)path{
    
    //注册编解码器
    av_register_all();
    //
    avformat_network_init();
    //初始化 贯穿整个解码的解码上下文
    pFormatCtx = avformat_alloc_context();
    
    //打开文件  返回0表示成功,所有数据存储在formatCtx中
    if (avformat_open_input(&pFormatCtx, path.UTF8String, NULL, NULL) != 0) {
        [self showAlerViewTitle:@"不能打开流文件"];
        return;
    }
    
    //读取数据包获取流媒体文件的信息
    if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
        [self showAlerViewTitle:@"不能读取到流信息"];
        return;
    }
    
    videoIndex = -1;
    //查找视频流
    //nb_streams视音频流的个数
    //streams视音频流
    for (i = 0; i < pFormatCtx->nb_streams; i ++) {
        //直至查找到视频流
        if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
            videoIndex = i;
            NSLog(@"videoIndex==%d",videoIndex);  //视频流的下标
            break;
        }
        if (videoIndex == -1) {
            [self showAlerViewTitle:@"没有视频流"];
            return;
        }
    }
    
    //取出查找到的视频流的解码器信息
    pCodecCtx = pFormatCtx->streams[videoIndex]->codec;
    
    //初始化解码器
    pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
    
    if (pCodec == NULL) {
        [self showAlerViewTitle:@"找不到解码器"];
        return;
    }
    
    //打开解码器
    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
        [self showAlerViewTitle:@"不能打开解码器"];
        return;
    }
    
    //初始化frame,packet
    //AVPacket里面的是H.264码流数据
    //AVFrame里面装的是YUV数据。YUV是经过decoder解码AVPacket的数据
    pFrame = av_frame_alloc();
    packet = (AVPacket *)malloc(sizeof(AVPacket));
    
    //打印一大堆时间、比特率、流、容器、编解码器和时间等
    av_dump_format(pFormatCtx, 0, path.UTF8String, 0);
    
    //为解码为image做准备
    img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
    frame_cnt = 0;
    
    //开辟线程操作
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //从formatCtx中读取,一帧一帧的读取,循环一次,就读取一帧
        while (av_read_frame(pFormatCtx, packet) >= 0) {
            NSLog(@"packet->data==%d",packet->size);
            if (packet->stream_index == videoIndex) {
                //根据获取到的packet生成pFrame(AVFrame)实际上就是解码
                //如果没有需要解码的帧则got_picture就会为0
                ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
                if (ret < 0) {
                    [self showAlerViewTitle:@"解码错误"];
                    return;
                }
                if (got_picture) {
                    
                    //这里是播放部分,有两种播放方式,用 OpenGL 播放
                    //OpenGL GPU渲染
                    [self makeYUVframe];
                    
                    //imageView播放,我们将解码获得的 YUV 数据解码为 image,然后交给 imageview
//                    [self makeImage];
                    
                }
            }
            av_free_packet(packet);
        }
        
        //最后释放我们用到的结构体
        sws_freeContext(img_convert_ctx);
        av_frame_free(&pFrameYUV);
        av_frame_free(&pFrame);
        avcodec_close(pCodecCtx);
        avformat_close_input(&pFormatCtx);
        [[NSNotificationCenter defaultCenter] postNotificationName:decodeDidFinishNotification object:nil];
    });
    
}

播放部分

OpenGL播放
//YUV转RGB
- (void)makeYUVframe{
    
    unsigned int lumaLength = (pCodecCtx->height)*(MIN(pFrame->linesize[0], pCodecCtx->width));
    unsigned int chromBLength = ((pCodecCtx->height)/2)*(MIN(pFrame->linesize[1], (pCodecCtx->width)/2));
    unsigned int chromRLength = ((pCodecCtx->height)/2)*(MIN(pFrame->linesize[1], (pCodecCtx->width)/2));
    
    //初始化
    H264YUV_Frame yuvFrame;
    
    //此函数的意思是将 sizeof(H264YUV_Frame) 大小的 0 数据,拷贝到这个地址&yuvFrame 实际上就是初始化
    memset(&yuvFrame, 0, sizeof(H264YUV_Frame));
    
    yuvFrame.luma.length = lumaLength;
    yuvFrame.chromaB.length = chromBLength;
    yuvFrame.chromaR.length = chromRLength;
    
    yuvFrame.luma.dataBuffer = (unsigned char*)malloc(lumaLength);
    yuvFrame.chromaB.dataBuffer = (unsigned char*)malloc(chromBLength);
    yuvFrame.chromaR.dataBuffer = (unsigned char*)malloc(chromRLength);
    
    //转RGB
    copyDecodedFrame(pFrame->data[0], yuvFrame.luma.dataBuffer, pFrame->linesize[0], pCodecCtx->width, pCodecCtx->height);
    copyDecodedFrame(pFrame->data[1], yuvFrame.chromaB.dataBuffer, pFrame->linesize[1], pCodecCtx->width/2, pCodecCtx->height/2);
    copyDecodedFrame(pFrame->data[2], yuvFrame.chromaR.dataBuffer, pFrame->linesize[2], pCodecCtx->width/2, pCodecCtx->height/2);
    
    yuvFrame.width = pCodecCtx->width;
    yuvFrame.height = pCodecCtx->height;
    
    //在主线程中把获得到的 RGB 更新出去,这里通过代理
    dispatch_sync(dispatch_get_main_queue(), ^{
        
        if([self.delegate respondsToSelector:@selector(updateYUVFrameOnMainThread:)]){
            [self.delegate updateYUVFrameOnMainThread:(H264YUV_Frame *)&yuvFrame];
        }
        
    });
    
    //最后释放
    free(yuvFrame.luma.dataBuffer);
    free(yuvFrame.chromaB.dataBuffer);
    free(yuvFrame.chromaR.dataBuffer);
    
}

//转RGB算法
void copyDecodedFrame(unsigned char *src, unsigned char *dist,int linesize, int width, int height)
{
    
    width = MIN(linesize, width);
    
    for (NSUInteger i = 0; i < height; ++i) {
        memcpy(dist, src, width);
        dist += width;
        src += linesize;
    }
    
}

这里的 OpenGL 播放我用的是前辈写的 OpenGL 播放器,只需要传入RGB 数据就可以播放了,下面是代理方法的实现

#pragma mark ------ LLQH264DecoderDelegate

- (void)updateYUVFrameOnMainThread:(H264YUV_Frame *)yuvFrame{
    
    //只需要调用这个方法,就可以播放了
    [_openGLFrameView render:yuvFrame];
    
}
imageView播放

同样是利用代理方法将获得的图片更新出去

//转为image
- (void)makeImage{
    
    //给picture分配空间
    AVPicture pictureL = [self AllocAVPicture];
    int pictRet = sws_scale (img_convert_ctx,(const uint8_t * const *)pFrame->data, pFrame->linesize,
                             0, pCodecCtx->height,
                             pictureL.data, pictureL.linesize);
    if (pictRet > 0) {
        UIImage * image = [self imageFromAVPicture:pictureL width:pCodecCtx->width height:pCodecCtx->height];
        [NSThread sleepForTimeInterval:1.0/80.0];
        if ([self.delegate respondsToSelector:@selector(updateImageOnMainTread:)]) {
            [self.delegate updateImageOnMainTread:image];
        }
        
    }
    //释放AVPicture
    avpicture_free(&pictureL);
    
}

这边是一些转为image用到的算法


-(AVPicture)AllocAVPicture
{
    //创建AVPicture
    AVPicture pictureL;
    sws_freeContext(img_convert_ctx);
    avpicture_alloc(&pictureL, PIX_FMT_RGB24,pCodecCtx->width,pCodecCtx->height);
    static int sws_flags =  SWS_FAST_BILINEAR;
    img_convert_ctx = sws_getContext(pCodecCtx->width,
                                     pCodecCtx->height,
                                     pCodecCtx->pix_fmt,
                                     pCodecCtx->width,
                                     pCodecCtx->height,
                                     PIX_FMT_RGB24,
                                     sws_flags, NULL, NULL, NULL);
    
    
    return pictureL;
}

/**AVPicture转UIImage*/
-(UIImage *)imageFromAVPicture:(AVPicture)pict width:(int)width height:(int)height {
    CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;
    CFDataRef data = CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, pict.data[0], pict.linesize[0]*height,kCFAllocatorNull);
    CGDataProviderRef provider = CGDataProviderCreateWithCFData(data);
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGImageRef cgImage = CGImageCreate(width,
                                       height,
                                       8,
                                       24,
                                       pict.linesize[0],
                                       colorSpace,
                                       bitmapInfo,
                                       provider,
                                       NULL,
                                       NO,
                                       kCGRenderingIntentDefault);
    CGColorSpaceRelease(colorSpace);
    UIImage *image = [UIImage imageWithCGImage:cgImage];
    CGImageRelease(cgImage);
    CGDataProviderRelease(provider);
    CFRelease(data);
    
    return image;
}

代理方法的实现,只需要将图片交给 imageView

#pragma mark ------ LLQH264DecoderDelegate
- (void)updateImageOnMainTread:(UIImage *)image{
    
    dispatch_sync(dispatch_get_main_queue(), ^{
       
        _imageView.image = image;
        
    });
    
}

最后附上源码地址
这个解码还是比较简单的,需要静下心来研究一下

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

推荐阅读更多精彩内容