【C++实战(71)】解锁C++音视频编写:FFmpeg从入门到实战


一、FFmpeg 的基础概念与环境搭建

1.1 FFmpeg 的组成

FFmpeg 是一个广泛使用的开源多媒体框架,它包含多个组件库,这些组件库各自承担着不同的功能,共同协作完成复杂的音视频处理任务。

  • libavformat:这是 FFmpeg 中负责处理多媒体容器格式的库,比如常见的 MP4、MKV、AVI 等格式 。它支持解复用(分离音视频流)和复用(合并音视频流)操作。在解复用过程中,libavformat 能够识别容器格式的结构,将其中封装的音频流、视频流以及字幕流等分离出来,为后续的解码等处理提供基础支持。例如,当我们打开一个 MP4 文件时,libavformat 会分析文件的结构,找到视频流和音频流的位置及相关信息,然后将它们分别提取出来,以便交给对应的解码器进行处理。
  • libavcodec:作为 FFmpeg 的编解码核心库,libavcodec 支持众多的音视频编码格式,如视频编码格式 H.264、H.265,音频编码格式 AAC 等。它的主要职责是将音视频数据进行解码,把压缩后的编码数据转换为原始的音视频数据,例如将 H.264 编码的视频数据解码为 YUV 格式的视频帧;或者在编码时,将原始的音视频数据编码为目标格式,用于存储或传输。在视频会议系统中,采集到的原始视频数据就需要通过 libavcodec 编码为 H.264 格式,以减少数据量,便于网络传输。
  • libavutil:这是一个通用工具库,为其他组件提供了一系列基础功能,包括内存管理、数学运算、随机数生成等。在音视频处理中,内存管理至关重要,libavutil 提供了高效的内存分配和释放函数,确保程序在处理大量音视频数据时的内存使用安全和高效。它还提供了一些数学运算函数,用于处理音视频相关的计算,如像素点的坐标计算等,是实现复杂音视频处理任务不可或缺的基础库。
  • libswscale:主要用于视频图像的缩放和色彩空间转换。在实际应用中,不同设备或场景对视频的分辨率和色彩空间要求不同,libswscale 能够将视频从一种分辨率和色彩空间调整为另一种,以满足后续处理或播放的需求。当我们需要将一个 1080p 分辨率的视频转换为 720p 分辨率,或者将 YUV420 格式的视频转换为 RGB 格式时,就可以使用 libswscale 来完成这些操作。

1.2 FFmpeg 开发环境搭建

Windows 下的库编译与配置

  1. 准备编译工具:首先需要安装 MSYS2,它是一个在 Windows 上模拟 Linux 环境的工具,能够帮助我们顺利编译 FFmpeg。还需要安装 Git,用于克隆 FFmpeg 的源代码;安装 CMake,作为构建管理工具。可以通过 Chocolatey 来安装这些工具,安装命令如下:
# 安装Chocolatey(如果还没有安装)
@"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))" && SET "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin"
# 安装Git
choco install git -y
# 安装Python
choco install python -y
# 安装CMake
choco install cmake -y
  1. 下载 FFmpeg 源代码:打开 MSYS2 终端,使用 Git 克隆 FFmpeg 的源代码库到本地计算机:
git clone https://github.com/FFmpeg/FFmpeg.git
  1. 配置编译环境:进入 FFmpeg 目录并执行配置,配置时需要指定一些参数,如目标操作系统、架构等。以下是一个示例配置命令:
cd FFmpeg
./configure --target-os=win32 --arch=x86_64 --enable-shared --disable-static --enable-gpl --enable-libx264 --enable-libmp3lame
  1. 编译 FFmpeg:执行编译命令,编译过程可能需要一些时间,具体取决于计算机性能:
make
  1. 配置开发环境:编译完成后,需要在开发环境(如 Visual Studio)中配置 FFmpeg 库。在项目属性中,添加 FFmpeg 的头文件目录到附加包含目录,添加库文件目录到附加库目录,并链接所需的库,如 avcodec.lib、avformat.lib 等。还需要将编译生成的 DLL 文件复制到执行程序目录,以确保程序运行时能够找到这些动态链接库。

Linux 下的库编译与配置

  1. 安装依赖项:在编译 FFmpeg 之前,需要安装一些必要的依赖项,包括编译工具和相关库。以 Ubuntu 系统为例,可以使用以下命令安装:
sudo apt-get update
sudo apt-get install build-essential git yasm
sudo apt-get install libx264-dev libx265-dev libvpx-dev libfdk-aac-dev libmp3lame-dev libopus-dev
  1. 下载 FFmpeg 源代码:使用 Git 下载 FFmpeg 源代码:
git clone https://github.com/FFmpeg/FFmpeg.git
  1. 配置编译环境:进入 FFmpeg 目录,执行配置命令,根据需求启用或禁用一些编解码器和功能:
cd FFmpeg
./configure --enable-gpl --enable-libx264 --enable-libx265 --enable-libvpx --enable-libfdk-aac --enable-libmp3lame --enable-libopus
  1. 编译 FFmpeg:执行编译命令,使用make -j ( n p r o c ) 可以加快编译速度, (nproc)可以加快编译速度, (nproc)可以加快编译速度,(nproc)表示当前系统的 CPU 核心数:
make -j$(nproc)
  1. 安装 FFmpeg:编译完成后,使用以下命令安装 FFmpeg 到系统中:
sudo make install
  1. 配置开发环境:在编写代码时,需要指定 FFmpeg 的头文件路径和库文件路径。在编译代码时,可以使用类似以下的命令指定:
