首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 使用 DirectShow 開發媒體播放器完整教學

     編輯:關於C語言

前言

DirectShow 是 Windows 多媒體串流架構,透過 Filter Graph 串接來源、解碼、渲染器等過濾器實現音視訊播放。C# 可透過 COM Interop 呼叫 QuartzTypeLib (DirectShow 核心介面) 開發播放器。本文完整說明介面繫結、播放控制、視窗嵌入、進度條同步、播放結束事件處理。

環境準備

  1. Visual Studio 建立 Windows Forms 專案
  2. 專案 → 加入參考 → COM → Active Movie control type library (QuartzTypeLib)
  3. 自動產生 Interop.QuartzTypeLib.dll,頂端加入 using QuartzTypeLib;

核心介面對應表

介面功能關鍵方法
IGraphBuilder / FilterGraph建立/管理過濾器圖RenderFile()
IMediaControl播放/暫停/停止控制Run(), Pause(), Stop(), GetState()
IMediaPosition進度/長度/速率控制CurrentPosition, Duration, put_CurrentPosition()
IVideoWindow視訊視窗嵌入/大小/樣式Owner, WindowStyle, SetWindowPosition()
IBasicAudio音量/平衡控制Volume, Balance
IMediaEventEx事件通知 (完成、錯誤)SetNotifyWindow(), GetEvent(), FreeEventParams()

播放器類別封裝

public class DirectShowPlayer : IDisposable
{
    private readonly FilgraphManager _graphBuilder;
    private IMediaControl _mediaControl;
    private IMediaPosition _mediaPosition;
    private IVideoWindow _videoWindow;
    private IMediaEventEx _mediaEventEx;
    private IBasicAudio _basicAudio;
    private readonly IntPtr _ownerHandle;
    private readonly Control _videoContainer; // Panel 等容器

    public event EventHandler PlaybackCompleted;
    public event EventHandler<ErrorEventArgs> PlaybackError;

    public DirectShowPlayer(Control videoContainer)
    {
        _videoContainer = videoContainer;
        _ownerHandle = videoContainer.Handle;
        _graphBuilder = new FilgraphManager();

        _mediaControl = (IMediaControl)_graphBuilder;
        _mediaPosition = (IMediaPosition)_graphBuilder;
        _basicAudio = (IBasicAudio)_graphBuilder;
        _mediaEventEx = (IMediaEventEx)_graphBuilder;

        // 設定事件通知視窗 (需覆寫 WndProc 的 Form)
        _mediaEventEx.SetNotifyWindow((int)_ownerHandle, WM_GRAPHNOTIFY, 0);
    }

    public void OpenFile(string filePath)
    {
        Stop(); // 確保前一個圖表停止
        _graphBuilder.RenderFile(filePath);
        SetupVideoWindow();
    }

    private void SetupVideoWindow()
    {
        try
        {
            _videoWindow = (IVideoWindow)_graphBuilder;
            _videoWindow.Owner = (int)_ownerHandle;
            _videoWindow.WindowStyle = WS_CHILD | WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
            ResizeVideoWindow();
        }
        catch
        {
            _videoWindow = null; // 僅音訊檔案無視訊視窗
        }
    }

    public void ResizeVideoWindow()
    {
        if (_videoWindow != null)
        {
            var rect = _videoContainer.ClientRectangle;
            _videoWindow.SetWindowPosition(rect.Left, rect.Top, rect.Width, rect.Height);
        }
    }

    public void Play() => _mediaControl?.Run();
    public void Pause() => _mediaControl?.Pause();
    public void Stop()
    {
        _mediaControl?.Stop();
        _mediaPosition?.put_CurrentPosition(0);
    }

    public double Duration => _mediaPosition?.Duration ?? 0;
    public double CurrentPosition
    {
        get => _mediaPosition?.CurrentPosition ?? 0;
        set { if (_mediaPosition != null) _mediaPosition.CurrentPosition = value; }
    }

    public int Volume
    {
        get => _basicAudio?.Volume ?? 0;
        set { if (_basicAudio != null) _basicAudio.Volume = value; } // -10000 ~ 0
    }

    // 必須在 Form 的 WndProc 中呼叫
    public void HandleGraphEvent(ref Message m)
    {
        if (m.Msg != WM_GRAPHNOTIFY) return;

        int evCode, param1, param2;
        while (_mediaEventEx.GetEvent(out evCode, out param1, out param2, 0) == 0)
        {
            _mediaEventEx.FreeEventParams(evCode, param1, param2);
            if (evCode == EC_COMPLETE) // 播放完成
                PlaybackCompleted?.Invoke(this, EventArgs.Empty);
            else if (evCode == EC_ERRORABORT) // 錯誤
                PlaybackError?.Invoke(this, new ErrorEventArgs(param1));
        }
    }

    public void Dispose()
    {
        Stop();
        if (_mediaEventEx != null) _mediaEventEx.SetNotifyWindow(0, 0, 0);
        Marshal.ReleaseComObject(_graphBuilder);
    }

