最簡單的基於 DirectShow 的視頻播放器


50行代碼實現的一個最簡單的基於 DirectShow 的視頻播放器

本文介紹一個最簡單的基於 DirectShow 的視頻播放器。該播放器對於初學者來說是十分有用的,它包含了使用 DirectShow 播放視頻所有必備的函數。

直接貼上代碼,具體代碼的含義都寫在注釋中了:

/* 雷霄驊
 * 中國傳媒大學/數字電視技術
 * leixiaohua1020@126.com
 *
 */
// aviplayer.cpp : 定義控制台應用程序的入口點。

#include "stdafx.h"
#include <dshow.h>

// 用到的DirectShow SDK鏈接庫
#pragma comment(lib,"strmiids.lib")

int _tmain(int argc, _TCHAR* argv[])  
{  
    IGraphBuilder *pGraph = NULL;  
    IMediaControl *pControl = NULL;  
    IMediaEvent   *pEvent = NULL;   
// 初始化COM庫.
HRESULT hr = CoInitialize(NULL);  
if (FAILED(hr))  
    {  
        printf("錯誤 - 無法初始化 COM 組件");  
return -1;  
    }  
// 創建濾波器圖表管理器
   hr=CoCreateInstance(CLSID_FilterGraph, NULL,CLSCTX_INPROC_SERVER,IID_IGraphBuilder, (void **)&pGraph);  
if (FAILED(hr))  
    {  
        printf("錯誤 - 無法創建 Filter Graph Manager.");  
return -1;  
    }  
// 查詢媒體控制和媒體事件接口
   hr = pGraph->QueryInterface(IID_IMediaControl, (void **)&pControl);  
    hr = pGraph->QueryInterface(IID_IMediaEvent, (void **)&pEvent);  
// 建立圖表,在這里你可以更改待播放的文件名稱
    hr = pGraph->RenderFile(L"E:\\movie\\外婆.VOB", NULL);  
if (SUCCEEDED(hr))  
    {  
// 運行圖表.
        hr = pControl->Run();  
if (SUCCEEDED(hr))  
        {  
//等待回放結束事件.
long evCode;  
            pEvent->WaitForCompletion(INFINITE, &evCode);  
// 切記: 在實際應用當中,不能使用INFINITE標識, 因為它會不確定的阻塞程序
        }  
    }  
// 釋放所有資源和關閉COM庫
    pControl->Release();  
    pEvent->Release();  
    pGraph->Release();  
    CoUninitialize();  
return 0;  
}

最簡單的基於DirectShow的示例:視頻播放器

本文記錄一個最簡單的基於DirectShow的視頻播放器。DirectShow是一個龐大的框架,可以在Windows下實現多種多樣的視頻處理需求。但是它的“龐大”也使得新手不太容易學習它的使用。本文的例子正是為解決這一問題而做的,它只包含了使用DirectShow播放一個視頻文件所需要的最重要的函數。

流程圖

最簡單的使用DirectShow播放視頻文件的流程如下圖所示。

流程圖中涉及到幾個接口如下所示。

IGraphBuilder:繼承自IFilterGraph,用於構建Filter Graph。相比於IFilterGraph來說IGraphBuilder提供了一些更加“智能”的方法,例如RenderFile()方法。
IMediaControl:提供和播放控制有關的一些接口。

IMediaEvent:用來處理Filter Graph發出的事件。


流程圖中關鍵函數的作用如下所示。

CoInitialize() :初始化COM運行環境。
CoCreateInstance(…,pGraph) :用指定的類標識符創建一個Com對象。在該播放器中類標識符為“CLSID_FilterGraph”,用於創建IGraphBuilder。
pGraph->QueryInterface(…,pControl) :通過QueryInterface()查詢某個組件是否支持某個特定的接口。在這里查詢IMediaControl接口。
pGraph->QueryInterface(…,pEvent) :同上。在這里查詢IMediaEvent接口。
pGraph->RenderFile("xxx.mkv"):為指定的文件智能的構建一個Filter Graph。
pControl->Run() :開始運行Filter Graph中的所有Filter。
pEvent->WaitForCompletion() :等待Filter Graph處理完所有數據。

CoUninitialize():釋放CoInitialize()初始化的COM運行環境。

注意上述幾個函數是構建一個基於DirectShow的視頻播放器所必須的函數,除了上述幾個接口之外還經常用到以下幾個接口:

IBasicVideo:提供和視頻有關的一些接口。
IBasicAudio:提供和音頻有關的一些接口。
IVideoWindow:提供和窗口有關的一些接口。
IMediaSeeking:提供和播放位置有關的一些接口。

源代碼

/**
 * 最簡單的基於DirectShow的視頻播放器
 * Simplest DirectShow Player
 *
 * 雷霄驊 Lei Xiaohua
 * leixiaohua1020@126.com
 * 中國傳媒大學/數字電視技術
 * Communication University of China / Digital TV Technology
 * http://blog.csdn.net/leixiaohua1020
 *
 * 本程序是一個最簡單的基於DirectShow的播放器。
 * 適合初學者學習DirectShow。
 *
 * This example is the simplest Player based on DirectShow.
 * Suitable for the beginner of DirectShow.
 */

#include "stdafx.h"

#include <dshow.h>
#include <atlconv.h>


#define OUTPUT_INFO 1