gcc your_code.c -o your_program -I/usr/local/include -L/usr/local/lib -lavformat -lavcodec -lavutil -lswscale -lswresample -lpostproc

其中,-I/usr/local/include指定头文件路径,-L/usr/local/lib指定库文件路径,后面的-lavformat等是链接的 FFmpeg 库。

1.3 FFmpeg 的核心数据结构

  • AVFormatContext:这是一个非常重要的数据结构,它代表了一个多媒体文件的格式上下文,包含了整个多媒体文件的信息,如文件格式、流信息等。在打开一个音视频文件时,会创建一个 AVFormatContext 结构体来存储文件的相关信息,通过它可以获取到文件中包含的视频流、音频流的数量和各自的参数,以及文件的时长、比特率等信息,是后续进行音视频处理的基础。
  • AVCodecContext:每个编解码器都有一个对应的 AVCodecContext 结构体,它包含了编解码器的参数和状态信息。在初始化编解码器时,需要设置 AVCodecContext 的各种参数,如编码格式、分辨率、帧率(对于视频)、采样率、声道数(对于音频)等,编解码器在工作过程中也会更新这个结构体中的状态信息,它是编解码过程中不可或缺的数据结构。
  • AVFrame:用于存储原始的音视频数据,对于视频来说,它可以存储一帧 YUV 或 RGB 格式的图像数据;对于音频来说,它可以存储 PCM 格式的音频数据。AVFrame 包含了数据指针、宽度、高度(对于视频)、采样率、声道数(对于音频)等信息,是音视频数据处理和传输的基本单元。
  • AVPacket:用于存储压缩后的音视频数据,通常是从多媒体文件中读取出来的一帧编码数据。它包含了数据缓冲区、数据大小、时间戳等信息,在音视频解码过程中,先从文件中读取 AVPacket,然后将其送入解码器进行解码,得到 AVFrame 格式的原始数据 。

二、FFmpeg 音视频解码实战

2.1 音视频文件的打开与信息读取

在 FFmpeg 中,使用avformat_open_input函数来打开音视频文件,这个函数的作用是初始化一个AVFormatContext结构体,并将其与指定的音视频文件关联起来。该函数的原型如下:

int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);
  • ps:指向AVFormatContext指针的指针,函数会在成功打开文件后填充这个指针,使其指向一个有效的AVFormatContext结构体。
  • url:要打开的音视频文件的路径或网络地址。
  • fmt:指定输入文件的格式,如果设置为NULL,FFmpeg 会自动探测文件格式。
  • options:一些额外的选项,比如设置打开超时时间等,通常可以设置为NULL。

打开文件后,还需要调用avformat_find_stream_info函数来获取文件中流的详细信息,如视频流和音频流的参数。该函数的原型如下:

int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
  • ic:已经打开的AVFormatContext结构体指针。
  • options:同样是一些额外选项,通常设为NULL。

下面是一个简单的示例代码,展示了如何打开一个音视频文件并获取其流信息:

#include <libavformat/avformat.h>
  int main(int argc, char *argv[]) {
  AVFormatContext *formatContext = NULL;
  // 打开音视频文件
  if (avformat_open_input(&formatContext, argv[1], NULL, NULL) != 0) {
  printf("无法打开文件\n");
  return -1;
  }
  printf("文件打开成功\n");
  // 获取流信息
  if (avformat_find_stream_info(formatContext, NULL) < 0) {
  printf("无法获取流信息\n");
  return -1;
  }
  printf("流信息获取成功\n");
  // 打印文件信息
  av_dump_format(formatContext, 0, argv[1], 0);
  // 释放资源
  avformat_close_input(&formatContext);
  return 0;
  }

在这个示例中,首先调用avformat_open_input打开文件,然后通过avformat_find_stream_info获取流信息,最后使用av_dump_format打印文件的详细信息,包括视频流和音频流的参数。

2.2 解码器查找与初始化

查找合适的解码器需要使用avcodec_find_decoder函数,该函数根据指定的编码 ID 在系统中查找对应的解码器。函数原型如下:

AVCodec *avcodec_find_decoder(enum AVCodecID id);
  • id:编码 ID,例如AV_CODEC_ID_H264表示 H.264 编码。

找到解码器后,需要使用avcodec_open2函数来初始化解码器,设置解码器的参数并为其分配资源。函数原型如下:

int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
  • avctx:指向AVCodecContext结构体的指针,该结构体包含了解码器的参数和状态信息。
  • codec:通过avcodec_find_decoder找到的解码器。
  • options:一些额外的选项,比如设置解码线程数等,通常可以设置为NULL。

下面是初始化视频解码器的示例代码:

#include <libavformat/avformat.h>
  #include <libavcodec/avcodec.h>
    int main(int argc, char *argv[]) {
    AVFormatContext *formatContext = NULL;
    AVCodecContext *codecContext = NULL;
    AVCodec *codec = NULL;
    int videoStreamIndex = -1;
    // 打开音视频文件并获取流信息
    if (avformat_open_input(&formatContext, argv[1], NULL, NULL) != 0) {
    printf("无法打开文件\n");
    return -1;
    }
    if (avformat_find_stream_info(formatContext, NULL) < 0) {
    printf("无法获取流信息\n");
    return -1;
    }
    // 查找视频流
    for (int i = 0; i < formatContext->nb_streams; i++) {
      if (formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
      videoStreamIndex = i;
      break;
      }
      }
      if (videoStreamIndex == -1) {
      printf("未找到视频流\n");
      return -1;
      }
      // 获取视频流对应的解码器上下文
      codecContext = avcodec_alloc_context3(NULL);
      if (!codecContext) {
      printf("无法分配解码器上下文\n");
      return -1;
      }
      if (avcodec_parameters_to_context(codecContext, formatContext->streams[videoStreamIndex]->codecpar) < 0) {
      printf("无法设置解码器上下文参数\n");
      return -1;
      }
      // 查找解码器
      codec = avcodec_find_decoder(codecContext->codec_id);
      if (!codec) {
      printf("无法找到解码器\n");
      return -1;
      }
      // 初始化解码器
      if (avcodec_open2(codecContext, codec, NULL) < 0) {
      printf("无法初始化解码器\n");
      return -1;
      }
      printf("解码器初始化成功\n");
      // 后续进行解码操作...
      // 释放资源
      avcodec_free_context(&codecContext);
      avformat_close_input(&formatContext);
      return 0;
      }

