使用Directshow + LAVFilter做一个万能格式的多媒体播放器

directshow 专栏收录该内容
9 篇文章 1 订阅

在Windows平台上做播放器很多人会选择用DirectShow框架,因为Directshow来做播放器流程简单,接口又比较丰富,只要接上Filter,视频解析、解码、渲染、回放以及复杂的音视频同步问题通通由框架去完成,开发者做的工作比较简单。这篇文章就详细给大家讲怎么开发一个DirectShow播放器,并且搭配目前播放器领域最著名的Filter---LAVFilter

微软的Directshow是一个实现多媒体播放,视音频处理等功能的多媒体库,并且提供了Filter的机制使得多媒体任务的各个处理单元(读取数据、分离、解码、编码、回放)达到模块化,提高了复用性。开发者可以用别人提供的Filter,也可以实现自己的Filter来处理特定的任务。每个Filter至少有一个Pin,Pin分输入Pin和输出Pin,按照Filter的类型输入Pin和输出Pin的数目不等。每个Pin都有自己接受的媒体格式,比如视频渲染器Filter的输入Pin只接受RGB或YUV的格式。两个Filter的Pin连接前需要对Pin进行媒体类型协商(确定两方的Pin的媒体类型是否一致),如果协商成功,两个Pin就可以连接起来。一连串的Filter通过这种机制连接形成一个Graph,从而完成复杂的多媒体任务,比如播放文件,录制麦克风的音频,采集摄像头图像。下面演示了一个播放AVI文件的Graph的Filter连接图:

在上面的Graph图中,包括几种Filter:Source Filter,Splitter,Video Decoder,Audio Decoder,Video Renderer,Audio Renderer。而这些Filter大部分系统已经提供了,还有少数功能需要第三方的Filter来完成。

 因为多媒体文件的格式众多,所以播放文件的Graph需要加入支持不同格式的Filter,其中最主要的Filter是:分离器,解码器。解码器Filter目前有很多了,免费的而又出名的解码器Filter有FFDshow。FFDshow底层用到了FFmpeg,基于Directshow框架来实现,解码效率很高,但是比较庞大和臃肿,现在已基本不维护了。并且FFDshow只提供了编码器和解码器,还没有实现分离器。以前微软平台上的Directshow开发员会了支持尽量多的媒体格式,想尽办法从各处收集到各种分离器Filter,结果程序打包之后体积很大,并且兼容各种格式要插入不同的的Filter使程序变得复杂。很多第三方Filter不开源,加上测试不够,稳定性不好,容易产生很多问题。幸好,现在有个老外开发了一套Directshow插件,叫LAV Filters。不错,是一套,包括分离器,视频解码器和音频解码器。只要安装这套插件,播放大多数的多媒体格式基本上没问题了,省了开发员很多功夫。还有,这个LAV Filters套件还是开源的,作者对工程的更新速度很快,已经修复了很多Bug,现在变得很稳定了,连大名鼎鼎的开源播放器MPC也使用了LAV Filters,将它列为优先选用的插件。

说了那么多,大家一定很想使用LAV Filters来做开发吧?不用急,我下面就介绍如何在Directshow Graph中使用它。

首先,我们要知道这几个Filter的CLSID,下面是这几个Filter的CLSID定义:

  DEFINE_GUID(CLSID_LAVSplitter,

                0x171252A0, 0x8820, 0x4AFE, 0x9D, 0xF8, 0x5C, 0x92, 0xB2, 0xD6, 0x6B, 0x04);

  DEFINE_GUID(CLSID_LAVVideoDecoder, 

                0xEE30215D, 0x164F, 0x4A92, 0xA4, 0xEB, 0x9D, 0x4C, 0x13, 0x39, 0x0F, 0x9F);

  DEFINE_GUID(CLSID_LAVAudioDecoder,

                0xE8E73B6B, 0x4CB3, 0x44A4, 0xBE, 0x99, 0x4F, 0x7B, 0xCB, 0x96, 0xE4, 0x91);

  另外还有一个LAV Source Filter,CLSID是:

  DEFINE_GUID(CLSID_LAVSource, 

                0xB98D13E7, 0x55DB, 0x4385, 0xA3, 0x3D, 0x09, 0xFD, 0x1B, 0xA2, 0x63, 0x38);

 这个LAV Source Filter的功能跟LAVSplitter差不多,都是分离器,不同的是它没有输入Pin,它集成了了Async Source Filter + LAVSplitter的功能。

