You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

DirectShow(C#)预览FPS与CPU占用下降问题及帧请求方案咨询

Fixing DirectShow FPS Drop on Low-End WinForms PCs

Hey there! I've run into similar DirectShow performance quirks on low-spec hardware before, so let's break down what's happening and how timed frame requests can fix it.

What's Causing the FPS & CPU Shift?

Your initial 40% CPU usage makes sense—DirectShow is pushing frames at full speed to your preview. After 30 minutes, though, two common culprits are at play:

  • Windows power-saving features throttle the CPU to save energy (super common on budget PCs), or
  • DirectShow's default push-mode filters automatically reduce frame rate to lower sustained CPU load.

Since other DirectShowLib examples have the same issue, this is definitely an environment/behavior problem, not a bug in your code.

Can Timed Frame Requests Fix This?

Absolutely! Switching from DirectShow's default push mode (filters send frames to your app automatically) to pull mode (your app actively requests frames on a schedule) lets you take control. This prevents the system from throttling performance because you're explicitly telling it how often to process frames.

Step-by-Step Implementation

Here's how to set this up in your WinForms app:

1. Use ISampleGrabber for Frame Caching

First, create a callback class to cache incoming frames so your timer can retrieve them later:

public class FrameCacheCallback : ISampleGrabberCB
{
    private byte[] _latestFrame;
    private readonly object _lockObj = new object();

    // Called when a new frame is pushed to the grabber
    public int BufferCB(double sampleTime, IntPtr pBuffer, int bufferLen)
    {
        lock (_lockObj)
        {
            if (_latestFrame == null || _latestFrame.Length != bufferLen)
                _latestFrame = new byte[bufferLen];
            
            Marshal.Copy(pBuffer, _latestFrame, 0, bufferLen);
        }
        return 0;
    }

    // Unused for our pull-mode setup
    public int SampleCB(double sampleTime, IMediaSample pSample) => 0;

    // Retrieve the latest cached frame safely
    public byte[] GetLatestFrame()
    {
        lock (_lockObj)
        {
            return _latestFrame?.Clone() as byte[];
        }
    }
}

2. Initialize the DirectShow Graph

Set up your capture device, sample grabber, and connect them in the graph:

// Initialize core DirectShow objects
var graphBuilder = (IGraphBuilder)new FilterGraph();
var mediaControl = (IMediaControl)graphBuilder;
var sampleGrabber = new SampleGrabber();
var frameCallback = new FrameCacheCallback();

// Configure sample grabber for RGB24 video capture
var mediaType = new AMMediaType
{
    majorType = MediaType.Video,
    subType = MediaSubType.RGB24,
    formatType = FormatType.VideoInfo
};
sampleGrabber.SetMediaType(mediaType);
sampleGrabber.SetCallback(frameCallback, 1); // Enable buffer callbacks

// Add your capture device filter (replace with your device setup logic)
var captureFilter = GetYourCaptureDeviceFilter(); 
graphBuilder.AddFilter(captureFilter, "Video Capture");
graphBuilder.AddFilter(sampleGrabber, "Frame Grabber");

// Connect capture device output to sample grabber input
var captureOutputPin = GetPin(captureFilter, PinDirection.Output);
var grabberInputPin = GetPin(sampleGrabber, PinDirection.Input);
graphBuilder.Connect(captureOutputPin, grabberInputPin);

// Helper to get a filter's pin by direction
IPin GetPin(IBaseFilter filter, PinDirection direction)
{
    IEnumPins enumPins;
    filter.EnumPins(out enumPins);
    IPin[] pins = new IPin[1];
    while (enumPins.Next(1, pins, IntPtr.Zero) == 0)
    {
        PinInfo pinInfo;
        pins[0].QueryPinInfo(out pinInfo);
        if (pinInfo.dir == direction)
            return pins[0];
    }
    return null;
}

3. Add a Timer to Request Frames

Use a WinForms Timer (adjust interval for your desired FPS) to pull frames and update your preview:

// Set timer for ~30 FPS (adjust interval as needed)
var frameTimer = new System.Windows.Forms.Timer
{
    Interval = 33 // 1000ms / 30 ≈ 33ms
};

frameTimer.Tick += (sender, e) =>
{
    var frameData = frameCallback.GetLatestFrame();
    if (frameData == null) return;

    // Get video resolution from the grabber's media type
    var mediaType = sampleGrabber.GetConnectedMediaType();
    var videoInfo = (VideoInfoHeader)Marshal.PtrToStructure(mediaType.pbFormat, typeof(VideoInfoHeader));
    
    // Convert frame data to Bitmap and update PictureBox
    using (var bitmap = new Bitmap(
        videoInfo.BmiHeader.biWidth,
        videoInfo.BmiHeader.biHeight,
        videoInfo.BmiHeader.biWidth * 3, // RGB24 = 3 bytes per pixel
        PixelFormat.Format24bppRgb,
        Marshal.UnsafeAddrOfPinnedArrayElement(frameData, 0)))
    {
        // Update UI safely (WinForms requires thread access)
        if (pictureBoxPreview.InvokeRequired)
        {
            pictureBoxPreview.Invoke(new Action(() => 
                pictureBoxPreview.Image = new Bitmap(bitmap)));
        }
        else
        {
            pictureBoxPreview.Image = new Bitmap(bitmap);
        }
    }
};

// Start the timer and media capture
frameTimer.Start();
mediaControl.Run();

4. Critical Cleanup

Don't forget to release resources when closing your app to avoid leaks:

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    frameTimer.Stop();
    mediaControl.Stop();
    
    // Release all COM objects
    Marshal.ReleaseComObject(sampleGrabber);
    Marshal.ReleaseComObject(captureFilter);
    Marshal.ReleaseComObject(graphBuilder);
}

Extra Tips for Low-End Hardware

  • Adjust Power Plan: Switch to "High Performance" mode to prevent CPU throttling.
  • Use Hardware Renderers: Replace DirectShow's default renderer with VMR-9 or EVR to offload work to the GPU.
  • Lower Resolution: Reduce capture resolution (e.g., 640x480) if FPS is still an issue to cut CPU load.

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

火山引擎 最新活动