在这个示例中,先打开文件并获取流信息,然后找到视频流,接着分配解码器上下文并设置参数,再查找解码器并进行初始化。

2.3 数据包读取与解码

读取数据包使用av_read_frame函数,该函数从输入文件中读取一个数据包(AVPacket),这个数据包包含了一帧压缩的音视频数据。函数原型如下:

int av_read_frame(AVFormatContext *s, AVPacket *pkt);
  • s:已经打开并获取流信息的AVFormatContext结构体指针。
  • pkt:指向AVPacket结构体的指针,用于存储读取到的数据包。

解码过程则使用avcodec_send_packet和avcodec_receive_frame函数。avcodec_send_packet将读取到的数据包发送到解码器中,avcodec_receive_frame从解码器中接收解码后的帧数据(AVFrame)。它们的函数原型如下:

int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);
  • avctx:已经初始化解码器的AVCodecContext结构体指针。
  • avpkt:要发送到解码器的数据包。
  • frame:用于接收解码后帧数据的AVFrame结构体指针。

下面是一个完整的音视频解码示例代码:

#include <libavformat/avformat.h>
  #include <libavcodec/avcodec.h>
    #include <libavutil/imgutils.h>
      #include <libswscale/swscale.h>
        #include <stdio.h>
          #define INBUF_SIZE 4096
          int main(int argc, char *argv[]) {
          AVFormatContext *formatContext = NULL;
          AVCodecContext *codecContext = NULL;
          AVCodec *codec = NULL;
          AVPacket *packet = NULL;
          AVFrame *frame = NULL;
          struct SwsContext *swsContext = NULL;
          int videoStreamIndex = -1;
          // 打开音视频文件并获取流信息
          if (avformat_open_input(&formatContext, argv[1], NULL, NULL) != 0) {
          printf("无法打开文件\n");
          return -1;
          }
          if (avformat_find_stream_info(formatContext, NULL) < 0) {
          printf("无法获取流信息\n");
          return -1;
          }
          // 查找视频流
          for (int i = 0; i < formatContext->nb_streams; i++) {
            if (formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            videoStreamIndex = i;
            break;
            }
            }
            if (videoStreamIndex == -1) {
            printf("未找到视频流\n");
            return -1;
            }
            // 获取视频流对应的解码器上下文
            codecContext = avcodec_alloc_context3(NULL);
            if (!codecContext) {
            printf("无法分配解码器上下文\n");
            return -1;
            }
            if (avcodec_parameters_to_context(codecContext, formatContext->streams[videoStreamIndex]->codecpar) < 0) {
            printf("无法设置解码器上下文参数\n");
            return -1;
            }
            // 查找解码器
            codec = avcodec_find_decoder(codecContext->codec_id);
            if (!codec) {
            printf("无法找到解码器\n");
            return -1;
            }
            // 初始化解码器
            if (avcodec_open2(codecContext, codec, NULL) < 0) {
            printf("无法初始化解码器\n");
            return -1;
            }
            // 分配数据包和帧
            packet = av_packet_alloc();
            frame = av_frame_alloc();
            if (!packet ||!frame) {
            printf("无法分配数据包或帧\n");
            return -1;
            }
            // 初始化图像缩放上下文(这里假设转换为RGB格式用于显示,实际应用中可根据需求调整)
            swsContext = sws_getContext(
            codecContext->width, codecContext->height, codecContext->pix_fmt,
            codecContext->width, codecContext->height, AV_PIX_FMT_RGB24,
            SWS_BILINEAR, NULL, NULL, NULL);
            if (!swsContext) {
            printf("无法初始化图像缩放上下文\n");
            return -1;
            }
            // 读取并解码数据包
            while (av_read_frame(formatContext, packet) >= 0) {
            if (packet->stream_index == videoStreamIndex) {
            // 发送数据包到解码器
            if (avcodec_send_packet(codecContext, packet) < 0) {
            printf("发送数据包到解码器失败\n");
            return -1;
            }
            // 从解码器接收解码后的帧
            while (avcodec_receive_frame(codecContext, frame) == 0) {
            // 这里可以对解码后的帧进行处理,例如保存为图片或者显示
            // 下面是一个简单的将YUV转换为RGB并保存为BMP文件的示例(省略了BMP文件头写入等完整操作,仅展示核心转换部分)
            uint8_t *rgbData = (uint8_t *) av_malloc(codecContext->width * codecContext->height * 3);
            int linesize[1] = {codecContext->width * 3};
            sws_scale(swsContext, frame->data, frame->linesize, 0, codecContext->height, &rgbData, linesize);
            // 这里rgbData即为转换后的RGB数据,可以进一步处理或保存
            av_free(rgbData);
            }
            }
            av_packet_unref(packet);
            }
            // 释放资源
            sws_freeContext(swsContext);
            av_frame_free(&frame);
            av_packet_free(&packet);
            avcodec_free_context(&codecContext);
            avformat_close_input(&formatContext);
            return 0;
            }