//Show Filter in FilterGpragh
int show_filters_in_filtergraph(IGraphBuilder *pGraph){  
    printf("Filters in FilterGpragh=======\n");  
    USES_CONVERSION;  
    IEnumFilters *pFilterEnum=NULL;  
if(FAILED(pGraph->EnumFilters(&pFilterEnum))){  
        pFilterEnum->Release();  
return -1;  
    }  
    pFilterEnum->Reset();  
    IBaseFilter * filter = NULL;  
ULONG fetchCount = 0;  
//Pin Info
while (SUCCEEDED(pFilterEnum->Next(1, &filter, &fetchCount)) && fetchCount){  
if (!filter){  
continue;  
        }  
        FILTER_INFO FilterInfo;  
if (FAILED(filter->QueryFilterInfo(&FilterInfo))){  
continue;  
        }  
        printf("[%s]\n",W2A(FilterInfo.achName));  
        filter->Release();  
    }  
    pFilterEnum->Release();  
    printf("==============================\n");  
return 0;  
}  


int _tmain(int argc, _TCHAR* argv[])  
{  
    IGraphBuilder *pGraph = NULL;  
    IMediaControl *pControl = NULL;  
    IMediaEvent   *pEvent = NULL;   
//Get some param--------------
HRESULT hr1;  
    IBasicVideo *pVideo=NULL;  
    IBasicAudio *pAudio=NULL;  
    IVideoWindow *pWindow=NULL;  
    IMediaSeeking *pSeeking=NULL;  


// Init COM
HRESULT hr = CoInitialize(NULL);  
if (FAILED(hr)){  
        printf("Error - Can't init COM.");  
return -1;  
    }  

// Create FilterGraph
   hr=CoCreateInstance(CLSID_FilterGraph, NULL,CLSCTX_INPROC_SERVER,IID_IGraphBuilder, (void **)&pGraph);  
if (FAILED(hr)){  
        printf("Error - Can't create Filter Graph.");  
return -1;  
    }  
//  Query Interface
    hr = pGraph->QueryInterface(IID_IMediaControl, (void **)&pControl);  
    hr = pGraph->QueryInterface(IID_IMediaEvent, (void **)&pEvent);  
// RenderFile
    hr = pGraph->RenderFile(L"cuc_ieschool.mov", NULL);  
if (FAILED(hr)){  
        printf("Error - Can't Render File.");  
return -1;  
    }  
#if OUTPUT_INFO
//Get some information----------
long video_w=0,video_h=0,video_bitrate=0,audio_volume=0;  
long long duration_1=0,position_1=0;  
    REFTIME avgtimeperframe=0;  
float framerate=0,duration_sec=0,progress=0,position_sec=0;  
//Video
    hr1=pGraph->QueryInterface(IID_IBasicVideo, (void **)&pVideo);  
    pVideo->get_VideoWidth(&video_w);  
    pVideo->get_VideoHeight(&video_h);  
    pVideo->get_AvgTimePerFrame(&avgtimeperframe);  
    framerate=1/avgtimeperframe;  
//pVideo->get_BitRate(&video_bitrate);
//Audio
    hr1=pGraph->QueryInterface(IID_IBasicAudio, (void **)&pAudio);  
//Mute
//pAudio->put_Volume(-10000);
    printf("Some Information:\n");  
    printf("Video Resolution:\t%dx%d\n",video_w,video_h);  
    printf("Video Framerate:\t%.3f\n",framerate);  
//Window
    hr1=pGraph->QueryInterface(IID_IVideoWindow, (void **)&pWindow);  
    pWindow->put_Caption(L"Simplest DirectShow Player");  
//pWindow->put_Width(480);
//pWindow->put_Height(272);
//Seek
    hr1=pGraph->QueryInterface(IID_IMediaSeeking, (void **)&pSeeking);  
    pSeeking->GetDuration(&duration_1);  
//time unit:100ns=0.0000001s
    duration_sec=(float)duration_1/10000000.0;  
    printf("Duration:\t%.2f s\n",duration_sec);  
//pSeeking->SetPositions();
//PlayBack Rate
//pSeeking->SetRate(2.0);

//Show Filter in FilterGpagh
    show_filters_in_filtergraph(pGraph);  
//----------------------
#endif

    printf("Progress Info\n");  
    printf("Position\tProgress\n");  
if (SUCCEEDED(hr)){  
// Run
        hr = pControl->Run();  
if (SUCCEEDED(hr)){  
long evCode=0;  
//pEvent->WaitForCompletion(INFINITE, &evCode);
while(evCode!=EC_COMPLETE){  
//Info
#if OUTPUT_INFO
                pSeeking->GetCurrentPosition(&position_1);  
                position_sec=(float)position_1/10000000.0;  
                progress=position_sec*100/duration_sec;  
                printf("%7.2fs\t%5.2f%%\n",position_sec,progress);  
#endif
//1000ms
                pEvent->WaitForCompletion(1000, &evCode);  
            }  
        }  
    }  
// Release resource
    pControl->Release();  
    pEvent->Release();  
    pGraph->Release();  
    CoUninitialize();  
return 0;  
}

運行結果

程序運行后即可開始播放一個“cuc_ieschool.mov”文件。程序運行時候的截圖如下所示。由圖可見運行的同時程序在控制台中打印出了兩種信息:
(1) 該視頻的相關信息

(2) 播放該視頻的 Filter Graph中的Filter(該功能通過函數show_filters_in_filtergraph()完成)。

可以通過定義在代碼最前面宏OUTPUT_INFO控制是否輸出視頻的信息。定義成“0”的話則不會輸出視頻的信息。如下所示。

#define OUTPUT_INFO 1

最簡單的基於DirectShow的示例:視頻播放器圖形界面版

本文記錄一個最簡單的基於DirectShow的圖形界面的視頻播放器。基於DirectShow的圖形界面的播放器的例子還是比較多的,但是大部分都是“層層封裝”的例子。“層層封裝”的例子相對來說更加穩定,但是卻不是很容易理解。因為DirectShow本身的接口函數的數量就比較多,如果再加上封裝DirectShow的函數,合起來的函數數量是非常大的,很容易讓人搞不清哪些才是真正的DirectShow接口函數。本播放器剝去了 DirectShow例子中的“層層封裝”,直接調用DirectShow的接口完成視頻的播放工作,更加適合DirectShow入門使用。

