07. 视频解码实战:把 H.264 码流变成 YUV

专栏导读:前面我们学习了解封装(从 MP4 提取数据包),现在到了最关键的一步——解码!本篇用最通俗的语言,带你从"压缩的 H.264 码流"到"可显示的 YUV 像素",彻底搞懂 FFmpeg 的 send/receive 解码循环。


🎬 开场:解码器是个"智能快递分拣机"

想象你在一个快递分拣中心工作:

输入: 一堆压缩的快递包裹(H.264 压缩包 AVPacket)
      - 包裹上贴着标签: DTS(处理顺序)和 PTS(送达顺序)
      
分拣机: 解码器(AVCodecContext)
      - 自动拆包、识别内容
      - 内部有个仓库(缓冲区),攒够材料才出货

输出: 一张张整理好的照片(YUV 像素帧 AVFrame)
      - 可以直接挂墙上展示(渲染到屏幕)

关键问题

  1. 包裹什么时候拆?→ 送进去(send)的时机
  2. 照片什么时候拿?→ 取出来(receive)的时机
  3. 顺序会乱吗?→ DTS 和 PTS 的区别

让我们一步步揭秘!


📦 什么是 H.264 压缩包(AVPacket)?

定义:从解封装器(Demuxer)读取的压缩数据,还没解码。

typedef struct AVPacket {
    uint8_t *data;         // 压缩数据(H.264/HEVC 码流)
    int size;              // 数据大小(字节)
    int64_t pts;           // 显示时间戳(Presentation Time Stamp)
    int64_t dts;           // 解码时间戳(Decode Time Stamp)
    int stream_index;      // 属于哪个流(0=视频, 1=音频)
    int flags;             // 标志位(是否为关键帧)
} AVPacket;

实际大小

1080p H.264 视频(30fps):
  I 帧 (关键帧):  50 KB ~ 150 KB  (完整画面)
  P 帧 (预测帧):  5 KB ~ 20 KB    (只存差异)
  B 帧 (双向帧):  2 KB ~ 10 KB    (参考前后帧)

平均: 
  每个 AVPacket ≈ 10-30 KB
  每秒 30 个包 ≈ 300 KB ~ 1 MB

请添加图片描述


🖼️ 什么是 YUV 帧(AVFrame)?

定义:解码器输出的原始像素数据,可以直接渲染。

typedef struct AVFrame {
    uint8_t *data[AV_NUM_DATA_POINTERS];  // 像素数据(YUV 三个平面)
    int linesize[AV_NUM_DATA_POINTERS];   // 每行字节数(可能有对齐)
    int width, height;                     // 分辨率
    int format;                            // 像素格式(YUV420P/NV12 等)
    int64_t pts;                           // 显示时间戳
    int key_frame;                         // 是否为关键帧
} AVFrame;

YUV420P 格式

Y 平面(亮度):   width × height 字节
U 平面(色度):   (width/2) × (height/2) 字节
V 平面(色度):   (width/2) × (height/2) 字节

总大小 = width × height × 1.5 字节

示例(1920×1080):
  Y: 1920 × 1080 = 2,073,600 字节
  U: 960 × 540 = 518,400 字节
  V: 960 × 540 = 518,400 字节
  总计: 3,110,400 字节 ≈ 3 MB

对比

压缩前(AVPacket): 10 KB ~ 30 KB
解压后(AVFrame):  3 MB(1080p)

压缩比: 100:1 ~ 300:1  🎉

📊 YUV420P 内存布局图

请添加图片描述


⚙️ 解码器(AVCodecContext)的工作原理

1. 解码器是个"状态机"

avcodec_open2()
avcodec_send_packet()
avcodec_receive_frame()
继续解码
send(nullptr)
所有帧输出完毕
未初始化
已打开
接收包
输出帧
冲刷中
已关闭

2. 内部缓冲区(Buffer)

解码器内部有个仓库:

输入仓库(发送缓冲):
  ┌─────────────────────────┐
  │ Packet 1 │ Packet 2 │... │  ← avcodec_send_packet() 放进来
  └─────────────────────────┘

