调用 FFmpeg 导出帧
4.95/5 (11投票s)
直接在 C# 中使用 ffmpeg DLL 提取帧并创建缩略图
引言
我之前发表了一篇名为“Another FFmpeg.exe C# Wrapper”的文章,通过命令行参数使用 FFmpeg.exe 来提取帧图像。但这种方法并不能让我满意,因为它一点都不干净。所以我开始研究直接在 .NET 中通过 PInvoke 调用 FFmpeg DLL 的可能性,在尝试了许多无效的项目后,我找到了 FFmpeg.AutoGen,它从一开始就可以配合示例工作。
解决方案包含两部分:一个 .NET 库,用于映射 FFmpeg DLL 并使用它们从多媒体文件中提取帧图像;一个应用程序,用于使用该库轻松创建缩略图。
这个项目的核心基于 Ruslan-B 在 GitHub 上的 FFmpeg.AutoGen 包装器(https://github.com/Ruslan-B/FFmpeg.AutoGen)。我做了一些修改,主要是为了支持元数据提取,否则我将直接导入该项目。
为了了解查找多媒体文件信息、搜索视频流和提取帧图像所需的类和函数,我使用了 FFmpeg 文档:https://ffmpeg.net.cn/doxygen/trunk/
使用应用程序

该应用程序可用于从多媒体文件创建缩略图。您可以定义缩略图的布局。以下参数可以设置
- 视频帧的列数和行数
- 单个帧的宽度(像素)
- 整个缩略图周围的边距(像素)
- 帧之间的间距(两次标题和帧之间)像素
- 背景颜色
- 标题字体样式和颜色
- 索引(时间位置)字体样式、文本和阴影颜色
- 帧的边框颜色以及是否绘制边框
还有一些选项可以配置应用程序应如何运行
- 当通过命令行传递的作业完成时自动关闭应用程序
- 自动选择文件名(与电影文件相同,但扩展名不同)
- 图像格式 Bitmap、GIF、JPEG、PNG 或 TIFF(如果选择 JPEG,则包括质量)
- 使用精确时间位置或仅使用关键帧
有两种方法可以设置这些选项:使用命令行参数或配置文件。
配置文件是标准的 MS INI 文件。您可以通过应用程序的“设置”对话框或使用文本编辑器来编辑或创建标准配置文件(文件名与应用程序相同,但扩展名为 .ini 而非 .exe)。选项必须位于“SheetOptions”部分。以下选项可用
| 选项 | 描述 |
|---|---|
| OutputFormat | 输出格式(bmp、gif、jpg、png 或 tif) |
| JpegQuality | 如果输出格式为 jpg,则输出质量(0-100) |
| AutoOutputFilename | 自动选择输出文件名,它将与多媒体文件相同,只是图像扩展名不同(0:禁用,1:启用) |
| AutoClose | 通过命令行参数传递的作业完成后自动关闭应用程序(0:禁用,1:启用) |
| ThumbColumns | 缩略图列数 |
| ThumbRows | 缩略图行数 |
| ThumbWidth | 缩略图宽度(像素) |
| Margin | 边距(像素) |
| 填充 | 缩略图之间的间距(像素) |
| BackgroundColor | 背景颜色(HEX 格式 - 例如,使用 RGB 的 #443322 或使用 ARGB 的 #88443322) |
| HeaderColor | 标题文本颜色(HEX 格式 - 例如,使用 RGB 的 #FFFFFF 或使用 ARGB 的 #88FFFFFF) |
| IndexColor | 索引文本颜色(HEX 格式 - 例如,使用 RGB 的 #FFFF88 或使用 ARGB 的 #88FFFF88) |
| IndexShadowColor | 索引阴影颜色(HEX 格式 - 例如,使用 RGB 的 #000000 或使用 ARGB 的 #88000000) |
| ThumbBorderColor | 帧边框颜色(HEX 格式 - 例如,使用 RGB 的 #000000 或使用 ARGB 的 #88000000) |
| DrawThumbnailBorder | 为每个帧绘制边框(0:禁用,1:启用) |
| ForceExactTimePosition | 为每个帧使用精确的时间位置(0:禁用/仅关键帧,1:启用) |
| HeaderFont | 标题文本的字体样式(格式:style|size|name)
|
| IndexFont | 索引文本的字体样式(格式:style|size|name)
|
这些是命令行参数,它们将覆盖配置文件中的设置
| 参数 | 描述 |
|---|---|
| 文件 | 多媒体输入文件 |
| /? | 显示一个包含可能命令行参数的对话框 |
| /A=0|1 | 启用(1)或禁用(0)自动输出文件名 |
| /F=0|1 | 启用(1)或禁用(0)使用精确时间位置(速度较慢但更准确) |
| /C=N | 将缩略图列数设置为 N |
| /R=N | 将缩略图行数设置为 N |
| /W=N | 将缩略图宽度设置为 N 像素 |
| /M=N | 将边距设置为 N 像素 |
| /P=N | 将缩略图间距设置为 N 像素 |
| /X=0|1 | 作业完成后启用(1)或禁用(0)自动退出 |
| /I=N | 设置输出格式 N:0=BMP, 1=GIF, 2=JPG, 3=PNG, 4=TIF |
| /O=file | 定义手动输出文件 - 与输入文件顺序相同,如果输入文件多于输出文件,则为每个不完整的对使用“另存为”对话框或自动文件名 |
| /V=file | 覆盖此配置文件中的标准选项 |
可以通过启动应用程序然后使用 **Open** 按钮选择一个或多个多媒体文件,或者通过将多媒体文件 **拖放** 到应用程序窗体上来创建缩略图,也可以通过命令行参数传递多媒体文件
ThumbSheetCreator.exe /I=0 /C=6 /R=2 /X=1 /F=1 /A=1 C:\MyMovie.mp4
使用代码
主类是 FFmpegMediaInfo ,如果您打算使用此代码,应该查看它。您应该使用想要使用的多媒体文件对其进行初始化。然后它将收集有关该文件的基本信息。然后可以使用信息和提取方法。不要忘记在使用完该类后将其释放 - 或者简单地使用“using”
// Load the multimedia file
using (FFmpegMediaInfo info = new FFmpegMediaInfo(@"C:\Videos\Test.mp4"))
{
// Get the duration
TimeSpan d = info.Duration;
string duration = String.Format("{0}:{1:00}:{2:00}", d.Hours, d.Minutes, d.Second);
// Get the video resolution
Size s = info.VideoResolution;
string resolution = String.Format("{0}x{1}", s.Width, s.Height);
// Get the first video stream information
FFmpegStreamInfo vs = info.Streams
.FirstOrDefault(v => v.StreamType == FFmpegStreamType.Video);
// If the video stream exists, extract two random frames
List<Bitmap> imgs = new List<Bitmap>();
if (vs != null)
{
// Prepare random timestamps
Random rnd = new Random();
long dTicks = info.Duration.Ticks;
TimeSpan t1 = new TimeSpan(Convert.ToInt64(dTicks * rnd.NextDouble()));
TimeSpan t2 = new TimeSpan(Convert.ToInt64(dTicks * rnd.NextDouble()));
// Extract images
imgs = info.GetFrames(
info.Streams.IndexOf(vs), // stream index
new List<TimeSpan>() { t1, t2 }, // time positions of the frames
true, // force exact time positions; not previous keyframes only
(index, count) =>
{
// Get the progress percentage
double percent = Convert.ToDouble(index) / Convert.ToDouble(count) * 100.0;
return false; // Not canceling the extraction
}
);
}
// Extract a standard 6x5 frame thumbnail sheet from the default video stream
Bitmap thumb = info.GetThumbnailSheet(
-1, // Default video stream, will throw Exception if there is none
new VideoThumbSheetOptions(6, 5), // preset sheet options with 6 columns and 5 rows
(index, count) =>
{
// Get the progress percentage
double percent = Convert.ToDouble(index) / Convert.ToDouble(count) * 100.0;
return false; // Not canceling the extraction
}
);
}
FFmpegMediaInfo - 基本方法
如果您需要更多功能,可以随时编辑 FFmpegMediaInfo 类 - 它并不意味着是完整的!有关如何使用 FFmpeg 类和函数的信息可以在 FFmpeg 文档 中找到。调用是实例安全的 - 意味着您可以同时使用多个应用程序实例;但我不确定同一实例中的线程,因为我还没有尝试过。代码会自动使用 32 位或 64 位版本的 FFmpeg,具体取决于选择的平台类型 - 即使选择了“any platform”,对于合适的系统也会使用 64 位版本。因此,两种版本都应始终提供!
首先,这是代码的基本方法:当加载文件时(调用的函数是 OpenFileOrUrl),FFmpeg 的所需部分将被初始化。之后,文件将被 FFmpeg 加载,并将其信息存储到 AVFormatContext 实例中。这个类已经包含了 FFmpegMediaInfo 提取的大部分信息。 nb_streams 字段的 AVFormatContext 包含有关不同流的信息,类型为 AVStream,这些流稍后在提取图像时会用到。AVFormatContext 和 AVStream 的元数据都存储为 AVDictionary 类型,其中一些函数需要将其转换为 Dictionary<string, string> 类型。信息以及 AVFormatContext 和 AVStream 实例都存储在 FFmpegMediaInfo 实例中。
该类专门用于查找时间位置。为了避免遍历到指定时间位置的每一帧,在查找时会选择该时间位置之前的一个关键帧。要搜索流,将使用函数 av_seek_frame(),并将 AVStream 的 skip_to_keyframe 设置为 1,并将标志 AVSEEK_FLAG_BACKWARD 设置为 1。然后(如果启用了 ForceExactTimePosition),将解码后续帧,直到流中的实际时间位置在时间基(每帧时间)内,或者再次偏离查找的时间位置。在查找过程中,必须将 AVFormatContext 的 seek2any 字段设置为 0 - 否则查找很可能会在两个关键帧之间结束,并且解码到下一个关键帧会导致图像损坏,因为中间的帧依赖于先前的关键帧。要提取帧图像,首先必须使用函数 av_read_frame() 加载所选视频流的下一个包,然后必须使用函数 avcodec_decode_video2() 解码该包,使用函数 sws_scale() 查找图像参数,然后可以将所有内容传递给 .NET Bitmap 类来加载图像。要确定解码帧的时间位置,可以使用函数 av_frame_get_best_effort_timestamp()。
关注点
使用 FFmpeg 和 PInvoke 时最烦人的一点是,如果出现错误,很难找到。我只能鼓励您多尝试和调试 - Visual Studio 会在您中断而未释放时负责释放已分配的内存。
使用关键帧模式存在一个我尚未解决甚至尚未找到的主要 bug:缩略图使用较快的关键帧方法创建时,前几张图片似乎总是相同的,并且时间戳与目标时间相差很多。
未来计划
说实话,我没有太多时间来处理这个项目,所以不要期望很快会有更新!不过,这是我目前想到的
- 修复使用关键帧模式时的相同图片 bug
- 实现一个类似枚举器的可搜索帧提供程序
法律
本文适用的 GNU Lesser General Public License v3 (LGPL3) 适用于 FFmpeg.AutoGen、FFmpeg 以及我编写的代码。对于 FFmpeg,许可证设置为 LGPL2.1 或更高版本。