在这个示例中,首先打开文件并获取流信息,找到视频流后初始化解码器,然后在循环中不断读取数据包并进行解码,对解码后的帧进行简单的处理(这里是转换为 RGB 格式),最后释放所有分配的资源。

三、FFmpeg 音视频格式转换

3.1 像素格式转换与音频采样格式转换

在 FFmpeg 中,sws_scale函数用于视频像素格式转换和图像缩放,它能够将一种像素格式的视频图像转换为另一种像素格式,并且可以同时调整图像的分辨率。例如,常见的应用场景是将 YUV 格式的视频帧转换为 RGB 格式,以便在支持 RGB 显示的设备上展示,或者将高分辨率的视频帧缩放为低分辨率,以适应不同的显示需求或减少数据量。

sws_scale函数的工作原理是基于一系列的图像插值和色彩空间转换算法。当进行像素格式转换时,它会根据输入和输出像素格式的特点,对图像中的每个像素点进行重新计算和映射。在将 YUV420P 格式转换为 RGB24 格式时,它会根据 YUV 与 RGB 之间的色彩转换公式,对 Y、U、V 分量进行计算,得到对应的 R、G、B 值 。在图像缩放时,sws_scale会根据指定的缩放算法(如双线性插值算法 SWS_BILINEAR、双立方插值算法 SWS_BICUBIC 等)对图像进行处理。双线性插值算法通过对相邻的四个像素点进行线性插值来计算新的像素值,从而实现图像的缩放。

使用sws_scale函数时,需要先创建一个SwsContext结构体,该结构体用于存储图像转换的上下文信息,包括输入和输出图像的尺寸、像素格式以及选择的缩放算法等。可以使用sws_getContext或sws_getCachedContext函数来创建SwsContext,其中sws_getCachedContext会缓存已创建的上下文,对于相同参数的转换操作可以提高效率。下面是一个简单的代码示例,展示如何将 YUV420P 格式的视频帧转换为 RGB24 格式:

#include <libswscale/swscale.h>
  #include <libavutil/imgutils.h>
    #include <libavcodec/avcodec.h>
      #include <libavformat/avformat.h>
        int main() {
        // 假设已经获取到解码后的YUV420P格式的AVFrame
        AVFrame *yuvFrame = av_frame_alloc();
        // 这里省略获取yuvFrame数据的代码
        int width = yuvFrame->width;
        int height = yuvFrame->height;
        // 创建用于存储RGB数据的AVFrame
        AVFrame *rgbFrame = av_frame_alloc();
        rgbFrame->width = width;
        rgbFrame->height = height;
        rgbFrame->format = AV_PIX_FMT_RGB24;
        int bufferSize = av_image_get_buffer_size(AV_PIX_FMT_RGB24, width, height, 1);
        uint8_t *rgbBuffer = (uint8_t *)av_malloc(bufferSize);
        av_image_fill_arrays(rgbFrame->data, rgbFrame->linesize, rgbBuffer, AV_PIX_FMT_RGB24, width, height, 1);
        // 创建SwsContext
        SwsContext *swsContext = sws_getContext(
        width, height, AV_PIX_FMT_YUV420P,
        width, height, AV_PIX_FMT_RGB24,
        SWS_BILINEAR, NULL, NULL, NULL);
        // 进行像素格式转换
        sws_scale(swsContext, yuvFrame->data, yuvFrame->linesize, 0, height, rgbFrame->data, rgbFrame->linesize);
        // 使用完后释放资源
        sws_freeContext(swsContext);
        av_frame_free(&yuvFrame);
        av_frame_free(&rgbFrame);
        av_free(rgbBuffer);
        return 0;
        }

对于音频采样格式转换,FFmpeg 提供了swr_convert函数,它可以将音频从一种采样格式转换为另一种采样格式,同时还能处理声道布局的转换和采样率的调整。在实际应用中,不同的音频设备或处理流程可能需要不同的采样格式和采样率,swr_convert就可以满足这些需求。将采样格式为AV_SAMPLE_FMT_FLTP(浮点型平面格式)、采样率为 48000Hz 的音频转换为采样格式为AV_SAMPLE_FMT_S16(16 位整型交错格式)、采样率为 44100Hz 的音频。

swr_convert函数的工作原理是通过重采样算法对音频数据进行处理。它首先根据输入和输出的采样格式、声道布局以及采样率等参数,计算出合适的重采样系数。然后,根据这些系数对输入的音频样本进行插值或抽取等操作,生成符合目标格式的音频样本。在将采样率从 48000Hz 转换为 44100Hz 时,会根据重采样算法对音频样本进行适当的插值或抽取,以保证转换后的音频质量。

使用swr_convert函数时,需要先创建一个SwrContext结构体,并对其进行初始化,设置好输入和输出的音频参数。可以使用swr_alloc函数分配SwrContext内存,然后通过av_opt_set*系列函数设置相关参数,最后调用swr_init函数进行初始化。下面是一个简单的代码示例,展示如何将音频的采样格式和采样率进行转换:

#include <libswresample/swresample.h>
  #include <libavutil/avutil.h>
    int main() {
    // 假设已经有输入音频数据,存储在inputBuffer中,采样格式为AV_SAMPLE_FMT_FLTP,采样率为48000Hz,双声道
    enum AVSampleFormat inputFormat = AV_SAMPLE_FMT_FLTP;
    int inputSampleRate = 48000;
    int inputChannels = 2;
    uint8_t **inputBuffer = NULL;
    // 这里省略inputBuffer数据的获取和分配代码
    // 目标输出音频参数,采样格式为AV_SAMPLE_FMT_S16,采样率为44100Hz,双声道
    enum AVSampleFormat outputFormat = AV_SAMPLE_FMT_S16;
    int outputSampleRate = 44100;
    int outputChannels = 2;
    uint8_t **outputBuffer = NULL;
    int outBufferSize = av_samples_get_buffer_size(NULL, outputChannels, 1024, outputFormat, 1);
    outputBuffer = (uint8_t **)av_malloc(outputChannels * sizeof(uint8_t *));
    outputBuffer[0] = (uint8_t *)av_malloc(outBufferSize);
    // 创建SwrContext并初始化
    SwrContext *swrContext = swr_alloc();
    av_opt_set_int(swrContext, "in_channel_count", inputChannels, 0);
    av_opt_set_int(swrContext, "in_sample_rate", inputSampleRate, 0);
    av_opt_set_sample_fmt(swrContext, "in_sample_fmt", inputFormat, 0);
    av_opt_set_int(swrContext, "out_channel_count", outputChannels, 0);
    av_opt_set_int(swrContext, "out_sample_rate", outputSampleRate, 0);
    av_opt_set_sample_fmt(swrContext, "out_sample_fmt", outputFormat, 0);
    swr_init(swrContext);
    // 进行音频采样格式转换
    int inSamples = 1024;
    int outSamples = av_rescale_rnd(swr_get_delay(swrContext, inputSampleRate) + inSamples, outputSampleRate, inputSampleRate, AV_ROUND_UP);
    swr_convert(swrContext, outputBuffer, outSamples, (const uint8_t **)inputBuffer, inSamples);
    // 使用完后释放资源
    swr_free(&swrContext);
    av_freep(&outputBuffer[0]);
    av_freep(&outputBuffer);
    // 释放inputBuffer相关资源,这里省略代码
    return 0;
    }

3.2 音视频帧数据的存储与简单处理

对于视频帧数据,常见的格式如 YUV 数据,可以通过将其写入文件的方式进行保存。在保存 YUV 数据时,需要注意数据的存储格式和排列顺序。YUV420P 格式是一种常用的 YUV 格式,它的存储方式是先存储所有的 Y 分量,然后存储所有的 U 分量,最后存储所有的 V 分量。下面是一个保存 YUV420P 格式视频帧数据的示例代码:

#include <stdio.h>
  #include <libavutil/imgutils.h>
    #include <libavcodec/avcodec.h>
      #include <libavformat/avformat.h>
        void saveYUVFrame(AVFrame *frame, const char *filename) {
        FILE *file = fopen(filename, "wb");
        if (!file) {
        printf("无法打开文件进行写入\n");
        return;
        }
        // 计算YUV数据大小
        int width = frame->width;
        int height = frame->height;
        int y_size = width * height;
        int uv_size = y_size / 4;
        // 写入Y分量
        fwrite(frame->data[0], 1, y_size, file);
        // 写入U分量
        fwrite(frame->data[1], 1, uv_size, file);
        // 写入V分量
        fwrite(frame->data[2], 1, uv_size, file);
        fclose(file);
        }
        int main() {
        // 假设已经获取到解码后的YUV420P格式的AVFrame
        AVFrame *yuvFrame = av_frame_alloc();
        // 这里省略获取yuvFrame数据的代码
        saveYUVFrame(yuvFrame, "output.yuv");
        av_frame_free(&yuvFrame);
        return 0;
        }

对于音频帧数据,以 PCM 数据为例,播放 PCM 数据可以使用一些音频播放库,如 SDL(Simple DirectMedia Layer)。在使用 SDL 播放 PCM 数据时,首先需要初始化 SDL 音频子系统,设置音频播放参数,包括采样率、声道数、采样格式等,然后将 PCM 数据写入音频设备进行播放。下面是一个使用 SDL 播放 PCM 数据的简单示例代码:

#include <SDL2/SDL.h>
  #include <stdio.h>
    // 音频回调函数,用于将PCM数据输出到音频设备
    void audio_callback(void *userdata, Uint8 *stream, int len) {
    // 这里假设userdata是一个包含PCM数据的缓冲区指针
    Uint8 *pcm_data = (Uint8 *)userdata;
    // 简单地将PCM数据复制到音频流中
    SDL_memcpy(stream, pcm_data, len);
    }
    int main(int argc, char *argv[]) {
    if (SDL_Init(SDL_INIT_AUDIO) < 0) {
    printf("SDL初始化失败: %s\n", SDL_GetError());
    return -1;
    }
    // 设置音频参数
    SDL_AudioSpec wanted_spec, obtained_spec;
    wanted_spec.freq = 44100;  // 采样率
    wanted_spec.format = AUDIO_S16SYS;  // 采样格式,16位有符号整数
    wanted_spec.channels = 2;  // 声道数
    wanted_spec.silence = 0;
    wanted_spec.samples = 1024;  // 音频缓冲区大小
    wanted_spec.callback = audio_callback;
    wanted_spec.userdata = NULL;  // 这里可以传入包含PCM数据的缓冲区指针,暂时设为NULL
    // 打开音频设备
    if (SDL_OpenAudio(&wanted_spec, &obtained_spec) < 0) {
    printf("无法打开音频设备: %s\n", SDL_GetError());
    SDL_Quit();
    return -1;
    }
    // 开始播放音频
    SDL_PauseAudio(0);
    // 保持程序运行,持续播放音频,这里可以添加更多逻辑,如读取PCM数据并更新userdata
    while (1) {
    SDL_Delay(100);
    }
    // 关闭音频设备
    SDL_CloseAudio();
    SDL_Quit();
    return 0;
    }

