使用FFmpeg實(shí)現(xiàn)H.264解碼

前面通過 H.264 編碼將 YUV 像素?cái)?shù)據(jù)壓縮生成了一個(gè) h264 文件。那么想要播放 h264 文件,就需要解壓縮取出每一幀的具體像素?cái)?shù)據(jù)進(jìn)行播放。本文的內(nèi)容主要是解碼裸流,即從本地讀取 h264 文件,解碼成 YUV 像素?cái)?shù)據(jù)的過程。

一、使用 FFmpeg 命令行進(jìn)行 H.264 解碼:
$ ffmpeg -c:v h264 -i in.h264 out.yuv

解碼時(shí) -c:v h264 是輸入?yún)?shù)。查看本地的解碼器:

$ ffmpeg -decoders | grep 264
 VFS..D h264                 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10
二、使用 FFmpeg 編程實(shí)現(xiàn) H.264 編碼

首先需要導(dǎo)入用到的 FFmpeg 庫(kù) libavcodeclibavutil。和前面 H.264 編碼用到的庫(kù)是一樣的,并且 H.264 解碼流程和 AAC 解碼流程也是類似的。

H.264 解碼流程

1、獲取解碼器

在我本地默認(rèn)的解碼器就是 h264,通過 ID 或者名稱獲取到的 H.264 解碼器都是 h264。

// 使用 ID 獲取編碼器:
codec = avcodec_find_decoder(AV_CODEC_ID_H264);
// 或者使用名稱獲取編碼器:
codec = avcodec_find_decoder_by_name("h264");

2、初始化解析器上下文

通過 ID 創(chuàng)建 H.264 解析器上下文:

parserCtx = av_parser_init(codec->id);

查看函數(shù) 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/parsers.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。函數(shù) av_parser_iterate 是 parser 迭代器,其內(nèi)部是在 parser_list 數(shù)組中查找 parser(parser_list 在源碼文件 ffmpeg-4.3.2/libavcodec/parser_list.c 中)。最終找到的 H.264 解析器是 ff_h264_parser。

3、創(chuàng)建解析器上下文

ctx = avcodec_alloc_context3(codec);

4、創(chuàng)建AVPacket

pkt = av_packet_alloc();

5、創(chuàng)建AVFrame

frame = av_frame_alloc();

6、打開解碼器

ret = avcodec_open2(ctx, codec, nullptr);

7、打開文件

inFile.open(QFile::ReadOnly)
outFile.open(QFile::WriteOnly)
inLen = inFile.read(inDataArray, IN_DATA_SIZE);

8、讀取文件數(shù)據(jù) & 解析數(shù)據(jù)

while ((inLen = inFile.read(inDataArray, IN_INBUF_SIZE)) > 0) {
    inData = inDataArray;
    while (inLen > 0) {
        // 解析器解析數(shù)據(jù)
        ret = av_parser_parse2(parserCtx, ctx, &pkt->data, &pkt->size, (uint8_t *) inData, inLen, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
        if (ret < 0) {
            ERRBUF(ret);
            qDebug() << "av_parser_parse2 error" << errbuf;
            goto end;
        }

        // 跳過已經(jīng)解析過的數(shù)據(jù)
        inData += ret;
        // 減去已經(jīng)解析過的數(shù)據(jù)大小
        inLen -= ret;

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

        // 解碼
        if (pkt->size > 0 && decode(ctx, pkt, frame, outFile) < 0) {
            goto end;
        }
    }
}

通過和在終端使用命令行解碼生成的 YUV 文件大小進(jìn)行比較,發(fā)現(xiàn)通過代碼解碼生成的 YUV 像素?cái)?shù)據(jù)有丟失:

$ ls -al
-rw-r--r--   1 mac  staff       110131200 Apr 12 14:22 out_640x480_yuv420p_code.yuv
-rw-r--r--   1 mac  staff       110592000 Apr 12 14:19 out_640x480_yuv420p_terminal.yuv

通過打印可以發(fā)現(xiàn)解碼結(jié)束后 parser 中還剩余 703 字節(jié)的數(shù)據(jù)沒有送入 AVPacket 中,需要讓 paeser把剩余數(shù)據(jù)“吐出來”:

pkt->size: 473 ret: 473
解碼完成第 237 幀
pkt->size: 0 ret: 703
解碼完成第 238 幀
解碼完成第 239 幀