参考帧缓存(Reference Frame Buffer):
  ┌─────────────────────────┐
  │ I 帧 │ P 帧 │ 用于预测  │  ← 解码 B 帧时需要参考
  └─────────────────────────┘

输出仓库(接收缓冲):
  ┌─────────────────────────┐
  │ Frame 1 │ Frame 2 │...   │  ← avcodec_receive_frame() 取出来
  └─────────────────────────┘

关键规律

  • 送包(send)可能立即返回,也可能返回 EAGAIN(仓库满了,先取帧)
  • 取帧(receive)可能立即返回,也可能返回 EAGAIN(还没准备好,继续送包)

🔄 核心循环:send_packet & receive_frame

标准模式(推荐)

成功
0 成功
EAGAIN
0 成功
EAGAIN
EOF
0
EOF
开始
读取 AVPacket
avcodec_send_packet
avcodec_receive_frame
处理 AVFrame
send nullptr 冲刷
receive_frame
处理剩余帧
解码完成

代码模板(伪代码)

// 打开解码器
AVCodecContext *ctx = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(ctx, stream->codecpar);
avcodec_open2(ctx, codec, nullptr);

AVPacket *packet = av_packet_alloc();
AVFrame *frame = av_frame_alloc();

// 主循环
while (av_read_frame(fmt_ctx, packet) >= 0) {
    if (packet->stream_index != video_stream_index) {
        av_packet_unref(packet);
        continue;
    }

    // 1️⃣ 送包
    int ret = avcodec_send_packet(ctx, packet);
    if (ret == AVERROR(EAGAIN)) {
        // 解码器缓冲满了,先取帧
    } else if (ret < 0) {
        // 错误处理
        break;
    }

    // 2️⃣ 取帧(循环)
    while (true) {
        ret = avcodec_receive_frame(ctx, frame);
        if (ret == 0) {
            // ✅ 成功取到一帧
            process_frame(frame);  // 处理(渲染/保存)
            av_frame_unref(frame);
        } else if (ret == AVERROR(EAGAIN)) {
            // 需要更多包,退出内层循环
            break;
        } else if (ret == AVERROR_EOF) {
            // 解码器已冲刷完毕
            goto end;
        } else {
            // 错误
            break;
        }
    }

    av_packet_unref(packet);
}

// 3️⃣ 冲刷解码器(送空包)
avcodec_send_packet(ctx, nullptr);
while (avcodec_receive_frame(ctx, frame) == 0) {
    process_frame(frame);
    av_frame_unref(frame);
}

end:
av_frame_free(&frame);
av_packet_free(&packet);
avcodec_free_context(&ctx);

⏱️ 时间戳详解:DTS 和 PTS 的区别

场景 1:只有 I 帧和 P 帧(无 B 帧)

解码顺序 = 显示顺序
DTS = PTS

时间轴:
  I₀ → P₁ → P₂ → P₃
  ↓    ↓    ↓    ↓
  显示顺序也是 I₀ P₁ P₂ P₃

场景 2:有 B 帧(需要重排)

解码顺序(DTS):  I₀  P₃  B₁  B₂  P₆  B₄  B₅
显示顺序(PTS):  I₀  B₁  B₂  P₃  B₄  B₅  P₆

解释:
  - B₁ 需要参考 I₀ 和 P₃,所以必须先解码 P₃
  - 解码器内部会缓存 I₀ 和 P₃,然后才能解码 B₁ 和 B₂
Demuxer Decoder Renderer Packet I₀ (DTS=0, PTS=0) Frame I₀ (PTS=0) Packet P₃ (DTS=3, PTS=3) 缓存 P₃,等待 B 帧 Packet B₁ (DTS=1, PTS=1) Frame B₁ (PTS=1) Packet B₂ (DTS=2, PTS=2) Frame B₂ (PTS=2) Frame P₃ (PTS=3) Demuxer Decoder Renderer

关键规律

对于 I 帧和 P 帧:
  DTS = PTS

对于 B 帧:
  DTS > PTS  (先解码,后显示)

播放器同步:
  永远以 PTS 为准!

请添加图片描述


🛠️ 实战 1:解码第一帧并保存为 YUV 文件

目标:读取 MP4 文件,解码第一帧视频,保存为原始 YUV 文件。