下面我们就准备在Directshow的Graph中加入这些Filter。首先,我们需要实现一个类来封装播放文件的所有接口,下面是类的声明:

class CDXGraph
{
private:
	IGraphBuilder *     mGraph;  
	IMediaControl *	    mMediaControl;
	IMediaEventEx *	    mEvent;
	IBasicVideo *	    mBasicVideo;
	IBasicAudio *	    mBasicAudio;
	IVideoWindow  *	    mVideoWindow;
	IMediaSeeking *	    mSeeking;
	IBaseFilter *       m_pSourceFilter;
	IBaseFilter *       m_pDumpFilter;

	DWORD		    mObjectTableEntry; 
	UINT                m_GraphMsg;

public:
	bool           m_bEnableSound;
	int            m_nJPEGQuality;
	SIZE           m_PictureSize;
	UINT           m_nFramerate;

public:
	CDXGraph();
	virtual ~CDXGraph();

public:
	virtual bool Create(void);
	virtual void Release(void);
	virtual bool Attach(IGraphBuilder * inGraphBuilder);

	IGraphBuilder * GetGraph(void); // Not outstanding reference count
	IMediaEventEx * GetEventHandle(void);

        UINT   GetGraphState();

	bool   SetClipSourceRect(RECT rcSource);
	bool   SetDisplayWindow(HWND inWindow, LPRECT  rcTarget);
	bool   SetNotifyWindow(HWND inWindow, long lMsg);
	bool   ResizeVideoWindow(long inLeft, long inTop, long inWidth, long inHeight);

	void   HandleEvent(LONG * eventCode);

	bool   Run(void);        // Control filter graph
	bool   Stop(void);
	bool   Pause(void);

	bool   IsRunning(void);  // Filter graph status
	bool   IsStopped(void);
	bool   IsPaused(void);

	//bool SetFullScreen(BOOL inEnabled);
	//bool GetFullScreen(void);

	// IMediaSeeking
	bool    GetCurrentPosition(LONGLONG * outPosition); //获得当前播放时间(单位为100纳秒, 等于10^(-4) ms )
	bool    GetStopPosition(LONGLONG * outPosition);
	bool    SetCurrentPosition(LONGLONG inPosition); //设置当前播放时间(单位为100纳秒, 等于10^(-4) ms )
	bool    SetStartStopPosition(LONGLONG inStart, LONGLONG inStop);
	bool    GetDuration(LONGLONG * outDuration); //获得文件时间长度(单位为100纳秒, 等于10^(-4) ms )
	bool    SetPlaybackRate(double inRate); //设置播放速度
	bool    GetPlaybackRate(double * outRate);

	// Attention: range from -10000 to 0, and 0 is FULL_VOLUME.
	bool    SetAudioVolume(long inVolume);//调节音量
	long    GetAudioVolume(void);
	// Attention: range from -10000(left) to 10000(right), and 0 is both.
	bool    SetAudioBalance(long inBalance);
	long    GetAudioBalance(void);

	bool    RenderFile(const TCHAR * inFile, DWORD & dwError);
	bool    SnapshotBitmap(const TCHAR * outFile);
	bool    GetTotalFrames(LONGLONG * outNum);

private:
	void    AddToObjectTable(void) ;
	void    RemoveFromObjectTable(void);
	
	bool    QueryInterfaces(void);

	HRESULT ConnectFilters(IPin * inOutputPin, IPin * inInputPin, const AM_MEDIA_TYPE * inMediaType = 0);
	void    DisconnectFilters(IPin * inOutputPin);

	HRESULT  RenderFilter(IBaseFilter * pFilter);
	HRESULT  GetVideoProps(IPin * pVideoOutputPin);
};

 接着列出最核心的一个函数CDXGraph::RenderFile,它负责创建FilterGraph,将各个Filter添加到Graph和连接起来。下面是函数的实现:

bool CDXGraph::RenderFile(const TCHAR * inFile, DWORD & dwError)
{
	if (mGraph == NULL)
	{
	   dwError = ERROR_INVALID_POINTER;
           return false;
	}

	dwError = 0;

	HRESULT hr;
	
	bool bVideoPinConnected = false;
	bool bAudioPinConnected = false;
	bool bPrivateStreamPinConnected = false;

#ifndef UNICODE
	WCHAR   wszFilePath[MAX_PATH] = {0};
	MultiByteToWideChar(CP_ACP, 0, inFile, -1, wszFilePath, MAX_PATH);
#else
	TCHAR   wszFilePath[MAX_PATH] = {0};
    lstrcpy(wszFilePath, inFile);
#endif
     
	CComPtr<IFileSourceFilter> pFileSource;
	CComPtr<IBaseFilter> pSplitter;
	CComPtr<IBaseFilter> pVideoDecoder;
	CComPtr<IBaseFilter> pVideoRenderer;

#if 0
	hr = AddFilterByCLSID(mGraph, CLSID_MpegSourceFilter, L"Mpeg Splitter ", &pSplitter );
	if(FAILED(hr))
	{
		OutputDebugString("Add Mpeg Splitter Filter Failed \n");
		return FALSE;
	}
#else
	hr = AddFilterByCLSID(mGraph, CLSID_LAVSource, L"LAV Source Splitter ", &pSplitter );
	if(FAILED(hr))
	{
		OutputDebugString("Add LAV Splitter Filter Failed \n");
		return FALSE;
	}
#endif
	hr  = pSplitter->QueryInterface(IID_IFileSourceFilter, (void**)&pFileSource);
	if(FAILED(hr))
	{
		dwError = ERROR_GET_INTERFACE_FAIL;
		return false;
	}

	hr = pFileSource->Load(wszFilePath, NULL);
	if(FAILED(hr))
	{
		dwError = ERROR_LOADFILE_FAIL;
		return false;
	}

	hr = AddFilterByCLSID(mGraph, CLSID_LAVVideoDecoder, L"LAV Video Decoder", &pVideoDecoder);
	if(FAILED(hr))
	{
		OutputDebugString("Add LAV Video Filter Failed \n");
		//return FALSE;
	}
		  
	hr = AddFilterByCLSID(mGraph, CLSID_VideoMixingRenderer9, L"VMR9 Renderer ", &pVideoRenderer);
	if(FAILED(hr))
	{
		OutputDebugString("Add VMR9 Renderer Filter Failed \n");
		return FALSE;
	}

	hr = RenderFilter(pSplitter);
	if(SUCCEEDED(hr))
		bVideoPinConnected = true;
	else
		bVideoPinConnected = false;

	if(!bAudioPinConnected && !bVideoPinConnected)
	{
	    OutputDebugString("RenderFilter Failed!\n");

	    OutputDebugString("第二次尝试用RenderFile自动连接\n");

		if (FAILED(mGraph->RenderFile(wszFilePath, NULL)))
		{
			OutputDebugString("RenderFile Failed!\n\n");

			dwError = ERROR_AUTO_RENDERFILE_FAIL;
			return false;
		}
		
		IBaseFilter * pVideoRenderer = NULL;
		hr = FindVideoRenderer(mGraph, &pVideoRenderer);
		if(SUCCEEDED(hr))
		{
		  CComPtr<IPin> pVideoPin = GetInPin(pVideoRenderer, 0);
		  GetVideoProps(pVideoPin);
		}
	}

	if(!m_bEnableSound)
	{
	   SetAudioVolume(-10000);
	}

	OutputDebugString("RenderFile Succeeded!\n");

   return true;
}

CDXGraph::RenderFile函数显示加入了这几个Filter:LAV Source,LAV Video Decoder,VMR9。其中,VMR9是视频渲染器,负责渲染图像和根据时间戳控制视频帧何时显示;LAVSource负责读取文件和从文件容器里分离出视频流和音频流,当我们调用AddFilterByCLSID函数(实际上调用了COM接口CoCreateInstance函数)创建这个Filter实例时,它是没有加载文件的,我们必须查询它的IFileSourceFilter接口指针,通过这个接口指针设置文件路径,把文件加载进去,如果文件加载成功,LAV Source会根据文件容器里媒体流的数目生成对应的OutputPin。接着,加入LAV Video Decoder,然后调用RenderFilter函数把LAV Source的每个OutputPin自动与下游的Filter进行连接,因为连接Filter的时候Graph Manager会优先选用已经添加到Graph中的Filter,那么LAV Source的Video Output Pin就会尝试与Video Decoder进行连接,而Audio Output Pin也一样,但是由于我们没有显示加入任何的Audio Decoder,Graph Manager会从系统安装的Filter中找到一个合适的解码器Filter插入进去,然后自动连接两个Filter的OutputPin与InputPin。如果所有Filter连接成功,那么播放文件Graph的Filter链路图就像下面这样子:

链路图构建完毕,后面调用IMediaControl接口的Run方法就可以运行播放流程。

下面是这个播放器的界面:

为了更方便调用该播放器的接口,我把播放的逻辑封装到一个DLL里面,做成一个SDK的形式,下面是SDK的接口:

//初始化SDK
PLAYASF_API BOOL  InitSDK();

//注销SDK
PLAYASF_API BOOL  UnitSDK();

//获取SDK版本号,如20110910
PLAYASF_API LONG  GetSDKVersion();

//打开一个文件
//参数:lpszFilePath -- 文件路径; handle -- 返回的文件句柄,用这个句柄来调用其他函数; dwError -- 失败时返回的错误码
PLAYASF_API BOOL  ASF_OpenFile(TCHAR * lpszFilePath, int & handle, DWORD & dwError); 

//播放文件
PLAYASF_API BOOL  ASF_PlayFile(int handle);

//停止播放
PLAYASF_API BOOL  ASF_StopFile(int handle);

//暂停播放
//参数:bPause -- true 表示暂停,false表示继续播放
PLAYASF_API BOOL  ASF_PauseFile(int handle, bool bPause);

//关闭文件,关闭后不能调用Play播放
PLAYASF_API BOOL  ASF_CloseFile(int handle);

//快进播放,快进的递增速度依次为:1x, 2x, 4x, 8x, 10x
PLAYASF_API BOOL  ASF_PlayForward(int handle);

//快退(目前只支持 -1x)
PLAYASF_API BOOL  ASF_PlayBackward(int handle);

//获取文件的当前倍速
PLAYASF_API double   ASF_GetCurrentSpeed(int handle);

//设置文件播放进度
//参数:fRelativePos -- 进度的百分比(0~100)
PLAYASF_API BOOL  ASF_SetPlayPercentagePos(int handle, float fRelativePos);


//获取文件播放进度,返回值为进度值(0~100)
PLAYASF_API float ASF_GetPlayPercentagePos(int handle);


//获取文件的播放时长(时间单位为毫秒)
PLAYASF_API BOOL  ASF_GetPlayDuration(int handle, DWORD * nDuration); //单位为毫秒

//获取文件的当前播放时间点(时间单位为毫秒)
PLAYASF_API BOOL  ASF_GetCurrentPlayTime(int handle, DWORD * nCurTime); //单位为毫秒

//设置文件的播放时间点
PLAYASF_API BOOL  ASF_SetCurrentPlayTime(int handle, DWORD nTimePos); //单位为毫秒

//获取文件的播放状态
//参数:nState -- 0:停止,1:暂停,2:运行
PLAYASF_API BOOL  ASF_GetPlayState(int handle, int * nState);


//获取图像的大小
// 参数:nWidth -- 图像宽度, nHeight -- 图像高度
PLAYASF_API BOOL  ASF_GetPictureSize(int handle, int * nWidth, int * nHeight);

//获取播放帧率
PLAYASF_API int   ASF_GetFramerate(int handle); 

//设置视频的源裁剪区域
//参数:rcSrc -- 源视频矩形的坐标,即(left,top,right,bottom)
PLAYASF_API BOOL  ASF_SetClipVideoRectangle(int handle, RECT rcSrc);

//设置视频的显示区域坐标
//参数:hWnd -- 显示视频的窗口句柄;rcTarget = (left,top,right,bottom)
PLAYASF_API  BOOL  ASF_SetPlayVideoWnd(int handle, HWND hWnd, LPRECT lpTarget);

//设置消息通知窗口句柄
PLAYASF_API    BOOL   ASF_SetNotifyWindow(int handle, HWND hwnd, long lMsg);


//处理/获取播放文件过程中Graph Manager发出的消息
PLAYASF_API BOOL  ASF_GetEventMsg(int handle, HWND hWnd,  long * lEvent);

//打开或关闭声音
//参数:bEnable -- true 打开声音,false 关闭声音
PLAYASF_API  BOOL  ASF_EnableSound(int handle, bool bEnable);

//设置声音音量大小(0 ~10000)
PLAYASF_API  BOOL  ASF_SetSoundVolume(int handle, int nLevel);


//截图保存为JPEG文件
//参数:lpszPicPath为保存路径
PLAYASF_API  BOOL  ASF_SnapShoot(int handle, TCHAR * lpszPicPath);


//获取文件的总帧数
PLAYASF_API BOOL  ASF_GetTotalFrames(int handle, LONGLONG * nFrames);

 

这个播放器的代码下载链接:https://download.csdn.net/download/zhoubotong2012/11874032

 

  • 1
    点赞
  • 0
    评论
  • 4
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:创作都市 设计师:CSDN官方博客 返回首页

打赏作者

zhoubotong2012

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值