解決辦法就是當(dāng) h264 文件中數(shù)據(jù)全部讀完后再調(diào)用一次 av_parser_parse2 函數(shù),將代碼改造如下:

// 是否讀到文件尾部
int inEnd = 0;

do {
    // 從文件中讀取h264數(shù)據(jù)
    inLen = inFile.read(inDataArray, IN_INBUF_SIZE);
    inData = inDataArray;
    inEnd = !inLen;
    while (inLen > 0 || inEnd) { // 到了文件尾部雖然沒有讀取到任何數(shù)據(jù),也要調(diào)用,最后要刷出解析器上下文中的數(shù)據(jù) 
        ret = av_parser_parse2(parserCtx, ctx, &pkt->data, &pkt->size, (const uint8_t *)inData, inLen, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
        if (ret < 0) {
            char errbuf[1024]; 
            av_strerror(ret, errbuf, sizeof (errbuf));
            qDebug() << "av_parser_parse2 error:" << errbuf;
            goto end;
        }

        // 跳過解析過的數(shù)據(jù)
        inData += ret;
        // 減去已解析過的數(shù)據(jù)大小
        inLen -= ret;

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

        // 解碼
        if (pkt->size > 0 && decode(ctx, pkt, frame, outFile) < 0) {
            goto end;
        }
        // 當(dāng)inEnd = 1時(shí)到了文件尾部
        if (inEnd) break;
    }
} while (!inEnd);

查看打印發(fā)現(xiàn) parser 中剩余數(shù)據(jù)已全部刷出,并且這次和在終端生成的 yuv 文件大小完全一樣:

inEnd: 0 pkt->size: 473 ret: 473
解碼完成第 237 幀
inEnd: 0 pkt->size: 0 ret: 703
inEnd: 1 pkt->size: 703 ret: 0
解碼完成第 238 幀
解碼完成第 239 幀
解碼完成第 240 幀

9、解碼

static int decode(AVCodecContext *ctx,
                  AVPacket *pkt,
                  AVFrame *frame,
                  QFile &outFile) {
    // 發(fā)送壓縮數(shù)據(jù)到解碼器
    int ret = avcodec_send_packet(ctx, pkt);
    if (ret < 0) {
        char errbuf[1024]; 
        av_strerror(ret, errbuf, sizeof (errbuf));
        qDebug() << "avcodec_send_packet error" << errbuf;
        return ret;
    }

    while (true) {
        // 獲取解碼后的數(shù)據(jù)
        ret = avcodec_receive_frame(ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else if (ret < 0) {
            char errbuf[1024]; 
            av_strerror(ret, errbuf, sizeof (errbuf));
            qDebug() << "avcodec_receive_frame error" << errbuf;
            return ret;
        }

        // 將解碼后的數(shù)據(jù)寫入文件
        int imgSize = av_image_get_buffer_size(ctx->pix_fmt, ctx->width, ctx->height, 1);
        outFile.write((char *) frame->data[0], imgSize);
    }
}

使用以上方式直接從 frame->data[0] 中讀取一幀大小寫入文件,你可能會(huì)發(fā)現(xiàn)播放解碼后的 YUV 像素?cái)?shù)據(jù)會(huì)有如下問題:

問題截圖

是因?yàn)?frame->data[0]frame->data[1] 以及 frame->data[1]frame->data[2] 之間是有 padding 的:

// 打印 frame->data:
qDebug() << frame->data[0] << frame->data[1] << frame->data[2];
// 輸出:
0x7fd554693000 0x7fd5546df000 0x7fd5546f2000

// 計(jì)算數(shù)據(jù)緩沖區(qū)各平面實(shí)際大?。?frame->data[1] - frame->data[0] = 0x7fd5546df000 - 0x7fd554693000 = 311296 字節(jié) = 實(shí)際 Y 平面大小
frame->data[2] - frame->data[1] = 0x7fd5546f2000 - 0x7fd5546df000 = 77824 字節(jié) = 實(shí)際 U 平面大小

// 各平面的期望大小:
Y 平面大小 = 640 * 480 * 1 = 307200 字節(jié)
U 平面大小 = (640 / 2) * (480 / 2) * 1 = 76800 字節(jié)
V 平面大小 = (640 / 2) * (480 / 2) * 1 = 76800 字節(jié)

可以使用下面方式將 YUV 像素?cái)?shù)據(jù)寫入文件,yuv420p 像素格式色度分量 U 和 V 是 1/2 垂直采樣,所以高度要除以 2:

outFile.write((const char *)frame->data[0], frame->linesize[0] * frame->height);
outFile.write((const char *)frame->data[1], frame->linesize[1] * frame->height >> 1);
outFile.write((const char *)frame->data[2], frame->linesize[2] * frame->height >> 1);

10、釋放資源

inFile.close();
outFile.close();
av_packet_free(&pkt);
av_frame_free(&frame);
av_parser_close(parserCtx);
avcodec_free_context(&ctx);

參考鏈接:https://patchwork.ffmpeg.org/project/ffmpeg/patch/tencent_609A2E9F73AB634ED670392DD89A63400008@qq.com/

完整示例代碼:

h264_decode.pro:

macx {
    INCLUDEPATH += /usr/local/ffmpeg/include
    LIBS += -L/usr/local/ffmpeg/lib \
        -lavcodec \
        -lavutil
}

ffmpegutils.h:

#ifndef FFMPEGUTILS_H
#define FFMPEGUTILS_H

extern "C" {
    #include <libavformat/avformat.h>
}

// 解碼后的YUV參數(shù)
typedef struct {
    const char *filename;
    int width;
    int height;
    AVPixelFormat pixFmt;
    int fps;
} VideoDecodeSpec;

class FFmpegUtils
{
public:
    FFmpegUtils();
    static void h264Decode(const char *inFilename, VideoDecodeSpec &out);
};

#endif // FFMPEGUTILS_H

ffmpegutils.cpp:

#include "ffmpegutils.h"

#include <QDebug>
#include <QFile>

extern "C" {
    #include <libavcodec/avcodec.h>
    #include <libavutil/avutil.h>
    #include <libavutil/imgutils.h>
}

#define ERRBUF(ret) \
    char errbuf[1024]; \
    av_strerror(ret, errbuf, sizeof (errbuf))

// 輸入緩沖區(qū)大小 官方示例程序建議大小
#define IN_INBUF_SIZE 4096

FFmpegUtils::FFmpegUtils()
{

}