完整代码(minimal_decode.cpp)

// minimal_decode.cpp
// 编译: g++ minimal_decode.cpp -o minimal_decode $(pkg-config --cflags --libs libavformat libavcodec libavutil)
// 运行: ./minimal_decode input.mp4 output.yuv

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

int main(int argc, char** argv) {
    if (argc < 3) {
        printf("Usage: %s <input.mp4> <output.yuv>\n", argv[0]);
        return 1;
    }

    const char* input_file = argv[1];
    const char* output_file = argv[2];

    // ========================================
    // 步骤 1:打开输入文件(解封装)
    // ========================================
    AVFormatContext* fmt_ctx = nullptr;
    if (avformat_open_input(&fmt_ctx, input_file, nullptr, nullptr) < 0) {
        printf("❌ Failed to open input file\n");
        return 1;
    }

    if (avformat_find_stream_info(fmt_ctx, nullptr) < 0) {
        printf("❌ Failed to find stream info\n");
        return 1;
    }

    // ========================================
    // 步骤 2:查找视频流
    // ========================================
    int video_stream_idx = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
    if (video_stream_idx < 0) {
        printf("❌ No video stream found\n");
        return 1;
    }

    AVStream* video_stream = fmt_ctx->streams[video_stream_idx];
    printf("✅ Found video stream: %dx%d, codec=%d\n", 
           video_stream->codecpar->width,
           video_stream->codecpar->height,
           video_stream->codecpar->codec_id);

    // ========================================
    // 步骤 3:打开解码器
    // ========================================
    const AVCodec* codec = avcodec_find_decoder(video_stream->codecpar->codec_id);
    if (!codec) {
        printf("❌ Codec not found\n");
        return 1;
    }

    AVCodecContext* codec_ctx = avcodec_alloc_context3(codec);
    if (!codec_ctx) {
        printf("❌ Failed to allocate codec context\n");
        return 1;
    }

    if (avcodec_parameters_to_context(codec_ctx, video_stream->codecpar) < 0) {
        printf("❌ Failed to copy codec parameters\n");
        return 1;
    }

    if (avcodec_open2(codec_ctx, codec, nullptr) < 0) {
        printf("❌ Failed to open codec\n");
        return 1;
    }

    printf("✅ Decoder opened: %s\n", codec->name);

    // ========================================
    // 步骤 4:分配 Packet 和 Frame
    // ========================================
    AVPacket* packet = av_packet_alloc();
    AVFrame* frame = av_frame_alloc();
    FILE* output_fp = fopen(output_file, "wb");

    if (!packet || !frame || !output_fp) {
        printf("❌ Allocation failed\n");
        return 1;
    }

    // ========================================
    // 步骤 5:解码循环(只解第一帧)
    // ========================================
    bool frame_decoded = false;

    while (av_read_frame(fmt_ctx, packet) >= 0 && !frame_decoded) {
        // 过滤非视频包
        if (packet->stream_index != video_stream_idx) {
            av_packet_unref(packet);
            continue;
        }

        // 发送包到解码器
        int ret = avcodec_send_packet(codec_ctx, packet);
        if (ret < 0) {
            printf("❌ Error sending packet to decoder\n");
            break;
        }

        // 接收解码后的帧
        while (ret >= 0) {
            ret = avcodec_receive_frame(codec_ctx, frame);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                break;  // 需要更多包或已结束
            } else if (ret < 0) {
                printf("❌ Error receiving frame from decoder\n");
                break;
            }

            // ✅ 成功解码一帧!
            printf("✅ Decoded frame: %dx%d, format=%d, pts=%ld\n",
                   frame->width, frame->height, frame->format, frame->pts);

            // 保存 YUV420P 数据(假设格式是 YUV420P)
            if (frame->format == AV_PIX_FMT_YUV420P) {
                // 写 Y 平面
                for (int y = 0; y < frame->height; y++) {
                    fwrite(frame->data[0] + y * frame->linesize[0], 1, frame->width, output_fp);
                }
                // 写 U 平面
                for (int y = 0; y < frame->height / 2; y++) {
                    fwrite(frame->data[1] + y * frame->linesize[1], 1, frame->width / 2, output_fp);
                }
                // 写 V 平面
                for (int y = 0; y < frame->height / 2; y++) {
                    fwrite(frame->data[2] + y * frame->linesize[2], 1, frame->width / 2, output_fp);
                }
                printf("✅ YUV data written to %s\n", output_file);
            } else {
                printf("⚠️  Pixel format is not YUV420P, got format=%d\n", frame->format);
            }

            frame_decoded = true;
            break;  // 只解第一帧
        }

        av_packet_unref(packet);
    }

    // ========================================
    // 步骤 6:清理资源
    // ========================================
    fclose(output_fp);
    av_frame_free(&frame);
    av_packet_free(&packet);
    avcodec_free_context(&codec_ctx);
    avformat_close_input(&fmt_ctx);

    if (frame_decoded) {
        printf("🎉 Success! View with: ffplay -f rawvideo -pixel_format yuv420p -video_size %dx%d %s\n",
               video_stream->codecpar->width,
               video_stream->codecpar->height,
               output_file);
    }

    return 0;
}

