ffmpeg 等待关闭管道以便开始处理数据

ffmpeg 等待关闭管道以便开始处理数据

在 Windows 系统上,我遇到了以下问题:

  • 我从 SDK 获得实时流;通过回调获得字节块;容器是 MPEG-PS
  • 我必须使用 VLC 向最终用户显示此直播流。但 VLC 无法播放我收到的此直播流
  • 我的解决方案是使用 ffmpeg 将其转换为 MPEG-TS(或其他格式),以便 VLC 可以播放

我尝试了不同的可能解决方案,例如制作一个公开此流的 TCP 服务器,指示 ffmpeg 从那里读取输入。但 ffmpeg 抱怨它无法检测容器类型。如果我将通过该回调收到的字节保存到磁盘,我可以看到第一个字节是“IMKH”,表示 MPEG-PS 容器。如果我随后使用 ffmpeg 将其转码为 MP4(作为文件),那就可以了。但我需要“动态”地执行此操作,因此需要对我收到的流进行实时转码。

我尝试的最后一件事是创建一个命名管道并将接收到的字节写入其中,然后使用以下命令创建实时流:ffmpeg -i \.\pipe\testpipe -f mpegts -c copy -listen 1 "http://127.0.0.1:5002” 然后在 VLC 中播放 URL“http://127.0.0.1:5002”。

发生了一些奇怪的事情,因为只有当我关闭管道时,ffmpeg 才会开始输出(在控制台中)找到的流,并且 VLC 可以连接到其 http 服务器并开始正确播放。我在写入每个字节块后刷新管道。

我希望 ffmpeg 在收到管道输入时开始处理它,并立即为 VLC 输出结果。我在下面的帖子中发现了类似的东西,但没有答案: https://ffmpeg.org/pipermail/ffmpeg-user/2016-December/034639.html

为什么 ffmpeg 等待关闭管道才能开始处理?是否可以配置它以启动接收流的实时转码?

编辑 - 更多细节 ======================================

我无法提供源流(它被隔离在内部网中),但正如我所提到的,我使用第三方 C# SDK 来访问它(实际上这是访问它的唯一方法)。该 SDK 通过一个简单的回调为我提供了对该流的访问权限,该回调如下所示:callback(byte[] data)。因此,使用这个回调,我可以收到所有流字节。我将这些字节保存到磁盘上的一个文件中。接收到的流是 MPEG-PS。如果我尝试将此文件转换为另一种可行的文件格式(如 avi):ffmpeg.exe -i input.mpeg out.avi,VLC 就可以播放这个 out.avi。所以我知道我得到的回调数据是正确的,所以 input.mpeg 是一个正确的文件,ffmpeg 可以处理它。

为了进行测试,我还使用了这个 input.mpeg 文件,我创建了一个简单的 C# 服务器来读取它,并使用此代码通过命名管道以块的形式公开它:

_pipeServer = new NamedPipeServerStream("testpipe", PipeDirection.Out);
_pipeServer.WaitForConnection(); // wait for ffmpeg to connect

byte[] allBytes = System.IO.File.ReadAllBytes(“input.mpeg”);

using (BinaryWriter bw = new BinaryWriter(_pipeServer))
{                    
   int index = 0;                       
    for(;;)
    {
      bw.Write(allBytes, idx, 100);
      index += 100;

      // added some delay to emulate a live stream
      System.Threading.Thread.Sleep(100);

     if (idx >= allBytes.Length) break;
    }
}

这是流程的准确描述:

  • 我按照上面的方法启动 C# 管道服务器
  • 我使用命令行启动 ffmpeg:ffmpeg -i \.\pipe\testpipe -f mpegts –c copy -listen 1http://127.0.0.1:5002
  • 然后我使用 URL 启动 VLChttp://127.0.0.1:5002

但什么也没发生;只有当 C# 服务器完成发送(因此 BinaryWriter 对象被释放,因此 ffmpeg 检测到输入流的结束)时,VLC 才开始播放。似乎 ffmpeg 会累积接收到的流,并且仅在输入完成时提供输出。

我还做了以下测试:

  • 我使用以下命令将 MPEG_PS 格式的 input.mpeg 转换为 MPEG-TS 格式:ffmpeg.exe -i input.mpeg input.ts
  • 然后我在命名管道“testpipe”中输入 input.ts,并使用相同的参数启动 ffmpeg:ffmpeg -i \.\pipe\testpipe -f mpegts –c copy -listen 1http://127.0.0.1:5002
  • 我使用以下方式启动 VLChttp://127.0.0.1:5002并立即启动。