幾個功能的實現機制

整個工程的代碼比較多,不再詳細記錄。在這里簡單記錄一下代碼中的幾個關鍵點。

視頻的播放/暫停/繼續/停止
播放

視頻“播放”的源代碼如下所示。簡單來說,完成了以下視頻播放的初始化工作:
(1) 輸入的URL轉換為Unicode編碼(RenderFile()函數支持的輸入是Unicode字符串)。
(2) 調用RenderFile()“智能”創建Filter Graph。
(3) 調用IMediaControl的Run()方法開始播放視頻。
(4) 開啟定時器,用於更新視頻播放的進度(后文詳細記錄)

void CplayerGUIDlg::OnBnClickedStart()  
{  
    CStringA cstr_urla;  
    CStringW cstr_urlw;  
HRESULT hr;  

//Render
#ifdef _UNICODE
    m_url.GetWindowText(cstr_urlw);  
#else
    USES_CONVERSION;  
    m_url.GetWindowText(cstr_urla);  
    cstr_urlw.Format(L"%s",A2W(cstr_urla));  
#endif
if(cstr_urlw.IsEmpty()){  
        AfxMessageBox(_T("Input URL is NULL!"));  
return;  
    }  

    hr = pGraph->RenderFile(cstr_urlw, NULL);  
if(FAILED(hr)){  
        AfxMessageBox(_T("Can't open input file!"));  
return;  
    }  

//Set Window
HWND screen_hwnd=NULL;  
    RECT windowRect;  
    screen_hwnd = this->GetDlgItem(IDC_SCREEN)->GetSafeHwnd();   
    ::GetClientRect(screen_hwnd, &windowRect);  

    pWindow->put_Visible(OAFALSE);  
    pWindow->put_Owner((OAHWND)screen_hwnd);  
    pWindow->put_Left(0);  
    pWindow->put_Top(0);  
    pWindow->put_Width(windowRect.right - windowRect.left);  
    pWindow->put_Height(windowRect.bottom - windowRect.top);  
    pWindow->put_WindowStyle(WS_CHILD|WS_CLIPCHILDREN|WS_CLIPSIBLINGS|WS_THICKFRAME);  
    pWindow->put_MessageDrain((OAHWND) screen_hwnd);//Receive Message
    pWindow->put_Visible(OATRUE);  

    pEvent->SetNotifyWindow((OAHWND)screen_hwnd, WM_GRAPHNOTIFY, 0);  

// Run
    hr = pControl->Run();  

    playerstate=STATE_PLAY;  
    SetBtn(STATE_PLAY);  
    SetTimer(1,1000,NULL);  
}
暫停/繼續

視頻“暫停/繼續”的源代碼如下所示。其中調用了IMediaControl的Pause()和Run()設定“暫停”或者是“繼續”。

void CplayerGUIDlg::OnBnClickedPause()  
{  
HRESULT hr;  
if(playerstate==STATE_PLAY){  
        hr=pControl->Pause();  
        playerstate=STATE_PAUSE;  
        GetDlgItem(ID_PAUSE)->SetWindowText(_T("Resume"));  
    }else if(playerstate==STATE_PAUSE){  
        hr=pControl->Run();  
        playerstate=STATE_PLAY;  
        GetDlgItem(ID_PAUSE)->SetWindowText(_T("Pause"));  
    }  

}
停止

視頻的“停止”的源代碼如下所示。該部分代碼完成了以下工作:
(1) 把播放的位置重新調整為0
(2) 調用IMediaControl的Pause()
(3) 關閉定時器
(4) 刪除Filter Graph中的Filter

void CplayerGUIDlg::OnBnClickedStop()  
{  
long long position = 0;  
HRESULT hr;  
    hr = pSeeking->SetPositions(&position, AM_SEEKING_AbsolutePositioning | AM_SEEKING_SeekToKeyFrame,   
        0, AM_SEEKING_NoPositioning);  
    KillTimer(1);  
    hr=pControl->Stop();  

// Enumerate the filters And remove them
    IEnumFilters *pEnum = NULL;  
    hr = pGraph->EnumFilters(&pEnum);  
if (SUCCEEDED(hr))  
    {  
        IBaseFilter *pFilter = NULL;  
while (S_OK == pEnum->Next(1, &pFilter, NULL))  
        {  
// Remove the filter.
            pGraph->RemoveFilter(pFilter);  
// Reset the enumerator.
            pEnum->Reset();  
            pFilter->Release();  
        }  
        pEnum->Release();  
    }  

    SystemClear();  
}
視頻播放進度在時間軸的顯示

隨着視頻的播放,需要在視頻播放進度的時間軸上更新播放進度信息。在程序中使用了一個定時器完成這個功能。
在視頻開始播放的時候,調用SetTimer()開啟定時器。時間間隔設置為1000ms。

SetTimer(1,1000,NULL);

在視頻停止播放的時候,調用KillTimer()結束定時器。

KillTimer(1);

在定時器的消息響應函數中,調用了IMediaSeeking的GetCurrentPosition()獲取視頻當前播放到的時間,調用了 IMediaSeeking的GetDuration ()獲取視頻的時長。根據以上函數得到的數值,計算后把結果設置到相應的控件上。這部分的代碼如下所示。

void CplayerGUIDlg::OnTimer(UINT_PTR nIDEvent)  
{  
if (nIDEvent == 1){  
        CString curtimestr,durationstr;  
long long curtime;  
long long duration;  
int tns, thh, tmm, tss;  
int progress;  
//ms
        pSeeking->GetCurrentPosition(&curtime);  
if(curtime!=0){  
//change to second
            tns = curtime/10000000;  
            thh  = tns / 3600;  
            tmm  = (tns % 3600) / 60;  
            tss  = (tns % 60);  
            curtimestr.Format(_T("%02d:%02d:%02d"),thh,tmm,tss);  
            m_curtime.SetWindowText(curtimestr);  
        }  
        pSeeking->GetDuration(&duration);  
if(duration!=0){  
            tns = duration/10000000;  
            thh  = tns / 3600;  
            tmm  = (tns % 3600) / 60;  
            tss  = (tns % 60);  
            durationstr.Format(_T("%02d:%02d:%02d"),thh,tmm,tss);  
            m_duration.SetWindowText(durationstr);  

            progress=curtime*100/duration;  
            m_progress.SetPos(progress);  
        }  
    }  
    CDialogEx::OnTimer(nIDEvent);  
}
視頻播放點的調整

當鼠標拖動滑動控制條(Slider Control)控件上的滑塊的時候,需要根據拖動的位置設置視頻的播放進度。此時調用IMediaSeeking的SetPositions()設定視頻的播放進度。消息響應函數中的代碼如下所示。

void CplayerGUIDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)  
{  
if (pScrollBar->GetSafeHwnd() == m_progress.GetSafeHwnd()){  
float pos_bar=0.0;  
long long duration=0.0;  
long long pos_time=0.0;  
if(nSBCode==SB_THUMBPOSITION){  
            pos_bar=(float)nPos/100.0;  
            pSeeking->GetDuration(&duration);  
            pos_time=pos_bar*duration;  

long long position = (long long)(pos_time);  
HRESULT hr = pSeeking->SetPositions(&position, AM_SEEKING_AbsolutePositioning | AM_SEEKING_SeekToKeyFrame,   
                0, AM_SEEKING_NoPositioning);  
        }  
    }  
    CDialogEx::OnHScroll(nSBCode, nPos, pScrollBar);  
}
“全屏播放”的問題

視頻的全屏播放通過IVideoWindow的put_FullScreenMode()實現,代碼如下所示。

void CplayerGUIDlg::OnBnClickedFullscreen()  
{  
    pWindow->put_FullScreenMode(OATRUE);  
}

同時,在“全屏模式”啟動后,如果按“ESC”鍵的話,可以關閉“全屏模式”。這部分的代碼在PreTranslateMessage()中實現,如下所示。

//Exit Full Screen mode when push "ESC"
BOOL CplayerGUIDlg::PreTranslateMessage(MSG* pMsg)  
{  
if (pMsg->message == WM_KEYDOWN){  
if (pMsg->wParam == VK_RETURN || pMsg->wParam == VK_ESCAPE){  
// Restore form fullscreen mode
            pWindow->put_FullScreenMode(OAFALSE);  

return 1;  
        }  
    }  
return CDialogEx::PreTranslateMessage(pMsg);  
}

在這里有一點需要注意,IVideoWindow的put_FullScreenMode()在Win7下是有問題的。只有在設置窗口樣式的的時候,在樣式中指定WS_THICKFRAME后才可以正常使用。例如如下代碼。

pWindow->put_WindowStyle(WS_CHILD|WS_CLIPCHILDREN|WS_CLIPSIBLINGS|WS_THICKFRAME);

如果沒有指定WS_THICKFRAME樣式的話,在退出“全屏”模式之后,視頻就顯示不出來了,取而代之的是一片黑色。

但是設定WS_THICKFRAME樣式之后,視頻窗口的外圍會有一層“白邊”,會影響到視頻顯示的美觀。因此我們如果希望正常使用全屏的話,可能需要找一種更好的方法,在這里我就沒有深入研究了。

運行結果

這是使用DirectShow基於MFC開發的一個示例播放器。實現了一個播放器的基本功能:播放,暫停/繼續,停止,播放時間軸的顯示,以及從任一點開始播放媒體。並且支持將媒體文件拖拽至播放器進行播放。播放前將媒體文件的路徑輸入到“URL”欄中,然后單擊“Start”即可開始播放。在軟件下方包含了“start”,“Pause”,“Stop”等按鈕用於控制媒體的播放。

播放時候的效果截圖如下所示。

單擊“Full Screen”可以全屏播放。單擊“Info”可以顯示正在播放媒體的信息,包括以下兩種信息:
(1) 該視頻的相關信息
(2) 播放該視頻的 Filter Graph中的Filter。

最簡單的基於DirectShow的示例:視頻播放器自定義版

流程圖

最簡單的基於DirectShow的自定義的視頻播放器的流程如下圖所示。

該流程圖中包含如下變量:

IGraphBuilder *pGraph:繼承自IFilterGraph,用於構建Filter Graph。
IMediaControl *pControl:提供和播放控制有關的一些接口。
IMediaEvent   *pEvent:用來處理Filter Graph發出的事件。
IBaseFilter *pF_source:源Filter。
IFileSourceFilter* pFileSource:源Filter的暴露的接口,用於設置輸入文件的路徑。
IBaseFilter *pF_demuxer:解復用Filter。
IBaseFilter *pF_decoder:解碼Filter。
IBaseFilter *pF_render:渲染Filter。
IPin *pOut:輸出Pin。
IPin *pIn:輸入Pin。
IPin **pPin:內部變量Pin。

該流程圖大體上可以分成以下步驟:
(1)       初始化DirectShow
包括以下幾個步驟:

a)       CoInitialize():初始化COM運行環境。
b)       CoCreateInstance(…,pGraph):用指定的類標識符創建一個Com對象。在這里創建IGraphBuilder。
c)       pGraph->QueryInterface(…,pControl):通過QueryInterface()查詢某個組件是否支持某個特定的接口。在這里查詢IMediaControl接口。
d)       pGraph->QueryInterface(…,pEvent):同上。在這里查詢IMediaEvent接口。

(2)       添加Source Filter
包括以下幾個步驟:

a)       CoCreateInstance(…,pF_source):創建Source Filter。
b)       pGraph->AddFilter(pF_source,…):將Source Filter加入Filter Graph。
c)       pF_source->QueryInterface(…,pFileSource):查找Source Filter的IFileSourceFilter接口。
d)       pFileSource->Load(L"xxx.mpg",pF_source):調用IFileSourceFilter的Load()方法加載視頻文件。

(3)       添加Demuxer Filter
包括以下幾個步驟:

a)       CoCreateInstance(…,pF_demuxer):創建Demuxer Filter。
b)       pGraph->AddFilter(pF_demuxer,…):將Demuxer Filter加入Filter Graph。

(4)       添加Decoder Filter
包括以下幾個步驟:

a)       CoCreateInstance(…,pF_decoder):創建Decoder Filter。
b)       pGraph->AddFilter(pF_decoder,…):將Decoder Filter加入Filter Graph。

(5)       添加Render Filter
包括以下幾個步驟:

a)       CoCreateInstance(…,pF_render):創建Render Filter。
b)       pGraph->AddFilter(pF_render,…):將Render Filter加入Filter Graph。

(6)       連接Source Filter和Demuxer Filter
調用了一個函數connect_filters()用於連接2個Filter。
connect_filters()的執行步驟如下:

a)       調用get_unconnected_pin()從源Filter中選擇一個沒有鏈接的輸出Pin。
b)       調用get_unconnected_pin()從目的Filter中選擇一個沒有鏈接的輸入Pin。
c)       連接這兩個Pin

get_unconnected_pin()的執行步驟如下:

a)       枚舉Filter上的Pin。
b)       遍歷這些Pin,查找符合輸出方向(通過IPin的QueryDirection()方法),而且沒有在使用的Pin(通過IPin的ConnectedTo()方法)。

(7)       連接Demuxer Filter和Decoder Filter
過程同上。
(8)       連接Decoder Filter和Render Filter
過程同上。
(9)       開始播放
包括以下步驟:

pControl->Run():開始運行Filter Graph中的所有Filter。
pEvent->WaitForCompletion():等待Filter Graph處理完所有數據。

上述步驟可以理解為在GraphEdit軟件中分別按照步驟添加以下控件。其中(1)、(2)、(3)、(4)為先添加的4個Filter,(5)、(6)、(7)為Filter之間的連接線。

源代碼

/**
 * 最簡單的基於DirectShow的視頻播放器(Custom)
 * Simplest DirectShow Player (Custom)
 *
 * 雷霄驊 Lei Xiaohua
 * leixiaohua1020@126.com
 * 中國傳媒大學/數字電視技術
 * Communication University of China / Digital TV Technology
 * http://blog.csdn.net/leixiaohua1020
 *
 * 本程序是一個簡單的基於DirectShow的視頻播放器。該播放器通過逐個添加
 * 濾鏡並連接這些濾鏡實現了視頻的播放。適合初學者學習DirectShow。
 * 
 * This software is a simple video player based on DirectShow.
 * It Add DirectShow Filter Manually and Link the Pins of these filters
 * to play videos.Suitable for the beginner of DirectShow.
 */

#include "stdafx.h"
#include <dshow.h>
//'1':Add filters manually
//'0':Add filters automatically
#define ADD_MANUAL 1

//Find unconnect pins
HRESULT get_unconnected_pin(  
    IBaseFilter *pFilter, // Pointer to the filter.
    PIN_DIRECTION PinDir, // Direction of the pin to find.
    IPin **ppPin) // Receives a pointer to the pin.
{  
    *ppPin = 0;  
    IEnumPins *pEnum = 0;  
    IPin *pPin = 0;  
HRESULT hr = pFilter->EnumPins(&pEnum);  
if (FAILED(hr))  
    {  
return hr;  
    }  
while (pEnum->Next(1, &pPin, NULL) == S_OK)  
    {  
        PIN_DIRECTION ThisPinDir;  
        pPin->QueryDirection(&ThisPinDir);  
if (ThisPinDir == PinDir)  
        {  
            IPin *pTmp = 0;  
            hr = pPin->ConnectedTo(&pTmp);  
if (SUCCEEDED(hr)) // Already connected, not the pin we want.
            {  
                pTmp->Release();  
            }  
else // Unconnected, the pin we want.
            {  
                pEnum->Release();  
                *ppPin = pPin;  
return S_OK;  
            }  
        }  
        pPin->Release();  
    }  
    pEnum->Release();  
// Did not find a matching pin.
return E_FAIL;  
}  

//Connect 2 filters
HRESULT connect_filters(  
    IGraphBuilder *pGraph,   
    IBaseFilter *pSrc,   
    IBaseFilter *pDest)  
{  
if ((pGraph == NULL) || (pSrc == NULL) || (pDest == NULL))  
    {  
return E_POINTER;  
    }  
//Find Output pin in source filter
    IPin *pOut = 0;  
HRESULT hr = NULL;  
    hr=get_unconnected_pin(pSrc, PINDIR_OUTPUT, &pOut);  
if (FAILED(hr)){  
return hr;  
    }  
//Find Input pin in destination filter
    IPin *pIn = 0;  
    hr = get_unconnected_pin(pDest, PINDIR_INPUT, &pIn);  
if (FAILED(hr)){  
return hr;  
    }  
//Connnect them
    hr = pGraph->Connect(pOut, pIn);  
    pIn->Release();  
    pOut->Release();  
return hr;  
}  

int _tmain(int argc, _TCHAR* argv[])  
{  
    IGraphBuilder *pGraph = NULL;  
    IMediaControl *pControl = NULL;  
    IMediaEvent   *pEvent = NULL;   
// Init COM
HRESULT hr = CoInitialize(NULL);  
if (FAILED(hr)){  
        printf("Error - Can't init COM.");  
return -1;  
    }  

// Create FilterGraph
   hr=CoCreateInstance(CLSID_FilterGraph, NULL,CLSCTX_INPROC_SERVER,IID_IGraphBuilder, (void **)&pGraph);  
if (FAILED(hr)){  
        printf("Error - Can't create Filter Graph.");  
return -1;  
    }  
// Query Interface
    hr = pGraph->QueryInterface(IID_IMediaControl, (void **)&pControl);  
    hr = pGraph->QueryInterface(IID_IMediaEvent, (void **)&pEvent);  

//1. Add Filters=======================
//Source
    IBaseFilter *pF_source = 0;  
    hr = CoCreateInstance(CLSID_AsyncReader, 0, CLSCTX_INPROC_SERVER,IID_IBaseFilter, (void**)(&pF_source));  
if (FAILED(hr)){  
        printf("Failed to create File Source.\n");  
return -1;  
    }  
    hr = pGraph->AddFilter(pF_source, L"Lei's Source");  
if (FAILED(hr)){  
        printf("Failed to add File Source to Filter Graph.\n");  
return -1;  
    }  
    IFileSourceFilter* pFileSource;  
    pF_source->QueryInterface(IID_IFileSourceFilter, (void**)&pFileSource);  
    pFileSource->Load(L"cuc_ieschool.mpg", NULL);  
    pFileSource->Release();  

#if ADD_MANUAL
//Demuxer
    IBaseFilter *pF_demuxer = 0;  
    hr = CoCreateInstance(CLSID_MPEG1Splitter, 0, CLSCTX_INPROC_SERVER,IID_IBaseFilter, (void**)(&pF_demuxer));  
if (FAILED(hr)){  
        printf("Failed to create Demuxer.\n");  
return -1;  
    }  
    hr = pGraph->AddFilter(pF_demuxer, L"Lei's Demuxer");  
if (FAILED(hr)){  
        printf("Failed to add Demuxer to Filter Graph.\n");  
return -1;  
    }  
//Decoder
    IBaseFilter *pF_decoder = 0;  
    hr = CoCreateInstance(CLSID_CMpegVideoCodec, 0, CLSCTX_INPROC_SERVER,IID_IBaseFilter, (void**)(&pF_decoder));  
if (FAILED(hr)){  
        printf("Failed to create Decoder.\n");  
return -1;  
    }  
    hr = pGraph->AddFilter(pF_decoder, L"Lei's Decoder");  
if (FAILED(hr)){  
        printf("Failed to add Decoder to Filter Graph.\n");  
return -1;  
    }  
//Render
    IBaseFilter *pF_render = 0;  
    hr = CoCreateInstance(CLSID_VideoRenderer, 0, CLSCTX_INPROC_SERVER,IID_IBaseFilter, (void**)(&pF_render));  
if (FAILED(hr)){  
        printf("Failed to create Video Render.\n");  
return -1;  
    }  
    hr = pGraph->AddFilter(pF_render, L"Lei's Render");  
if (FAILED(hr)){  
        printf("Failed to add Video Render to Filter Graph.\n");  
return -1;  
    }  
//2. Connect Filters=======================
    hr = connect_filters(pGraph, pF_source, pF_demuxer);  
if (FAILED(hr)){  
        printf("Failed to link Source and Demuxer.\n");  
return -1;  
    }  
    hr = connect_filters(pGraph, pF_demuxer, pF_decoder);  
if (FAILED(hr)){  
        printf("Failed to link Demuxer and Decoder.\n");  
return -1;  
    }  
    hr = connect_filters(pGraph, pF_decoder, pF_render);  
if (FAILED(hr)){  
        printf("Failed to link Decoder and Render.\n");  
return -1;  
    }  

    pF_source->Release();  
    pF_demuxer->Release();  
    pF_decoder->Release();  
    pF_render->Release();  
#else
    IPin*    Pin;  
ULONG    fetched;  
//  get output pin
    IEnumPins* pEnumPins;  
    hr = pF_source->EnumPins(&pEnumPins);  
    hr = pEnumPins->Reset();  
    hr = pEnumPins->Next(1, &Pin, &fetched);  
    pEnumPins->Release();  
//  render pin, graph builder automatically complete rest works
    hr = pGraph->Render(Pin);  
#endif

if (SUCCEEDED(hr)){  
// Run
        hr = pControl->Run();  
if (SUCCEEDED(hr)){  
long evCode=0;  
            pEvent->WaitForCompletion(INFINITE, &evCode);  
        }  
    }  
//Release
    pControl->Release();  
    pEvent->Release();  
    pGraph->Release();  
    CoUninitialize();  
return 0;  
}

運行結果

程序的運行結果如下圖所示。運行后會播放“cuc_ieschool.mpg”文件。需要注意的是,本程序並沒有加入音頻解碼和播放的Filter,所以播放視頻的時候是沒有聲音的。

除了手動一個一個添加Filter之外,也可以在獲得“源”Filter的Pin之后,直接調用IFilterGraph的Render()方法“智能”自動構建Filter Graph。注意Render()方法和RenderFile()方法是不一樣的。RenderFile()是指定一個文件路徑后,自動構建整個 Filter Graph,相對來說更加簡單些;而Render()方法則是首先要創建一個Source Filter之后,才可以自動構建整個Filter Graph。
可以通過修改源文件首部的宏定義ADD_MANUAL來設定是否手動添加Filter,如下所示。

//'1':Add filters manually
//'0':Add filters automatically
#define ADD_MANUAL 1

最簡單的基於DirectShow的示例:獲取Filter信息

流程圖


該程序的流程圖如下所示。由於該圖的尺寸比較大,在頁面中顯示不下,所以在相冊中上傳了一份:

接口

該流程圖中涉及到以下接口:

ICreateDevEnum *pSysDevEnum:設備列舉接口。
IEnumMoniker *pEnumCat:Moniker(別名)枚舉接口。
IMoniker *pMoniker:Moniker(別名)接口。
IPropertyBag *pPropBag:存儲屬性值的接口。
IBaseFilter *pFilter:Filter接口。
IEnumPins * pinEnum:Filter枚舉接口。
IPin * pin: Pin接口。
PIN_INFO pinInfo:存儲Pin的信息的結構體。
IEnumMediaTypes *mtEnum:MediaType枚舉接口。

AM_MEDIA_TYPE   *mt:描述媒體類型的結構體。

流程圖

該流程圖中涉及到以下函數:

【初始化】

CoInitialize():初始化COM運行環境。

CoCreateInstance(…,pSysDevEnum):用指定的類標識符創建一個Com對象。在該示例中類標識符為“IID_ICreateDevEnum”,用於創建ICreateDevEnum。

【Filter的枚舉】

pSysDevEnum->CreateClassEnumerator(…,pEnumCat):通過ICreateDevEnum查詢IEnumMoniker枚舉接口,枚舉指定類型目錄下的設備Moniker(別名)。
pEnumCat->Next(…,pMoniker):通過IEnumMoniker查詢下一個IMoniker接口。
pMoniker->BindToStorage(…,pPropBag):通過IMoniker查詢IPropertyBag接口(用於獲取Filter信息)。
pPropBag->Read("FriendlyName"):通過IPropertyBag獲取“FriendlyName”屬性的值。

pMoniker->BindToObject(…,pFilter):通過IMoniker查詢IBaseFilter接口(用於獲取Filter,注意和BindToStorage()區別)。

【Pin的枚舉】

pFilter->EnumPins(pinEnum):通過IBaseFilter查詢IEnumPins枚舉接口。
pinEnum->Next(…,pin):通過IEnumPins查詢下一個IPin接口。

pin->QueryPinInfo(PinInfo):通過IPin獲取Pin的信息。

【MediaType的枚舉】

pin->EnumMediaTypes(&mtEnum):通過IPin查詢IEnumMediaTypes枚舉接口。
mtEnum->Next(…, &mt):通過IEnumMediaTypes查詢下一個AM_MEDIA_TYPE。

GuidToString(mt->majortype):把AM_MEDIA_TYPE的GUID轉換成字符串(方便輸出)。

【釋放】

CoUninitialize():釋放CoInitialize()初始化的COM運行環境。

再附上一張代碼中涉及到的接口之間的關系:

可以看出從上到下他們之間順序的排列如下所示:

ICreateDevEnum-->IEnumMoniker-->IMoniker-->IBaseFilter-->IEnumPins-->IPin-->IEnumMediaTypes-->AM_MEDIA_TYPE

源代碼

/**
 * 最簡單的Directshow信息顯示例子
 * Simplest DirectShow Info
 *
 * 雷霄驊 Lei Xiaohua
 * leixiaohua1020@126.com
 * 中國傳媒大學/數字電視技術
 * Communication University of China / Digital TV Technology
 * http://blog.csdn.net/leixiaohua1020
 *
 * 本程序是一段獲取DirectShow濾鏡信息的代碼。通過本代碼可以獲得
 * DirectShow濾鏡信息。適合初學者學習DirectShow。
 * 
 * This code can be used to get Directshow Filter's information.
 * Suitable for the beginner of DirectShow.
 */

#include "stdafx.h"
#include <dshow.h>
#include <atlconv.h>

#define OUTPUT_PIN       1
#define OUTPUT_MEDIATYPE 1


char* GuidToString(const GUID &guid)  
{  
int buf_len=64;  
char *buf =(char *)malloc(buf_len);  
    _snprintf(  
        buf,  
        buf_len,  
"{%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}",  
        guid.Data1, guid.Data2, guid.Data3,  
        guid.Data4[0], guid.Data4[1],  
        guid.Data4[2], guid.Data4[3],  
        guid.Data4[4], guid.Data4[5],  
        guid.Data4[6], guid.Data4[7]);  
//printf("%s\n",buf);
return buf;  
}  


int _tmain(int argc, _TCHAR* argv[])  
{  
    USES_CONVERSION;  
// Init COM
HRESULT hr=NULL;  
    hr= CoInitialize(NULL);  
if (FAILED(hr)){  
        printf("Error, Can not init COM.");  
return -1;  
    }  
    printf("===============Directshow Filters ===============\n");  
    ICreateDevEnum *pSysDevEnum = NULL;  
    hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER,  
        IID_ICreateDevEnum, (void **)&pSysDevEnum);  
if (FAILED(hr)){  
return hr;  
    }  

    IEnumMoniker *pEnumCat = NULL;  
//Category  
/************************************************************************
    Friendly Name                         CLSID
    -------------------------------------------------------------------------
    Audio Capture Sources                 CLSID_AudioInputDeviceCategory
    Audio Compressors                     CLSID_AudioCompressorCategory
    Audio Renderers                       CLSID_AudioRendererCategory
    Device Control Filters                CLSID_DeviceControlCategory
    DirectShow Filters                    CLSID_LegacyAmFilterCategory
    External Renderers                    CLSID_TransmitCategory
    Midi Renderers                        CLSID_MidiRendererCategory
    Video Capture Sources                 CLSID_VideoInputDeviceCategory
    Video Compressors                     CLSID_VideoCompressorCategory
    WDM Stream Decompression Devices      CLSID_DVDHWDecodersCategory
    WDM Streaming Capture Devices         AM_KSCATEGORY_CAPTURE
    WDM Streaming Crossbar Devices        AM_KSCATEGORY_CROSSBAR
    WDM Streaming Rendering Devices       AM_KSCATEGORY_RENDER
    WDM Streaming Tee/Splitter Devices    AM_KSCATEGORY_SPLITTER
    WDM Streaming TV Audio Devices        AM_KSCATEGORY_TVAUDIO
    WDM Streaming TV Tuner Devices        AM_KSCATEGORY_TVTUNER
    WDM Streaming VBI Codecs              AM_KSCATEGORY_VBICODEC 
    ************************************************************************/
    hr = pSysDevEnum->CreateClassEnumerator(CLSID_VideoCompressorCategory, &pEnumCat, 0);  
