我该如何计时从视频文件中呈现和提取帧?

3

目标是在后台工作器的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个回答

6
我建议对当前的代码进行一些改动(实际上是相当多的改动 :))。 主要要点:
  1. 创建一个异步方法来执行视频播放。VideoFileReader 在线程池线程上工作(实际上是2个),不会导致窗体冻结。
  2. 使用一个IProgress<T>委托(类型为Progress<Bitmap>,这里命名为videoProgress),将新数据传递到UI线程,用于更新PictureBox控件。委托方法命名为Updater
  3. 使用一个Bitmap对象,设置为PictureBox的Image属性。
  4. 使用从该Bitmap派生的Graphics对象绘制视频帧。这样可以包含所使用的资源。只需使PictureBox无效,即可显示Bitmap的当前内容。
  5. 允许视频播放方法接受帧率值,这里设置为每秒25帧。当然,可以根据需要调整播放速度(请注意,设置超过32~35帧每秒时,会丢失一些帧)。
  6. 使用一个CancellationTokenSource来通知视频播放方法停止播放并终止,可以通过按下停止按钮或在播放期间关闭窗体来触发。
重要提示:
  • VideoFileReader返回的位图必须被释放。如果不释放,你会看到图形资源的消耗增加,而且不会停止。
  • 使用一个单独的位图,并使用派生的Graphics对象绘制每一帧,可以保留图形资源。如果在视频播放时保持诊断工具窗格打开,你会注意到没有泄漏任何资源,内存使用量保持恒定。
    当然,在打开此窗体并创建容器位图时会有轻微的增加,但是当关闭窗体时,那些少量的资源会被回收。
  • 这也可以实现更平滑的过渡和更快的渲染速度(在视频播放时移动窗体)。另外,尝试将PictureBox设置为锚定/停靠,设置SizeMode = Zoom并最大化窗体(设置PictureBox的Zoom模式会影响性能,应该调整位图大小)。

、和是用于启动、停止和暂停播放的按钮的点击处理程序。
对象在这里并不是严格必需的,但保留它可能在某些时候会有用。
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);
    }
}

我已经做了一些更改,以更好地处理暂停/停止请求。请使用修改后的代码。 - Jimi
请原谅我的无知,这真是个天才问题!FFMPEG能合并两个视频吗? - user16612111
1
@VibrantWaves 这难道不是一个很好的问题吗?不管怎样,可以的。VideoFileWriter 类可以使用指定的编解码器(枚举)、比特率和帧大小来初始化视频输出。然后你可以为每一帧写入一个缓冲区(字节数组)。所以你可以从两个视频文件中提取帧(如此示例所示),确保所有帧都使用相同的大小(必要时重新采样),然后以任意顺序写入帧。使用 ImageConverter 类将位图转换为字节数组。试一下,如果结果与预期不符,请提问。 - Jimi
1
@ChristophRackwitz 是的,Task.Delay()在间隔方面并不保证任何一致性,但在这里并不是非常必要。当然你可以对这个说法提出异议 :) 在这种情况下,有一个PeriodicTimer类,它保证了对间隔的尊重(除非不可能,这意味着阻塞UI线程超过间隔值)。此外,WaitForNextTickAsync()明显是异步的。或者使用多媒体定时器。 - Jimi
1
@ChristophRackwitz 是的,Task.Delay()在间隔方面并不保证任何一致性,但在这里并不是非常必要。当然,你可以对这个说法提出异议 :) 在这种情况下,有一个PeriodicTimer类,它保证了对间隔的尊重(除非不可能,这意味着阻塞UI线程超过间隔值)。此外,WaitForNextTickAsync()明显是异步的。或者使用多媒体定时器。 - undefined
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,