c# - 如何计时从视频文件中显示和提取帧?

标签 c# .net winforms graphics

目标是控制 BackGroundWorker 的 DoWork 事件中的帧提取速度。
我尝试了 Thread.Sleep(),但它引发了异常。

这就是我想要做的。在上面和底部对其进行了描述。

using Accord.Video;
using Accord.Video.FFMPEG;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Windows.Forms;

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        using (var vFReader = new VideoFileReader())
        {
            vFReader.Open(@"C:\Users\Chocolade 1972\Downloads\MyVid.mp4");
            trackBar1.Maximum = (int)vFReader.FrameCount;
        }
    }

    private void button1_Click(object sender, EventArgs e)
    {
        backgroundWorker1.RunWorkerAsync();
    }

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        using (var vFReader = new VideoFileReader())
        {
            vFReader.Open(@"C:\Users\Chocolade 1972\Downloads\MyVid.mp4");
            for (var i = 0; i < vFReader.FrameCount; i++)
            {
                backgroundWorker1.ReportProgress(0, vFReader.ReadVideoFrame());
            }

            // Not sure that this would be required as it might happen implicitly at the end of the 'using' block.
            vFReader.Close();
        }
    }

    private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        pictureBox1.Image?.Dispose();
        pictureBox1.Image = (Image)e.UserState;
    }

    private void Form1_Resize(object sender, EventArgs e)
    {
        label1.Text = this.Size.ToString();
    }
}

它工作正常但速度太快。我想使用计时器,或允许我控制帧提取速度的东西。

最佳答案

我建议对当前代码进行一些更改(实际上相当多:)。

要点:

  1. 创建一个异步方法来执行视频播放。 VideoFileReader在 ThreadPool 线程(实际上是 2 个)上工作,它不会导致表单卡住
  2. 使用IProgress<T>委托(delegate)(属于 Progress<Bitmap> 类型,在此处命名为 videoProgress)将新数据编码到 UI 线程,用于更新 PictureBox 控件。委托(delegate)方法名为 Updater
  3. 使用单个位图对象,设置为 Image图片框的属性
  4. 使用派生自该 Bitmap 的 Graphics 对象来绘制视频帧。这允许包含使用的资源。 PictureBox 只是无效化,以显示 Bitmap 的当前内容
  5. 允许视频播放方法接受帧速率值,此处设置为每秒 25 帧。当然,可以适配放慢或加快播放(注意,设置超过每秒32~35帧,你开始丢失一些帧)
  6. 使用 CancellationTokenSource向视频播放方法发出信号以停止播放并终止,当播放处于事件状态时按下停止按钮或关闭表单时

重要提示:

  • VideoFileReader 的位图返回必须处理。如果不这样做,您将看到图形资源消耗的增加,而且这种情况不会停止
  • 使用单个 Bitmap 并使用派生的 Graphics 对象绘制每个新帧,可以保留图形资源。如果在播放视频时让“诊断工具” Pane 保持打开状态,您会注意到没有泄漏任何资源并且内存使用量保持不变。
    当您打开此窗体并创建容器位图时,当然会略有增加,但是当关闭窗体时,会回收少量资源
  • 这还允许更平滑的过渡和更快的渲染速度(在播放视频时四处移动表单)。另外,尝试锚定/停靠 PictureBox,设置 SizeMode = Zoom并最大化表单(设置 PictureBox 的 Zoom 模式会影响性能,您应该改为调整位图的大小)

buttonStart_Click , buttonStop_ClickbuttonPause_Click是用于开始、停止和暂停播放的按钮的点击处理程序。
syncRoot object 在这里不是严格要求的,但保留在那里,它可能在某些时候有用

using System.Drawing;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Accord.Video.FFMPEG;

public partial class Form1 : Form
{
    Bitmap frame = null;
    Graphics frameGraphics = null;
    bool isVideoRunning = false;
    IProgress<Bitmap> videoProgress = null;
    private CancellationTokenSource cts = null;
    private readonly object syncRoot = new object();
    private static long pause = 0;

    public Form1() => InitializeComponent(); 

    private async void buttonStart_Click(object sender, EventArgs e) {
        string fileName = "[The Video File Path]";

        if (isVideoRunning) return;
        isVideoRunning = true;

        using (var videoReader = new VideoFileReader()) {
            videoReader.Open(fileName);
            frame = new Bitmap(videoReader.Width + 2, videoReader.Height + 2);
            trackBar1.Maximum = (int)videoReader.FrameCount;
        }

        videoProgress = new Progress<Bitmap>(Updater);
        cts = new CancellationTokenSource();
        pictureBox1.Image = frame;
        try {
            frameGraphics = Graphics.FromImage(frame);
            // Set the frame rate to 25 frames per second
            int frameRate = 1000 / 25;
            await GetVideoFramesAsync(videoProgress, fileName, frameRate, cts.Token);
        }
        finally {
            StopPlayback(false);
            frameGraphics?.Dispose();
            pictureBox1.Image?.Dispose();
            pictureBox1.Image = null;
            buttonPause.Text = "Pause";
            pause = 0;
            isVideoRunning = false;
        }
    }

    private void buttonStop_Click(object sender, EventArgs e) => StopPlayback(true);

    private void buttonPause_Click(object sender, EventArgs e)
    {
        if (pause == 0) {
            buttonPause.Text = "Resume";
            Interlocked.Increment(ref pause);
        }
        else {
            Interlocked.Decrement(ref pause);
            buttonPause.Text = "Pause";
        }
    }

    private void StopPlayback(bool cancel) {
        lock (syncRoot) {
            if (cancel) cts?.Cancel();
            cts?.Dispose();
            cts = null;
        }
    }

    private void Updater(Bitmap videoFrame) {
        using (videoFrame) frameGraphics.DrawImage(videoFrame, Point.Empty);
        pictureBox1.Invalidate();
    }

    private async Task GetVideoFramesAsync(IProgress<Bitmap> updater, string fileName, int intervalMs, CancellationToken token = default) {
        using (var videoReader = new VideoFileReader()) {
            if (token.IsCancellationRequested) return;
            videoReader.Open(fileName);

            while (true) {
                if (token.IsCancellationRequested) break;
                // Resumes on a ThreadPool Thread
                await Task.Delay(intervalMs).ConfigureAwait(false);

                if (Interlocked.Read(ref pause) == 0) {
                    var frame = videoReader.ReadVideoFrame();
                    if (frame is null) break;
                    updater.Report(frame);
                }
            }
        }
    }

    protected override void OnFormClosing(FormClosingEventArgs e) {
        if (isVideoRunning) StopPlayback(true);
        base.OnFormClosing(e);
    }
}

关于c# - 如何计时从视频文件中显示和提取帧?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/74157315/

相关文章:

c# - 比较两个声波

.net - 更新多行 Linq 与 SQL

.net - 发布设计审计表以跟踪更改

c# - 如何将搜索引擎添加到 GeckoFx 网络浏览器?

c# - Visual Studio 2012 如何绘制其窗口边框?

c# - 为什么这没有得到括号之间的东西?

c# - vstest.console 可以按所有匹配的特征过滤测试吗?

c# - MDIchild 窗体位于面板后面

c# - 如何解决winform控件的本地语言文本编码问题

c# - 等待异步脚本结果超时 Selenium C# Protractor