//hr = pSysDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &pEnumCat, 0);
//hr = pSysDevEnum->CreateClassEnumerator(CLSID_AudioCompressorCategory, &pEnumCat, 0);
//hr = pSysDevEnum->CreateClassEnumerator(CLSID_AudioInputDeviceCategory, &pEnumCat, 0);
//hr = pSysDevEnum->CreateClassEnumerator(CLSID_MediaMultiplexerCategory, &pEnumCat, 0);
//hr = pSysDevEnum->CreateClassEnumerator(CLSID_LegacyAmFilterCategory, &pEnumCat, 0);

if (hr != S_OK) {  
        pSysDevEnum->Release();  
return -1;  
    }  

    IMoniker *pMoniker = NULL;  
ULONG monikerFetched;  
//Filter
while(pEnumCat->Next(1, &pMoniker, &monikerFetched) == S_OK){  
        IPropertyBag *pPropBag;  
        VARIANT varName;  
        IBaseFilter *pFilter;  
        hr = pMoniker->BindToStorage(0, 0, IID_IPropertyBag,(void **)&pPropBag);  
if (FAILED(hr)){  
            pMoniker->Release();  
continue;  
        }  
        VariantInit(&varName);  
        hr = pPropBag->Read(L"FriendlyName", &varName, 0);  
//"FriendlyName": The name of the device.
//"Description": A description of the device.
//Filter Info================
        printf("[%s]\n",W2A(varName.bstrVal));  
        VariantClear(&varName);  
//========================
#if OUTPUT_PIN
        hr = pMoniker->BindToObject(NULL, NULL, IID_IBaseFilter,(void**)&pFilter);  
if (!pFilter){  
continue;  
        }  

        IEnumPins * pinEnum = NULL;  
        IPin * pin = NULL;  
ULONG pinFetched = 0;  
if (FAILED(pFilter->EnumPins(&pinEnum))){  
            pinEnum->Release();  
continue;     
        }  
        pinEnum->Reset();  
//Pin Info
while (SUCCEEDED(pinEnum->Next(1, &pin, &pinFetched)) && pinFetched){  
if (!pin){  
continue;  
            }  
            PIN_INFO pinInfo;  
if (FAILED(pin->QueryPinInfo(&pinInfo))){  
continue;  
            }  
            printf("\t[Pin] ");  
switch(pinInfo.dir){  
case PINDIR_INPUT:printf("Dir:Input  \t");break;  
case PINDIR_OUTPUT:printf("Dir:Output \t");break;  
default:printf("Dir:Unknown\n");break;  
            }  
            printf("Name:%s\n",W2A(pinInfo.achName));  

//MediaType
#if OUTPUT_MEDIATYPE
            IEnumMediaTypes *mtEnum=NULL;  
            AM_MEDIA_TYPE   *mt=NULL;  
if( FAILED( pin->EnumMediaTypes( &mtEnum )) )  
break;  
            mtEnum->Reset();  

ULONG mtFetched = 0;  

while (SUCCEEDED(mtEnum->Next(1, &mt, &mtFetched)) && mtFetched){  

                printf("\t\t[MediaType]\n");  
//Video
char *MEDIATYPE_Video_str=GuidToString(MEDIATYPE_Video);  
//Audio
char *MEDIATYPE_Audio_str=GuidToString(MEDIATYPE_Audio);  
//Stream
char *MEDIATYPE_Stream_str=GuidToString(MEDIATYPE_Stream);  
//Majortype
char *majortype_str=GuidToString(mt->majortype);  
//Subtype
char *subtype_str=GuidToString(mt->subtype);  

                printf("\t\t  Majortype:");  
if(strcmp(majortype_str,MEDIATYPE_Video_str)==0){  
                    printf("Video\n");  
                }else if(strcmp(majortype_str,MEDIATYPE_Audio_str)==0){  
                    printf("Audio\n");  
                }else if(strcmp(majortype_str,MEDIATYPE_Stream_str)==0){  
                    printf("Stream\n");  
                }else{  
                    printf("Other\n");  
                }  
                printf("\t\t  Subtype GUID:%s",subtype_str);  

                free(MEDIATYPE_Video_str);  
                free(MEDIATYPE_Audio_str);  
                free(MEDIATYPE_Stream_str);  
                free(subtype_str);  
                free(majortype_str);  
                printf("\n");  

            }  
#endif
            pin->Release();  

        }  
        pinEnum->Release();  

        pFilter->Release();  
#endif

        pPropBag->Release();  
        pMoniker->Release();  
    }  
    pEnumCat->Release();  
    pSysDevEnum->Release();  
    printf("=================================================\n");  
    CoUninitialize();  
return 0;  
}

運行結果


程序運行的結果如下圖所示。從圖中可以看出,程序打印出了系統中DirectShow的Filter信息。每個Filter的信息中包含了它的Pin的信息。每個Pin中又包含了Pin中的MediaType信息。

可以通過定義在代碼最前面宏 控制輸出的Filter信息的類型。定義成“0”的話則不會輸出該類的信息。如下所示。

#define OUTPUT_PIN       1
#define OUTPUT_MEDIATYPE 1


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM