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

news/2025/10/20 20:55:30/文章来源:https://www.cnblogs.com/yxysuanfa/p/19153620

目录

  • 一、FFmpeg 的基础概念与环境搭建
    • 1.1 FFmpeg 的组成
    • 1.2 FFmpeg 开发环境搭建
    • 1.3 FFmpeg 的核心数据结构
  • 二、FFmpeg 音视频解码实战
    • 2.1 音视频文件的打开与信息读取
    • 2.2 解码器查找与初始化
    • 2.3 数据包读取与解码
  • 三、FFmpeg 音视频格式转换
    • 3.1 像素格式转换与音频采样格式转换
    • 3.2 音视频帧数据的存储与简单处理
    • 3.3 FFmpeg 错误处理与资源释放
  • 四、实战项目:简易视频播放器(FFmpeg 解码版)
    • 4.1 项目需求
    • 4.2 FFmpeg 解码流程与数据转换代码实现
    • 4.3 解码效率测试与音视频同步处理


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

1.1 FFmpeg 的组成

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

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(nproc)可以加快编译速度,(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 4096int 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格式的AVFrameAVFrame *yuvFrame = av_frame_alloc();// 这里省略获取yuvFrame数据的代码int width = yuvFrame->width;int height = yuvFrame->height;// 创建用于存储RGB数据的AVFrameAVFrame *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);// 创建SwsContextSwsContext *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格式的AVFrameAVFrame *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数据并更新userdatawhile (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 4096int 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>// 初始化FFmpegav_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;}// 初始化SDLif (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;
}
  • 策略:为了更好地处理音视频同步,还可以引入缓冲区机制。对于音频,可以设置一个音频缓冲区,将解码后的音频数据先存入缓冲区,然后根据音频时钟从缓冲区中读取数据进行播放,这样可以减少因网络波动或解码延迟导致的音频卡顿。对于视频,同样可以设置视频缓冲区,根据音频时钟来从缓冲区中取出合适的视频帧进行显示,确保音视频在时间上的一致性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/941591.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

20251020

正睿NOIP 二十连测 A 有 \(m(m \le 95)\) 种药剂,每种药剂有 \(n_i(\sum n_i \le 10^{15})\) 瓶,等级为 \(p_i\)(\(2 \le p_i \le 499\))。要将这些药剂分成两个不相交的集合 \(X, Y\),\(X\) 的价值为其组内所有药…

低代码赋能业务创新:打破数字鸿沟,释放业务潜能

在数字化转型的浪潮中,一个突出的矛盾日益显现:业务部门汹涌的创新需求,与IT部门有限的开发资源之间,形成了一道难以逾越的“数字鸿沟”。当市场部门需要一个临时的活动报名系统,当HR部门渴望一个高效的内部推荐工…

【大模型】大模型训练的几个不同阶段

总结:各方法的典型关联(以大语言模型为例)Pre-Training:先让模型学“通识知识”(如语言、世界知识)。 Supervised Fine-Tuning (SFT):用标注数据让模型学“任务基本模式”(如指令遵循)。 Reward Modeling:训…

详细介绍:1、手把手教你入门设计半桥LLC开关电源设计,LLC谐振腔器件计算

详细介绍:1、手把手教你入门设计半桥LLC开关电源设计,LLC谐振腔器件计算pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family…

十六天

今日重点学习关系型数据库基础,核心掌握三个模块:一是数据表的结构化设计,明确字段类型(如INT、VARCHAR)需与数据属性匹配,避免后续数据存储异常;二是主键的作用,通过实操验证其“唯一标识记录”的必要性——未…

10/20/2025杂题 关于在线性时间内求解低次多项式的幂

例 设 \(g = ax^2 + bx + c\),求: \[ f = g^n\]其中 \(0 \leq n \leq 3 \times 10^5\)。结果对 \(10^9 + 7\) 取模。 首先可以直接用 MTT 在 \(O(n \log n)\) 的时间复杂度内求解。然而此做法常数太大,在需要多次求…

歌手与模特儿

https://www.luogu.com.cn/problem/AT_nikkei2019_2_final_h 第一次见到能 manacher 但不能二分+哈希的题! 直接上 manacher,当尝试将区间拓展为 \([l,r]\) 时,考察 \(nxt_l\) 和 \(lst_r\) 的位置关系,可以 check…

