DirectShow(C#)预览FPS与CPU占用下降问题及帧请求方案咨询
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




