另一个 FFmpeg.exe C# 包装器





5.00/5 (21投票s)
使用 FFmepg.exe 创建视频快照
引言
我正在为我的视频文件创建一个库,并希望它能够尽可能多地从文件中提取信息,以便用户(主要是我自己)能够尽可能懒惰地添加新视频。信息还包括视频快照,这样您就可以立即看到视频文件是什么。本文将介绍如何从几乎任何视频文件中截取快照。
背景
我在工作中主要使用 C# 3.5 CF,可耻的是,我对其他编程语言的经验不多。在处理紧凑型框架和各种移动设备时,会有一种妥协,那就是如果您想做 Windows 操作系统在设备上没有原生提供的功能,您最终会进行大量的即兴创作。幸运的是,这种技能让我为视频库实现了目标 - 这是高度即兴创作的。
我尝试在 C# 中使用 ActiveX 及其 COM 接口。在编辑 COM 接口后,我设法在指定位置抓取帧 - 我不记得具体位置了,但我必须将一个字节参数替换为 IntPtr 参数。当我尝试使用我的标准测试视频 (AVI DivX MP3) 以外的其他视频格式时,例如带有 H.264 视频和 AAC 音频编解码器的 MP4 容器或简单的 FLV 视频,结果令人失望。尽管我安装了正确的编解码器,但 MediaDet 类无法处理这些类型。我做了一些研究,发现这些编解码器似乎缺少 ActiveX 使用的一个接口。
我的第二个方法是使用许多 FFmpeg 包装器之一,它们将 FFmpeg DLL 直接包装到 C# 中。但它们对我不起作用。有些具有一定的功能,但搜索(抓取快照最重要的方法之一)在不解码到该点的整个视频的情况下无法工作,这当然花费了太长时间。
我玩了一些 ffmpeg 命令行实用程序,发现它们实际上正是我所需要的 - 只是必须使用文件有点麻烦。
获取媒体信息
首先,我想解释如何使用命令行参数来抓取媒体信息和快照。
ffprobe.exe 使用以下参数提供视频属性的命令行输出
-hide_banner | 隐藏命令行输出开头的横幅 |
-show_format | 输出有关视频文件的常规信息 |
-show_streams | 输出有关视频文件中每个流的信息 |
-pretty | 以 MS INI 格式格式化输出,带有 [/...] 结束标签 |
{file} | 输入文件 - 必须放在最后 |
所以命令行应该看起来像这样
ffprobe.exe -hide_banner -show_format -show_streams -pretty {video_file}
要使用 C# 读取命令行输出,必须启动一个带有重定向输出的进程。因此,我编写了这个辅助方法来执行命令并在进程终止后返回其输出。
private static string Execute(string exePath, string parameters)
{
string result = String.Empty;
using (Process p = new Process())
{
p.StartInfo.UseShellExecute = false;
p.StartInfo.CreateNoWindow = true;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.FileName = exePath;
p.StartInfo.Arguments = parameters;
p.Start();
p.WaitForExit();
result = p.StandardOutput.ReadToEnd();
}
return result;
}
输出看起来像这个例子
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'c:\file.mp4':
Metadata:
major_brand : isom
minor_version : 1
compatible_brands: isomavc1
creation_time : 2013-05-05 07:16:05
Duration: 01:06:09.07, start: 0.000000, bitrate: 887 kb/s
Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, bt470bg), 720x576 [SAR 64:45 DAR 16:9], 706 kb/s, 25 fps, 25 tbr, 25k tbn, 50 tbc (default)
Metadata:
creation_time : 2013-05-05 07:16:05
Stream #0:1(und): Audio: aac (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 176 kb/s (default)
Metadata:
creation_time : 2013-05-05 07:16:07
[STREAM]
index=0
codec_name=h264
codec_long_name=H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10
profile=High
codec_type=video
codec_time_base=1/50
codec_tag_string=avc1
codec_tag=0x31637661
width=720
height=576
duration=1:06:08.760000
bit_rate=706.941000 Kbit/s
[/STREAM]
[STREAM]
index=1
codec_name=aac
codec_long_name=AAC (Advanced Audio Coding)
codec_type=audio
codec_time_base=1/48000
codec_tag_string=mp4a
codec_tag=0x6134706d
duration=1:06:09.066667
bit_rate=176.062000 Kbit/s
[/STREAM]
[FORMAT]
filename=c:\file.mp4
nb_streams=2
nb_programs=0
format_name=mov,mp4,m4a,3gp,3g2,mj2
format_long_name=QuickTime / MOV
duration=1:06:09.066667
size=419.768014 Mibyte
bit_rate=887.178000 Kbit/s
[/FORMAT]
所以我只需要解析这些信息。在附加的类中,这将在构造函数中完成。
拍摄快照
ffmpeg.exe 语法最重要的方面是使用的参数始终适用于下一个提到的文件(输入或输出) - 因此,您首先说明选项,然后是使用的文件。我将使用这些选项
-hide_banner | 隐藏命令行输出开头的横幅 |
-ss {hh:mm:ss.fff} | 跳转到视频中的指定位置 - 如果在输入之前定义,则搜索输入视频;如果在输出之前定义,则解码输入直到该位置 |
-i {file} | 定义输入文件 |
-r {n} | 设置强制帧率 |
-t {n} | 设置要输出的帧长度 |
-f {format} | 设置输入或输出使用的强制格式 - 我使用 'image2' 来获得 JPEG 输出 |
{file} | 输出文件 - 必须放在最后 |
所以调用的命令行应该是这样的
ffmpeg.exe -hide_banner -ss {timespan} -i {video_file} -r 1 -t 1 -f image2 {temp_file}
为了在拍摄快照时抑制命令行控制台窗口的显示 - 这取决于视频文件和计算机性能,最多可能需要几秒钟 - 我使用与上面相同的方法来执行命令。C# 提供了一种直接获取临时文件名的方法,所以这里几乎没有什么不寻常的
public Bitmap GetSnapshot(TimeSpan atPosition, string filename)
{
if (filename.Contains(' '))
filename = "\"" + filename + "\"";
string tmpFileName = Path.GetTempFileName();
if (tmpFileName.Contains(' '))
tmpFileName = "\"" + tmpFileName + "\"";
string cmdParams = String.Format("-hide_banner -ss {0} -i {1} -r 1 -t 1 -f image2 {2}",
atPosition, filename, tmpFileName);
Bitmap result = null;
try
{
Execute(FFMPEG_EXE_PATH, cmdParams);
if (File.Exists(tmpFileName))
{
byte[] fileData = File.ReadAllBytes(tmpFileName);
result = new Bitmap(new MemoryStream(fileData));
File.Delete(tmpFileName);
}
}
catch { }
return result;
}
棘手的部分是如何将保存的位图加载到 C# 中:如果您使用 new Bitmap(tmpFileName)
创建图像,文件将被锁定直到 Bitmap 被释放,因此 tmpFileName 无法被删除。所以,我首先读取所有字节,然后使用 MemoryStream 初始化 Bitmap。
使用代码
我将这些方法与一些其他辅助方法一起包装到了附加的类中。您可以使用类似以下方式来简单地使用它:
FFmpegMediaInfo info = new FFmpegMediaInfo("C:\file.mp4");
double length = info.Duration.TotalSeconds;
double step = length / 10;
double pos = 0.0;
Dictionary<TimeSpan, Bitmap> snapshots = new Dictionary<TimeSpan,Bitmap>();
while (pos < length)
{
TimeSpan position = TimeSpan.FromSeconds(pos);
Bitmap bmp = info.GetSnapshot(position);
snapshots[position] = bmp;
pos += step;
}
此示例打开 C:\file.mp4 文件 - 视频信息在构造函数中自动加载,因此持续时间是已知的。然后,每十分之一的视频时长拍摄一个快照,并以时间戳作为键存储在 Dictionary 中。
历史
版本 1.2 的更改
- 为属性添加了描述性注释
- 为代码添加了更多注释
- 在 Int32 和 Int64 解析周围添加了 try-catch 包装器
- 使用 Split() 而不是 IndexOf() 和 Substring() 进行输出行解析
- 添加了 ffprobe.exe 输出数据的示例