最近更新于 2024-05-05 23:07
1 前言
以前只是单纯拿 FFmpeg 命令行工具来编码视频(图片合成视频、视频格式转换、编码器转换),并没有深入学习过,这次打算尝试一下使用 FFmpeg 的库搞搞开发。
2 测试环境
-
Intel i7-12700H(Iris Xe Graphics)
-
NVIDIA RTX4060 Laptop GPU
-
Windows 11 专业版 23H2
-
ffmpeg-master-latest-win64-gpl-shared – Auto-Build (2024-02-18 12:47):https://github.com/BtbN/FFmpeg-Builds/releases
-
Visual Studio 2022(指定 C17 标准编译测试)
3 配置开发环境
纯配置命令行工具可以参考:https://blog.iyatt.com/?p=8613
这里要做开发,下载的时候选动态库版本,里面也包含了命令行工具
bin 目录中的是命令行工具(可执行文件),添加到 PATH 环境变量,就能在终端直接执行。FFmpeg 是 C 语言开发的,include 中是头文件,lib 目录中的 lib 文件相当于动态库的索引,bin 目录中的 dll 文件是动态库的实现。
现在选一个路径创建项目目录,我在桌面创建了一个 FFmpeg 目录。将上面的 include 和 lib 两个目录复制到创建的目录中,另外将上面的 bin 目录中的 .dll 文件也复制到项目目录的 lib 目录中。在项目目录中再创建一个 src 目录,用于存放自己编写的代码。
Visual Studio 中记得勾选安装 C++ 开发,这里面包含 C/C++ 开发的工具链
3.1 创建 Visual Studio 项目演示
创建项目演示
打开 VS 创建一个空项目
路径选在创建的 src 目录
新建源码
文件命名时注意扩展名写 .c,Visual Studio 默认给的扩展名是 .cpp(C++,C Plus Plus)。谨记 C 语言和 C++ 是两门语言,只是 C++ 基本兼容了 C 语言的语法,但是两者在编译器处理上很大不同,单纯源码兼容性还高一些,这里是直接链接 FFmpeg 的 C 语言库文件(已经按照 C 语言编译),最明显的一点就是 C++ 为了函数重载,在编译时为了区别同名函数,会加上参数类型重命名,而 C 语言没有这个,在已经编译好的库文件中,C++ 引用 C 库就可能出现问题。一定要在 C++ 中引用 C 的话,需要使用 extern “C” { } 把引用 C 头文件部分用大括号括起来。这样编译的是时候在括起来的部分就会自动调用 C 语言编译器来编译,其它部分按照 C++ 编译。
然后在项目名上右键-属性
注意属性页上方可选,是配置给 Debug(调试)还是 Release(发布版)或者所用,以及 64 位配置还是 32 位配置,对应着编译模式。
初学阶段也不涉及发布自己编写的软件可执行程序,不会针对不同版本配置不同的参数,这个其实无所谓。
在 C/C++ -> 常规 -> 附加包目录 中添加上 include 的路径(两个英文句点代表上级目录)
根据自己的项目目录来,按照我这里的项目结构,在三个上级目录以上才是 include
同样的在 链接器 -> 常规 -> 附加库目录 写入 lib 路径
在 链接器 -> 输入 -> 附加依赖项 中添加上需要使用的库的 lib 文件
下面的测试案例中需要的库文件
也可以不在属性里配置 lib 文件,直接在代码中引用头文件后加上对应的语句,比如这里的
#pragma comment(lib, "avutil.lib")
写个简单的例子测试
在前面新建的 .c 文件写入下面内容
#include <stdio.h>
#include <libavutil/avutil.h>
int main()
{
printf("%s\n", av_version_info());
}
调试运行
这段代码会显示当前 FFmpeg 库的版本
3.2 创建 CMake 项目演示(Visual Studio 或 Visual Studio Code)
VS 默认项目管理是自己的方式,VS 也是支持 CMake 的,在添加 C++ 开发工具时,里面默认就包含了 CMake,即可以采用 VS + CMake + MSVC 的组合,把 VS 换成 VScode 也行。以前在 Linux 下搞开发的时候,我基本就是用 CMake 管理项目,采用 VScode + CMake + GNU 的搭配,所以我是比较倾向于使用 CMake。VS 项目管理属性里的那一堆东西就全部在 CMakeLists.txt 里配置了,下面我给出了一个参考模板。
CMakeLists.txt 文件参考
cmake_minimum_required (VERSION 3.8) # CMake 最低版本要求
project(test) # 项目名称
set(CMAKE_C_STANDARD 17) # C 语言编译标准(C17)
set(exe_name test) # 自定义变量,用于统一设定生成的可执行文件名字,这里设定为 test,就会生成 test.exe 文件
include_directories(${CMAKE_SOURCE_DIR}/../../include/) # 头文件路径 - 路径注意用斜杠,下同
# link_directories(${CMAKE_SOURCE_DIR}/../../lib/) # 链接库方式一。这种方式指定库目录,源码中需要使用 #pragma 链接
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_SOURCE_DIR}/build/) # 指定生成文件路径
add_compile_options(/GS-) # 这里填写的参数会传递给编译器。/CG- 禁用安全检查
# 链接库方式二
# 三段参数:
# ffmpeg 是个变量,存放找到的库
# 要寻找的库文件
# 查找的路径
find_library(ffmpeg avutil.lib ${CMAKE_SOURCE_DIR}/../../lib/)
aux_source_directory(src ${CMAKE_SOURCE_DIR}/) # 自动寻找指定目录下的源文件。${CMAKE_SOURCE_DIR} 变量存储着主脚本文件CMakeLists.txt自己所在路径,即本文件同级目录。
add_executable(${exe_name} main.c) # 编译源文件
target_link_libraries(${exe_name} ${ffmpeg}) # 将上面找到的库文件链接到编译好的目标文件中
# 链接库方式三:手动指定链接,这里列出了所有 FFmpeg 库文件
# set(FFmpeg_LIB_PATH ${CMAKE_SOURCE_DIR}/../../lib/)
# target_link_libraries(${exe_name}
# ${FFmpeg_LIB_PATH}/avcodec.lib
# ${FFmpeg_LIB_PATH}/avdevice.lib
# ${FFmpeg_LIB_PATH}/avfilter.lib
# ${FFmpeg_LIB_PATH}/avformat.lib
# ${FFmpeg_LIB_PATH}/avutil.lib
# ${FFmpeg_LIB_PATH}/postproc.lib
# ${FFmpeg_LIB_PATH}/swresample.lib
# ${FFmpeg_LIB_PATH}/swscale.lib
# )
4 一些可能用到的
4.1 VS 禁止安全检查
Visual Studio 非常讨厌的一点就是默认强制使用安全函数,而其它编译器至少我还没见过支持的。实际上安全函数不会用,一样不安全,标准函数用好了一样安全,这个是看使用者。
在项目名上,右键打开属性。点开 C/C++ -> 代码生成 -> 安全检查,选择禁用
4.2 文件选择
这里写一个文件选择的 demo,基于 Windows API
#include <Windows.h>
#include <stdio.h> // printf 函数用到
int main()
{
OPENFILENAME ofn; // 打开文件对话框结构体
char file_name[MAX_PATH]; // 存储文件路径的缓冲区,打开文件后路径就保存在这里
// 初始化填充为 0
ZeroMemory(&ofn, sizeof(ofn));
ZeroMemory(file_name, sizeof(file_name));
ofn.lStructSize = sizeof(ofn);
ofn.lpstrFile = file_name;
ofn.lpstrFile[0] = '\0';
ofn.nMaxFile = sizeof(file_name);
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;
if (GetOpenFileName(&ofn)) // 打开文件对话框
{
printf("你选择的路径是:");
WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), file_name, wcslen(file_name), NULL, NULL); // 打印到终端,不要使用 printf,这里是宽字符,printf 不能正常显示
}
return 0;
}
5 FFmpeg 官方参考案例
官方文档:https://ffmpeg.org/documentation.html
选择 API 版本进入对应的文档,在 API 文档中 Examples 栏中有案例程序
我打算是把参考案例理一遍搞懂,可能做一定功能修改,添加中文注释,再放到下文中,基于 API 6.0。
5.1 视频格式转换
这个案例来源于 remux.c
单纯重新打包文件格式,不会重新编码。传入两个参数,第一个参数输入视频,第二个参数为要生成的视频文件名,程序通过扩展名识别视频格式。
相当于命令
ffmpeg -i 输入视频 -c:v copy 输出视频
#include <libavutil/timestamp.h>
#include <libavformat/avformat.h>
/**
* @brief 打印 AVPacket 信息
* @param fmt_ctx
* @param pkt
* @param tag
*/
static void log_packet(const AVFormatContext* fmt_ctx, const AVPacket* pkt, const char* tag)
{
AVRational* time_base = &fmt_ctx->streams[pkt->stream_index]->time_base;
printf("%s: pts:%s pts_time:%s dts:%s dts_time:%s duration:%s duration_time:%s stream_index:%d\n",
tag,
av_ts2str(pkt->pts), av_ts2timestr(pkt->pts, time_base),
av_ts2str(pkt->dts), av_ts2timestr(pkt->dts, time_base),
av_ts2str(pkt->duration), av_ts2timestr(pkt->duration, time_base),
pkt->stream_index);
}
int main(int argc, char** argv)
{
if (argc < 3)
{
printf("用法:%s 输入 输出\n"
"使用 libavformat 和 libavcodec 转换视频格式\n"
"根据文件扩展名推测格式\n", argv[0]);
return 1;
}
AVFormatContext* ifmt_ctx = NULL; // 用于储存输入格式的上下文信息
const char* in_filename = argv[1]; // 从命令行参数列表获取输入文件名
int ret; // 用于接受后续多个函数返回值,以便判断异常
if ((ret = avformat_open_input(&ifmt_ctx, in_filename, NULL, NULL)) < 0)
{
fprintf(stderr, "不能打开输入文件:%s\n", in_filename);
goto end;
}
if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0)
{
fprintf(stderr, "检索输入流信息失败\n");
goto end;
}
av_dump_format(ifmt_ctx, 0, in_filename, 0); // 打印输入文件的格式和流信息
const char* out_filename = argv[2]; // 从命令行参数列表获取输出文件名
AVFormatContext* ofmt_ctx = NULL; // 用于储存输出文件的音视频格式的上下文信息
avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, out_filename);
if (!ofmt_ctx)
{
fprintf(stderr, "不能创建输出格式上下文\n");
ret = AVERROR_UNKNOWN;
goto end;
}
unsigned int stream_mapping_size = ifmt_ctx->nb_streams; // 获取输入文件的流个数
int* stream_mapping = av_calloc(stream_mapping_size, sizeof(int*));
if (!stream_mapping)
{
ret = AVERROR(ENOMEM);
goto end;
}
const AVOutputFormat* ofmt = ofmt_ctx->oformat; // 获取输出格式上下文的格式
int stream_index = 0;
// 遍历输入文件的每个流
for (unsigned int i = 0; i < stream_mapping_size; ++i)
{
AVStream* in_stream = ifmt_ctx->streams[i]; // 输入流
AVCodecParameters* in_codecpar = in_stream->codecpar; // 获取输入流的编解码参数
if (in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO &&
in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO &&
in_codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) // 输入类型不是音频、视频或做字幕时,不复制该流
{
stream_mapping[i] = -1;
continue;
}
stream_mapping[i] = stream_index++;
AVStream* out_stream = avformat_new_stream(ofmt_ctx, NULL);
if (!out_stream)
{
fprintf(stderr, "创建输出流失败\n");
ret = AVERROR_UNKNOWN;
goto end;
}
if ((ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar)) < 0)
{
fprintf(stderr, "复制输入流的编解码参数到输出流失败\n");
goto end;
}
out_stream->codecpar->codec_tag = 0; // 设置为 0,让 FFmpeg 自己选择
}
av_dump_format(ofmt_ctx, 0, out_filename, 1); // 打印输出文件的格式和流信息
if (!(ofmt->flags & AVFMT_NOFILE)) // 是否需要输出文件
{
if ((ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE)) < 0)
{
fprintf(stderr, "打开输出文件失败: %s\n", out_filename);
goto end;
}
}
if ((ret = avformat_write_header(ofmt_ctx, NULL)) < 0)
{
fprintf(stderr, "在写入输出文件的头部信息时发生错误\n");
goto end;
}
AVPacket* pkt = av_packet_alloc(); // 分配空间,用于存储压缩数据(视频、音频)的数据结构
if (!pkt)
{
fprintf(stderr, "不能创建 AVPacket\n");
return 1;
}
while (1)
{
if ((ret = av_read_frame(ifmt_ctx, pkt)) < 0)
{
break;
}
AVStream* in_stream = ifmt_ctx->streams[pkt->stream_index];
if (pkt->stream_index >= stream_mapping_size ||
stream_mapping[pkt->stream_index] < 0)
{
av_packet_unref(pkt);
continue;
}
pkt->stream_index = stream_mapping[pkt->stream_index];
AVStream* out_stream = ofmt_ctx->streams[pkt->stream_index];
log_packet(ifmt_ctx, pkt, "in");
av_packet_rescale_ts(pkt, in_stream->time_base, out_stream->time_base);
pkt->pos = -1;
log_packet(ofmt_ctx, pkt, "out");
if ((ret = av_interleaved_write_frame(ofmt_ctx, pkt)) < 0)
{
fprintf(stderr, "Error muxing packet\n");
break;
}
}
av_write_trailer(ofmt_ctx);
end:
av_packet_free(&pkt);
avformat_close_input(&ifmt_ctx);
if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE))
{
avio_closep(&ofmt_ctx->pb);
}
avformat_free_context(ofmt_ctx);
av_freep(&stream_mapping);
if (ret < 0 && ret != AVERROR_EOF)
{
fprintf(stderr, "发生错误:%s\n", av_err2str(ret));
return 1;
}
return 0;
}