IOS-FFmpeg解码

作者: Johnny_Wu | 来源:发表于2019-04-22 11:26 被阅读1次

    为了简单化,这里只是从H264视频编码格式,解码为yuv420p的原始图片格式。

    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是拆分不准确的。可以自己进行拆分。

    相关文章

      网友评论

        本文标题:IOS-FFmpeg解码

        本文链接:https://www.haomeiwen.com/subject/kwwegqtx.html