美文网首页
27_H.264解码实战

27_H.264解码实战

作者: 咸鱼Jay | 来源:发表于2022-11-13 04:37 被阅读0次

    本文的主要内容:对H.264数据进行解码(解压缩)。

    使用FFmpeg命令进行H.264解码

    如果是命令行的操作,非常简单。

    ffmpeg -c:v h264 -i in.h264 out_cmd.yuv
    # -c:v h264是指定使用h264作为解码器
    

    使用FFmpeg代码进行H.264解码

    接下来主要讲解如何通过代码的方式解码H.264数据,用到了avcodecavutil两个库,整体过程跟《AAC解码实战》类似。

    H.264 解码流程

    1、获取解码器

    通过ID或者名称获取到的H.264解码器都是h264。

    // 使用 ID 获取编码器:
    codec = avcodec_find_decoder(AV_CODEC_ID_H264);
    // 或者使用名称获取编码器:
    codec = avcodec_find_decoder_by_name("h264");
    if (!codec) {
        qDebug() << "decoder libfdk_aac not found";
        return;
    }
    

    2、初始化解析器上下文

    通过ID创建H.264解析器上下文:

    parserCtx = av_parser_init(codec->id);
    if (!parserCtx) {
        qDebug() << "av_parser_init error";
        return;
    }
    

    查看函数av_parser_init源码:

    // 源码位置:ffmpeg-4.3.2/libavcodec/parser.c
    AVCodecParserContext *av_parser_init(int codec_id)
    {
        AVCodecParserContext *s = NULL;
        const AVCodecParser *parser;
        void *i = 0;
        int ret;
    
        if (codec_id == AV_CODEC_ID_NONE)
            return NULL;
    
        while ((parser = av_parser_iterate(&i))) {
            if (parser->codec_ids[0] == codec_id ||
                parser->codec_ids[1] == codec_id ||
                parser->codec_ids[2] == codec_id ||
                parser->codec_ids[3] == codec_id ||
                parser->codec_ids[4] == codec_id)
                goto found;
        }
        return NULL;
    
    found:
        s = av_mallocz(sizeof(AVCodecParserContext));
        if (!s)
            goto err_out;
        s->parser = (AVCodecParser*)parser;
        s->priv_data = av_mallocz(parser->priv_data_size);
        if (!s->priv_data)
            goto err_out;
        s->fetch_timestamp=1;
        s->pict_type = AV_PICTURE_TYPE_I;
        if (parser->parser_init) {
            ret = parser->parser_init(s);
            if (ret != 0)
                goto err_out;
        }
        s->key_frame            = -1;
    #if FF_API_CONVERGENCE_DURATION
    FF_DISABLE_DEPRECATION_WARNINGS
        s->convergence_duration = 0;
    FF_ENABLE_DEPRECATION_WARNINGS
    #endif
        s->dts_sync_point       = INT_MIN;
        s->dts_ref_dts_delta    = INT_MIN;
        s->pts_dts_delta        = INT_MIN;
        s->format               = -1;
    
        return s;
    
    err_out:
        if (s)
            av_freep(&s->priv_data);
        av_free(s);
        return NULL;
    }
    
    // 源码片段 ffmpeg-4.3.2/libavcodec/parsers.c
    const AVCodecParser *av_parser_iterate(void **opaque)
    {
        uintptr_t i = (uintptr_t)*opaque;
        const AVCodecParser *p = parser_list[i];
    
        if (p)
            *opaque = (void*)(i + 1);
    
        return p;
    }
    
    // 源码片段 ffmpeg-4.3.2/libavcodec/h264_parser.c
    AVCodecParser ff_h264_parser = {
        .codec_ids      = { AV_CODEC_ID_H264 },
        .priv_data_size = sizeof(H264ParseContext),
        .parser_init    = init,
        .parser_parse   = h264_parse,
        .parser_close   = h264_close,
        .split          = h264_split,
    };
    

    源码中的第一步就是通过ID查找parser,此处传入的codec->id就是AV_CODEC_ID_H264。函数av_parser_iterateparser迭代器,其内部是在parser_list数组中查找parserparser_list在源码文件ffmpeg-4.3.2/libavcodec/h264_parser.c中)。最终找到的H.264解析器是ff_h264_parser

    3、创建解析器上下文

    ctx = avcodec_alloc_context3(codec);
    if (!ctx) {
        qDebug() << "avcodec_alloc_context3 error";
        goto end;
    }
    

    4、创建AVPacket

    pkt = av_packet_alloc();
    if (!pkt) {
        qDebug() << "av_packet_alloc error";
        goto end;
    }
    

    5、创建AVFrame

    frame = av_frame_alloc();
    if (!frame) {
        qDebug() << "av_frame_alloc error";
        goto end;
    }
    

    6、打开解码器

    ret = avcodec_open2(ctx, codec, nullptr);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "avcodec_open2 error" << errbuf;
        goto end;
    }
    

    7、打开文件

    if (!inFile.open(QFile::ReadOnly)) {
        qDebug() << "file open error:" << inFilename;
        goto end;
    }
    if (!outFile.open(QFile::WriteOnly)) {
        qDebug() << "file open error:" << out.filename;
        goto end;
    }
    

    8、读取文件数据 & 解析数据

    // 读取数据
    while ((inLen = inFile.read(inDataArray,IN_DATA_SIZE)) >0) {
        // 让inData指向数组的首元素
        inData = inDataArray;
    
        // 只要输入缓冲区中还有等待进行解码的数据
        while (inLen > 0) {
            // 经过解析器上下文处理
            ret = av_parser_parse2(parserCtx, ctx,
                                   &pkt->data, &pkt->size,
                                   (uint8_t *) inData, inLen,
                                   AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
            if (ret < 0) {
                ERROR_BUF(ret);
                qDebug() << "av_parser_parse2 error" << errbuf;
                goto end;
            }
    
            // 跳过已经解析过的数据
            inData += ret;
            // 减去已经解析过的数据大小
            inLen -= ret;
    
            // 解码
            if (pkt->size > 0 && decode(ctx, pkt, frame, outFile) < 0) {
                goto end;
            }
        }
    }
    

    关于av_parser_parse2函数可以参考 ffmpeg的av_parser_parse2( )

    9、解码

    static int decode(AVCodecContext *ctx,AVPacket *pkt,AVFrame *frame,QFile &outFile){
        // 发送压缩数据到解码器
        int ret = avcodec_send_packet(ctx,pkt);
        if (ret < 0) {
            ERROR_BUF(ret);
            qDebug() << "avcodec_send_packet error" << errbuf;
            return ret;
        }
    
        while (true) {
            // 获取解码后的数据
            ret = avcodec_receive_frame(ctx, frame);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                return 0;
            } else if (ret < 0) {
                ERROR_BUF(ret);
                qDebug() << "avcodec_receive_frame error" << errbuf;
                return ret;
            }
    
            // 将解码后的数据写入文件
            int imgSize = av_image_get_buffer_size(ctx->pix_fmt,ctx->width,ctx->height,1);// 解码后一帧图片大小
            outFile.write((char *)frame->data[0],imgSize);
        }
    }
    

    这里我们根据yuv420p的平面格式YUV紧挨着的规则来写数据,从frame->data[0]开始写,写一帧的大小imgSize

    10、运行代码

    然后运行代码生成YUV文件后,可以发现和通过FFmpeg命令行解码生成的YUV文件大小进行比较,发现通过代码解码生成的YUV像素数据有丢失:


    那么这个代码生成的YUV文件可以播放吗?我们通过ffplay命令播放,可以发现可以播放,但是显示的不正常。

    ffplay -video_size 640x480 -pix_fmt yuv420p out.yuv
    

    分析

    通过添加log来分析,在解码时添加

    static int decode(AVCodecContext *ctx,AVPacket *pkt,AVFrame *frame,QFile &outFile){
        ......
    
        while (true) {
            ......
    
            qDebug()<<"解码出第"<< ++frameIdx << "帧";
            qDebug() << frame->data[0] << frame->data[1]<< frame->data[2];// 0x96faa80 0x9746000 0x9758e00
            /**
              * frame->data[0] 0x96faa80
              * frame->data[1] 0x9746000
              * frame->data[1] 0x9758e00
              *
              * frame->data[1] - frame->data[0] = 308608 = Y平面大小
              * frame->data[2] - frame->data[1] = 77312 =U平面大小
              *
              * Y平面大小 = 640 * 480 *1 = 307200
              * U平面大小 = 640 * 480 *0.25 = 76800
              * V平面大小 = 640 * 480 *0.25 = 76800
              */
    
            // 将解码后的数据写入文件
            int imgSize = av_image_get_buffer_size(ctx->pix_fmt,ctx->width,ctx->height,1);
            qDebug()<<"每一帧大小:"<<imgSize;
            outFile.write((char *)frame->data[0],imgSize);
        }
    }
    

    通过上面的log打印信息分析来看,Y、U、V三个平面大小比实际的要大些,通过之前的yuv420p格式可以知道,它是平面格式的也就意味着yuv是紧挨着的,但是分析结果来看yuv的大小比实际的要大,感觉yuv并不是紧挨着的是有空隙。

    那么如何处理呢?其实我们可以将Y、U、V分别写入

    // 写入Y平面
    outFile.write((char *) frame->data[0],frame->linesize[0] * ctx->height);
    // 写入U平面
    outFile.write((char *) frame->data[1],frame->linesize[1] * ctx->height >> 1);// 除以2
    // 写入V平面
    outFile.write((char *) frame->data[2],frame->linesize[2] * ctx->height >> 1);// 除以2
    

    这样在运行生成yuv文件后,在使用ffplay命令播放,可以发现视频显示正常了,但是它的大小还是跟ffmpeg生成的不一样。我们可以计算它俩大小的差值,


    116121600 - 115660800 = 460800
    

    可以发现它们的差值正好是一帧的大小,说明代码生成的少了一帧数据没有写入到文件中。

    这时我们在解析数据里在加个打印

    qDebug() << "pkt->size:" << pkt->size << "ret:" << ret;
    

    通过打印可以发现解码结束后parser中还剩余2925字节的数据没有送入AVPacket中,需要让paeser把剩余数据继续送入到AVPacket中。

    解决办法就是当h264文件中数据全部读完后再调用一次av_parser_parse2函数,可以参考https://patchwork.ffmpeg.org/project/ffmpeg/patch/tencent_609A2E9F73AB634ED670392DD89A63400008@qq.com/的解决办法。

    将代码改造如下:

    // 读取数据
    do{
        // 从文件中读取h264数据
        inLen = inFile.read(inDataArray, IN_DATA_SIZE);
        // 设置是否到了文件尾部
        inEnd = !inLen;
    
        // 让inData指向数组的首元素
        inData = inDataArray;
        // 只要输入缓冲区中还有等待进行解码的数据
        while (inLen > 0 || inEnd) {
            // 到了文件尾部(虽然没有读取任何数据,但也要调用av_parser_parse2,修复bug)
            // 经过解析器解析
           ret = av_parser_parse2(parserCtx, ctx,
                                  &pkt->data, &pkt->size,
                                  (uint8_t *) inData, inLen,
                                  AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
    
           if (ret < 0) {
               ERROR_BUF(ret);
               qDebug() << "av_parser_parse2 error" << errbuf;
               goto end;
           }
    
           // 跳过已经解析过的数据
           inData += ret;
           // 减去已经解析过的数据大小
           inLen -= ret;
    
           qDebug() << "pkt->size:" << pkt->size << "ret:" << ret;
           
           // 解码
           if (pkt->size > 0 && decode(ctx, pkt, frame, outFile) < 0) {
               goto end;
           }
    
           // 如果到了文件尾部
           if (inEnd) break;
        }
    }while (!inEnd);
    

    这个时候在运行代码,查看打印发现parser中剩余数据已全部刷出,并且这次和在ffmpeg生成的yuv文件大小完全一样:


    代码链接

    相关文章

      网友评论

          本文标题:27_H.264解码实战

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