除了存储和播放,对音视频帧数据还可以进行一些简单的处理。对于视频帧,可以对其进行裁剪、添加水印等操作。裁剪视频帧时,可以根据需要的区域坐标和尺寸,从原始视频帧中提取相应的像素数据,生成新的视频帧。添加水印则是在视频帧上绘制特定的图像或文字信息,以标识视频的来源或版权等信息。对于音频帧,可以进行音量调整、静音处理等操作。音量调整可以通过对 PCM 数据中的每个样本值乘以一个音量系数来实现,静音处理则是将 PCM 数据中的样本值全部设置为 0 。

3.3 FFmpeg 错误处理与资源释放

在使用 FFmpeg 进行音视频处理时,错误处理至关重要。当调用 FFmpeg 的函数出现错误时,会返回一个错误码,这个错误码是一个负数。av_strerror函数可以将这些错误码转换为可读的错误信息,方便开发者定位和解决问题。例如,在打开音视频文件时,如果文件路径错误或者文件格式不支持,avformat_open_input函数会返回一个错误码,通过av_strerror可以获取具体的错误描述,如 “没有这样的文件或目录”“无法识别的文件格式” 等。下面是一个使用av_strerror处理错误的示例代码:

#include <libavformat/avformat.h>
  #include <libavutil/error.h>
    #include <stdio.h>
      int main(int argc, char *argv[]) {
      AVFormatContext *formatContext = NULL;
      int ret = avformat_open_input(&formatContext, argv[1], NULL, NULL);
      if (ret != 0) {
      char errbuf[1024];
      av_strerror(ret, errbuf, sizeof(errbuf));
      printf("打开文件失败: %s\n", errbuf);
      return -1;
      }
      // 后续处理代码...
      // 释放资源
      avformat_close_input(&formatContext);
      return 0;
      }

资源释放同样是不可忽视的环节。在 FFmpeg 中,使用完各种结构体和内存分配后,需要及时释放资源,以避免内存泄漏和资源浪费。AVFormatContext、AVCodecContext、AVFrame、AVPacket等结构体在使用完毕后,都需要调用相应的释放函数。使用avformat_close_input关闭并释放AVFormatContext,使用avcodec_free_context释放AVCodecContext,使用av_frame_free释放AVFrame,使用av_packet_free释放AVPacket。对于通过av_malloc等函数分配的内存,要使用av_free进行释放。下面是一个完整的资源释放示例代码,结合前面的音视频解码示例:

#include <libavformat/avformat.h>
  #include <libavcodec/avcodec.h>
    #include <libavutil/imgutils.h>
      #include <libswscale/swscale.h>
        #include <stdio.h>
          #define INBUF_SIZE 4096
          int main(int argc, char *argv[]) {
          AVFormatContext *formatContext = NULL;
          AVCodecContext *codecContext = NULL;
          AVCodec *codec = NULL;
          AVPacket *packet = NULL;
          AVFrame *frame = NULL;
          struct SwsContext *swsContext = NULL;
          int videoStreamIndex = -1;
          // 打开音视频文件并获取流信息
          if (avformat_open_input(&formatContext, argv[1], NULL, NULL) != 0) {
          printf("无法打开文件\n");
          return -1;
          }
          if (avformat_find_stream_info(formatContext, NULL) < 0) {
          printf("无法获取流信息\n");
          return -1;
          }
          // 查找视频流
          for (int i = 0; i < formatContext->nb_streams; i++) {
            if (formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            videoStreamIndex = i;
            break;
            }
            }
            if (videoStreamIndex == -1) {
            printf("未找到视频流\n");
            return -1;
            }
            // 获取视频流对应的解码器上下文
            codecContext = avcodec_alloc_context3(NULL);
            if (!codecContext) {
            printf("无法分配解码器上下文\n");
            return -1;
            }
            if (avcodec_parameters_to_context(codecContext, formatContext->streams[videoStreamIndex]->codecpar) < 0) {
            printf("无法设置解码器上下文参数\n");
            return -1;
            }
            // 查找解码器
            codec = avcodec_find_decoder(codecContext->codec_id);
            if (!codec) {
            printf("无法找到解码器\n");
            return -1;
            }
            // 初始化解码器
            if (avcodec_open2(codecContext, codec, NULL) < 0) {
            printf("无法初始化解码器\n");
            return -1;
            }
            // 分配数据包和帧
            packet = av_packet_alloc();
            frame = av_frame_alloc();
            if (!packet ||!frame) {
            printf("无法分配数据包或帧\n");
            return -1;
            }
            // 初始化图像缩放上下文(这里假设转换为RGB格式用于显示,实际应用中

四、实战项目:简易视频播放器(FFmpeg 解码版)

4.1 项目需求

本实战项目旨在开发一个简易视频播放器,该播放器基于FFmpeg库实现,具备以下核心功能:

  1. 支持MP4格式解码:能够读取并解析MP4格式的视频文件,将其中封装的音视频流分离出来,并对视频流和音频流进行解码处理,使其转换为可供后续显示和播放的原始数据。
  2. YUV数据显示:对于解码后的视频数据,通常以YUV格式存在,播放器需要将YUV数据进行处理,例如转换为适合显示设备的格式(如RGB),并在窗口中实时显示视频画面,确保画面流畅、清晰,帧率稳定。
  3. PCM音频播放:解码后的音频数据为PCM格式,播放器需要利用音频播放库(如SDL)将PCM数据输出到音频设备进行播放,实现声音的正常输出,并且要保证音频与视频的同步播放,避免出现音画不同步的现象。

4.2 FFmpeg 解码流程与数据转换代码实现

#include <iostream>
  #include <libavformat/avformat.h>
    #include <libavcodec/avcodec.h>
      #include <libswscale/swscale.h>
        #include <libswresample/swresample.h>
          #include <SDL2/SDL.h>
            // 初始化FFmpeg
            av_register_all();
            avformat_network_init();
            // 打开视频文件
            AVFormatContext *formatContext = NULL;
            if (avformat_open_input(&formatContext, "test.mp4", NULL, NULL) != 0) {
            std::cerr << "无法打开文件" << std::endl;
            return -1;
            }
            if (avformat_find_stream_info(formatContext, NULL) < 0) {
            std::cerr << "无法获取流信息" << std::endl;
            return -1;
            }
            // 查找视频流和音频流
            int videoStreamIndex = -1, audioStreamIndex = -1;
            for (unsigned int i = 0; i < formatContext->nb_streams; ++i) {
              if (formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
              videoStreamIndex = i;
              } else if (formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
              audioStreamIndex = i;
              }
              }
              if (videoStreamIndex == -1 || audioStreamIndex == -1) {
              std::cerr << "未找到视频流或音频流" << std::endl;
              return -1;
              }
              // 获取视频解码器上下文并初始化解码器
              AVCodecContext *videoCodecContext = avcodec_alloc_context3(NULL);
              avcodec_parameters_to_context(videoCodecContext, formatContext->streams[videoStreamIndex]->codecpar);
              AVCodec *videoCodec = avcodec_find_decoder(videoCodecContext->codec_id);
              if (avcodec_open2(videoCodecContext, videoCodec, NULL) < 0) {
              std::cerr << "无法初始化解码器" << std::endl;
              return -1;
              }
              // 获取音频解码器上下文并初始化解码器
              AVCodecContext *audioCodecContext = avcodec_alloc_context3(NULL);
              avcodec_parameters_to_context(audioCodecContext, formatContext->streams[audioStreamIndex]->codecpar);
              AVCodec *audioCodec = avcodec_find_decoder(audioCodecContext->codec_id);
              if (avcodec_open2(audioCodecContext, audioCodec, NULL) < 0) {
              std::cerr << "无法初始化解码器" << std::endl;
              return -1;
              }
              // 初始化SDL
              if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) {
              std::cerr << "SDL初始化失败: " << SDL_GetError() << std::endl;
              return -1;
              }
              // 创建SDL窗口和渲染器
              SDL_Window *window = SDL_CreateWindow("简易视频播放器", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
              videoCodecContext->width, videoCodecContext->height, SDL_WINDOW_SHOWN);
              SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
              SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING,
              videoCodecContext->width, videoCodecContext->height);
              // 初始化音频重采样上下文
              SwrContext *swrContext = swr_alloc();
              av_opt_set_int(swrContext, "in_channel_count", audioCodecContext->channels, 0);
              av_opt_set_int(swrContext, "in_sample_rate", audioCodecContext->sample_rate, 0);
              av_opt_set_sample_fmt(swrContext, "in_sample_fmt", audioCodecContext->sample_fmt, 0);
              av_opt_set_int(swrContext, "out_channel_count", 2, 0);
              av_opt_set_int(swrContext, "out_sample_rate", 44100, 0);
              av_opt_set_sample_fmt(swrContext, "out_sample_fmt", AV_SAMPLE_FMT_S16, 0);
              swr_init(swrContext);
              // 初始化图像缩放上下文
              SwsContext *swsContext = sws_getContext(videoCodecContext->width, videoCodecContext->height, videoCodecContext->pix_fmt,
              videoCodecContext->width, videoCodecContext->height, AV_PIX_FMT_YV12,
              SWS_BILINEAR, NULL, NULL, NULL);
              AVPacket *packet = av_packet_alloc();
              AVFrame *videoFrame = av_frame_alloc();
              AVFrame *audioFrame = av_frame_alloc();
              AVFrame *resampledAudioFrame = av_frame_alloc();
              resampledAudioFrame->format = AV_SAMPLE_FMT_S16;
              resampledAudioFrame->channel_layout = AV_CH_LAYOUT_STEREO;
              resampledAudioFrame->sample_rate = 44100;
              // 音频回调函数,用于播放音频
              void audio_callback(void *userdata, Uint8 *stream, int len) {
              // 这里假设userdata是一个包含PCM数据的缓冲区指针
              Uint8 *pcm_data = (Uint8 *) userdata;
              // 简单地将PCM数据复制到音频流中
              SDL_memcpy(stream, pcm_data, len);
              }
              // 打开音频设备
              SDL_AudioSpec wanted_spec, obtained_spec;
              wanted_spec.freq = 44100;
              wanted_spec.format = AUDIO_S16SYS;
              wanted_spec.channels = 2;
              wanted_spec.silence = 0;
              wanted_spec.samples = 1024;
              wanted_spec.callback = audio_callback;
              wanted_spec.userdata = NULL;
              if (SDL_OpenAudio(&wanted_spec, &obtained_spec) < 0) {
              std::cerr << "无法打开音频设备: " << SDL_GetError() << std::endl;
              return -1;
              }
              // 开始播放音频
              SDL_PauseAudio(0);
              // 读取并解码数据包
              while (av_read_frame(formatContext, packet) >= 0) {
              if (packet->stream_index == videoStreamIndex) {
              // 发送数据包到视频解码器
              if (avcodec_send_packet(videoCodecContext, packet) < 0) {
              std::cerr << "发送视频数据包到解码器失败" << std::endl;
              continue;
              }
              while (avcodec_receive_frame(videoCodecContext, videoFrame) == 0) {
              // 视频格式转换
              uint8_t *yuvData[AV_NUM_DATA_POINTERS];
              int yuvLinesize[AV_NUM_DATA_POINTERS];
              av_image_alloc(yuvData, yuvLinesize, videoCodecContext->width, videoCodecContext->height, AV_PIX_FMT_YV12, 1);
              sws_scale(swsContext, videoFrame->data, videoFrame->linesize, 0, videoCodecContext->height,
              yuvData, yuvLinesize);
              // 更新SDL纹理
              SDL_UpdateYUVTexture(texture, NULL, yuvData[0], yuvLinesize[0],
              yuvData[1], yuvLinesize[1],
              yuvData[2], yuvLinesize[2]);
              // 清除渲染器
              SDL_RenderClear(renderer);
              // 复制纹理到渲染器
              SDL_RenderCopy(renderer, texture, NULL, NULL);
              // 显示渲染结果
              SDL_RenderPresent(renderer);
              av_freep(&yuvData[0]);
              }
              } else if (packet->stream_index == audioStreamIndex) {
              // 发送数据包到音频解码器
              if (avcodec_send_packet(audioCodecContext, packet) < 0) {
              std::cerr << "发送音频数据包到解码器失败" << std::endl;
              continue;
              }
              while (avcodec_receive_frame(audioCodecContext, audioFrame) == 0) {
              // 音频重采样
              resampledAudioFrame->nb_samples = av_rescale_rnd(swr_get_delay(swrContext, audioCodecContext->sample_rate) +
              audioFrame->nb_samples, 44100,
              audioCodecContext->sample_rate, AV_ROUND_UP);
              av_frame_get_buffer(resampledAudioFrame, 0);
              swr_convert(swrContext, resampledAudioFrame->data, resampledAudioFrame->nb_samples,
              (const uint8_t **) audioFrame->data, audioFrame->nb_samples);
              // 播放音频
              SDL_QueueAudio(SDL_GetAudioDeviceID(NULL, 0), resampledAudioFrame->data[0],
              resampledAudioFrame->nb_samples * av_get_bytes_per_sample(AV_SAMPLE_FMT_S16) * 2);
              }
              }
              av_packet_unref(packet);
              }
              // 释放资源
              swr_free(&swrContext);
              sws_freeContext(swsContext);
              av_frame_free(&videoFrame);
              av_frame_free(&audioFrame);
              av_frame_free(&resampledAudioFrame);
              av_packet_free(&packet);
              avcodec_free_context(&videoCodecContext);
              avcodec_free_context(&audioCodecContext);
              avformat_close_input(&formatContext);
              SDL_DestroyTexture(texture);
              SDL_DestroyRenderer(renderer);
              SDL_DestroyWindow(window);
              SDL_Quit();

