如何把一个压缩的视频文件,解压成一张张原始图片-decode_video.c

目录

第一部分:核心音视频概念(小白必读)

[1. 编解码 (Codec)](#1. 编解码 (Codec))

[2. 容器 (Container) 与 流 (Stream)](#2. 容器 (Container) 与 流 (Stream))

[3. 两个核心对象:Packet 与 Frame (最重要!)](#3. 两个核心对象:Packet 与 Frame (最重要!))

[4. 解析器 (Parser)](#4. 解析器 (Parser))

[5. YUV 颜色空间](#5. YUV 颜色空间)

[6. I帧、P帧、B帧 (视频压缩的核心魔法)](#6. I帧、P帧、B帧 (视频压缩的核心魔法))

[7. 进阶:DTS/PTS 与 缓冲机制 (为什么不会乱?)](#7. 进阶:DTS/PTS 与 缓冲机制 (为什么不会乱?))

第二部分:代码深度解析

[1. 准备阶段:找工具 (Setup)](#1. 准备阶段:找工具 (Setup))

[2. 循环读取与切包 (Parsing Loop)](#2. 循环读取与切包 (Parsing Loop))

[3. 解码动作:发送与接收 (Send & Receive)](#3. 解码动作:发送与接收 (Send & Receive))

[4. 保存图像 (Processing)](#4. 保存图像 (Processing))

总结


第一部分:核心音视频概念(小白必读)

在看代码之前,你需要先建立 7 个核心概念的认知。想象你正在经营一家**"冷冻食品加工厂"**。

1. 编解码 (Codec)

  • 概念:视频原始数据极大(1秒钟的高清视频可能有几百兆)。为了存储和传输,必须压缩(编码),播放时再解压(解码)。

  • 比喻

    • 原始视频 = 新鲜蔬菜(体积大,易坏)。

    • 编码 = 脱水干燥,做成压缩蔬菜包(体积小,方便运输)。

    • 解码 = 泡水还原,变回新鲜蔬菜(恢复体积,用于食用/观看)。

  • 代码对应AVCodec (解码器)。

2. 容器 (Container) 与 流 (Stream)

  • 概念 :mp4, avi, mkv 只是容器(外壳),里面装着视频流、音频流、字幕流。

  • 比喻

    • 容器 = 一个纸箱子。

    • = 箱子里装的一袋袋东西(一袋是视频,一袋是音频)。

  • 代码对应 :本示例比较特殊,它直接读取的是 MPEG1 裸流(没有 MP4 这种外壳),所以代码里没有解封装(Demux)的步骤,直接就是处理流数据。

3. 两个核心对象:Packet 与 Frame (最重要!)

这是音视频开发中最容易混淆,也是最重要的概念。

对象 术语 状态 比喻 特点
AVPacket 数据包 压缩数据 压缩蔬菜块 体积小,电脑看不懂,必须解压。
AVFrame 原始数据 泡开的蔬菜 体积大 (YUV/RGB),包含了具体的像素点,屏幕能直接显示。
  • 流程文件 -> Packet (解压前) -> 解码器 -> Frame (解压后)

4. 解析器 (Parser)

  • 概念:当我们从文件里读数据时,读到的是一长串字节流(010101...)。电脑不知道哪里是一帧的开始,哪里是结束。

  • 作用 :Parser 就像一个断句符号,它从乱糟糟的数据流中,精准地切出一个个完整的 Packet

  • 代码对应av_parser_parse2

5. YUV 颜色空间

  • 概念:屏幕通常用 RGB(红绿蓝)显示,但视频通常用 YUV 存储。

    • Y:亮度(灰度图)。人眼对亮度最敏感。

    • U/V:色度(颜色)。

  • 现象 :示例代码最终保存的是 PGM 格式,这是一种灰度图格式。因为它只保存了 Y (亮度) 分量。

  • 代码对应frame->data[0] (存放 Y 数据)。

6. I帧、P帧、B帧 (视频压缩的核心魔法)

这是理解解码器行为逻辑的关键。为了极致压缩,视频不是每张图都完整存下来。

  • I帧 (Intra Frame)关键帧。一张完整的照片,不依赖别人。你可以直接看懂。

  • P帧 (Predicted Frame)前向预测帧。只存储跟前一帧"不一样"的地方(比如背景不动,只记走动的人)。省空间。

  • B帧 (Bi-directional Frame)双向预测帧 。最省空间!它参考前一帧后一帧

7. 进阶:DTS/PTS 与 缓冲机制 (为什么不会乱?)

这里解答"边吃边吐"是否会乱序的问题。

  • 流式处理:播放器不是把整个视频解完才播,而是**"喂几个 Packet,吐几张 Frame"**。

  • DTS (Decoding Time Stamp)喂食顺序。视频文件内部是按这个存的。为了解出 B 帧,必须先把后面被参考的 P 帧先存进去。

  • PTS (Presentation Time Stamp)显示顺序。最终吐出来给人看的顺序。

  • 防乱逻辑(候车室)

    • 目标显示顺序 (PTS):I(1) -> B(2) -> P(3)

    • 文件存放顺序 (DTS) :I(1) -> P(3) -> B(2) (因为 B(2) 需要参考 P(3),所以 P(3) 必须先进入解码器)

    • 过程 :解码器收到 P(3) 后,发现它是 P 帧且时间靠后,会把它锁在内部缓存(候车室),暂时不吐出来。等收到 B(2) 并解码完成后,再按正确顺序释放。

第二部分:代码深度解析

现在我们带着上面的概念,来看这 100 多行代码究竟在干什么。流程图如下:

初始化 -> 读取文件 -> Parser切包 -> 发送Packet给解码器 -> 接收Frame -> 保存图片

1. 准备阶段:找工具 (Setup)

复制代码
// 1. 找解码器:我要解 MPEG1 格式的视频
codec = avcodec_find_decoder(AV_CODEC_ID_MPEG1VIDEO);

// 2. 创建解析器 (Parser):为了从流里切出 Packet
parser = av_parser_init(codec->id);

// 3. 创建上下文 (Context):这是解码器的"工位",记录各种状态
c = avcodec_alloc_context3(codec);

// 4. 正式打开解码器:工位整理好,工具放好,准备开工
avcodec_open2(c, codec, NULL);

2. 循环读取与切包 (Parsing Loop)

这是代码中最难理解的 while 循环部分。

复制代码
// 从文件读一大块数据到 buffer (比如 4096 字节)
data_size = fread(inbuf, 1, INBUF_SIZE, f);

while (data_size > 0 || eof) {
    // 【核心动作】切包
    // parser 帮我们在乱序的 data 中找到一个完整的 Packet
    // ret 是 parser 吃掉的字节数
    ret = av_parser_parse2(parser, c, &pkt->data, &pkt->size,
                           data, data_size, ...);
    
    data += ret;      // 指针后移,处理剩下的数据
    data_size -= ret; // 剩余数据量减少

    // 如果切出来一个包 (pkt->size > 0),就拿去解码
    if (pkt->size)
        decode(c, frame, pkt, outfilename);
}
  • 白话解说 :就像吃一条很长的甘蔗。fread 砍下一截拿在手里,parser 负责把这一截甘蔗切成一口一口的小块(Packet)。切好一块,就吐出来拿去榨汁(Decode)。

3. 解码动作:发送与接收 (Send & Receive)

这是 FFmpeg 新版本标准解码模式:供需模式

复制代码
static void decode(...) {
    // 1. 【投喂】把压缩包 (Packet) 扔进解码器 (按 DTS 顺序)
    ret = avcodec_send_packet(dec_ctx, pkt);
    
    // 2. 【索取】尝试从解码器里拿出来原始帧 (Frame) (按 PTS 顺序)
    while (ret >= 0) {
        ret = avcodec_receive_frame(dec_ctx, frame);
        
        // 情况A: 解码器说"我还需要更多 Packet 才能拼出一张图",或者"没数据了"
        // 场景举例:你喂进去一个 P(3),解码器把它存入"候车室",此时没有输出,返回 EAGAIN
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            return;
            
        // 情况B: 拿到了一帧画面!(frame 里现在有数据了)
        // ... 保存图片 ...
    }
}
  • 为什么是 while 循环?

    • 因为视频编码很复杂。有时候你喂进去 1 个 Packet,解码器就能吐出 1 个 Frame。

    • B 帧场景 :你喂进去 Packet(B帧),解码器会把它先存起来(Buffer),它说"等会儿,我还要看后面的帧才能解这一帧",此时 receive_frame 返回 EAGAIN,它暂时不吐数据。

    • I/P 帧场景:等你后来喂进去一个 P 帧,解码器可能一下子把刚才存的 B 帧和现在的 P 帧都吐出来(或者按顺序吐)。

    • 所以原则是:一直喂,直到喂进去;一直取,直到取不出来。

4. 保存图像 (Processing)

复制代码
// frame->data[0] 存放的是 Y (亮度/灰度) 分量
// frame->linesize[0] 是这一行数据的内存宽度 (通常大于等于图像宽度,为了内存对齐)
pgm_save(frame->data[0], frame->linesize[0], frame->width, frame->height, buf);
  • 这里只保存了黑白画面。如果要保存彩色,需要把 data[1] (U) 和 data[2] (V) 也处理,或者把 YUV 转成 RGB。

总结

这份代码虽然短,但涵盖了音视频开发的万能公式

数据源 \\rightarrow \\text{Parser (切分)} \\rightarrow \\text{Packet (压缩包/DTS顺序)} \\rightarrow \\text{Decoder (缓冲/重排)} \\rightarrow \\text{Frame (原始画面/PTS顺序)} \\rightarrow \\text{业务逻辑}

希望这份讲解能帮你推开音视频开发的大门!

相关推荐
fpcc1 小时前
C++编程实践——手动实现std::visit
c++
重启的码农1 小时前
enet源码解析(4)多通道机制 (Channels)
c++·网络协议
重启的码农1 小时前
enet源码解析(3)数据包 (ENetPacket)
c++·网络协议
wefg12 小时前
【C++】智能指针
开发语言·c++·算法
MSTcheng.2 小时前
【C++模板进阶】C++ 模板进阶的拦路虎:模板特化和分离编译,该如何逐个突破?
开发语言·c++·模板
Demon--hx2 小时前
[c++]string的三种遍历方式
开发语言·c++·算法
valan liya3 小时前
C++list
开发语言·数据结构·c++·list
小毅&Nora3 小时前
【后端】【C++】智能指针详解:从裸指针到 RAII 的优雅演进(附 5 个可运行示例)
c++·指针
万粉变现经纪人3 小时前
如何解决 pip install 编译报错 ‘cl.exe’ not found(缺少 VS C++ 工具集)问题
开发语言·c++·人工智能·python·pycharm·bug·pip