static int decode(AVCodecContext *ctx, AVPacket *pkt, AVFrame *frame, QFile &outFile)
{
    int ret = 0;

    // 發(fā)送數(shù)據(jù)到解碼器
    ret = avcodec_send_packet(ctx, pkt);

    if (ret < 0) {
        ERRBUF(ret);
        qDebug() << "avcodec_send_packet error:" << errbuf;
        return ret;
    }

    while (true) {
        // 獲取解碼后的數(shù)據(jù)
        ret = avcodec_receive_frame(ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else if (ret < 0) {
            ERRBUF(ret);
            qDebug() << "avcodec_receive_frame error:" << errbuf;
            return ret;
        }

        // 將解碼后的數(shù)據(jù)寫入文件
        // 寫入Y平面數(shù)據(jù)
        outFile.write((const char *)frame->data[0], frame->linesize[0] * frame->height);
        // 寫入U(xiǎn)平面數(shù)據(jù)
        outFile.write((const char *)frame->data[1], frame->linesize[1] * frame->height >> 1);
        // 寫入V平面數(shù)據(jù)
        outFile.write((const char *)frame->data[2], frame->linesize[2] * frame->height >> 1);
    }
}

void FFmpegUtils::h264Decode(const char *inFilename, VideoDecodeSpec &out)
{
    // 返回值
    int ret = 0;

    // 輸入文件(h264文件)
    QFile inFile(inFilename);
    // 輸出文件(yuv文件)
    QFile outFile(out.filename);

    // 解碼器
    AVCodec *codec = nullptr;
    // 解碼上下文
    AVCodecContext *ctx = nullptr;
    // 解析器上下文
    AVCodecParserContext *parserCtx = nullptr;
    // 存放解碼前的h264數(shù)據(jù)
    AVPacket *pkt = nullptr;
    // 存放解碼后的yuv數(shù)據(jù)
    AVFrame *frame = nullptr;

    // 存放讀取的h264文件數(shù)據(jù)
    // 加上AV_INPUT_BUFFER_PADDING_SIZE是為了防止某些優(yōu)化過的reader一次性讀取過多導(dǎo)致越界(參考了FFmpeg示例代碼)
    char inDataArray[AUDIO_INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
    char *inData = nullptr;
    // 輸入數(shù)據(jù)緩沖區(qū)中剩余的待解碼的數(shù)據(jù)長(zhǎng)度
    int inLen = 0;
    // 是否讀取到了輸入文件尾部
    int inEnd = 0;

    // 獲取H264解碼器,也可以根據(jù)解碼器名稱獲取
    codec = avcodec_find_decoder(AV_CODEC_ID_H264);
    if (!codec) {
        qDebug() << "decoder h264 not found";
        return;
    }

    // 初始化解析器上下文
    parserCtx = av_parser_init(codec->id);
    if (!parserCtx) {
        qDebug() << "av_parser_init error";
        return;
    }

    // 創(chuàng)建解碼上下文
    ctx = avcodec_alloc_context3(codec);
    if (!ctx) {
        qDebug() << "avcodec_alloc_context3 error";
        goto end;
    }

    // 創(chuàng)建AVPacket
    pkt = av_packet_alloc();
    if (!pkt) {
        qDebug() << "av_packet_alloc error";
        goto end;
    }

    // 創(chuàng)建AVFrame
    frame = av_frame_alloc();
    if (!frame) {
        qDebug() << "av_frame_alloc error";
        goto end;
    }

    // 打開解碼器
    ret = avcodec_open2(ctx, codec, nullptr);
    if (ret < 0) {
        ERRBUF(ret);
        qDebug() << "open decoder error:" << errbuf;
        goto end;
    }

    // 打開h264文件
    if (!inFile.open(QFile::ReadOnly)) {
        qDebug() << "open file failure:" << inFilename;
        goto end;
    }

    // 打開yuv文件
    if (!outFile.open(QFile::WriteOnly)) {
        qDebug() << "open file failure:" << out.filename;
    }

    do {
        // 從文件中讀取h264數(shù)據(jù)
        inLen = inFile.read(inDataArray, AUDIO_INBUF_SIZE);
        // inData指向inDataArray首元素
        inData = inDataArray;
        // 設(shè)置是否到了文件尾部
        inEnd = !inLen;

        while (inLen > 0 || inEnd) { // 到了文件尾部,雖然沒有讀取任何數(shù)據(jù),但也要調(diào)用av_parser_parse2(修復(fù)bug)
            ret = av_parser_parse2(parserCtx, ctx, &pkt->data, &pkt->size, (const uint8_t *)inData, inLen, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
            if (ret < 0) {
                ERRBUF(ret);
                qDebug() << "av_parser_parse2 error:" << errbuf;
                goto end;
            }

            // 跳過解析過的數(shù)據(jù)
            inData += ret;
            // 減去已解析過的數(shù)據(jù)大小
            inLen -= ret;

            // 解碼
            if (pkt->size > 0 && decode(ctx, pkt, frame, outFile) < 0) {
                goto end;
            }

            // 當(dāng)inEnd = 1時(shí)到了文件尾部
            if (inEnd) break;
        }
    } while (!inEnd);

    // 刷出緩沖區(qū)中剩余數(shù)據(jù)

    // 方式一:
    decode(ctx, nullptr, frame, outFile);

    // 方式二:
    // pkt->data = nullptr;
    // pkt->size = 0;
    // decode(ctx, pkt, frame, outFile);

    // 輸出參數(shù)
    out.width = ctx->width;
    out.height = ctx->height;
    out.pixFmt = ctx->pix_fmt;
    // 用framerate.num獲取幀率,并不是time_base.den
    out.fps = ctx->framerate.num;

end:
    inFile.close();
    outFile.close();
    av_packet_free(&pkt);
    av_frame_free(&frame);
    av_parser_close(parserCtx);
    avcodec_free_context(&ctx);
}

方法調(diào)用:

#include "mainwindow.h"
#include "ui_mainwindow.h"

#include <QDebug>

#include <ffmpegutils.h>

extern "C" {
    #include <libavutil/imgutils.h>
}

#define IN_FILE "/Users/mac/Downloads/pic/in_640x480_yuv420p.h264"
#define OUT_FILE "/Users/mac/Downloads/pic/out_640x480_yuv420p_code.yuv"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_decodeH264Button_clicked()
{
    VideoDecodeSpec spec;
    spec.filename = OUT_FILE;
    FFmpegUtils::h264Decode(IN_FILE, spec);

    qDebug() << "寬度:" << spec.width;
    qDebug() << “高度:" << spec.height;
    qDebug() << “像素格式:" << av_get_pix_fmt_name(spec.pixFmt);
    qDebug() << “幀率:" << spec.fps;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容