编译与运行

# Linux/macOS/WSL
g++ minimal_decode.cpp -o minimal_decode \
    $(pkg-config --cflags --libs libavformat libavcodec libavutil)

# 运行
./minimal_decode input.mp4 output.yuv

# 预览 YUV 文件
ffplay -f rawvideo -pixel_format yuv420p -video_size 1920x1080 output.yuv

输出示例

✅ Found video stream: 1920x1080, codec=27
✅ Decoder opened: h264
✅ Decoded frame: 1920x1080, format=0, pts=0
✅ YUV data written to output.yuv
🎉 Success! View with: ffplay -f rawvideo -pixel_format yuv420p -video_size 1920x1080 output.yuv

🔍 关键 API 详解

1. avcodec_send_packet()

int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);

作用:向解码器发送压缩数据包。

返回值

  • 0:成功
  • AVERROR(EAGAIN):解码器输入缓冲已满,需要先调用 receive_frame 取出一些帧
  • AVERROR_EOF:解码器已进入冲刷模式,不再接受输入
  • 其他负值:错误

特殊用法

// 冲刷解码器(Flush)
avcodec_send_packet(ctx, nullptr);  // 发送空包

2. avcodec_receive_frame()

int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);

作用:从解码器接收解码后的帧。

返回值

  • 0:成功获取一帧
  • AVERROR(EAGAIN):当前没有可用帧,需要先调用 send_packet 发送更多数据
  • AVERROR_EOF:解码器已冲刷完毕,没有更多帧
  • 其他负值:错误

注意事项

// ❌ 错误用法:只调用一次 receive
avcodec_send_packet(ctx, packet);
avcodec_receive_frame(ctx, frame);  // 可能会漏掉多帧输出

// ✅ 正确用法:循环调用 receive
avcodec_send_packet(ctx, packet);
while (avcodec_receive_frame(ctx, frame) == 0) {
    // 处理帧
}

3. av_frame_unref()

void av_frame_unref(AVFrame *frame);

作用:释放帧的引用计数,但不释放 AVFrame 结构体本身。

为什么需要

AVFrame *frame = av_frame_alloc();  // 分配结构体

while (decode) {
    avcodec_receive_frame(ctx, frame);  // 填充数据
    process(frame);
    av_frame_unref(frame);  // ⭐ 释放数据,但 frame 指针仍然可用
}

av_frame_free(&frame);  // 最后释放结构体

🧠 思考题 1:为什么要循环调用 receive_frame?

点击查看答案

原因:一个 AVPacket 可能对应多个 AVFrame

场景 1:B 帧重排

送入: Packet P₃
输出: Frame B₁, Frame B₂, Frame P₃  (3 个帧!)

解释:
  解码器内部缓存了 I₀ 和 P₃
  现在可以同时输出 B₁, B₂, P₃

场景 2:解码延迟

送入: Packet 1, Packet 2, Packet 3
输出: (无)

送入: Packet 4
输出: Frame 1, Frame 2  (一次输出多帧)

正确做法

avcodec_send_packet(ctx, packet);

// 循环取帧,直到返回 EAGAIN
while (avcodec_receive_frame(ctx, frame) == 0) {
    printf("Got frame %ld\n", frame->pts);
}

