三、IOS-FFmpeg解码

为了简单化,这里只是从H264视频编码格式,解码为yuv420p的原始图片格式。
代码仓库:https://github.com/wulang150/FFmpegTest.git
代码文件:DecoderViewController.m

H264裸流格式:

这里谈谈H264裸流格式,虽然对本次解码表面上帮助不大,但理解了,你就更好地理解问题,解决问题。我之前做的项目是设备采集视频数据,然后p2p发送给手机端,手机端进行解码,播放。所以我对H264裸流还是有一定的了解。
H264可以说分两种格式:Annex-B和AVCC两种格式。
Annex-B格式:一般用于流在网络中传播,所以那些直播,监控,全部都是这个格式的H264。
AVCC格式:封装在Mp4后的流的格式。

本次使用的是Annex-B格式的H264流,具体格式可以看看它的16进制:

屏幕快照 2019-04-22 上午10.04.21.png

每一个包含完整信息的item(一般叫nal)都是用0x000001或0x00000001作为分割。
0x0000000167:sps
0x0000000168:pps
0x0000000165:IDR,主帧
其他都是P或B帧。sps跟pps包含了解码主帧的一些信息,有了他们才可以正确解码主帧。
具体可以参考:https://blog.csdn.net/romantic_energy/article/details/50508332

总的代码

#import "DecoderViewController.h"
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>

#define INBUF_SIZE 4096

static AVFrame *pFrameYUV;
static struct SwsContext *img_convert_ctx;

@interface DecoderViewController ()

@end

@implementation DecoderViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor whiteColor];
    self.title = @"解码";
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self mainFunc];
}

static AVFrame *alloc_picture(enum AVPixelFormat pix_fmt, int width, int height)
{
    AVFrame *picture;
    int ret;
    picture = av_frame_alloc();
    if (!picture)
        return NULL;
    picture->format = pix_fmt;
    picture->width  = width;
    picture->height = height;
    /* allocate the buffers for the frame data */
    ret = av_frame_get_buffer(picture, 32);
    if (ret < 0) {
        fprintf(stderr, "Could not allocate frame data.\n");
        return NULL;
    }
    return picture;
}

static void pgm_save(AVFrame *frame, FILE *f)
{
    //进行转码操作,转成yuv420
    if(frame->format!=AV_PIX_FMT_YUV420P){
        if(!pFrameYUV){
            pFrameYUV = alloc_picture(AV_PIX_FMT_YUV420P, frame->width, frame->height);
        }
        if(!img_convert_ctx){
            //转码器
            img_convert_ctx = sws_getContext(frame->width, frame->height,
                                             frame->format,
                                             pFrameYUV->width, pFrameYUV->height,
                                             AV_PIX_FMT_YUV420P,
                                             SWS_BICUBIC, NULL, NULL, NULL);
        }
        sws_scale(img_convert_ctx, (const uint8_t* const*)frame->data, frame->linesize, 0, frame->height,
                                    pFrameYUV->data, pFrameYUV->linesize);
        frame = pFrameYUV;
    }
    printf("fmx=%d size=%dx%d\n",frame->format,frame->width,frame->height);
    int i;
    //Y
    int width = MIN(frame->linesize[0], frame->width);
    for(i=0;i<frame->height;i++)
    {
        fwrite(frame->data[0]+i*frame->linesize[0], 1, width, f);
    }
    //u
    width = MIN(frame->linesize[1], frame->width/2);
    for(i=0;i<frame->height/2;i++)
    {
        fwrite(frame->data[1]+i*frame->linesize[1], 1, width, f);
    }
    //v
    width = MIN(frame->linesize[2], frame->width/2);
    for(i=0;i<frame->height/2;i++)
    {
        fwrite(frame->data[2]+i*frame->linesize[2], 1, width, f);
    }
}