    // Win32 常數
    private const int WM_GRAPHNOTIFY = 0x8001; // 自訂訊息碼,避免衝突
    private const int EC_COMPLETE = 0x01;
    private const int EC_ERRORABORT = 0x02;
    private const int WS_CHILD = 0x40000000;
    private const int WS_CLIPCHILDREN = 0x02000000;
    private const int WS_CLIPSIBLINGS = 0x04000000;
}

表單整合

UI 元件

  • Panel pnlVideo:視訊渲染容器,Dock = Fill
  • TrackBar trkProgress:進度條
  • Label lblTime:顯示 目前時間 / 總長度
  • Button btnOpen, btnPlay, btnPause, btnStop
  • Timer tmrUpdate (Interval=500ms):更新進度條

表單程式碼

public partial class PlayerForm : Form
{
    private DirectShowPlayer _player;

    public PlayerForm()
    {
        InitializeComponent();
        _player = new DirectShowPlayer(pnlVideo);
        _player.PlaybackCompleted += OnPlaybackCompleted;
        _player.PlaybackError += OnPlaybackError;
        tmrUpdate.Tick += TmrUpdate_Tick;
    }

    protected override void WndProc(ref Message m)
    {
        _player.HandleGraphEvent(ref m);
        base.WndProc(ref m);
    }

    private void btnOpen_Click(object sender, EventArgs e)
    {
        using var dlg = new OpenFileDialog
        {
            Filter = "媒體檔案|*.mp4;*.avi;*.mkv;*.mov;*.wmv;*.mp3;*.wav;*.wma|所有檔案|*.*"
        };
        if (dlg.ShowDialog() == DialogResult.OK)
        {
            _player.OpenFile(dlg.FileName);
            _player.Play();
            tmrUpdate.Start();
            UpdateUIState(true);
        }
    }

    private void btnPlay_Click(object sender, EventArgs e) => _player.Play();
    private void btnPause_Click(object sender, EventArgs e) => _player.Pause();
    private void btnStop_Click(object sender, EventArgs e) { _player.Stop(); tmrUpdate.Stop(); UpdateUIState(false); }

    private void TmrUpdate_Tick(object sender, EventArgs e)
    {
        if (_player.Duration > 0)
        {
            trkProgress.Maximum = (int)_player.Duration;
            trkProgress.Value = (int)Math.Min(_player.CurrentPosition, _player.Duration);
            lblTime.Text = $"{FormatTime(_player.CurrentPosition)} / {FormatTime(_player.Duration)}";
        }
    }

    private void trkProgress_MouseUp(object sender, MouseEventArgs e)
    {
        _player.CurrentPosition = trkProgress.Value;
    }

    private void OnPlaybackCompleted(object sender, EventArgs e)
    {
        Invoke(() => { _player.Stop(); tmrUpdate.Stop(); UpdateUIState(false); });
    }

    private void OnPlaybackError(object sender, ErrorEventArgs e)
    {
        Invoke(() => MessageBox.Show($"播放錯誤: 0x{e.ErrorCode:X8}"));
    }

    private void PlayerForm_Resize(object sender, EventArgs e) => _player.ResizeVideoWindow();
    private void PlayerForm_FormClosed(object sender, FormClosedEventArgs e) => _player.Dispose();

    private string FormatTime(double seconds)
    {
        var ts = TimeSpan.FromSeconds(seconds);
        return ts.ToString(ts.Hours > 0 ? @"hh\:mm\:ss" : @"mm\:ss");
    }
}

常見問題與解決方案

問題原因解決
視訊畫面不顯示/黑畫面視窗樣式錯誤、Owner 未設定、容器 Handle 尚未建立確保 WS_CHILD | WS_CLIPCHILDREN、在 Load 後設定、Resize 時同步
播放完成事件未觸發未正確呼叫 SetNotifyWindow、WndProc 未轉發檢查 WM_GRAPHNOTIFY 訊息碼唯一、GetEvent 迴圈處理所有事件
無法播放特定格式 (HEVC, VP9)系統缺少對應解碼器安裝 LAV Filters、K-Lite Codec Pack 或使用 Media Foundation 替代
記憶體洩漏 / 程式結束殘留進程COM 物件未釋放、Filter Graph 未停止實作 IDisposable、Marshal.ReleaseComObject、Stop() 釋放資源
64 位元專案找不到 QuartzTypeLibDirectShow 為 32 位元 COM,需啟用「Prefer 32-bit」或使用 Media Foundation專案屬性 → Build → 勾選 Prefer 32-bit,或改用 MediaFoundation NuGet 套件

現代化替代方案

  • Media Foundation (MF):Windows Vista+ 原生新一代多媒體平台,支援硬體加速、HEVC、受保護內容。C# 可用 MediaFoundation.Net 或 SharpMediaFoundation 封裝。
  • LibVLC / Vlc.DotNet:跨平台、格式支援最完整、硬體解碼、串流協定豐富。
  • FFmpeg + SDL2 / SkiaSharp:完全自控渲染管線,適合高階影像處理需求。

總結

DirectShow 雖為成熟技術,但在 Windows 7/10/11 仍具相容性,適合快速開發簡易播放器或維護舊專案。核心在於正確串接 Filter Graph、處理視窗嵌入與事件迴圈。新專案建議優先評估 Media Foundation 或 LibVLC 以獲得更好的長期支援與效能。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved