C语言如何用FFmpeg实现解码?

99ANYc3cd6
预计阅读时长 32 分钟
位置: 首页 C语言 正文

核心概念

在开始写代码之前,你需要理解 FFmpeg 解码流程中的几个核心结构体和概念:

  1. AVFormatContext: 格式上下文,包含了文件或流的格式信息,比如视频流、音频流的索引、时长、比特率等,它是整个 FFmpeg 框架的入口。
  2. AVCodecContext: 编解码器上下文,包含了特定流(比如视频流)的编解码器参数,比如宽度、高度、像素格式、帧率等,你需要用它来初始化和解码。
  3. AVCodec: 编解码器,一个指向具体编解码器实现的指针,H.264H.265MPEG-4 等,FFmpeg 会根据文件信息自动找到合适的解码器。
  4. AVPacket: 数据包,编码后的数据单元,解码器接收的就是一包一包的 AVPacket,一个 AVPacket 可能包含一帧或多帧的画面数据。
  5. AVFrame: 原始帧,解码后的原始数据单元,比如解码后的 YUV 或 RGB 图像数据,我们的目标就是得到 AVFrame
  6. SwsContext: 软件缩放上下文,FFmpeg 提供的一个强大的软件库,用于图像转换和缩放,我们用它来将解码出来的 YUV 图像(通常是 AV_PIX_FMT_YUV420P)转换为易于显示的 RGB 图像。

解码流程总结如下:

  1. 打开输入文件:使用 avformat_open_input() 打开视频文件,并获取 AVFormatContext
  2. 查找流信息:使用 avformat_find_stream_info() 从文件中解析出视频流、音频流等信息。
  3. 找到视频流:遍历 AVFormatContext 中的流,找到类型为 AVMEDIA_TYPE_VIDEO 的流。
  4. 查找解码器:根据视频流的 codecpar 参数,找到对应的 AVCodec
  5. 创建解码器上下文:使用 avcodec_alloc_context3() 为找到的解码器创建 AVCodecContext,并用 avcodec_parameters_to_context() 将流的参数复制到上下文中。
  6. 打开解码器:使用 avcodec_open2() 打开解码器。
  7. 读取数据包并解码
    • 循环调用 av_read_frame()AVFormatContext 中读取 AVPacket
    • AVPacket 属于视频流,就调用 avcodec_send_packet() 将其发送给解码器。
    • 循环调用 avcodec_receive_frame() 从解码器中接收解码后的 AVFrame,直到没有更多帧为止。
  8. 处理帧数据:得到 AVFrame 后,可以使用 sws_scale() 将其转换为 RGB 格式,然后显示或保存。
  9. 清理资源:关闭所有打开的对象,释放所有分配的内存。

环境准备

在编译代码之前,你必须确保你的系统上安装了 FFmpeg 开发库。

在 Ubuntu/Debian 上安装

sudo apt update
sudo apt install build-essential libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev

在 macOS 上安装 (使用 Homebrew)

brew install ffmpeg

在 Windows 上安装

  1. FFmpeg 官网 下载构建好的库,或者使用 vcpkg 进行安装。
  2. include 目录添加到你的编译器的包含路径中。
  3. lib 目录下的 .lib 文件添加到你的链接器输入中。
  4. bin 目录添加到系统的 PATH 环境变量中。

完整代码示例

下面是一个完整的 C 程序,它会解码一个视频文件,并将每一帧保存为 .ppm 格式的图片文件,PPM 是一种简单的、未压缩的图像格式,非常适合用来验证解码结果。