static void decode(AVCodecContext *dec_ctx, AVFrame *frame, AVPacket *pkt,
                   FILE *f)
{
//    char buf[1024];
    int ret;
    ret = avcodec_send_packet(dec_ctx, pkt);
    if (ret < 0) {
        fprintf(stderr, "Error sending a packet for decoding\n");
        exit(1);
    }
    while (ret >= 0) {
        ret = avcodec_receive_frame(dec_ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            return;
        else if (ret < 0) {
            fprintf(stderr, "Error during decoding\n");
            exit(1);
        }
        fflush(stdout);
        printf("saving frame %3d ",dec_ctx->frame_number);
        pgm_save(frame, f);
    }
}

- (int)mainFunc{
    av_register_all();
    avcodec_register_all();
    
    const char *filename, *outfilename;
    const AVCodec *codec;
    AVCodecParserContext *parser;
    AVCodecContext *c= NULL;
    FILE *f, *outF;
    AVFrame *frame;
    uint8_t inbuf[INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
    uint8_t *data;
    size_t   data_size;
    int ret;
    AVPacket *pkt;
    
    //input
    NSString *filePath = [CommonFunc getDocumentWithFile:@"movieH264.ts"];
    filename = [filePath cStringUsingEncoding:NSASCIIStringEncoding];
    //output
    filePath = [CommonFunc getDefaultPath:@"movie.yuv"];
    outfilename = [filePath cStringUsingEncoding:NSASCIIStringEncoding];
    
    pkt = av_packet_alloc();
    if (!pkt)
        exit(1);
    /* set end of buffer to 0 (this ensures that no overreading happens for damaged MPEG streams) */
    memset(inbuf + INBUF_SIZE, 0, AV_INPUT_BUFFER_PADDING_SIZE);
    /* find video decoder */
    codec = avcodec_find_decoder(AV_CODEC_ID_H264);
    if (!codec) {
        fprintf(stderr, "Codec not found\n");
        exit(1);
    }
    parser = av_parser_init(codec->id);
    if (!parser) {
        fprintf(stderr, "parser not found\n");
        exit(1);
    }
    c = avcodec_alloc_context3(codec);
    if (!c) {
        fprintf(stderr, "Could not allocate video codec context\n");
        exit(1);
    }
//    c->pix_fmt = AV_PIX_FMT_YUV420P;
    /* For some codecs, such as msmpeg4 and mpeg4, width and height
     MUST be initialized there because this information is not
     available in the bitstream. */
    /* open it */
    if (avcodec_open2(c, codec, NULL) < 0) {
        fprintf(stderr, "Could not open codec\n");
        exit(1);
    }
    f = fopen(filename, "rb");
    if (!f) {
        fprintf(stderr, "Could not open %s\n", filename);
        exit(1);
    }
    outF = fopen(outfilename, "wb");
    if (!outF) {
        fprintf(stderr, "Could not open %s\n", outfilename);
        exit(1);
    }
    frame = av_frame_alloc();
    if (!frame) {
        fprintf(stderr, "Could not allocate video frame\n");
        exit(1);
    }
    while (!feof(f)) {
        /* read raw data from the input file */
        data_size = fread(inbuf, 1, INBUF_SIZE, f);
        if (!data_size)
            break;
        /* use the parser to split the data into frames */
        data = inbuf;
        while (data_size > 0) {
            //相当于在annex-b格式的流中拆出每一个nal,可能得多次操作才有一个完整的pkt出来
            ret = av_parser_parse2(parser, c, &pkt->data, &pkt->size,
                                   data, (int)data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
            if (ret < 0) {
                fprintf(stderr, "Error while parsing\n");
                exit(1);
            }
            data      += ret;
            data_size -= ret;
            if (pkt->size)
                decode(c, frame, pkt, outF);
        }
    }
    /* flush the decoder */
    decode(c, frame, NULL, outF);
    fclose(f);
    fclose(outF);
    sws_freeContext(img_convert_ctx);
    av_parser_close(parser);
    avcodec_free_context(&c);
    av_frame_free(&frame);
    av_frame_free(&pFrameYUV);
    av_packet_free(&pkt);
    return 0;
}
@end

分析一下:
这里是解码Annex-B格式的H264,所以需要配置的参数少点,因为需要的解码信息都是在上面提到的sps,pps带过来了,后面会说到解码AVCC格式的H264,需要配置的参数就会多点:

//初始化解码器
codec = avcodec_find_decoder(AV_CODEC_ID_H264);
//通过解码器,初始化解码上下文
c = avcodec_alloc_context3(codec);
//打开解码器
avcodec_open2(c, codec, NULL)
//传入需要解码的AVPacket
ret = avcodec_send_packet(dec_ctx, pkt);
//解码得到AVFrame
ret = avcodec_receive_frame(dec_ctx, frame);

上面有个比较重要的步骤是:把H264流封装成一个个AVPacket。AVPacket需要的信息就是前面提到的一个个nal,要怎么拆分,你可以使用AVCodecParserContext,当然也可以自己拆分出来。代码:

ret = av_parser_parse2(parser, c, &pkt->data, &pkt->size,
                                   data, (int)data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
使用AVCodecParserContext进行拆分
把每一个nal赋值给packet->data。

解码的api最新的改为了:
avcodec_send_packet
avcodec_receive_frame
之前的为:
avcodec_decode_video2
改为新的也更容易理解吧,不一定是来一个nal,就可以解出来一个,像有B帧的情况,来了B帧,还得继续send入后面的帧才有可能解出来这个B帧,所以改为send跟receive比较好操控跟理解。
avcodec_receive_frame上层加了个循环,其实也是为了处理上面说的那种情况,B跟后面的P帧都来了,解码了B帧后,后面的P帧也是解码的了,所以这种情况avcodec_receive_frame可以有多个解码的AVFrame,通过循环读取完整。
最后还得执行:
decode(c, frame, NULL, outF);
把最后剩下解码的几帧个读取出来。

解码后:

进行了一次yuv420p的转换,为了统一为yuv420p格式。因为原始图片格式也是有很多种的。统一转为yuv420p格式后,我就可以统一按yuv420p的格式写入文件了。

得到最终的yuv文件后,你会发现原来的1多兆的H264流竟然解码后达到了800多兆。所以压缩率还是很高的。

验证:

得到yuv文件后,怎么验证是解码成功呢?这时候就得利用ffplay了(前提是的在mac安装ffmpeg跟ffplay,前面有提到怎么安装)。可以播放,就证明解码成功了。
指令为:

ffplay -i 11_42_18_movie.yuv -pixel_format yuv420p -video_size 1600x1200
-i:对应的yuv文件
-pixel_format:对应的格式
-video_size:1600x1200,视频的分辨率

H264裸流文件:
链接:https://pan.baidu.com/s/1fx60ynWJ2vVAGFfYZVatng 密码:keyx
视频是黑白的,不是出问题了!!
除了使用我提供的裸流,如果你有MP4视频文件,也可以自己生成:

ffmpeg -i output.mp4 -an -vcodec copy -bsf:v h264_mp4toannexb output.h264
可以截取mp4得到更短的视频:(截取前5秒)
ffmpeg -t 00:00:05 -i input.mp4 -vcodec copy -acodec copy output.mp4

但是,这个生成的H264裸流,有很多干扰的东西,所以用AVCodecParserContext是拆分不准确的。可以自己进行拆分。

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