所以我认为这证明不是管道问题,而是格式/容器问题。当管道被输入 MPEG-PS 时,ffmpeg 会等待 EOF;但是当被输入 MPEG-TS 时,ffmpeg 会立即开始产生输出(这就是我想要实现的,直播)。

顺便说一句:如果我在到达 input.mpeg 中间时关闭上面的 C# 服务器,效果是一样的,VLC 会开始播放可用的流。因此,我认为 ffmpeg 并不需要下载整个流才能开始将其转发到输出。

您知道如何让 ffmpeg 立即开始生成输出吗?可能是 ffmpeg 的一些参数,但我找不到它……我已将 input.mpeg 文件放在此处:

https://www.dropbox.com/s/zsdktikfq4yuak0/input.mpeg?dl=0

谢谢你!

答案1

我在稍微不同的环境中遇到了同样的问题,在对 FFMPEG 代码进行了大量研究和深入研究之后,我终于找到了输入管道挂起的原因:FFMPEG 试图在开始时尽可能多地找到有关输入流的信息,为此,它会读取并解码输入,直到它确信拥有足够的信息为止。

根据您的编解码器和格式,这可能需要更多或更少的数据,并且在某些情况下(至少我的情况),4 秒的视频是不够的(为什么是另一个话题)。

默认的最大探测大小为 500000 字节,但可以使用选项进行修改-probesize(请参阅https://ffmpeg.org/ffmpeg-formats.html),例如:

fmpeg -probesize 8192 -i input ...

将此值设置为您确保在流开始时可用的最小数据大小将使您能够顺利进行(但您可能拥有的有关流的信息比您想要的要少)。

答案2

您需要使用一种可以即时工作的格式。例如 ogv、webm 或更好的 hls。例如,尝试一下: $ ffmpeg -i video.mp4 -f hls ./m3u8/video.m3u8 而且,ffplay ./m3u8/video.m3u8第一次就没问题。我推荐这一系列帖子: https://www.martin-riedl.de/ffmpeg/

答案3

我无法让它与 VLC 一起工作,但可以使用 FFplay 或乘用车,无需转换格式即可工作。

我们可以将数据直接写入FFplay(或mpv)子进程的stdin管道。

以下是 C# 代码示例:

using System;
using System.Diagnostics;


namespace PipeFfmpeg
{
    class Program
    {
        static void Main(string[] args)
        {
            //https://stackoverflow.com/questions/19658415/how-to-use-pipe-in-ffmpeg-within-c-sharp
            Process proc = new Process();

            //Get mpegps in stdin pipe, and convert to mpegts stream (the -bsf:v dump_extra may or may not be need).
            //proc.StartInfo.FileName = @"ffplay.exe";
            proc.StartInfo.FileName = @"mpv.exe";            
            proc.StartInfo.Arguments = String.Format(" - ");    //Get data from input pipe
            proc.StartInfo.UseShellExecute = false;
            proc.StartInfo.RedirectStandardInput = true;
            proc.StartInfo.RedirectStandardOutput = false;

            proc.Start();

            //https://superuser.com/questions/1708271/ffmpeg-waits-to-close-the-pipe-in-order-to-start-processing-data
            byte[] allBytes = System.IO.File.ReadAllBytes("input.mpeg");

            //Write the same content 500 times.
            for (int i = 0; i < 500; i++)
            {
                int idx = 0;
                for (;;)
                {
                    proc.StandardInput.BaseStream.Write(allBytes, idx, Math.Min(100, allBytes.Length - idx));
                    idx += 100;

                    if (idx >= allBytes.Length) break;
                }
            }
        }
    }
}

示例输出:
在此处输入图片描述


更新:

  • 将 mpeg-ps 数据写入 FFmpeg 的标准输入管道。
  • FFmpeg 生成新的时间戳,转换为原始 AVI 格式,并传递到 stdout 管道。
  • 一个线程从FFmpeg的stdout读取数据并将其写入MPV的stdin管道。

代码示例:

using System;
using System.Diagnostics;
using System.Threading.Tasks;


namespace PipeFfmpeg
{
    class Program
    {
        static void Main(string[] args)
        {
            //https://stackoverflow.com/questions/19658415/how-to-use-pipe-in-ffmpeg-within-c-sharp
            //Process proc = new Process();

            //https://stackoverflow.com/questions/21213895/how-to-stream-live-videos-with-no-latency-ffplay-mplayer-and-what-kind-of-wra
            //https://github.com/mpv-player/mpv/issues/4213
            //proc.StartInfo.FileName = @"mpv.exe";
            //proc.StartInfo.Arguments = String.Format("--profile=low-latency -f mpeg -vcodec h264 -acodec pcm_alaw - ");    //Get data from input pipe
            //proc.StartInfo.Arguments = String.Format(" - ");    //Get data from input pipe

            Process ffmpeg_proc = new Process();
            ffmpeg_proc.StartInfo.FileName = @"ffmpeg.exe";
            ffmpeg_proc.StartInfo.Arguments = String.Format("-fflags +genpts+igndts+ignidx -probesize 1024 -i pipe: -vf setpts=PTS-STARTPTS -af asetpts=PTS-STARTPTS -vcodec rawvideo -acodec pcm_s16le -pix_fmt bgr24 -f avi pipe:");
            ffmpeg_proc.StartInfo.UseShellExecute = false;
            ffmpeg_proc.StartInfo.RedirectStandardInput = true;
            ffmpeg_proc.StartInfo.RedirectStandardOutput = true;

            Process mpv_proc = new Process();
            mpv_proc.StartInfo.FileName = @"mpv.exe";
            mpv_proc.StartInfo.Arguments = String.Format(" - ");    //Get data from input pipe
            mpv_proc.StartInfo.UseShellExecute = false;
            mpv_proc.StartInfo.RedirectStandardInput = true;
            mpv_proc.StartInfo.RedirectStandardOutput = false;


            //https://stackoverflow.com/questions/16658873/how-to-minimize-the-delay-in-a-live-streaming-with-ffmpeg
            //proc.StartInfo.FileName = @"ffplay.exe";
            //proc.StartInfo.Arguments = String.Format("-f mpegts -i pipe:");    //Get data from input pipe

            //proc.StartInfo.FileName = @"c:\\Program Files\\VideoLAN\\VLC\\vlc.exe";
            //proc.StartInfo.Arguments = String.Format(" - ");    //Get data from input pipe

            mpv_proc.Start();
            ffmpeg_proc.Start();
            
            // Added 3 seconds delay - wait for FFplay or MPV to start.
            System.Threading.Thread.Sleep(3000);

            byte[] allBytes = System.IO.File.ReadAllBytes("input.mpeg");    //12fps

            var helper = Task.Run(() =>
            {
                int n_bytes;
                byte[] buf = new byte[1024];

                while (true)
                {
                    n_bytes = ffmpeg_proc.StandardOutput.BaseStream.Read(buf, 0, 1024);

                    if (n_bytes > 0)
                    {
                        mpv_proc.StandardInput.BaseStream.Write(buf, 0, n_bytes);
                    }
                }
            });

            //Write the same content 500000 times.
            for (int i = 0; i < 500000; i++)
            {
                int idx = 0;

                while (true)
                {
                    int block_size = 500;

                    ffmpeg_proc.StandardInput.BaseStream.Write(allBytes, idx, Math.Min(block_size, allBytes.Length - idx));
                    ffmpeg_proc.StandardInput.BaseStream.Flush();
                    idx += block_size;

                    System.Threading.Thread.Sleep(50);  //Sleep 50msec (83.333msec applies 12fps, assume some overhead).

                    Console.WriteLine("idx = {0}", idx);

                    if (idx >= allBytes.Length) break;
                }
            }
        }
    }
}

使用 FFplay 代替 mpv 播放器的相同代码:

using System;
using System.Diagnostics;
using System.Threading.Tasks;


namespace PipeFfmpeg
{
    class Program
    {
        static void Main(string[] args)
        {
            Process ffmpeg_proc = new Process();
            ffmpeg_proc.StartInfo.FileName = @"ffmpeg.exe";
            ffmpeg_proc.StartInfo.Arguments = String.Format("-fflags +genpts+igndts+ignidx -probesize 1024 -i pipe: -vf setpts=PTS-STARTPTS -af asetpts=PTS-STARTPTS -vcodec rawvideo -acodec pcm_s16le -pix_fmt bgr24 -f avi pipe:");
            ffmpeg_proc.StartInfo.UseShellExecute = false;
            ffmpeg_proc.StartInfo.RedirectStandardInput = true;
            ffmpeg_proc.StartInfo.RedirectStandardOutput = true;

            //Process mpv_proc = new Process();
            //mpv_proc.StartInfo.FileName = @"mpv.exe";
            //mpv_proc.StartInfo.Arguments = String.Format(" - ");    //Get data from input pipe
            //mpv_proc.StartInfo.UseShellExecute = false;
            //mpv_proc.StartInfo.RedirectStandardInput = true;
            //mpv_proc.StartInfo.RedirectStandardOutput = false;

            Process ffplay_proc = new Process();
            ffplay_proc.StartInfo.FileName = @"ffplay.exe";
            ffplay_proc.StartInfo.Arguments = String.Format(" - ");    //Get data from input pipe
            ffplay_proc.StartInfo.UseShellExecute = false;
            ffplay_proc.StartInfo.RedirectStandardInput = true;
            ffplay_proc.StartInfo.RedirectStandardOutput = false;


            ffplay_proc.Start();
            ffmpeg_proc.Start();
            
            // Added 3 seconds delay - wait for FFplay or MPV to start.
            System.Threading.Thread.Sleep(3000);

            byte[] allBytes = System.IO.File.ReadAllBytes("input.mpeg");    //12fps

            var helper = Task.Run(() =>
            {
                int n_bytes;
                byte[] buf = new byte[1024];

                while (true)
                {
                    n_bytes = ffmpeg_proc.StandardOutput.BaseStream.Read(buf, 0, 1024);

                    if (n_bytes > 0)
                    {
                        //mpv_proc.StandardInput.BaseStream.Write(buf, 0, n_bytes);
                        ffplay_proc.StandardInput.BaseStream.Write(buf, 0, n_bytes);
                    }
                }
            });

            //Write the same content 500000 times.
            for (int i = 0; i < 500000; i++)
            {
                int idx = 0;

                while (true)
                {
                    int block_size = 500;

                    ffmpeg_proc.StandardInput.BaseStream.Write(allBytes, idx, Math.Min(block_size, allBytes.Length - idx));
                    ffmpeg_proc.StandardInput.BaseStream.Flush();
                    idx += block_size;

                    System.Threading.Thread.Sleep(50);  //Sleep 50msec (83.333msec applies 12fps, assume some overhead).

                    Console.WriteLine("idx = {0}", idx);

                    if (idx >= allBytes.Length) break;
                }
            }
        }
    }
}

VLC 示例 - 重新编码为 H.264(和 aac)并重新混合为 mpegts:

using System;
using System.Diagnostics;
using System.Threading.Tasks;


namespace PipeFfmpeg
{
    class Program
    {
        static void Main(string[] args)
        {
            Process ffmpeg_proc = new Process();
            ffmpeg_proc.StartInfo.FileName = @"ffmpeg.exe";
            ffmpeg_proc.StartInfo.Arguments = String.Format("-fflags +genpts+igndts+ignidx -probesize 1024 -i pipe: -vf setpts=PTS-STARTPTS -af asetpts=PTS-STARTPTS -vcodec libx264 -crf 10 -acodec aac -pix_fmt yuv420p -f mpegts pipe:");
            ffmpeg_proc.StartInfo.UseShellExecute = false;
            ffmpeg_proc.StartInfo.RedirectStandardInput = true;
            ffmpeg_proc.StartInfo.RedirectStandardOutput = true;

            Process vlc_proc = new Process();
            vlc_proc.StartInfo.FileName = @"c:\\Program Files\\VideoLAN\\VLC\\vlc.exe";
            vlc_proc.StartInfo.Arguments = String.Format(" - ");    //Get data from input pipe
            vlc_proc.StartInfo.UseShellExecute = false;
            vlc_proc.StartInfo.RedirectStandardInput = true;
            vlc_proc.StartInfo.RedirectStandardOutput = false;

            vlc_proc.Start();
            ffmpeg_proc.Start();
            
            byte[] allBytes = System.IO.File.ReadAllBytes("input.mpeg");    //12fps

            var helper = Task.Run(() =>
            {
                int n_bytes;
                byte[] buf = new byte[1024];

                while (true)
                {
                    n_bytes = ffmpeg_proc.StandardOutput.BaseStream.Read(buf, 0, 1024);

                    if (n_bytes > 0)
                    {
                        vlc_proc.StandardInput.BaseStream.Write(buf, 0, n_bytes);
                    }
                }
            });

            //Write the same content 500000 times.
            for (int i = 0; i < 500000; i++)
            {
                int idx = 0;

                while (true)
                {
                    int block_size = 500;

                    ffmpeg_proc.StandardInput.BaseStream.Write(allBytes, idx, Math.Min(block_size, allBytes.Length - idx));
                    ffmpeg_proc.StandardInput.BaseStream.Flush();
                    idx += block_size;

                    System.Threading.Thread.Sleep(50);  //Sleep 50msec (83.333msec applies 12fps, assume some overhead).

                    Console.WriteLine("idx = {0}", idx);

                    if (idx >= allBytes.Length) break;
                }
            }
        }
    }
}

相关内容