🛠️ 实战 2:解码所有帧并统计(完整版)

目标:解码整个视频文件,统计总帧数、总耗时、平均码率、I/P/B 帧分布等信息。

完整代码(decode_stats.cpp)

// decode_stats.cpp
// 编译: g++ decode_stats.cpp -o decode_stats $(pkg-config --cflags --libs libavformat libavcodec libavutil)
// 运行: ./decode_stats input.mp4

extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/time.h>
}
#include <cstdio>
#include <chrono>

int main(int argc, char** argv) {
    if (argc < 2) {
        printf("Usage: %s <input.mp4>\n", argv[0]);
        return 1;
    }

    const char* input_file = argv[1];

    // ========================================
    // 步骤 1:打开输入文件
    // ========================================
    AVFormatContext* fmt_ctx = nullptr;
    if (avformat_open_input(&fmt_ctx, input_file, nullptr, nullptr) < 0) {
        printf("❌ Failed to open input file\n");
        return 1;
    }

    if (avformat_find_stream_info(fmt_ctx, nullptr) < 0) {
        printf("❌ Failed to find stream info\n");
        return 1;
    }

    // ========================================
    // 步骤 2:查找视频流
    // ========================================
    int video_stream_idx = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
    if (video_stream_idx < 0) {
        printf("❌ No video stream found\n");
        return 1;
    }

    AVStream* video_stream = fmt_ctx->streams[video_stream_idx];
    
    // 计算总时长(秒)
    double duration_sec = video_stream->duration * av_q2d(video_stream->time_base);
    if (duration_sec <= 0) {
        duration_sec = fmt_ctx->duration / (double)AV_TIME_BASE;
    }

    printf("📹 Video Info:\n");
    printf("   Resolution: %dx%d\n", video_stream->codecpar->width, video_stream->codecpar->height);
    printf("   Codec: %s (ID=%d)\n", avcodec_get_name(video_stream->codecpar->codec_id), video_stream->codecpar->codec_id);
    printf("   Duration: %.2f seconds\n", duration_sec);
    printf("   Bitrate: %ld kbps\n", video_stream->codecpar->bit_rate / 1000);
    printf("\n");

    // ========================================
    // 步骤 3:打开解码器
    // ========================================
    const AVCodec* codec = avcodec_find_decoder(video_stream->codecpar->codec_id);
    if (!codec) {
        printf("❌ Codec not found\n");
        return 1;
    }

    AVCodecContext* codec_ctx = avcodec_alloc_context3(codec);
    if (!codec_ctx) {
        printf("❌ Failed to allocate codec context\n");
        return 1;
    }

    if (avcodec_parameters_to_context(codec_ctx, video_stream->codecpar) < 0) {
        printf("❌ Failed to copy codec parameters\n");
        return 1;
    }

    // 🚀 性能优化:启用多线程解码
    codec_ctx->thread_count = 4;  // 使用 4 线程
    codec_ctx->thread_type = FF_THREAD_FRAME;  // 帧级并行

    if (avcodec_open2(codec_ctx, codec, nullptr) < 0) {
        printf("❌ Failed to open codec\n");
        return 1;
    }

    printf("✅ Decoder opened: %s (threads=%d)\n\n", codec->name, codec_ctx->thread_count);

    // ========================================
    // 步骤 4:统计变量初始化
    // ========================================
    AVPacket* packet = av_packet_alloc();
    AVFrame* frame = av_frame_alloc();

    int total_frames = 0;
    int i_frames = 0, p_frames = 0, b_frames = 0, other_frames = 0;
    int64_t total_packet_size = 0;
    
    auto start_time = std::chrono::high_resolution_clock::now();

    // ========================================
    // 步骤 5:解码主循环
    // ========================================
    printf("🎬 Decoding...\n");

    while (av_read_frame(fmt_ctx, packet) >= 0) {
        if (packet->stream_index != video_stream_idx) {
            av_packet_unref(packet);
            continue;
        }

        total_packet_size += packet->size;

        // 发送包到解码器
        int ret = avcodec_send_packet(codec_ctx, packet);
        if (ret < 0) {
            printf("❌ Error sending packet to decoder\n");
            break;
        }

        // 接收解码后的帧(循环)
        while (ret >= 0) {
            ret = avcodec_receive_frame(codec_ctx, frame);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                break;
            } else if (ret < 0) {
                printf("❌ Error receiving frame from decoder\n");
                break;
            }

            // 统计帧类型
            total_frames++;
            if (frame->key_frame) {
                i_frames++;
            } else if (frame->pict_type == AV_PICTURE_TYPE_P) {
                p_frames++;
            } else if (frame->pict_type == AV_PICTURE_TYPE_B) {
                b_frames++;
            } else {
                other_frames++;
            }

            // 每 100 帧打印一次进度
            if (total_frames % 100 == 0) {
                printf("   Decoded %d frames (I=%d, P=%d, B=%d)...\r", 
                       total_frames, i_frames, p_frames, b_frames);
                fflush(stdout);
            }

            av_frame_unref(frame);
        }

        av_packet_unref(packet);
    }

    // ========================================
    // 步骤 6:冲刷解码器
    // ========================================
    printf("\n🔄 Flushing decoder...\n");
    avcodec_send_packet(codec_ctx, nullptr);  // 发送空包

    while (avcodec_receive_frame(codec_ctx, frame) == 0) {
        total_frames++;
        if (frame->key_frame) {
            i_frames++;
        } else if (frame->pict_type == AV_PICTURE_TYPE_P) {
            p_frames++;
        } else if (frame->pict_type == AV_PICTURE_TYPE_B) {
            b_frames++;
        } else {
            other_frames++;
        }
        av_frame_unref(frame);
    }

    auto end_time = std::chrono::high_resolution_clock::now();
    auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);

    // ========================================
    // 步骤 7:输出统计结果
    // ========================================
    printf("\n");
    printf("📊 Decoding Statistics:\n");
    printf("   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
    printf("   Total Frames:     %d\n", total_frames);
    printf("   I-Frames (关键帧): %d (%.1f%%)\n", i_frames, i_frames * 100.0 / total_frames);
    printf("   P-Frames (预测帧): %d (%.1f%%)\n", p_frames, p_frames * 100.0 / total_frames);
    printf("   B-Frames (双向帧): %d (%.1f%%)\n", b_frames, b_frames * 100.0 / total_frames);
    if (other_frames > 0) {
        printf("   Other Frames:     %d\n", other_frames);
    }
    printf("   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
    printf("   Total Decode Time: %ld ms\n", elapsed.count());
    printf("   Average FPS:      %.2f\n", total_frames * 1000.0 / elapsed.count());
    printf("   Time per Frame:   %.2f ms\n", (double)elapsed.count() / total_frames);
    printf("   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
    printf("   Total Packet Size: %.2f MB\n", total_packet_size / (1024.0 * 1024.0));
    printf("   Average Bitrate:  %.2f Mbps\n", (total_packet_size * 8.0) / (duration_sec * 1000000.0));
    printf("   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");

    // ========================================
    // 步骤 8:清理资源
    // ========================================
    av_frame_free(&frame);
    av_packet_free(&packet);
    avcodec_free_context(&codec_ctx);
    avformat_close_input(&fmt_ctx);

    printf("\n✅ Done!\n");
    return 0;
}

运行示例

$ ./decode_stats big_buck_bunny_1080p.mp4

📹 Video Info:
   Resolution: 1920x1080
   Codec: h264 (ID=27)
   Duration: 596.46 seconds
   Bitrate: 3481 kbps

✅ Decoder opened: h264 (threads=4)

🎬 Decoding...
   Decoded 14315 frames (I=60, P=4761, B=9494)...
🔄 Flushing decoder...

📊 Decoding Statistics:
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   Total Frames:     14315
   I-Frames (关键帧): 60 (0.4%)
   P-Frames (预测帧): 4761 (33.3%)
   B-Frames (双向帧): 9494 (66.3%)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   Total Decode Time: 8234 ms
   Average FPS:      1738.56
   Time per Frame:   0.58 ms
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   Total Packet Size: 251.23 MB
   Average Bitrate:  3.37 Mbps
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

✅ Done!

关键发现

从统计结果可以看出:

  1. B 帧占比高:66.3%,说明编码器使用了高效的双向预测
  2. I 帧稀疏:只有 0.4%,GOP(Group of Pictures)很大,有利于降低码率
  3. 解码速度快:1738 fps,远高于实时播放(24 fps),说明硬件性能充足

常见错误排查表

错误码 含义 可能原因 解决方法
EAGAIN 需要更多数据 解码器缓冲区状态 正常,继续循环
EOF 文件结束 已读取完所有数据 正常,结束解码
ENOMEM 内存不足 系统内存耗尽 检查内存泄漏
EINVAL 参数错误 API 调用顺序错误 检查初始化流程
INVALIDDATA 数据损坏 文件损坏或编码错误 跳过当前包
DECODER_NOT_FOUND 解码器未找到 缺少编解码器支持 安装对应解码器

⚡ 性能优化技巧

1. 多线程解码

AVCodecContext* codec_ctx = avcodec_alloc_context3(codec);

// 🚀 启用多线程
codec_ctx->thread_count = 0;  // 0 = 自动检测 CPU 核心数
codec_ctx->thread_type = FF_THREAD_FRAME;  // 帧级并行(推荐)
// codec_ctx->thread_type = FF_THREAD_SLICE;  // 切片级并行(适合高分辨率)

avcodec_open2(codec_ctx, codec, nullptr);

🧠 思考题 2:为什么要发送空包(nullptr)冲刷解码器?

点击查看答案

原因:解码器内部有缓冲区,可能还有未输出的帧!

场景 1:B 帧延迟输出

输入: I₀ P₃ B₁ B₂ [EOF]
      ↓
解码器内部缓冲: B₁ B₂ 等待输出

如果不冲刷:
  B₁ 和 B₂ 永远不会输出 ❌
  视频末尾丢失几帧

冲刷后:
  send(nullptr) → 解码器进入"排空"模式
  receive() → 输出 B₁
  receive() → 输出 B₂
  receive() → 返回 EOF

场景 2:多线程解码

解码器内部有 4 个线程:
  线程 1: 正在解码帧 100
  线程 2: 正在解码帧 101
  线程 3: 正在解码帧 102
  线程 4: 正在解码帧 103

如果直接关闭:
  4 个线程的结果都丢失 ❌

冲刷后:
  等待所有线程完成 ✅
  输出所有已解码的帧

正确流程

// 1. 读取所有包
while (av_read_frame(fmt_ctx, packet) >= 0) {
    avcodec_send_packet(ctx, packet);
    while (avcodec_receive_frame(ctx, frame) == 0) {
        process(frame);
    }
}

// 2. ⭐ 冲刷解码器
avcodec_send_packet(ctx, nullptr);  // 发送空包
while (avcodec_receive_frame(ctx, frame) == 0) {
    process(frame);  // 处理剩余帧
}

// 3. 清理
avcodec_free_context(&ctx);

🧠 思考题 3:如何判断一个 AVPacket 是否为关键帧?

点击查看答案

方法 1:检查 flags 字段

if (packet->flags & AV_PKT_FLAG_KEY) {
    printf("This is a key frame (I-frame)\n");
}

方法 2:解码后检查 AVFrame

avcodec_receive_frame(ctx, frame);
if (frame->key_frame) {
    printf("This is a key frame\n");
}
// 或者检查帧类型
if (frame->pict_type == AV_PICTURE_TYPE_I) {
    printf("This is an I-frame\n");
}

区别

AVPacket->flags:
  - 解码前就知道
  - 适合 Seek 操作(跳转到关键帧)

AVFrame->key_frame:
  - 解码后才知道
  - 更准确(解码器确认)

实际应用

// Seek 到最近的关键帧
int64_t target_pts = 5000;  // 5 秒
av_seek_frame(fmt_ctx, video_stream_idx, target_pts, AVSEEK_FLAG_BACKWARD);

// 跳过非关键帧(快速播放)
while (av_read_frame(fmt_ctx, packet) >= 0) {
    if (!(packet->flags & AV_PKT_FLAG_KEY)) {
        av_packet_unref(packet);
        continue;  // 跳过 P/B 帧
    }
    decode_and_display(packet);
}
Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