#include <stdio.h>
#include <stdlib.h>
// 引入 FFmpeg 的头文件
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libswscale/swscale.h>
}
#define SAVE_PPM 1 // 定义宏,决定是否保存为PPM文件
// 保存 AVFrame 为 PPM 文件
void save_frame_as_ppm(AVFrame *pFrame, int width, int height, int iFrame) {
    FILE *pFile;
    char szFilename[32];
    int  y;
    // 生成文件名,如 frame_0001.ppm
    sprintf(szFilename, "frame_%04d.ppm", iFrame);
    pFile = fopen(szFilename, "wb");
    if (pFile == NULL) {
        return;
    }
    // 写入 PPM 文件头
    fprintf(pFile, "P6\n%d %d\n255\n", width, height);
    // 将 AVFrame 中的数据写入文件
    for (y = 0; y < height; y++) {
        fwrite(pFrame->data[0] + y * pFrame->linesize[0], 1, width * 3, pFile);
    }
    fclose(pFile);
}
int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("Usage: %s <input_file>\n", argv[0]);
        return -1;
    }
    const char *filename = argv[1];
    AVFormatContext *pFormatCtx = NULL;
    int videoStream, i;
    AVCodecContext *pCodecCtx = NULL;
    AVCodec *pCodec = NULL;
    AVFrame *pFrame = NULL;
    AVFrame *pFrameRGB = NULL;
    AVPacket *packet = NULL;
    int frameFinished;
    uint8_t *buffer = NULL;
    struct SwsContext *sws_ctx = NULL;
    // --- 1. 初始化 FFmpeg 库 ---
    av_register_all(); // 在较新版本的FFmpeg中已弃用,但为了兼容性保留
    avformat_network_init();
    // --- 2. 打开输入文件 ---
    if (avformat_open_input(&pFormatCtx, filename, NULL, NULL) != 0) {
        fprintf(stderr, "Could not open file '%s'\n", filename);
        return -1;
    }
    // --- 3. 获取流信息 ---
    if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
        fprintf(stderr, "Could not find stream information\n");
        return -1;
    }
    // --- 4. 查找视频流 ---
    videoStream = -1;
    for (i = 0; i < pFormatCtx->nb_streams; i++) {
        if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            videoStream = i;
            break;
        }
    }
    if (videoStream == -1) {
        fprintf(stderr, "Did not find a video stream\n");
        return -1;
    }
    // --- 5. 获取解码器上下文并查找解码器 ---
    AVCodecParameters *pCodecParams = pFormatCtx->streams[videoStream]->codecpar;
    pCodec = avcodec_find_decoder(pCodecParams->codec_id);
    if (pCodec == NULL) {
        fprintf(stderr, "Unsupported codec!\n");
        return -1;
    }
    pCodecCtx = avcodec_alloc_context3(pCodec);
    if (!pCodecCtx) {
        fprintf(stderr, "Could not allocate codec context\n");
        return -1;
    }
    // 将流的参数复制到解码器上下文
    if (avcodec_parameters_to_context(pCodecCtx, pCodecParams) < 0) {
        fprintf(stderr, "Could not copy codec context\n");
        return -1;
    }
    // --- 6. 打开解码器 ---
    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
        fprintf(stderr, "Could not open codec\n");
        return -1;
    }
    // --- 7. 分配帧和数据包 ---
    pFrame = av_frame_alloc();
    pFrameRGB = av_frame_alloc();
    packet = av_packet_alloc();
    if (!pFrame || !pFrameRGB || !packet) {
        fprintf(stderr, "Could not allocate frame or packet\n");
        return -1;
    }
    // 用于转换格式的缓冲区
    int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, pCodecCtx->width, pCodecCtx->height, 1);
    buffer = (uint8_t *)av_malloc(numBytes * sizeof(uint8_t));
    // 设置 pFrameRGB 的数据指针
    av_image_fill_arrays(pFrameRGB->data, pFrameRGB->linesize, buffer,
                         AV_PIX_FMT_RGB24, pCodecCtx->width, pCodecCtx->height, 1);
    // --- 8. 解码循环 ---
    int frame_index = 0;
    while (av_read_frame(pFormatCtx, packet) >= 0) {
        // 如果是视频流的数据包
        if (packet->stream_index == videoStream) {
            // 发送数据包到解码器
            avcodec_send_packet(pCodecCtx, packet);
            // 从解码器中接收解码后的帧
            while (avcodec_receive_frame(pCodecCtx, pFrame) == 0) {
                frameFinished = 1; // 成功解码一帧
                // --- 9. 转换图像格式 ---
                // 创建一个缩放/转换上下文
                sws_ctx = sws_getContext(
                    pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
                    pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_RGB24,
                    SWS_BILINEAR, NULL, NULL, NULL);
                if (sws_ctx == NULL) {
                    fprintf(stderr, "Could not initialize the conversion context\n");
                    return -1;
                }
                // 执行转换
                sws_scale(sws_ctx, pFrame->data, pFrame->linesize, 0,
                          pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize);
                // --- 10. 保存帧 ---
#if SAVE_PPM
                save_frame_as_ppm(pFrameRGB, pCodecCtx->width, pCodecCtx->height, frame_index);
                printf("Saved frame %d\n", frame_index);
#endif
                frame_index++;
            }
        }
        av_packet_unref(packet); // 释放数据包的引用
    }
    // --- 11. 清理资源 ---
    av_free(buffer);
    av_frame_free(&pFrameRGB);
    av_frame_free(&pFrame);
    av_packet_free(&packet);
    avcodec_close(pCodecCtx);
    avformat_close_input(pFormatCtx);
    sws_freeContext(sws_ctx);
    printf("Decoding finished. Total frames: %d\n", frame_index);
    return 0;
}

