如何计算视频文件中帧的呈现和提取的时间?

Dan*_*Lip 3 .net c# graphics winforms

目标是控制 BackGroundWorkerDoWork事件中的帧提取速度。

我尝试过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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Jim*_*imi 7

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

要点:

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

重要笔记:

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

buttonStart_ClickbuttonStop_ClickbuttonPause_Click是用于开始、停止和暂停播放的按钮的单击处理程序。
此处syncRoot并不严格要求该对象,但将其保留在那里,在某些时候它可能会变得有用

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);
            // Adds two pixels, to see the Frame's boundaries in the container
            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 (!token.IsCancellationRequested) {
                // Resumes on a ThreadPool Thread
                await Task.Delay(intervalMs, token).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);
    }
}
Run Code Online (Sandbox Code Playgroud)