如何在C#中使用FFmpeg在一个PictureBox中播放视频?

huangapple go评论99阅读模式
英文:

How to Play a Video in a PictureBox Using FFmpeg in C#?

问题

我正在尝试开发一个使用FFmpeg在PictureBox控件中播放视频的C#应用程序。我有ffmpeg.exe文件,并想实时流传视频帧以更新PictureBox。

我已经尝试了一些解决方案,但在将Base64字符串转换为图像数据时遇到了问题。我收到的错误是:

输入不是有效的Base-64字符串,因为它包含一个非Base 64字符,超过两个填充字符,或者在填充字符中包含一个非法字符。

请问是否能提供指导或示例,以正确地在C#中使用FFmpeg播放视频在PictureBox中,而不遇到这个Base64转换问题?我将非常感慨不尽。

以下是我迄今为止所做的事情的总结:

private void button5_Click(object sender, EventArgs e)
{
    // ...
}

private void StopVideo()
{
    // ...
}

private void FfmpegOutputDataReceived(object sender, DataReceivedEventArgs e)
{
    if (!string.IsNullOrEmpty(e.Data))
    {
        // 解码接收到的图像数据
        byte[] imageData = Convert.FromBase64String(e.Data);

        // 在PictureBox控件中显示图像
        pictureBox1.Invoke((MethodInvoker)(() =>
        {
            using (MemoryStream memoryStream = new MemoryStream(imageData))
            {
                pictureBox1.Image = new System.Drawing.Bitmap(memoryStream);
            }
        }));
    }
}

private void FfmpegProcessExited(object sender, EventArgs e)
{
    StopVideo();
}

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    StopVideo();
}
  1. 我有ffmpeg.exe文件,并将其添加到了我的项目中。
  2. 我创建了一个带有PictureBox控件的Windows窗体应用程序。
  3. 我正在启动FFmpeg进程并重定向输出以读取视频帧。
  4. 然而,在将FFmpeg输出数据从Base64字符串转换为图像数据时遇到了困难。
英文:

I'm trying to develop a C# application that plays a video within a PictureBox control using FFmpeg. I have the ffmpeg.exe file and would like to stream the video frames to update the PictureBox in real-time.

I have already tried some solutions, but I'm encountering issues with the conversion from Base64 string to image data. The error I'm receiving is:

> The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.

Could someone please provide guidance or an example of how to properly play a video in a PictureBox using FFmpeg in C# without encountering this Base64 conversion issue? I would greatly appreciate any help or code snippets demonstrating the correct approach.

Here's a summary of what I've done so far:

private void button5_Click(object sender, EventArgs e)
{
    if (_ffmpegProcess != null)
        return;

    _ffmpegProcess = new Process();
    _ffmpegProcess.StartInfo.FileName = "C:/Users/google/Desktop/ffmpeg.exe";
    _ffmpegProcess.StartInfo.Arguments = $"-i \"{"C:/Users/google/Desktop/New folder/video.mp4"}\" -vf format=rgb24 -f image2pipe -";
    _ffmpegProcess.StartInfo.RedirectStandardOutput = true;
    _ffmpegProcess.StartInfo.UseShellExecute = false;
    _ffmpegProcess.StartInfo.CreateNoWindow = true;
    _ffmpegProcess.EnableRaisingEvents = true;
    _ffmpegProcess.OutputDataReceived += FfmpegOutputDataReceived;
    _ffmpegProcess.Exited += FfmpegProcessExited;

    _ffmpegProcess.Start();
    _ffmpegProcess.BeginOutputReadLine();
}

private void StopVideo()
{
    if (_ffmpegProcess == null)
        return;

    _ffmpegProcess.OutputDataReceived -= FfmpegOutputDataReceived;
    _ffmpegProcess.Exited -= FfmpegProcessExited;
    _ffmpegProcess.Kill();
    _ffmpegProcess.Dispose();
    _ffmpegProcess = null;
}

private void FfmpegOutputDataReceived(object sender, DataReceivedEventArgs e)
{
    if (!string.IsNullOrEmpty(e.Data))
    {
        // Decode the received image data
        byte[] imageData = Convert.FromBase64String(e.Data);

        // Display the image in the PictureBox control
        pictureBox1.Invoke((MethodInvoker)(() =>
        {
            using (MemoryStream memoryStream = new MemoryStream(imageData))
            {
                pictureBox1.Image = new System.Drawing.Bitmap(memoryStream);
            }
        }));
    }
}
private void FfmpegProcessExited(object sender, EventArgs e)
{
    StopVideo();
}

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    StopVideo();
}
  1. I have the ffmpeg.exe file available and added to my project.
  2. I have created a Windows Forms application with a PictureBox control.
  3. I am starting the FFmpeg process and redirecting the output to read the video frames.
  4. However, I'm facing difficulties when converting the FFmpeg output data from base64 string to image data.

答案1

得分: 1

简要总结:ffmpeg未将图像输出为base64字符串,也未将字节以完全整齐的方式发送,因此需要将每幅图像分开。我的回答包括一个可工作的项目以及我如何解决这个问题的解释。

下载完整的VS项目链接在此。该项目在公共领域中,无需给出任何资料(但仍然赞赏)。

你的代码实际操作

首先,我使用了你提供的代码重现了你的项目。

你期望OutputDataReceived事件包含base64字符串形式的数据,但事实并非如此。通过检查FfmpegOutputDataReceived函数中的e.Data的值,我发现如下所示:
如何在C#中使用FFmpeg在一个PictureBox中播放视频?

如你所见,它并不包含视频数据的base64字符串形式,而是以string形式的原始数据。

获取正确格式的数据

为什么你的代码不能工作

我的第一次尝试是只从e.Data字符串中获取图片作为字节数组,使用一个简单的for循环来遍历字符串的每个字符并将其放入字节数组中。

然而,出于以下原因,这并没有起作用:

  • 在你的ffmpeg命令行中,你传递了参数“-vf format=rgb24”,这使得ffmpeg以非标准的方式输出数据。请记住C#位图仅能由BMP、GIF、EXIF、JPG、PNG和TIFF文件创建

  • C#字符串是Unicode编码的,这意味着char是两个字节长,因此不能直接转换为单字节值。

  • 数据被发送为一个字符串,看起来是从字节作为字符的ANSI/CP1252直接转换而来。当我将输出更改为PNG并检查标头时,第一个PNG字节必须始终具有值0x89,但e.Data输出的字符值为0x2030,这是的Unicode表示,而在ANSI/CP1252中是0x89。 这基本上意味着这种数据接收方法会导致数据丢失,我还发现0x0D0x0A字节丢失了。

修复ffmpeg进程部分

不幸的是,必须完全放弃了你获取ffmpeg输出数据的方法。

为了将来自ffmpeg的管道输出作为完整的字节数组获取,我使用了来自此答案的代码,一旦适应到你的进程,它看起来是这样的:

Stream baseStream = _ffmpegProcess.StandardOutput.BaseStream;

这一行将ffmpeg输出的数据作为Stream获取,该流按字节读取,保证不会丢失数据。

我还将其更改为ffmpeg输出PNG数据,而不是你似乎使用的rgb24格式,希望这不会降低我的回答质量。

最终,初始化ffmpeg的整段代码如下:

// 创建FFMPEG进程
Process _ffmpegProcess = new Process();
_ffmpegProcess.StartInfo.FileName = "ffmpeg.exe";
_ffmpegProcess.StartInfo.Arguments = $"-hide_banner -loglevel error -i \"{video.mp4}\" -c:v png -f image2pipe -"; // 无用输出,以video.mp4作为输入,将其输出为以流传输的PNG文件
_ffmpegProcess.StartInfo.RedirectStandardOutput = true;
_ffmpegProcess.StartInfo.UseShellExecute = false;
_ffmpegProcess.StartInfo.CreateNoWindow = true;
_ffmpegProcess.EnableRaisingEvents = true;

// 启动FFMPEG
_ffmpegProcess.Start();

// 输出的PNG图像以流的方式传送到进程的标准输出基本流
Stream baseStream = _ffmpegProcess.StandardOutput.BaseStream;

如何处理流式传输的字节以显示视频

这是我建议处理此问题的方法:

英文:

TLDR: ffmpeg doesn't output images as base64 strings, and also doesn't send bytes in a perfectly trimmed way so you have each picture separate from one another. My answer includes a working project and explanations on how I figured out how to do it.

Download the full VS project here. It's in the public domain, no credits needed (but still appreciated).

What your code actually does

First things first, I have reproduced your project using the code you have given.

You expected the OutputDataReceived event to contain data as a base64 string, but it is not.
By checking e.Data's value in your FfmpegOutputDataReceived function, I found the following:
如何在C#中使用FFmpeg在一个PictureBox中播放视频?

As you can see, it does not contain the video data as a base64 string, but as raw data in the form of a string.

Getting data in the correct form

Why your code couldn't have worked

My first attempt was to just get the pictures as a byte array from the e.Data string, using a simple for loop that iterates through each character of the string and places it in a byte array.

However, this didn't work for the following reasons:

  • In your ffmpeg command line you have passed the argument "-vf format=rgb24", which makes ffmpeg output the data in a non-standard way. Remember that C# Bitmaps can be made from BMP, GIF, EXIF, JPG, PNG, and TIFF files only.

  • C# string are Unicode-encoded, which means that chars are two bytes long, and thus can't be directly converted to single byte values

  • Data is sent as a string which seems to be a direct conversion from what bytes as characters would be in ANSI/CP1252 to Unicode.
    I have noticed this when I changed the output to PNG and check the header: the first PNG byte must always have value 0x89, but e.Data outputed character value 0x2030, which is the Unicode for , which is 0x89 in ANSI/CP1252.
    This pretty much means that this whole data receiving method causes data loss, and I also found that 0x0D and 0x0A bytes were missing.

Fixing the ffmpeg process part

So unfortunately, your system to get ffmpeg's outputed data had to be entirely ditched.

To get the pipe'd output from ffmpeg as an intact byte array, I used the code from this answer, which, once adapted to your process, looks like this:

Stream baseStream = _ffmpegProcess.StandardOutput.BaseStream;

This line gets the data ffmpeg outputs as a Stream, which is read bytes by bytes and is guaranteed to have no data loss.

I also changed it so ffmpeg outputs PNG data instead of that rgb24 format you seemed to use, I hope it doesn't lower the quality of my answer.

In the end, the whole code snippet to initialize ffmpeg is as follows:

// Create the FFMPEG process
Process _ffmpegProcess = new Process();
_ffmpegProcess.StartInfo.FileName = "ffmpeg.exe";
_ffmpegProcess.StartInfo.Arguments = $"-hide_banner -loglevel error -i \"{"video.mp4"}\" -c:v png -f image2pipe -"; // No useless outputs, uses video.mp4 as an input, outputs it as PNG files streamed to the pipe
_ffmpegProcess.StartInfo.RedirectStandardOutput = true;
_ffmpegProcess.StartInfo.UseShellExecute = false;
_ffmpegProcess.StartInfo.CreateNoWindow = true;
_ffmpegProcess.EnableRaisingEvents = true;

// Start FFMPEG
_ffmpegProcess.Start();

// Output PNGs are streamed into the process' standard output base stream
Stream baseStream = _ffmpegProcess.StandardOutput.BaseStream;

How to handle the streamed bytes to display the video

Here is the way I suggest to handle this:

The main reason we need to use a BackgroundWorker is that Controls can't be accessed from a thread other than the one who created them.

And multithreading is required here, because otherwise, the entire form would be frozen until the video is done playing, because it'd be busy rending frames at all times.

Cutting down ffmpeg's data into individual frames to render

Unfortunately, ffmpeg doesn't stream the data as perfectly trimmed down PNG files that you can render.
(Which, by the way, is normal since that's how process pipes work)

So what you get is a giant stream with many PNGs appended to each other.
So we need to figure out where each PNG ends so we can render them.

To split them, I refered to the PNG File Format Specifications, which indicate the following:

  • PNG files are made out of a header, and an undefined amount of chunks
  • The header is always 8 bytes of values 89 50 4E 47 0D 0A 1A 0A 00
  • Chunks are made out of:
    • 4 bytes indicating the size (in bytes) of the chunk's content
    • 4 bytes representing the chunk's identifier
    • 4 bytes containing the CRC32 checksum of the chunk
    • The chunk's content
  • The first chunk is always an IHDRchunk of size 13, and the last one is always an IEND chunk of size 0

This means that we can cut each PNGs from the stream by simply adding each chunk to a buffer until we hit the IEND chunk, in which case it means that we are done reading the current PNG, so we can send it for rendering.

Here is the code that handles this, please read the comments for detailed explanations of its inner workings:

// Buffer to hold the current PNG. 16588800 bytes should be enough to hold any 1080p PNG file (1920 * 1080 * 4 * 2 = 16588800)
// Source: https://stackoverflow.com/a/20415800/9399492
// Note: This answer claims that the max size in bytes is "width * height * 4", I multiplied the total by two to have a good enough margin.
//       16588800 bytes is 16 MB, which is absolutely nothing by today's standards.
//       So even if bigger videos need to be played, you can increase this number without worrying too much about RAM space.
byte[] buffer = new byte[16588800];

// Current writing index in the buffer
int currentIdx = 0;

// Keep going for as long as the video background worker wasn't scheduled for cancellation, or if we reached the end of the video
while (!videoBackgroundWorker.CancellationPending && !_ffmpegProcess.HasExited)
{
    // The goal with this loop is to cut down each PNG sent by FFMPEG and to display them.
    // Problem is, all PNGs are appended to each other. Cutting down each PNGs is therefore not a very easy task.
    // The PNG file format specification tells us more: http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html#Chunk-layout
    // PNG files are divided in chunks that are defined by their size in bytes, a four bytes identifier, a checksum and their content.
    // And fortunately, there is an end chunk named "IEND", of size 0.
    // Thus, all we have to do is to gather bytes from all chunks until we find the IEND chunk, in which case it means that we hit the end of the current PNG file.
    // Then, we can parse it as a C# Bitmap and go back to the beginning of this loop to read the next PNG.

    // Reset the index to 0 on every new PNG
    currentIdx = 0;

    // Read the PNG Header
    if (baseStream.Read(buffer, currentIdx, 8) <= 0)
        break;

    currentIdx += 8;

    // The PNG Header must always have these values according to http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html#PNG-file-signature
    // If these aren't present, it likely means that the stream is corrupted and therefore the video can't be further read.
    if (buffer[0] != 0x89
        || buffer[1] != 0x50
        || buffer[2] != 0x4E
        || buffer[3] != 0x47
        || buffer[4] != 0x0D
        || buffer[5] != 0x0A
        || buffer[6] != 0x1A
        || buffer[7] != 0x0A
        )
        throw new Exception("Invalid PNG header");

    // This loop will go through each chunk of the PNG file and read them into the buffer.
    // When the IEND chunk is read, the loop is broken
    while (true)
    {
        // Read the chunk header
        baseStream.Read(buffer, currentIdx, 12);

        int bIdx = currentIdx;
        currentIdx += 12;

        // Get the chunk's content size in bytes from the header we just read
        int totalSize = (buffer[bIdx] << 24) | (buffer[bIdx + 1] << 16) | (buffer[bIdx + 2] << 8) | (buffer[bIdx + 3]);

        // If the size is positive, then read the chunk's content and write it into the buffer
        if (totalSize > 0)
        {
            // We are sometimes faster than FFMPEG can output the video, so we need to keep reading for as long as we didn't read the entirety of the section
            int totalRead = 0;
            while (totalRead < totalSize)
            {
                int read = baseStream.Read(buffer, currentIdx, (totalSize - totalRead));
                currentIdx += read;
                totalRead += read;
            }
        }

        // If the size is 0 and the section identifier is IEND, then break out of the loop: we are done with this PNG file
        else if (totalSize == 0
            && buffer[bIdx + 4] == 0x49 // I
            && buffer[bIdx + 5] == 0x45 // E
            && buffer[bIdx + 6] == 0x4E // N
            && buffer[bIdx + 7] == 0x44 // D
            )
        {
            break;
        }
    }

    // Get the PNG file's bytes as a memory buffer to be converted to a C# Bitmap
    using (var ms = new MemoryStream(buffer, 0, currentIdx))
    {
        // Report progress as the Bitmap of the frame to display.
        videoBackgroundWorker.ReportProgress(0, new Bitmap(ms)); // Byte array to bitmap conversion takes about 10ms on my computer
    }
}

// Report null as progress, to indicate being done
videoBackgroundWorker.ReportProgress(0, null);

// If we broke out of the loop due to FFMPEG being done, no need to manually kill it
if (_ffmpegProcess.HasExited)
    return;

// If we broke out of the loop due to a cancellation, forcefully kill FFMPEG and dispose of it.
_ffmpegProcess.Kill();
_ffmpegProcess.Dispose();

Performance

As expected from such a hacky way to render a video, it performs very poorly.

On my computer, the part that cuts PNGs takes 1 millisecond most of the time, but every few seconds there seem to be a lag spike (maybe due to disk reading?) that makes it take 30 to 40 milliseconds for one to three frames.

The part that converts the PNG byte array into a Bitmap seems to take a constant 10 to 12 milliseconds on my computer though.

To achieve a fluid 60 frames per second, we'd need to render each frame in 16.67 milliseconds (or less, in which case we'd need to wait the remaining amount of time).

I do not doubt that there might be ways to optimize this, though remember that Windows Forms is a windows rendering framework, not a video player. It wasn't designed for kind of thing.

There might be video playing libraries that could help do all of this much more simply, but considering you wanted to use ffmpeg and render the video on a PictureBox, I sticked to those constraints, or it wouldn't have really been an answer.

End result

I have also added an FPS counter:

如何在C#中使用FFmpeg在一个PictureBox中播放视频?

Do note that the framerate is uncapped, meaning that assuming you have a computer with infinite performances, the video will run at an infinite speed.

I do not know how to get the video's framerate from ffmpeg to cap it.

You can download the full VS project here. It's in the public domain, no credits needed (but still appreciated).

Note: I uploaded it on Google Drive, I'm not sure what is the recommended platform to upload zips like this, moderators please feel free to reupload the file in a more appropriate place and to edit my post to update the link

End words

That's all.

You might want to take a look at the ffmpeg documentation to see if there are better ways to stream videos, or maybe check if there is a NuGet package that could do the work for you as well.

Thanks for reading.

huangapple
  • 本文由 发表于 2023年6月26日 03:00:39
  • 转载请务必保留本文链接:https://go.coder-hub.com/76551997.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定