如何编译和运行

  1. 保存代码:将上面的代码保存为 decoder.c

  2. 准备视频文件:准备一个视频文件,input.mp4,并将其放在与 decoder.c 相同的目录下。

  3. 编译:打开终端,使用以下命令进行编译。关键在于链接 FFmpeg 的库

    # 对于 Linux/macOS
    gcc decoder.c -o decoder -lavformat -lavcodec -lavutil -lswscale
    # -o decoder: 指定输出的可执行文件名为 decoder
    # -lavformat: 链接 libavformat
    # -lavcodec: 链接 libavcodec
    # -lavutil: 链接 libavutil
    # -lswscale: 链接 libswscale

    Windows (MinGW) 用户

    gcc decoder.c -o decoder.exe -I/path/to/ffmpeg/include -L/path/to/ffmpeg/lib -lavformat -lavcodec -lavutil -lswscale

    请将 -I-L 的路径替换为你自己 FFmpeg 库的实际路径。

  4. 运行

    ./decoder input.mp4

    运行后,你会在当前目录下看到一系列 frame_XXXX.ppm 文件,你可以使用任何图像查看器(如 GIMP, Photoshop, 或一些在线 PPM 查看器)来打开它们,查看解码后的视频帧。


进阶与改进

这个示例是基础,实际应用中你可能需要:

  • 显示图像:将 RGB 数据显示在窗口上,而不是保存为文件,这需要使用 GUI 库,如 SDL2、GTK 或 Qt,SDL2 是一个非常流行的选择,因为它专门为多媒体设计,与 FFmpeg 配合得很好。
  • 处理音频:流程与视频类似,你需要找到 AVMEDIA_TYPE_AUDIO 的流,使用对应的音频解码器(如 AAC, MP3)进行解码,然后将 PCM 数据播放出来。
  • 错误处理:代码中的错误处理比较简单,一个健壮的程序需要对每一个可能失败的 FFmpeg API 调用都进行返回值检查。
  • 多线程:FFmpeg 的 avformat_open_inputavformat_find_stream_info 可能很耗时,可以使用 avformat_open_inputAVFormatContextinterrupt_callback 来实现超时和中断,解码过程也可以放在单独的线程中。
  • 硬件加速:现代 CPU 解码高分辨率视频(如 4K H.265)会很吃力,FFmpeg 支持使用 NVIDIA 的 NVDEC、AMD 的 AMF 或 Intel 的 Quick Sync Video 等硬件进行解码,这需要更复杂的初始化过程,但能极大地提升性能。
-- 展开阅读全文 --
头像
dede文章目录形式如何实现?
« 上一篇 04-05
dede采集为何无内容?
下一篇 » 04-05

相关文章

取消
微信二维码
支付宝二维码

目录[+]