DirectShow 是 Windows 多媒體串流架構,透過 Filter Graph 串接來源、解碼、渲染器等過濾器實現音視訊播放。C# 可透過 COM Interop 呼叫 QuartzTypeLib (DirectShow 核心介面) 開發播放器。本文完整說明介面繫結、播放控制、視窗嵌入、進度條同步、播放結束事件處理。
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;
}
Panel pnlVideo:視訊渲染容器,Dock = FillTrackBar trkProgress:進度條Label lblTime:顯示 目前時間 / 總長度Button btnOpen, btnPlay, btnPause, btnStopTimer 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 位元專案找不到 QuartzTypeLib | DirectShow 為 32 位元 COM,需啟用「Prefer 32-bit」或使用 Media Foundation | 專案屬性 → Build → 勾選 Prefer 32-bit,或改用 MediaFoundation NuGet 套件 |
MediaFoundation.Net 或 SharpMediaFoundation 封裝。DirectShow 雖為成熟技術,但在 Windows 7/10/11 仍具相容性,適合快速開發簡易播放器或維護舊專案。核心在於正確串接 Filter Graph、處理視窗嵌入與事件迴圈。新專案建議優先評估 Media Foundation 或 LibVLC 以獲得更好的長期支援與效能。