You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#中FFmpeg编码解码串行执行异常问题排查

问题分析:FFmpeg编码解码进程阻塞问题

我在C#中创建了FFmpeg编码和解码进程,尝试执行以下流程:

  1. 获取测试原始数据
  2. 将数据流式传输至FFmpeg进程进行编码
  3. 读取编码后的数据并流式传输至FFmpeg进程进行解码
  4. 向控制台输出字节大小

为实现1/30秒的执行间隔,我还添加了Thread.Sleep(1000/30)。但实际运行时,控制台仅输出一次原始帧和编码帧的字节大小后便无任何反应,以下是我的代码:

internal byte[] GetFrame(int width, int height)
{
    int frameSize = width * height * 3;

    byte[] frameData = new byte[frameSize];

    Random random = new Random();

    for(int i = 0; i < frameData.Length; i++)
    {
        frameData[i] = (byte)random.Next(256);
    }

    return frameData;
}

internal void EncodeAndDecode(int width, int height, int fps, int bitrate, string codec, string preset, string tune)
{
    Process ffmpegEncodeProcess = new Process
    {
        StartInfo = new ProcessStartInfo
        {
            FileName = "ffmpeg",
            Arguments = $"-f rawvideo -pixel_format rgb24 -s 540x720 -r 30 " +
                        $"-i - -c:v libx264 -preset ultrafast -tune zerolatency -maxrate 1500k -b:v 1500k " +
                        $"-f h264 pipe:1",
            RedirectStandardInput = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,      
            CreateNoWindow = true  
        }
    };

    Process ffmpegDecodeProcess = new Process
    {
        StartInfo = new ProcessStartInfo
        {
            FileName = "ffmpeg",
            Arguments = $"-f h264 -i pipe:0 " +
                        $"-f rawvideo -pixel_format rgb24 -s 540x720 pipe:1",
            RedirectStandardInput = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            CreateNoWindow = true
        }
    };

    //start process
    ffmpegEncodeProcess.Start();
    ffmpegDecodeProcess.Start();

    //input stream
    var ffmpegEncodeInput = ffmpegEncodeProcess.StandardInput.BaseStream;
    var ffmpegDecodeInput = ffmpegDecodeProcess.StandardInput.BaseStream;

    //output stream
    var ffmpegEncodeOutput = ffmpegEncodeProcess.StandardOutput.BaseStream;
    var ffmpegDecodeOutput = ffmpegDecodeProcess.StandardOutput.BaseStream;

    new Thread(async () =>
    {
        try
        {
            while(true)
            {
                byte[] encodedChunk = new byte[width * height * 3];
                byte[] decodedChunk = new byte[width * height * 3];

                byte[] frameData = GetFrame(width, height);

                Console.WriteLine($"[INFO] Original frame size: {frameData.Length}bytes.");
                
                if(frameData.Length == 0)
                {
                    break;
                }

                await ffmpegEncodeInput.WriteAsync(frameData, 0, frameData.Length);
                await ffmpegEncodeInput.FlushAsync();

                int encodedBytesRead = await ffmpegEncodeOutput.ReadAsync(encodedChunk, 0, encodedChunk.Length);
                
                Console.WriteLine($"[INFO] Encoded frame size: {encodedBytesRead}bytes.");

                if(encodedBytesRead > 0)
                {
                    await ffmpegDecodeInput.WriteAsync(encodedChunk, 0, encodedBytesRead);
                    await ffmpegDecodeInput.FlushAsync();

                    int decodedBytesRead = await ffmpegDecodeOutput.ReadAsync(decodedChunk, 0, decodedChunk.Length);

                    Console.WriteLine($"[INFO] Decoded frame size: {decodedBytesRead}bytes.");

                    if(decodedBytesRead > 0)
                    {
                        //do something...
                    }
                }

                Thread.Sleep(1000 / fps); //1/30s
            }
        }
        catch(Exception e)
        {
            Console.WriteLine(e);
        }
        finally
        {
            ffmpegEncodeInput.Close();
            ffmpegDecodeInput.Close();

            if(!ffmpegEncodeProcess.HasExited)
            {
                ffmpegEncodeProcess.Kill();
            }

            if(!ffmpegDecodeProcess.HasExited)
            {
                ffmpegDecodeProcess.Kill();
            }

            Console.WriteLine("[INFO]: FFmpeg process clean up.");
        }
    }).Start();
}

核心问题与修复方案

1. FFmpeg错误流未读取导致进程阻塞

FFmpeg会持续向StandardError输出日志,若不读取该流,缓冲区会被填满,直接导致FFmpeg进程挂起,无法处理后续输入输出。必须后台读取错误流释放缓冲区:

// 启动进程后立即后台读取错误流
_ = Task.Run(() => ffmpegEncodeProcess.StandardError.ReadToEnd());
_ = Task.Run(() => ffmpegDecodeProcess.StandardError.ReadToEnd());

2. 编码输出读取逻辑错误

H.264编码后的帧大小远小于原始RGB帧,你用width*height*3作为编码缓冲区时,ReadAsync会等待缓冲区填满才返回,而编码数据不足以填满缓冲区,导致线程卡死。修复方式:无需修改缓冲区大小,但要接受ReadAsync返回的实际读取字节数(即使未填满缓冲区)。

3. 异步线程中误用Thread.Sleep

Thread.Sleep会阻塞整个线程,在异步lambda中应改用await Task.Delay(1000/fps),避免阻塞异步执行流。

4. 随机数生成器重复初始化

每次调用GetFrame新建Random实例,短时间内会生成重复随机数(默认种子基于系统时钟),应将Random作为类成员复用:

private readonly Random _random = new Random();

internal byte[] GetFrame(int width, int height)
{
    int frameSize = width * height * 3;
    byte[] frameData = new byte[frameSize];
    _random.NextBytes(frameData); // 用内置方法更高效
    return frameData;
}

5. 解码帧读取不完整

解码后的原始帧大小固定,单次ReadAsync可能无法读取完整帧,需循环读取直到获取全部数据:

int totalDecodedBytes = 0;
while (totalDecodedBytes < decodedChunk.Length)
{
    int bytesRead = await ffmpegDecodeOutput.ReadAsync(decodedChunk, totalDecodedBytes, decodedChunk.Length - totalDecodedBytes);
    if (bytesRead == 0) break;
    totalDecodedBytes += bytesRead;
}
Console.WriteLine($"[INFO] Decoded frame size: {totalDecodedBytes}bytes.");

修复后的完整代码

private readonly Random _random = new Random();

internal byte[] GetFrame(int width, int height)
{
    int frameSize = width * height * 3;
    byte[] frameData = new byte[frameSize];
    _random.NextBytes(frameData);
    return frameData;
}

internal void EncodeAndDecode(int width, int height, int fps, int bitrate, string codec, string preset, string tune)
{
    Process ffmpegEncodeProcess = new Process
    {
        StartInfo = new ProcessStartInfo
        {
            FileName = "ffmpeg",
            Arguments = $"-f rawvideo -pixel_format rgb24 -s {width}x{height} -r {fps} " +
                        $"-i - -c:v libx264 -preset {preset} -tune {tune} -maxrate {bitrate}k -b:v {bitrate}k " +
                        $"-f h264 pipe:1",
            RedirectStandardInput = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            CreateNoWindow = true
        }
    };

    Process ffmpegDecodeProcess = new Process
    {
        StartInfo = new ProcessStartInfo
        {
            FileName = "ffmpeg",
            Arguments = $"-f h264 -i pipe:0 " +
                        $"-f rawvideo -pixel_format rgb24 -s {width}x{height} pipe:1",
            RedirectStandardInput = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            CreateNoWindow = true
        }
    };

    ffmpegEncodeProcess.Start();
    ffmpegDecodeProcess.Start();

    // 后台读取错误流防止阻塞
    _ = Task.Run(() => ffmpegEncodeProcess.StandardError.ReadToEnd());
    _ = Task.Run(() => ffmpegDecodeProcess.StandardError.ReadToEnd());

    var ffmpegEncodeInput = ffmpegEncodeProcess.StandardInput.BaseStream;
    var ffmpegDecodeInput = ffmpegDecodeProcess.StandardInput.BaseStream;
    var ffmpegEncodeOutput = ffmpegEncodeProcess.StandardOutput.BaseStream;
    var ffmpegDecodeOutput = ffmpegDecodeProcess.StandardOutput.BaseStream;

    new Thread(async () =>
    {
        try
        {
            while (true)
            {
                byte[] encodedChunk = new byte[width * height * 3];
                byte[] decodedChunk = new byte[width * height * 3];

                byte[] frameData = GetFrame(width, height);
                Console.WriteLine($"[INFO] Original frame size: {frameData.Length}bytes.");

                await ffmpegEncodeInput.WriteAsync(frameData, 0, frameData.Length);
                await ffmpegEncodeInput.FlushAsync();

                int encodedBytesRead = await ffmpegEncodeOutput.ReadAsync(encodedChunk, 0, encodedChunk.Length);
                Console.WriteLine($"[INFO] Encoded frame size: {encodedBytesRead}bytes.");

                if (encodedBytesRead > 0)
                {
                    await ffmpegDecodeInput.WriteAsync(encodedChunk, 0, encodedBytesRead);
                    await ffmpegDecodeInput.FlushAsync();

                    // 循环读取完整解码帧
                    int totalDecodedBytes = 0;
                    while (totalDecodedBytes < decodedChunk.Length)
                    {
                        int bytesRead = await ffmpegDecodeOutput.ReadAsync(decodedChunk, totalDecodedBytes, decodedChunk.Length - totalDecodedBytes);
                        if (bytesRead == 0) break;
                        totalDecodedBytes += bytesRead;
                    }
                    Console.WriteLine($"[INFO] Decoded frame size: {totalDecodedBytes}bytes.");
                }

                // 异步延迟替代Thread.Sleep
                await Task.Delay(1000 / fps);
            }
        }
        catch (Exception e)
        {
            Console.WriteLine($"[ERROR] {e.Message}");
        }
        finally
        {
            ffmpegEncodeInput.Close();
            ffmpegDecodeInput.Close();

            if (!ffmpegEncodeProcess.HasExited)
                ffmpegEncodeProcess.Kill();
            if (!ffmpegDecodeProcess.HasExited)
                ffmpegDecodeProcess.Kill();

            Console.WriteLine("[INFO]: FFmpeg process cleaned up.");
        }
    }).Start();
}

额外优化建议

  • 给编码进程添加-g 1参数,强制每帧为独立I帧,避免解码依赖前序帧导致的阻塞。
  • 监听进程Exited事件,及时处理FFmpeg异常退出的情况。

内容的提问来源于stack exchange,提问作者Minwoo Kim

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 22:54:54