65.9K
CodeProject 正在变化。 阅读更多。
Home

另一个 FFmpeg.exe C# 包装器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (21投票s)

2014年5月17日

CPOL

5分钟阅读

viewsIcon

116620

downloadIcon

5787

使用 FFmepg.exe 创建视频快照

 
如果您正在寻找 FFmpeg 的 PInvoke 包装器,请查看我关于它的新文章:调用 FFmpeg 导出帧

引言

我正在为我的视频文件创建一个库,并希望它能够尽可能多地从文件中提取信息,以便用户(主要是我自己)能够尽可能懒惰地添加新视频。信息还包括视频快照,这样您就可以立即看到视频文件是什么。本文将介绍如何从几乎任何视频文件中截取快照。

背景

我在工作中主要使用 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 输出数据的示例

 

© . All rights reserved.