TLDR:帧速率非常低是由于启动之间存在巨大的延迟ffmpeg
进程和实际数据开始发送ffmpeg
.
我对您的另一篇文章(“How to Play a Video in a PictureBox using FFmpeg in C#?”)的回答与这篇文章基本相同,展示了如何更快地完成它,尽管仍然不是每秒 60 帧 https://stackoverflow.com/a/76608543/9399492
测量性能
Using Stopwatch
es,我测量了代码每个步骤所需的时间。
注意:对于较小的单个帧尺寸,我已将格式从 bmp 更改为 png
Code
这是我的测量代码:
private bool move = false;
private int master_frame = 0;
Stopwatch sw = new Stopwatch();
private void pic()
{
using (Process process = new Process())
{
process.StartInfo.FileName = "ffmpeg.exe";
process.StartInfo.Arguments = $"-i \"video.mp4\" -vf \"select=gte(n\\,{master_frame})\" -vframes 1 -q:v 2 -f image2pipe -c:v png -";
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.Start();
using (MemoryStream outputStream = new MemoryStream())
{
Console.WriteLine("-");
sw.Restart();
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = process.StandardOutput.BaseStream.Read(buffer, 0, buffer.Length)) > 0)
{
outputStream.Write(buffer, 0, bytesRead);
}
sw.Stop();
Console.WriteLine("Reading bytes: " + sw.ElapsedMilliseconds + "ms");
framePictureBox.Invoke((MethodInvoker)(() =>
{
sw.Restart();
framePictureBox.Image?.Dispose();
sw.Stop();
Console.WriteLine("Disposing of old picture: " + sw.ElapsedMilliseconds + "ms");
sw.Restart();
Bitmap bmp = new Bitmap(outputStream);
sw.Stop();
Console.WriteLine("Converting stream to bitmap: " + sw.ElapsedMilliseconds + "ms");
sw.Restart();
framePictureBox.Image = bmp;
sw.Stop();
Console.WriteLine("Assigning to PictureBox: " + sw.ElapsedMilliseconds + "ms");
sw.Restart();
framePictureBox.Refresh();
sw.Stop();
Console.WriteLine("Rendering: " + sw.ElapsedMilliseconds + "ms");
}));
}
}
}
测量
这是我的结果,对于 1 分 36 秒长的 480p 30fps 视频,在我的计算机上,我的平均结果是:
- 读取ffmpeg发送的字节:
110ms
,峰值约为300ms
- 处理前一帧:
<1ms
- 将流转换为位图:
1ms
- 将转换后的位图分配给 PictureBox:
<1ms
- 将 PictureBox 渲染到屏幕上:
2ms
提醒一下,要达到 60 FPS,您需要渲染每一帧16.67ms
, or 33.33ms
30 FPS。
从中我们能明白什么
很明显,罪魁祸首是读取 ffmpeg 发送的流所花费的时间。
顺便说一句,我也尝试过@Charlieface的建议,但不,使用process.StandardOutput.BaseStream.CopyTo(outputStream);
并没有节省太多时间。
事实上,你可以使用new Bitmap(process.StandardOutput.BaseStream);
直接,但它并不会让整个事情变得更快。
真正的问题
乍一看,似乎很明显应该责怪 ffmpeg 将我们要求的帧转换为所需格式的速度很慢。
正如 @ChristophRackwitz 所指出的,这确实是代码缓慢的一个因素。
当向 ffmpeg 请求特定帧时,它实际上需要解码直到该帧的整个视频流,导致随着时间的推移,每个帧花费的时间越来越多。
不过,这对我来说不是问题,因为我使用的视频质量和帧率都很低。因此,我的测量是简约的,实际上,渲染每一帧所花费的时间可能比我测量的要长得多。我让你想象一下获取 1 小时长的 4K 144fps 视频的最后一帧需要多长时间。
还有另一个因素,它与流程创建有关。
事情是这样的,当你启动一个新的 ffmpeg 进程实例时,会发生以下情况:
- Windows需要分配空间来启动新进程
- ffmpeg 需要要求 Windows 打开您的视频文件
- Windows 为 ffmpeg 提供文件句柄
- ffmpeg 需要在完成转换后向 Windows 请求一个流来输出其数据
- Windows 为 ffmpeg 提供了一个流来将其数据发送到
- ffmpeg 终于可以完成它的工作并将其发送到流
- ffmpeg 需要告诉 Windows 关闭视频文件,因为它已完成
- Windows 关闭文件
- ffmpeg 需要告诉 Windows 关闭它打开的流,因为它不再需要它
- Windows 关闭流
- ffmpeg需要告诉Windows它已经完成并且可以关闭
- Windows 可以释放 ffmpeg 进程
这相当多了,对吧?
这里的问题是,在所有这些步骤中,只有一个步骤对于每个帧都是唯一的,那就是“ffmpeg 最终可以完成其工作并将其发送到流”步骤。
每一帧都会重复所有其他步骤,这需要花费大量时间。
Hence 我对你另一篇文章的回答 https://stackoverflow.com/a/76608543/9399492,我们不是为每个帧打开一个流,而是为所有帧打开一个流并实时渲染它们。这避免了为每个帧创建新的流程实例,也避免了必须从每个帧的开头解码整个视频流。
最终解决方案
您无法以当前的工作方式提高代码的速度,您需要从每帧一个进程切换为每个进程一个进程,这就是我对你另一篇文章的回答 https://stackoverflow.com/a/76608543/9399492 does.
谢谢阅读。