4.3 解码效率测试与音视频同步处理

  1. 解码效率测试
    • 方法:为了测试解码效率,可以在解码循环中记录解码每一帧的时间戳。在读取视频帧之前记录开始时间start_time,在解码完成一帧后记录结束时间end_time,通过计算end_time - start_time得到解码一帧所需的时间。通过统计一定数量帧(如 100 帧)的解码总时间,再除以帧数,就可以得到平均每帧的解码时间。
    • 示例代码
#include <chrono>
  auto start_time = std::chrono::high_resolution_clock::now();
  // 解码循环
  for (int i = 0; i < 100; ++i) {
  // 解码操作
  //...
  }
  auto end_time = std::chrono::high_resolution_clock::now();
  auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count();
    double average_time = static_cast<double>(duration) / 100;
      std::cout << "平均每帧解码时间: " << average_time << " 毫秒" << std::endl;
  1. 音视频同步处理
    • 方法:音视频同步是视频播放中一个关键问题,常见的解决方法是使用时间戳(PTS - Presentation Time Stamp)。在 FFmpeg 解码过程中,每个音频帧和视频帧都有对应的 PTS。可以选择一个参考时钟,比如以音频时钟为参考。在播放视频时,根据音频的 PTS 来调整视频的播放速度。如果视频的 PTS 比音频的 PTS 快,就适当延迟视频的显示;如果视频的 PTS 比音频的 PTS 慢,就加快视频的显示,以保持音视频同步。
    • 示例代码
// 假设已经获取到音频和视频的PTS
int64_t audio_pts = audio_frame->pts * av_q2d(formatContext->streams[audioStreamIndex]->time_base);
int64_t video_pts = video_frame->pts * av_q2d(formatContext->streams[videoStreamIndex]->time_base);
// 以音频PTS为参考进行同步
if (video_pts > audio_pts) {
// 视频比音频快,适当延迟视频显示
SDL_Delay((video_pts - audio_pts) * 1000);
} else if (video_pts < audio_pts) {
// 视频比音频慢,加快视频显示,这里简单跳过一些帧来加快速度
continue;
}
  • 策略:为了更好地处理音视频同步,还可以引入缓冲区机制。对于音频,可以设置一个音频缓冲区,将解码后的音频数据先存入缓冲区,然后根据音频时钟从缓冲区中读取数据进行播放,这样可以减少因网络波动或解码延迟导致的音频卡顿。对于视频,同样可以设置视频缓冲区,根据音频时钟来从缓冲区中取出合适的视频帧进行显示,确保音视频在时间上的一致性。
posted @ 2025-10-20 20:47  yxysuanfa  阅读(31)  评论(0)    收藏  举报