20251019

正睿 NOIP 十连测 B 有 \(n\) 个数 \(a_1 \sim a_n\)。初始有一个 \(x = 1\),每次需要将 \(x\) 变为某个 \(i\),花费代价为 \(\min(|i - x|, n - |i - x|)\),且 \(a_x \le a_i\)。问访问所有 \(i\) 需花费的最小代价…

计算机毕业设计 基于EChants的海洋气象数据可视化平台设计与建立 Python 大数据毕业设计 Hadoop毕业设计选题【附源码+文档报告+安装调试】

计算机毕业设计 基于EChants的海洋气象数据可视化平台设计与建立 Python 大数据毕业设计 Hadoop毕业设计选题【附源码+文档报告+安装调试】pre { white-space: pre !important; word-wrap: normal !important; overflo…

SpringBoot整合Redis教程

一、Redis 简介 Redis(Remote Dictionary Server)是一个开源的高性能键值对存储数据库,基于内存运行并支持持久化,常用于缓存、会话存储、消息队列等场景。其核心特点包括:速度快:基于内存操作,单线程模型避免上…

https://www.luogu.com.cn/problem/CF1635E

考虑一个事情,两辆车方向一定相反,弱化限制后,建二元关系图,发现一定是一张二分图。 钦定左部点为向左,其他点为向右,然后发现位置满足一个二元大小关系限制,建 DAG 跑拓扑序即可。

ZR 2025 NOIP 二十连测 Day 5

85 + 32 + 5 + 5 = 127, Rank 67/128.呜呜我错了……我再也不开太大的 vector 了呜呜……/dk /dk /dk25noip二十连测day5 链接:link 题解:题目内 时间:4h (2025.10.20 14:00~18:00) 题目数:4 难度:A B C D\(\colo…

关于单片机内部ADC采样率,采样精度的理解与计算整理 - 实践

关于单片机内部ADC采样率,采样精度的理解与计算整理 - 实践pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Conso…

整体架构与数据流

下面给出对题目及当前代码求解方法的系统、深入解析,涵盖三问建模逻辑、数据流、关键算法、假设与局限、以及改进建议。内容按“题目需求 -> 代码实现 -> 差异/假设 -> 评估/改进”结构展开,方便你写论文或…

【上青了】

【上青了】赶紧把面板记录一下先,怕下次又又又掉了 没什么好讲的,本来上场打完就差 11 分,这场只要正常发挥就没问题变色,所以也没什么激动,该激动的上次都激动完了,哎哎哎 要说就是这次状态还行,不算差,前面 …

[VIM] reverse multiple lines in VIM

推荐方法: If you’re on a Unix-like system (FreeBSD, Linux, macOS), use :14,19!tac.来自chatgptTo reverse the display order of lines 51 to 54 in Vim, you can use the :g and :tac-style command combinati…

DeviceNet 转 Ethernet/IP:三菱 Q 系列 PLC 与欧姆龙 CJ2M PLC 在食品饮料袋装生产线包装材料余量预警的通讯配置案例

案例背景 DeviceNet 转 Ethernet/IP在食品饮料行业,包装生产线涉及多种设备,如灌装机、贴标机、封口机等。不同设备可能采用不同的工业总线协议,为了实现整个包装生产线的自动化控制和数据共享,需要将采用 Etherne…

【大模型】【扫盲】几种不同的微调方法

四种微调方式Full 对预训练模型的所有参数进行微调,让模型从底层到顶层的所有参数都参与更新,彻底适配下游任务 优点:模型对任务的适配性最强,在数据充足、任务复杂时效果通常最优 缺点:资源消耗极大(千亿参数模…

Tuack 生成比赛题目 PDF 笔记

Part 1. Tuack 的基本介绍 Tuack,是一个由来自 THU 的大佬 Mulab11 开发的,用于造算法竞赛题目的工具。 你可以用它:导出 PDF、Markdown、HTML 等多种不同格式,NOI、CPC 等不同风格的题面。 简单的出题人自评测功能…

在 wrapper 类里实现重载方法

现有包装类 Wrapper 欲覆盖 inner 的方法 g class A{void f(){g();}void g(){} }class Wrapper extends A{A inner;void f(){inner.f() // 不调用 Wrapper.g}void g(){ // override g} }inner 通过 this.g 调用时仍然会…