FFmpeg пропускает кадры рендеринга

Пока я извлекаю кадры из видео, я заметил, что ffmpeg закончит рендеринг определенных изображений. Проблема закончилась тем, что была "заполнением" байта между двумя jpeg изображениями. Если размер моего буфера равен 4096 и если в этом буфере расположены байты от предыдущего изображения и следующего изображения, и если они не разделены никаким количеством байтов, тогда следующее изображение не будет отображаться должным образом. Это почему?

-i path -f image2pipe -c:v mjpeg -q:v 2 -vf fps=25 pipe:1

enter image description here

Представленный кадр:

enter image description here

Пример кода:

public void ExtractFrames()
{
    string FFmpegPath = "Path...";
    string Arguments = $"-i { VideoPath } -f image2pipe -c:v mjpeg -q:v 2 -vf fps=25/1 pipe:1";
    using (Process cmd = GetProcess(FFmpegPath, Arguments))
    {
        cmd.Start();
        FileStream fStream = cmd.StandardOutput.BaseStream as FileStream;

        bool Add = false;
        int i = 0, n = 0, BufferSize = 4096;
        byte[] buffer = new byte[BufferSize + 1];

        MemoryStream mStream = new MemoryStream();

        while (true)
        {
            if (i.Equals(BufferSize))
            {
                i = 0;
                buffer[0] = buffer[BufferSize];
                if (fStream.Read(buffer, 1, BufferSize) == 0)
                    break;
            }

            if (buffer[i].Equals(255) && buffer[i + 1].Equals(216))
            {
                Add = true;
            }

            if (buffer[i].Equals(255) && buffer[i + 1].Equals(217))
            {
                n++;
                Add = false;
                mStream.Write(new byte[] { 255, 217 }, 0, 2);
                File.WriteAllBytes([email protected]"C:\Path...\{n}.jpg", mStream.ToArray());
                mStream = new MemoryStream();
            }

            if (Add)
                mStream.WriteByte(buffer[i]);

            i++;
        }
        cmd.WaitForExit();
        cmd.Close();
    }
}

private Process GetProcess(string FileName, string Arguments)
{
    return new Process
    {
        StartInfo = new ProcessStartInfo
        {
            FileName = FileName,
            Arguments = Arguments,
            UseShellExecute = false,
            RedirectStandardOutput = true,
            CreateNoWindow = false,
        }
    };
}

Для тестирования следует использовать образец видео (> 480p) длиной 60 секунд или выше.

Ответ 1

Если файл сохранен, тогда проще было бы просто сообщить FFmpeg, чтобы преобразовать этот видеофайл в Jpegs.

(1) Чтение видеофайлов и выходных кадров Jpegs (без использования потоков или потоков памяти/файлов):

string str_MyProg = "C:/FFmpeg/bin/ffmpeg.exe";
string VideoPath = "C:/someFolder/test_vid.mp4";

string save_folder = "C:/someOutputFolder/";

//# Setup the arguments to directly output a sequence of images (frames)
string str_CommandArgs = "-i " + VideoPath + " -vf fps=25/1 " + save_folder + "n_%03d.jpg"; //the n_%03d replaces "n++" count

System.Diagnostics.ProcessStartInfo cmd_StartInfo = new System.Diagnostics.ProcessStartInfo(str_MyProg, str_CommandArgs);

cmd_StartInfo.RedirectStandardError = false; //set false
cmd_StartInfo.RedirectStandardOutput = false; //set false
cmd_StartInfo.UseShellExecute = true; //set true
cmd_StartInfo.CreateNoWindow = true;  //don't need the black window

//Create a process, assign its ProcessStartInfo and start it
System.Diagnostics.Process cmd = new System.Diagnostics.Process();
cmd.StartInfo = cmd_StartInfo;

cmd.Start();

//# Started process. Check output folder for images...

(2) Метод труб:

При использовании труб FFmpeg будет передавать поток, как широковещательный. Если достигнут последний видеокадр, то такое же "изображение" последнего кадра будет повторяться бесконечно. Вы должны вручную сообщить FFmpeg, когда прекратить отправку в ваше приложение (в этой ситуации нет кода выхода).

Эта строка в коде будет определять, как любые кадры извлекаться до остановки:

int frames_expected_Total = 0; //is... (frame_rate x Duration) = total expected frames

Вы можете рассчитать предел как: input-Duration/output-FPS или как output-FPS * input-Duration.
Пример: продолжительность видео составляет 4,88 секунды, поэтому 25 * 4.88 = 122 кадра ограничено для этого видео.

"Если размер моего буфера равен 4096... тогда следующее изображение не отображается правильно. Почему?"

У вас есть "сбитые" изображения, потому что буфер слишком мал, чтобы содержать полное изображение...

Формула размера буфера:

int BufferSize = ( video_Width * video_Height );

Поскольку конечный сжатый jpeg будет меньше этой суммы, он гарантирует BufferSize который может содержать любой полный кадр без ошибок. Из интереса, откуда вы получаете номер 4096? Стандартный вывод обычно дает максимальный размер пакетов 32 кбайта (32768 байт).

Решение (проверено):
Это полный рабочий пример для решения проблемы с ошибкой "сбой", проверки кодовых комментариев...

using System;
using System.IO;
using System.Net;
using System.Drawing;
using System.Diagnostics;
using System.Collections.Generic;


namespace FFmpeg_Vid_to_JPEG //replace with your own project "namespace"
{
    class Program
    {
        public static void Main(string[] args)
        {
            //# testing the Extract function...

            ExtractFrames();
        }

        public static void ExtractFrames()
        {
            //# define paths for PROCESS
            string FFmpegPath = "C:/FFmpeg/bin/ffmpeg.exe";
            string VideoPath = "C:/someFolder/test_vid.mp4";

            //# FFmpeg arguments for PROCESS
            string str_myCommandArgs = "-i " + VideoPath + " -f image2pipe -c:v mjpeg -q:v 2 -vf fps=25/1 pipe:1";

            //# define paths for SAVE folder & filename
            string save_folder = "C:/someOutputFolder/";
            string save_filename = ""; //update name later on, during SAVE commands

            MemoryStream mStream = new MemoryStream(); //create once, recycle same for each frame

            ////// # also create these extra variables...

            bool got_current_JPG_End = false; //flag to begin extraction of image bytes within stream

            int pos_in_Buffer = 0; //pos in buffer(when checking for Jpeg Start/End bytes)
            int this_jpeg_len = 0; // holds bytes of single jpeg image to save... correct length avoids cropping effect
            int pos_jpeg_start = 0; int pos_jpeg_end = 0; //marks the start/end pos of one image within total stream

            int jpeg_count = 0; //count of exported Jpeg files (replaces the "n++" count)
            int frames_expected_Total = 0; //number of frames to get before stopping

            //# use input video width x height as buffer size //eg: size 921600 = 1280 W x 720H 
            int BufferSize = 921600;  
            byte[] buffer = new byte[BufferSize + 1];

            // Create a process, assign its ProcessStartInfo and start it
            ProcessStartInfo cmd_StartInfo = new ProcessStartInfo(FFmpegPath, str_myCommandArgs);

            cmd_StartInfo.RedirectStandardError = true;
            cmd_StartInfo.RedirectStandardOutput = true; //set true to redirect the process stdout to the Process.StandardOutput StreamReader
            cmd_StartInfo.UseShellExecute = false;
            cmd_StartInfo.CreateNoWindow = true; //do not create the black window

            Process cmd = new System.Diagnostics.Process();
            cmd.StartInfo = cmd_StartInfo;

            cmd.Start();

            if (cmd.Start())
            {
                //# holds FFmpeg output bytes stream...
                var ffmpeg_Output = cmd.StandardOutput.BaseStream; //replaces: fStream = cmd.StandardOutput.BaseStream as FileStream;

                cmd.BeginErrorReadLine(); //# begin receiving FFmpeg output bytes stream

                //# get (read) first two bytes in stream, so can check for Jpegs' SOI (xFF xD8)
                //# each "Read" auto moves forward by read "amount"...
                ffmpeg_Output.Read(buffer, 0, 1);
                ffmpeg_Output.Read(buffer, 1, 1);

                pos_in_Buffer = this_jpeg_len = 2; //update reading pos

                //# we know first jpeg SOI is always at buffer pos: [0] and [1]
                pos_jpeg_start = 0; got_current_JPG_End = false;

                //# testing amount... Duration 4.88 sec, FPS 25 --> (25 x 4.88) = 122 frames        
                frames_expected_Total = 122; //122; //number of Jpegs to get before stopping.

                while(true)
                {
                    //# For Pipe video you must exit stream manually
                    if ( jpeg_count == (frames_expected_Total + 1) )
                    {
                        cmd.Close(); cmd.Dispose(); //exit the process
                        break; //exit if got required number of frame Jpegs
                    }

                    //# otherwise read as usual    
                    ffmpeg_Output.Read(buffer, pos_in_Buffer, 1);
                    this_jpeg_len +=1; //add 1 to expected jpeg bytes length

                    //# find JPEG start (SOI is bytes 0xFF 0xD8)
                    if ( (buffer[pos_in_Buffer] == 0xD8)  && (buffer[pos_in_Buffer-1] == 0xFF) )
                    {
                        if  (got_current_JPG_End == true) 
                        {   
                            pos_jpeg_start = (pos_in_Buffer-1);
                            got_current_JPG_End = false; 
                        }
                    }

                    //# find JPEG ending (EOI is bytes 0xFF 0xD9) then SAVE FILE
                    if ( (buffer[pos_in_Buffer] == 0xD9) && (buffer[pos_in_Buffer-1] == 0xFF) )
                    {
                        if  (got_current_JPG_End == false) 
                        { 
                            pos_jpeg_end = pos_in_Buffer; got_current_JPG_End = true;

                            //# update saved filename 
                            save_filename = save_folder + "n_" + (jpeg_count).ToString() + ".jpg";

                            try
                            {
                                //# If the Jpeg save folder doesn't exist, create it.
                                if ( !Directory.Exists( save_folder ) ) { Directory.CreateDirectory( save_folder ); }
                            } 
                            catch (Exception)
                            { 
                                //# handle any folder create errors here.
                            }

                            mStream.Write(buffer, pos_jpeg_start, this_jpeg_len); //

                            //# save to disk...
                            File.WriteAllBytes(@save_filename, mStream.ToArray());

                            //recycle MemoryStream, avoids creating multiple = new MemoryStream();
                            mStream.SetLength(0); mStream.Position = 0;

                            //# reset for next pic
                            jpeg_count +=1; this_jpeg_len=0;

                            pos_in_Buffer = -1; //allows it to become 0 position at incrementation part
                        }
                    }

                    pos_in_Buffer += 1; //increment to store next byte in stdOut stream

                } //# end While

            }
            else
            {
               // Handler code here for "Process is not running" situation
            }

        } //end ExtractFrame function


    } //end class
} //end program

Примечание. При модификации вышеприведенного кода убедитесь, что Process создания внутри самой функции ExtractFrames() не работает, если вы используете какую-либо внешнюю функцию для возврата Process. Не using (Process cmd = GetProcess(FFmpegPath, Arguments)) как: using (Process cmd = GetProcess(FFmpegPath, Arguments)).

Удачи. Дайте мне знать, как это происходит.

(PS: Извините "слишком много" комментариев кода, это для будущих читателей, которые могут или не могут понять, что этот код делает для правильной работы в буфере).

Ответ 2

Эта проблема происходит во всем мире, для справки, взятой с сайта Adobe:

Ответ - все это - выходной вывод по умолчанию несжатый, что приводит к столь высокой скорости передачи данных, даже довольно мясистые компьютеры никогда не смогут воспроизвести его плавно.

Дело здесь простое: вы получаете высокую скорость передачи данных, даже при использовании низкого качества. Максимальный размер буфера для этого случая - 4096. Если в этом буфере есть байты из предыдущего и следующего изображений, а ARE не разделены запятой, FFmpeg не может решить, какой кадр должен быть рендерингом, поэтому он пропускает кадр, потому что он уменьшает его правильность, вместо того, чтобы случайно предложить, какой кадр обновиться.

Если вы отделите байты по запятой, вы поможете FFmpeg связать байты проницаемого и следующего изображений, что упростит выделение кадра для рендеринга, не пропуская кадры.