DirectShow过滤器开发-读MP4视频文件过滤器

下载由本文代码制作的DLL
本过滤器解析MP4文件,读取视频流解码为RGB32,由视频输出引脚输出。读取音频流解码为16位PCM,由音频输出引脚输出。
本过滤器使用下面方式开发:
1.使用Win7系统。
2.安装Windows SDK 7.1软件开发工具包。
3.使用Visual Studio 2010,或Visual Studio 2008,或Visual Studio 2005开发环境。

过滤器从CSource类派生,实现了IFileSourceFilter,IMediaSeeking接口。IFileSourceFilter接口用于指定和获取要读取的MP4文件路径。IMediaSeeking接口用于调整播放的当前位置。在IFileSourceFilter接口的Load方法中,创建了1个源读取器,用于获取MP4文件的视频音频的一些参数。

视频引脚和音频引脚从CSourceStream类派生。重写了 GetMediaType,DecideBufferSize,FillBuffer,OnThreadDestroy,ThreadProc,DoBufferProcessingLoop函数。GetMediaType指定首选媒体类型;DecideBufferSize确定缓冲区大小;FillBuffer从源读取器读取音频或视频样本,填充引脚样本缓冲区;在OnThreadDestroy函数中,释放源读取器;在音频引脚ThreadProc函数中,创建了第2个源读取器,专门读取MP4中的音频样本;在视频引脚ThreadProc函数中,创建了第3个源读取器,专门读取MP4中的视频样本。从第3个源读取器读取的视频样本,格式类型实际为FORMAT_VideoInfo2,格式结构的图像高度值为负。

本过滤器的参数信息

过滤器GUID:{4EE72E8C-8E40-4AC2-8AB8-5A38AB9BC1A9}
DLL注册函数名:DllRegisterServer
删除注册函数名:DllUnregisterServer

视频输出引脚媒体类型:
主要类型:MEDIATYPE_Video
子类型:MEDIASUBTYPE_RGB32
格式类型:FORMAT_VideoInfo
样本为固定大小。
不使用时间压缩。

音频输出引脚媒体类型:
主要类型:MEDIATYPE_Audio
子类型:MEDIASUBTYPE_PCM
格式类型:FORMAT_WaveFormatEx
样本是固定大小。
不使用时间压缩。
样本为16位。
采样率为44100。

下面是本过滤器DLL的全部代码:

DLL头文件:MP4Reader.h

#ifndef  DLL_FILE
#define DLL_FILE

#include <streams.h>
#include <initguid.h>
#include <wmcodecdsp.h>

#if _DEBUG
	#pragma comment(lib, "Strmbasd.lib")//C:\Program Files\Microsoft SDKs\Windows\v7.1\Samples\multimedia\directshow\baseclasses\Debug\strmbasd.lib
#else
	#pragma comment(lib, "Strmbase.lib")//C:\Program Files\Microsoft SDKs\Windows\v7.1\Samples\multimedia\directshow\baseclasses\Release\strmbase.lib
#endif
#pragma comment(lib, "Winmm.lib")//C:\Program Files\Microsoft SDKs\Windows\v7.1\Lib\Winmm.lib
#pragma comment(lib, "msvcrt.lib")//C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\lib\msvcrt.lib

#include "dvdmedia.h"
#include <strsafe.h>
#include "strmif.h"
#include "mfapi.h"
#include "mfidl.h"
#include "mfreadwrite.h"

#pragma comment(lib, "mfplat")
#pragma comment(lib, "mfreadwrite")
#pragma comment(lib, "mfuuid")

template <class T> void SafeRelease(T** ppT)
{
	if (*ppT)
	{
		(*ppT)->Release();
		*ppT = NULL;
	}
}

// {4EE72E8C-8E40-4AC2-8AB8-5A38AB9BC1A9}
DEFINE_GUID(CLSID_MP4Reader, 
0x4ee72e8c, 0x8e40, 0x4ac2, 0x8a, 0xb8, 0x5a, 0x38, 0xab, 0x9b, 0xc1, 0xa9);

#endif // DLL_FILE


DLL源文件:MP4Reader.cpp

#include "MP4Reader.h"
#include "CFilter.h"

const AMOVIESETUP_MEDIATYPE sudPin1Types =   // 引脚1媒体类型
{
	&MEDIATYPE_Video,    // 主要类型
	&MEDIASUBTYPE_RGB32  // 子类型
};

const AMOVIESETUP_MEDIATYPE sudPin2Types =   // 引脚2媒体类型
{
    &MEDIATYPE_Audio,     // 主要类型
    &MEDIASUBTYPE_PCM     // 子类型
};

const AMOVIESETUP_PIN sudPins[]  =  // 引脚信息
{
	{
		L"VideoOut",                //引脚名称
		FALSE,                      //必须渲染输入引脚
		TRUE,                       //输出引脚
		FALSE,                      //具有该引脚的零个实例
		FALSE,                      //可以创建一个以上引脚的实例
		&CLSID_NULL,                //该引脚连接的过滤器的类标识
		NULL,                       //该引脚连接的引脚名称
		1,                          //引脚支持的媒体类型数
		&sudPin1Types               //媒体类型信息
	},
	{
		L"AudioOut",                //引脚名称
		FALSE,                      //必须渲染输入引脚
		TRUE,                       //输出引脚
		FALSE,                      //具有该引脚的零个实例
		FALSE,                      //可以创建一个以上引脚的实例
		&CLSID_NULL,                //该引脚连接的过滤器的类标识
		NULL,                       //该引脚连接的引脚名称
		1,                          //引脚支持的媒体类型数
		&sudPin2Types               //媒体类型信息
	}
} ;            

const AMOVIESETUP_FILTER MP4Reader =  //过滤器的注册信息
{
    &CLSID_MP4Reader,              //过滤器的类标识
    L"读MP4",                      //过滤器的名称
    MERIT_DO_NOT_USE,              //过滤器优先值
    2,                             //引脚数量
    sudPins                        //引脚信息
};

CFactoryTemplate g_Templates []  = {
    {
		L"读MP4"
      , &CLSID_MP4Reader
      , CFilter::CreateInstance
      , NULL
      , &MP4Reader 
	}
};

int g_cTemplates = 1;

STDAPI DllRegisterServer()//注册DLL
{
    return AMovieDllRegisterServer2(TRUE);
} 

STDAPI DllUnregisterServer()//删除DLL注册
{
    return AMovieDllRegisterServer2(FALSE);
}

extern "C" BOOL WINAPI DllEntryPoint(HINSTANCE, ULONG, LPVOID);

BOOL APIENTRY DllMain(HANDLE hModule, DWORD  dwReason, LPVOID lpReserved)
{
    return DllEntryPoint((HINSTANCE)(hModule), dwReason, lpReserved);
}

DLL模块定义文件:MP4Reader.def

LIBRARY     MP4Reader.dll

EXPORTS
            DllGetClassObject          PRIVATE
            DllCanUnloadNow            PRIVATE
            DllRegisterServer          PRIVATE
            DllUnregisterServer        PRIVATE

过滤器头文件:CFilter.h

#ifndef  FILTER_FILE
#define FILTER_FILE

#include  "MP4Reader.h"
#include  "CVideoPin.h"
#include  "CAudioPin.h" 

class CFilter : public CSource, public IFileSourceFilter, public IMediaSeeking
{
	friend class CVideoPin;
	friend class CAudioPin;
public:
	CFilter(LPUNKNOWN pUnk,HRESULT *phr);
	~CFilter();
	static CUnknown * WINAPI CreateInstance(LPUNKNOWN pUnk, HRESULT *phr);
	DECLARE_IUNKNOWN
    STDMETHODIMP Load(LPCOLESTR lpwszFileName, const AM_MEDIA_TYPE *pmt);
	STDMETHODIMP GetCurFile(LPOLESTR * ppszFileName, AM_MEDIA_TYPE *pmt);
	CVideoPin* pCVideoOut;//视频引脚指针
	CAudioPin* pCAudioOut;//音频引脚指针
	LPWSTR m_pFileName;//要读取的媒体文件路径
	HANDLE hInit;//“获取信息完成”事件句柄
	HANDLE hAReset;//“音频更改播放位置”事件句柄
	LONGLONG VResetPos;//视频新位置
	LONGLONG AResetPos;//音频新位置
	LONGLONG DUR;//媒体持续时间,100纳秒单位
	LONGLONG CUR;//音频当前时间,100纳秒单位
	IMFMediaType* pAudioMT;//音频媒体类型
	IMFMediaType* pVideoMT;//视频媒体类型
private:
	STDMETHODIMP NonDelegatingQueryInterface(REFIID riid, void ** ppv);
public:
	HRESULT STDMETHODCALLTYPE CheckCapabilities(DWORD *pCapabilities);
	HRESULT STDMETHODCALLTYPE ConvertTimeFormat(LONGLONG *pTarget, const GUID *pTargetFormat, LONGLONG Source, const GUID *pSourceFormat);
	HRESULT STDMETHODCALLTYPE GetAvailable(LONGLONG *pEarliest, LONGLONG *pLatest);
	HRESULT STDMETHODCALLTYPE GetCapabilities(DWORD *pCapabilities);
	HRESULT STDMETHODCALLTYPE GetCurrentPosition(LONGLONG *pCurrent);
	HRESULT STDMETHODCALLTYPE GetDuration(LONGLONG *pDuration);
	HRESULT STDMETHODCALLTYPE GetPositions(LONGLONG *pCurrent, LONGLONG *pStop);
	HRESULT STDMETHODCALLTYPE GetPreroll(LONGLONG *pllPreroll);
	HRESULT STDMETHODCALLTYPE GetRate(double *pdRate);
	HRESULT STDMETHODCALLTYPE GetStopPosition(LONGLONG *pStop);
	HRESULT STDMETHODCALLTYPE GetTimeFormat(GUID *pFormat);
	HRESULT STDMETHODCALLTYPE IsFormatSupported(const GUID *pFormat);
	HRESULT STDMETHODCALLTYPE IsUsingTimeFormat(const GUID *pFormat);
	HRESULT STDMETHODCALLTYPE QueryPreferredFormat(GUID *pFormat);
	HRESULT STDMETHODCALLTYPE SetPositions(LONGLONG *pCurrent, DWORD dwCurrentFlags, LONGLONG *pStop, DWORD dwStopFlags);
	HRESULT STDMETHODCALLTYPE SetRate(double dRate);
	HRESULT STDMETHODCALLTYPE SetTimeFormat(const GUID *pFormat);
};

#endif // FILTER_FILE


过滤器源文件:CFilter.cpp

#include "CFilter.h"
#include "CVideoPin.h"
#include "CAudioPin.h"


CFilter::CFilter(LPUNKNOWN lpunk, HRESULT *phr) : CSource(NAME("读MP4"), lpunk, CLSID_MP4Reader)
{
	pCAudioOut=new CAudioPin(phr, this, L"AudioOut");
	pCVideoOut=new CVideoPin(phr, this, L"VideoOut");
	hInit = CreateEvent( NULL, TRUE, FALSE, NULL); //创建事件,为手动重置,初始状态无信号 
	hAReset = CreateEvent( NULL, FALSE, FALSE, NULL); //创建事件,自动重置,初始状态无信号 
	m_pFileName=NULL;DUR=0;CUR=0; pVideoMT=NULL;pAudioMT=NULL;
	VResetPos=0;AResetPos=0;
}

CFilter::~CFilter()
{
	CloseHandle(hAReset);
	CloseHandle(hInit);
	SafeRelease(&pAudioMT);SafeRelease(&pVideoMT);
	if(m_pFileName!=NULL)delete[] m_pFileName;
}

STDMETHODIMP CFilter::NonDelegatingQueryInterface(REFIID iid, void ** ppv)
{
	if(iid==IID_IMediaSeeking)
	{
		return GetInterface(static_cast<IMediaSeeking*>(this), ppv);
	}
	else if(iid==IID_IFileSourceFilter)
	{
		return GetInterface(static_cast<IFileSourceFilter*>(this), ppv);
	}
	else
		return CBaseFilter::NonDelegatingQueryInterface(iid, ppv);
}

CUnknown * WINAPI CFilter::CreateInstance(LPUNKNOWN pUnk, HRESULT *phr)
{
    return new CFilter(pUnk, phr);//创建过滤器
} 

DWORD WINAPI  GetInfoThread(LPVOID pParam);//读取视频音频参数线程函数声明

STDMETHODIMP CFilter::Load(LPCOLESTR lpwszFileName, const AM_MEDIA_TYPE *pmt)
{
    CheckPointer(lpwszFileName,E_POINTER);
    if(wcslen(lpwszFileName) > MAX_PATH || wcslen(lpwszFileName)<4)
        return ERROR_FILENAME_EXCED_RANGE;

    size_t len = 1+lstrlenW(lpwszFileName);
    m_pFileName = (LPWSTR)new WCHAR[len];
    if (m_pFileName == NULL)return E_OUTOFMEMORY;
    HRESULT hr = StringCchCopyW(m_pFileName, len, lpwszFileName);

	wchar_t ExName[5]={m_pFileName[len-5],m_pFileName[len-4],m_pFileName[len-3],m_pFileName[len-2],0};
	if(wcscmp(ExName,L".mp4") != 0)//如果不是MP4文件
	{
		delete[] m_pFileName; m_pFileName=NULL;
		return VFW_E_INVALID_FILE_FORMAT;//设置文件名失败
	}
	CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)GetInfoThread,this,0,NULL);//创建读取视频音频参数线程
    return S_OK;
}

STDMETHODIMP CFilter::GetCurFile(LPOLESTR * ppszFileName, AM_MEDIA_TYPE *pmt)
{
	CheckPointer(ppszFileName, E_POINTER);
	*ppszFileName = NULL;
	if (m_pFileName !=NULL) 
	{
		DWORD n = sizeof(WCHAR)*(1+lstrlenW(m_pFileName));
		*ppszFileName = (LPOLESTR)CoTaskMemAlloc(n);
		if (*ppszFileName!=NULL)CopyMemory(*ppszFileName, m_pFileName, n);
	}
	return S_OK;
}

DWORD WINAPI  GetInfoThread(LPVOID pParam)//读取视频音频参数线程
{
	CFilter* pCFilter=(CFilter*)pParam;//过滤器指针
	ResetEvent(pCFilter->hInit);//设置“获取信息完成”无信号
	HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
    if (hr != S_OK)
	{
		MessageBox(NULL,L"初始化COM库失败",NULL,MB_OK); 
		return 0;
	}
    hr = MFStartup(MF_VERSION);
    if (hr != S_OK)
	{
		MessageBox(NULL,L"初始化媒体基础失败",NULL,MB_OK); 
		CoUninitialize();//关闭COM库
		return 0; 
	}
	IMFAttributes* pIMFAttributes = NULL;
	hr=MFCreateAttributes(&pIMFAttributes, 0);
	if (SUCCEEDED(hr))
	{
		hr = pIMFAttributes->SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, (UINT32)1);//使用基于硬件的媒体基础转换
	}
	if (SUCCEEDED(hr))
	{
		hr = pIMFAttributes->SetUINT32(MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING,  (UINT32)1);//允许源读取器进行有限的视频处理
	}
	IMFSourceReader* pIMFSourceReader = NULL;
	if (SUCCEEDED(hr))
	{
		hr = MFCreateSourceReaderFromURL(pCFilter->m_pFileName, pIMFAttributes, &pIMFSourceReader);
	}
    if (hr != S_OK)
	{
		MessageBox(NULL,L"创建源读取器失败",NULL,MB_OK); 
		MFShutdown();//关闭媒体基础
		CoUninitialize();//关闭COM库
		return 0; 
	} 	

	IMFMediaType* pAudioMTA = NULL;
	if (SUCCEEDED(hr))
	{
		hr = MFCreateMediaType(&pAudioMTA);//创建音频媒体类型
	}
	if (SUCCEEDED(hr))
	{
		hr = pAudioMTA->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);//设置主要类型音频
	}
	if (SUCCEEDED(hr))
	{
		hr = pAudioMTA->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM);//设置子类型PCM
	}
	if (SUCCEEDED(hr))
	{
		hr = pIMFSourceReader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, NULL, pAudioMTA);//设置音频媒体类型
	}
	SafeRelease(&pAudioMTA);

	IMFMediaType* pVideoMTV = NULL;
	if (SUCCEEDED(hr))
	{
		hr = MFCreateMediaType(&pVideoMTV);//创建视频媒体类型
	}
	if (SUCCEEDED(hr))
	{
		hr = pVideoMTV->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);//设置主要类型为视频
	}
	if (SUCCEEDED(hr))
	{
		hr =pVideoMTV->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32);//设置子类型RGB32
	}
	if (SUCCEEDED(hr))
	{
		hr = pIMFSourceReader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, NULL, pVideoMTV);//设置视频媒体类型
	}
	SafeRelease(&pVideoMTV);


	if (SUCCEEDED(hr))
	{
		hr = pIMFSourceReader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM,&pCFilter->pAudioMT);//获取当前音频媒体类型
	}
	if (SUCCEEDED(hr))
	{
		hr = pIMFSourceReader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM,&pCFilter->pVideoMT);//获取当前视频媒体类型
	}

	PROPVARIANT var;
	if (SUCCEEDED(hr))
	{
		PropVariantInit(&var);
		hr=pIMFSourceReader->GetPresentationAttribute(MF_SOURCE_READER_MEDIASOURCE ,MF_PD_DURATION,&var);//获取媒体时间长度,100纳秒单位
	}
    if (SUCCEEDED(hr))
    {
		pCFilter->DUR=(LONGLONG)var.uhVal.QuadPart;
		PropVariantClear(&var);
    }

	//释放所有接口
	SafeRelease(&pIMFSourceReader);SafeRelease(&pIMFAttributes);
	MFShutdown();//关闭媒体基础
	CoUninitialize();//关闭COM库
	SetEvent(pCFilter->hInit);//发出“获取信息完成”信号
	return 1;
}

HRESULT STDMETHODCALLTYPE CFilter::CheckCapabilities(DWORD *pCapabilities)//检查是否具有指定的查找功能
{
	if(pCapabilities==NULL)return E_POINTER;
	if(*pCapabilities == (AM_SEEKING_CanSeekAbsolute  | AM_SEEKING_CanGetDuration))
		return S_OK;
	else if(*pCapabilities & AM_SEEKING_CanSeekAbsolute) //可以查找到绝对位置
	{
		return  S_FALSE;
	}
	else if(*pCapabilities & AM_SEEKING_CanGetDuration)//可以获取持续时间
	{
		return  S_FALSE;
	}
	else return E_FAIL;
}

HRESULT STDMETHODCALLTYPE CFilter::ConvertTimeFormat(LONGLONG *pTarget, const GUID *pTargetFormat, LONGLONG Source, const GUID *pSourceFormat)
{
	return E_NOTIMPL;
}

HRESULT STDMETHODCALLTYPE CFilter::GetAvailable(LONGLONG *pEarliest, LONGLONG *pLatest)
{
	return E_NOTIMPL;
}

HRESULT STDMETHODCALLTYPE CFilter::GetCapabilities(DWORD *pCapabilities)
{
	*pCapabilities=AM_SEEKING_CanSeekAbsolute | AM_SEEKING_CanGetDuration;
	return S_OK;
}

HRESULT STDMETHODCALLTYPE CFilter::GetCurrentPosition(LONGLONG *pCurrent)
{
	*pCurrent=CUR;return S_OK;
}

HRESULT STDMETHODCALLTYPE CFilter::GetDuration(LONGLONG *pDuration)
{
	DWORD dw=WaitForSingleObject(hInit,0);
	if(dw!=WAIT_OBJECT_0)return E_NOTIMPL;
	*pDuration=DUR;
	return S_OK;
}

HRESULT STDMETHODCALLTYPE CFilter::GetPositions(LONGLONG *pCurrent, LONGLONG *pStop)
{
	DWORD dw=WaitForSingleObject(hInit,0);
	if(dw==WAIT_OBJECT_0)
	{
		*pCurrent=CUR;return S_OK;
	}
	else return S_FALSE;
}

HRESULT STDMETHODCALLTYPE CFilter::GetPreroll(LONGLONG *pllPreroll)
{
	*pllPreroll=0;
	return S_OK;
}

HRESULT STDMETHODCALLTYPE CFilter::GetRate(double *pdRate)
{
	*pdRate=1.0;return S_OK;
}

HRESULT STDMETHODCALLTYPE CFilter::GetStopPosition(LONGLONG *pStop)
{
	*pStop=DUR-AResetPos;
	return S_OK;
}

HRESULT STDMETHODCALLTYPE CFilter::GetTimeFormat(GUID *pFormat)
{
	if(pFormat==NULL)return E_POINTER;
	*pFormat=TIME_FORMAT_MEDIA_TIME;return S_OK;
}

HRESULT STDMETHODCALLTYPE CFilter::IsFormatSupported(const GUID *pFormat)
{
	if(TIME_FORMAT_MEDIA_TIME==*pFormat)return S_OK;
	else return S_FALSE;
}

HRESULT STDMETHODCALLTYPE CFilter::IsUsingTimeFormat(const GUID *pFormat)
{
	if(*pFormat==TIME_FORMAT_MEDIA_TIME)return S_OK;
	else return S_FALSE;
}

HRESULT STDMETHODCALLTYPE CFilter::QueryPreferredFormat(GUID *pFormat)
{
	if(pFormat==NULL)return E_POINTER;
	*pFormat=TIME_FORMAT_MEDIA_TIME;return S_OK;
}

HRESULT STDMETHODCALLTYPE CFilter::SetPositions(LONGLONG *pCurrent, DWORD dwCurrentFlags, LONGLONG *pStop, DWORD dwStopFlags)
{
	DWORD dw=WaitForSingleObject(hInit,0);
	if((dw==WAIT_OBJECT_0) && (dwCurrentFlags & AM_SEEKING_AbsolutePositioning))
	{
		HRESULT hr;
		VResetPos=*pCurrent;AResetPos=*pCurrent;*pStop=DUR-*pCurrent;
		hr=pCVideoOut->DeliverBeginFlush();
		hr=pCVideoOut->Stop();
		hr=pCVideoOut->DeliverEndFlush();
		hr=pCVideoOut->Run();
		SetEvent(hAReset);//发出“音频更改播放位置”信号
		return S_OK;
	}
	else
	{
		return S_FALSE;
	}
}

HRESULT STDMETHODCALLTYPE CFilter::SetRate(double dRate)
{
	if(dRate==1.0)return S_OK;
	else return S_FALSE;
}

HRESULT STDMETHODCALLTYPE CFilter::SetTimeFormat(const GUID *pFormat)
{
	if(*pFormat==TIME_FORMAT_MEDIA_TIME)return S_OK;
	else return S_FALSE;
}


音频输出引脚头文件:CAudioPin.h

#ifndef  PIN_FILE2
#define PIN_FILE2

#include "MP4Reader.h"
#include "CFilter.h"

class CAudioPin : public CSourceStream
{
	friend class CFilter;
public:
    CAudioPin(HRESULT *phr, CSource *pParent, LPCWSTR pPinName);
    ~CAudioPin();
	HRESULT GetMediaType(CMediaType *pmt);
	HRESULT DecideBufferSize(IMemAllocator * pAlloc, ALLOCATOR_PROPERTIES * pRequest);//确定缓冲区大小
	HRESULT FillBuffer(IMediaSample *pms);
	STDMETHODIMP Notify(IBaseFilter * pSender, Quality q);
	HRESULT OnThreadDestroy(void);
	CFilter* pCFilter;//过滤器指针
	IMFSourceReader* pIMFSourceReader;
protected:
	DWORD ThreadProc(void);
	HRESULT DoBufferProcessingLoop(void);
};


#endif // PIN_FILE2

音频输出引脚源文件:CAudioPin.cpp

#include "CAudioPin.h"

CAudioPin::CAudioPin(HRESULT *phr, CSource *pParent, LPCWSTR pPinName) : CSourceStream(NAME("AudioOut"), phr, pParent, pPinName)
{
	pCFilter=(CFilter*)pParent;
	pIMFSourceReader=NULL;
}

CAudioPin::~CAudioPin()
{

}

HRESULT CAudioPin::GetMediaType(CMediaType *pmt)
{
	DWORD dw=WaitForSingleObject(pCFilter->hInit,2000);//最多等待2秒
	if(dw!=WAIT_OBJECT_0) return E_UNEXPECTED;//如果没有“获取信息完成”信号,返回错误
	AM_MEDIA_TYPE* pMt=NULL;
	pCFilter->pAudioMT->GetRepresentation(AM_MEDIA_TYPE_REPRESENTATION,(void**)&pMt);//将IMFMediaType表示的媒体类型,转换为AM_MEDIA_TYPE结构形式
	pmt->Set(*pMt);
	pCFilter->pAudioMT->FreeRepresentation(AM_MEDIA_TYPE_REPRESENTATION,pMt);//释放GetRepresentation分配的内存
	return S_OK;
}

HRESULT CAudioPin::DecideBufferSize(IMemAllocator * pAlloc, ALLOCATOR_PROPERTIES * pRequest)
{
	DWORD dw=WaitForSingleObject(pCFilter->hInit,2000);//最多等待2秒
	if(dw!=WAIT_OBJECT_0) return E_FAIL;//如果没有“获取信息完成”信号
	HRESULT hr;
   	pRequest->cBuffers = 1;//1个缓冲区
    pRequest->cbBuffer = 1000000;//缓冲区的大小1M
    ALLOCATOR_PROPERTIES Actual;
    hr = pAlloc->SetProperties(pRequest,&Actual);
    if(FAILED(hr))return hr;
    if(Actual.cbBuffer < pRequest->cbBuffer)// 这个分配器是否不合适
    {
        return E_FAIL;
    }
    return NOERROR;
}

HRESULT CAudioPin::FillBuffer(IMediaSample *pms)
{
	HRESULT hr;
    BYTE *pBuffer;
    pms->GetPointer(&pBuffer);//获取样本缓冲区指针
    long BufferLen = pms->GetSize();//获取样本缓冲区大小

	DWORD dw=WaitForSingleObject(pCFilter->hAReset,0);
	if(dw==WAIT_OBJECT_0)//如果有“音频更改播放位置”信号
	{
		hr=DeliverBeginFlush();
		Sleep(1);
		hr=DeliverEndFlush();
		hr=DeliverNewSegment(0,pCFilter->DUR-pCFilter->AResetPos,1.0);
		PROPVARIANT pv;
		PropVariantInit(&pv);
		pv.vt=20;
		pv.hVal.QuadPart=pCFilter->AResetPos;
		hr=pIMFSourceReader->SetCurrentPosition(GUID_NULL, pv);//更改源读取器位置
		PropVariantClear(&pv);
		pms->SetDiscontinuity(TRUE);//设置流中断
	}

	DWORD index,flags;LONGLONG stime;
	IMFSample* pIMFSample=NULL;IMFMediaBuffer* pIMFMediaBuffer=NULL;
AganRead:
	hr=pIMFSourceReader->ReadSample(MF_SOURCE_READER_FIRST_AUDIO_STREAM,0,&index,&flags,&stime,&pIMFSample);//读取音频样本
	if(MF_SOURCE_READERF_ENDOFSTREAM == flags)return S_FALSE;//如果到达流的末尾,返回S_FALSE
	if(pIMFSample==NULL)
	{
		Sleep(1);goto AganRead;
	}
	LONGLONG Cur, Dur, End;
	hr=pIMFSample->GetSampleTime(&Cur);//获取显示时间
	pCFilter->CUR=Cur;
	hr=pIMFSample->GetSampleDuration(&Dur);//获取持续时间
	End=Cur+Dur;//计算样本结束时间
	Cur-=pCFilter->AResetPos;End-=pCFilter->AResetPos;
	DWORD Lt;
	hr=pIMFSample->GetTotalLength(&Lt);//获取有效长度
	hr=pIMFSample->GetBufferByIndex(0,&pIMFMediaBuffer);//获取缓冲区接口
	BYTE* pSB=NULL;
	hr=pIMFMediaBuffer->Lock(&pSB,NULL,NULL);//锁定缓冲区
	if(Lt<=(DWORD)BufferLen)//防止数据长度大于样本缓冲区大小时,进行数据复制
		CopyMemory(pBuffer, pSB, Lt);//复制数据
	pms->SetTime(&Cur,&End);//设置时间戳
	pms->SetActualDataLength(Lt);//设置有效数据长度
	pms->SetSyncPoint(TRUE);//设置样本为同步点
	hr=pIMFMediaBuffer->Unlock();//解锁缓冲区
	SafeRelease(&pIMFMediaBuffer);SafeRelease(&pIMFSample);//释放接口
    return S_OK;
} 

STDMETHODIMP CAudioPin::Notify(IBaseFilter * pSender, Quality q)
{
	 return NOERROR;
}

DWORD CAudioPin::ThreadProc()
{
	HRESULT hr;
    hr = MFStartup(MF_VERSION);
    if (hr != S_OK)
	{
		MessageBox(NULL,L"初始化媒体基础失败",NULL,MB_OK); 
		return 1;
	}
	IMFAttributes* pIMFAttributesAudio = NULL;
	hr=MFCreateAttributes(&pIMFAttributesAudio, 0);
	if (SUCCEEDED(hr))
	{
		hr = pIMFAttributesAudio->SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, (UINT32)1);//使用基于硬件的媒体基础转换
	}
	if (SUCCEEDED(hr))
	{
		hr = pIMFAttributesAudio->SetUINT32(MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, (UINT32)1);//允许源读取器进行有限的视频处理
	}
	if (SUCCEEDED(hr))
	{
		hr = MFCreateSourceReaderFromURL(pCFilter->m_pFileName, pIMFAttributesAudio, &pIMFSourceReader);//创建源读取器
	}
	SafeRelease(&pIMFAttributesAudio);
    if (hr != S_OK)
	{
		MessageBox(NULL,L"创建源读取器失败",NULL,MB_OK); 
		return 1;
	} 	
	WaitForSingleObject(pCFilter->hInit,INFINITE);//等待“获取信息完成”信号
	
	IMFMediaType* pAudioMTA = NULL;
	if (SUCCEEDED(hr))
	{
		hr = MFCreateMediaType(&pAudioMTA);//创建音频媒体类型
	}
	if (SUCCEEDED(hr))
	{
		hr = pAudioMTA->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);//设置主要类型音频
	}
	if (SUCCEEDED(hr))
	{
		hr = pAudioMTA->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM);//设置子类型PCM
	}
	if (SUCCEEDED(hr))
	{
		hr = pIMFSourceReader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, NULL, pAudioMTA);//设置音频媒体类型
	}
	SafeRelease(&pAudioMTA);
	if(FAILED(hr))
	{
		MessageBox(NULL,L"初始化音频媒体类型失败",NULL,MB_OK);return 1;
	}

    Command com;
    do 
	{
		com = GetRequest();
		if (com != CMD_INIT) 
		{
			DbgLog((LOG_ERROR, 1, TEXT("Thread expected init command")));
			Reply((DWORD) E_UNEXPECTED);
		}
    } while (com != CMD_INIT);

    DbgLog((LOG_TRACE, 1, TEXT("CSourceStream worker thread initializing")));

    hr = OnThreadCreate(); 
    if (FAILED(hr)) 
	{
        DbgLog((LOG_ERROR, 1, TEXT("CSourceStream::OnThreadCreate failed. Aborting thread.")));
        OnThreadDestroy();
		Reply(hr);	
        return 1;
    }
    Reply(NOERROR);

    Command cmd;
    do 
	{
		cmd = GetRequest();
		switch (cmd) 
		{
		case CMD_EXIT:
			Reply(NOERROR);
			break;
		case CMD_RUN:
			DbgLog((LOG_ERROR, 1, TEXT("CMD_RUN received before a CMD_PAUSE???")));
		
		case CMD_PAUSE:
			Reply(NOERROR);
			DoBufferProcessingLoop();
			break;
		case CMD_STOP:
			Reply(NOERROR);
			break;
		default:
			DbgLog((LOG_ERROR, 1, TEXT("Unknown command %d received!"), cmd));
			Reply((DWORD) E_NOTIMPL);
			break;
		}
    } while (cmd != CMD_EXIT);
    hr = OnThreadDestroy();
    if (FAILED(hr)) 
	{
        DbgLog((LOG_ERROR, 1, TEXT("CSourceStream::OnThreadDestroy failed. Exiting thread.")));
        return 1;
    }
    DbgLog((LOG_TRACE, 1, TEXT("CSourceStream worker thread exiting")));
    return 0;
}

HRESULT CAudioPin::OnThreadDestroy(void) 
{
	SafeRelease(&pIMFSourceReader);
	MFShutdown();//关闭媒体基础
	return NOERROR;
}

HRESULT CAudioPin::DoBufferProcessingLoop(void) 
{
    Command com;
    OnThreadStartPlay();
    do 
	{
		while (!CheckRequest(&com)) 
		{
			IMediaSample *pSample;
			HRESULT hr = GetDeliveryBuffer(&pSample,NULL,NULL,0);
			if (FAILED(hr)) 
			{
                Sleep(1);
				continue;	
			}
			hr = FillBuffer(pSample);
			if (hr == S_OK) 
			{
				hr = Deliver(pSample);
                pSample->Release();
                if(hr != S_OK)
                {
                  DbgLog((LOG_TRACE, 2, TEXT("Deliver() returned %08x; stopping"), hr));
                  return S_OK;
                }
			} 
			else if (hr == S_FALSE) 
			{
				pSample->Release();
				DeliverEndOfStream();
				return S_OK;
			} 
			else 
			{
                pSample->Release();
				DbgLog((LOG_ERROR, 1, TEXT("Error %08lX from FillBuffer!!!"), hr));
                DeliverEndOfStream();
                m_pFilter->NotifyEvent(EC_ERRORABORT, hr, 0);
                return hr;
			}
		}//while
		if (com == CMD_RUN || com == CMD_PAUSE) 
		{
			Reply(NOERROR);
		} 
		else if (com != CMD_STOP) 
		{
			Reply((DWORD) E_UNEXPECTED);
			DbgLog((LOG_ERROR, 1, TEXT("Unexpected command!!!")));
		}
	} while (com != CMD_STOP);
    return S_FALSE;
}

视频输出引脚头文件:CVideoPin.h

#ifndef  PIN_FILE1
#define PIN_FILE1

#include "MP4Reader.h"
#include "CFilter.h"

class CVideoPin : public CSourceStream
{
	friend class CFilter;
public:
    CVideoPin(HRESULT *phr, CSource *pParent, LPCWSTR pPinName);
    ~CVideoPin();
	HRESULT GetMediaType(CMediaType *pmt);
	HRESULT DecideBufferSize(IMemAllocator * pAlloc, ALLOCATOR_PROPERTIES * pRequest);//确定缓冲区大小
	HRESULT FillBuffer(IMediaSample *pms);
	STDMETHODIMP Notify(IBaseFilter * pSender, Quality q);
	HRESULT OnThreadStartPlay(void);
	HRESULT OnThreadDestroy(void);
	CFilter* pCFilter;
	IMFSourceReader* pIMFSourceReader;
	AM_MEDIA_TYPE* pMt;
	LONG BufferSize;
protected:
	DWORD ThreadProc(void);
	HRESULT DoBufferProcessingLoop(void);
};


#endif // PIN_FILE1

视频输出引脚源文件:CVideoPin.cpp

#include "CVideoPin.h"


CVideoPin::CVideoPin(HRESULT *phr, CSource *pParent, LPCWSTR pPinName) : CSourceStream(NAME("VideoOut"),phr, pParent, pPinName)
{
	pCFilter=(CFilter*)pParent;
	pIMFSourceReader=NULL;
	pMt=NULL;
}

CVideoPin::~CVideoPin()
{
	
}

HRESULT CVideoPin::GetMediaType(CMediaType *pmt)
{
	DWORD dw=WaitForSingleObject(pCFilter->hInit,2000);//最多等待2秒
	if(dw!=WAIT_OBJECT_0) return E_UNEXPECTED;//如果没有“获取信息完成”信号,返回错误
	pCFilter-> pVideoMT->GetRepresentation(AM_MEDIA_TYPE_REPRESENTATION,(void**)&pMt);//将IMFMediaType表示的媒体类型,转换为AM_MEDIA_TYPE结构形式
	VIDEOINFOHEADER2* pH2=(VIDEOINFOHEADER2*)(pMt->pbFormat);
	pmt->SetType(&MEDIATYPE_Video);//设置主要类型
	pmt->SetSubtype(&MEDIASUBTYPE_RGB32);//设置子类型
    pmt->SetFormatType(&FORMAT_VideoInfo);//设置格式类型
    pmt->SetTemporalCompression(FALSE);//不使用时间压缩
	VIDEOINFOHEADER* pH=(VIDEOINFOHEADER*)pmt->AllocFormatBuffer(sizeof(VIDEOINFOHEADER));//分配格式块内存
	pH->rcSource=pH2->rcSource;
	pH->rcTarget=pH2->rcTarget;
	pH->dwBitRate=pH2->dwBitRate;
	pH->dwBitErrorRate=pH2->dwBitErrorRate;
	pH->AvgTimePerFrame=pH2->AvgTimePerFrame;
	pH->bmiHeader.biSize=sizeof(BITMAPINFOHEADER);
	pH->bmiHeader.biWidth=pH2->bmiHeader.biWidth;
	pH->bmiHeader.biHeight=pH2->bmiHeader.biHeight;
	pH->bmiHeader.biPlanes=1;
	pH->bmiHeader.biBitCount=32;
	pH->bmiHeader.biCompression=BI_RGB;
	BufferSize=pH->bmiHeader.biSizeImage=pH2->bmiHeader.biWidth*abs(pH2->bmiHeader.biHeight)*4;
	pH->bmiHeader.biXPelsPerMeter=0;
	pH->bmiHeader.biYPelsPerMeter=0;
	pH->bmiHeader.biClrUsed=0;
	pH->bmiHeader.biClrImportant=0;
	pmt->SetSampleSize(BufferSize);//设置样本大小
	pCFilter-> pVideoMT->FreeRepresentation(AM_MEDIA_TYPE_REPRESENTATION,pMt);//释放GetRepresentation分配的内存
	return S_OK;
}

HRESULT CVideoPin::DecideBufferSize(IMemAllocator * pAlloc, ALLOCATOR_PROPERTIES * pRequest)
{
	DWORD dw=WaitForSingleObject(pCFilter->hInit,2000);//最多等待2秒
	if(dw!=WAIT_OBJECT_0) return E_FAIL;//如果没有“获取信息完成”信号
	HRESULT hr;
   	pRequest->cBuffers = 1;//1个缓冲区
	pRequest->cbBuffer =BufferSize;//缓冲区的大小
    ALLOCATOR_PROPERTIES Actual;
    hr = pAlloc->SetProperties(pRequest,&Actual);
    if(FAILED(hr))return hr;
    if(Actual.cbBuffer < pRequest->cbBuffer)// 这个分配器是否不合适
    {
        return E_FAIL;
    }
    return NOERROR;
}

HRESULT CVideoPin::FillBuffer(IMediaSample *pms)
{
	HRESULT hr;
    BYTE *pBuffer;
    pms->GetPointer(&pBuffer);//获取样本缓冲区指针
    long BufferLen = pms->GetSize();//获取样本缓冲区大小
	pms->SetSyncPoint(TRUE);//设置样本为同步点
	hr=pms->SetDiscontinuity(FALSE);

	DWORD index,flags;LONGLONG stime;
	IMFSample* pIMFSample=NULL;IMFMediaBuffer* pIMFMediaBuffer=NULL;
AganRead:
	hr=pIMFSourceReader->ReadSample(MF_SOURCE_READER_FIRST_VIDEO_STREAM,0,&index,&flags,&stime,&pIMFSample);//读取视频样本
	if(MF_SOURCE_READERF_ENDOFSTREAM == flags)return S_FALSE;//如果到达流的末尾,返回S_FALSE
	if(pIMFSample==NULL)
	{
		Sleep(1);goto AganRead;
	}
	LONGLONG Cur, Dur, End;
	hr=pIMFSample->GetSampleTime(&Cur);//获取显示时间
	hr=pIMFSample->GetSampleDuration(&Dur);//获取持续时间
	End=Cur+Dur;//计算样本结束时间
	Cur-=pCFilter->VResetPos;
	End-=pCFilter->VResetPos;
	DWORD Lt;
	hr=pIMFSample->GetTotalLength(&Lt);//获取有效长度
	hr=pIMFSample->GetBufferByIndex(0,&pIMFMediaBuffer);//获取缓冲区接口
	BYTE* pSB=NULL;
	hr=pIMFMediaBuffer->Lock(&pSB,NULL,NULL);//锁定缓冲区
	CopyMemory(pBuffer,pSB,BufferLen);
	pms->SetTime(&Cur,&End);//设置时间戳
	pms->SetActualDataLength(BufferLen);//设置有效数据长度
	hr=pIMFMediaBuffer->Unlock();//解锁缓冲区
	SafeRelease(&pIMFMediaBuffer);SafeRelease(&pIMFSample);//释放接口
    return S_OK;
} 

STDMETHODIMP CVideoPin::Notify(IBaseFilter * pSender, Quality q)
{
	 return NOERROR;
}

DWORD CVideoPin::ThreadProc()
{
	HRESULT hr;
    hr = MFStartup(MF_VERSION);
    if (hr != S_OK)
	{
		MessageBox(NULL,L"初始化媒体基础失败",NULL,MB_OK); 
		return 1;
	}
	IMFAttributes* pIMFAttributesVideo= NULL;
	hr=MFCreateAttributes(&pIMFAttributesVideo, 0);
	if (SUCCEEDED(hr))
	{
		hr = pIMFAttributesVideo->SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, (UINT32)1);//使用基于硬件的媒体基础转换
	}
	if (SUCCEEDED(hr))
	{
		hr = pIMFAttributesVideo->SetUINT32(MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, (UINT32)1);//允许源读取器进行有限的视频处理
	}
	if (SUCCEEDED(hr))
	{
		hr = MFCreateSourceReaderFromURL(pCFilter->m_pFileName, pIMFAttributesVideo, &pIMFSourceReader);//创建源读取器
	}
	SafeRelease(&pIMFAttributesVideo);
    if (hr != S_OK)
	{
		MessageBox(NULL,L"创建源读取器失败",NULL,MB_OK); 
		return 1;
	} 	
	WaitForSingleObject(pCFilter->hInit,INFINITE);//等待“获取信息完成”信号

	IMFMediaType* pVideoMTV = NULL;
	if (SUCCEEDED(hr))
	{
		hr = MFCreateMediaType(&pVideoMTV);//创建视频媒体类型
	}
	if (SUCCEEDED(hr))
	{
		hr = pVideoMTV->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);//设置主要类型为视频
	}
	if (SUCCEEDED(hr))
	{
		hr =pVideoMTV->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32);//设置子类型RGB32
	}
	if (SUCCEEDED(hr))
	{
		hr = pIMFSourceReader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, NULL, pVideoMTV);//设置视频媒体类型
	}
	SafeRelease(&pVideoMTV);
	if(FAILED(hr))
	{
		MessageBox(NULL,L"初始化媒体类型失败",NULL,MB_OK);return 1;
	}

    Command com;
    do 
	{
		com = GetRequest();
		if (com != CMD_INIT) 
		{
			DbgLog((LOG_ERROR, 1, TEXT("Thread expected init command")));
			Reply((DWORD) E_UNEXPECTED);
		}
    } while (com != CMD_INIT);

    DbgLog((LOG_TRACE, 1, TEXT("CSourceStream worker thread initializing")));

    hr = OnThreadCreate(); 
    if (FAILED(hr)) 
	{
        DbgLog((LOG_ERROR, 1, TEXT("CSourceStream::OnThreadCreate failed. Aborting thread.")));
        OnThreadDestroy();
		Reply(hr);	
        return 1;
    }
    Reply(NOERROR);

    Command cmd;
    do 
	{
		cmd = GetRequest();
		switch (cmd) 
		{
		case CMD_EXIT:
			Reply(NOERROR);
			break;
		case CMD_RUN:
			DbgLog((LOG_ERROR, 1, TEXT("CMD_RUN received before a CMD_PAUSE???")));
		
		case CMD_PAUSE:
			Reply(NOERROR);
			DoBufferProcessingLoop();
			break;
		case CMD_STOP:
			Reply(NOERROR);
			break;
		default:
			DbgLog((LOG_ERROR, 1, TEXT("Unknown command %d received!"), cmd));
			Reply((DWORD) E_NOTIMPL);
			break;
		}
    } while (cmd != CMD_EXIT);
    hr = OnThreadDestroy();
    if (FAILED(hr)) 
	{
        DbgLog((LOG_ERROR, 1, TEXT("CSourceStream::OnThreadDestroy failed. Exiting thread.")));
        return 1;
    }
    DbgLog((LOG_TRACE, 1, TEXT("CSourceStream worker thread exiting")));
    return 0;
}

HRESULT CVideoPin::OnThreadStartPlay(void)
{
	HRESULT hr;
	PROPVARIANT var;
	PropVariantInit(&var);
	var.vt=20;var.hVal.QuadPart=pCFilter->VResetPos;
	hr=pIMFSourceReader->SetCurrentPosition(GUID_NULL, var);//更改源读取器位置
	PropVariantClear(&var);
	return DeliverNewSegment(0,pCFilter->DUR-pCFilter->VResetPos,1.0);
}

HRESULT CVideoPin::OnThreadDestroy(void) 
{
	SafeRelease(&pIMFSourceReader);
	MFShutdown();//关闭媒体基础
	return NOERROR;
}

HRESULT CVideoPin::DoBufferProcessingLoop(void) 
{
    Command com;
    OnThreadStartPlay();
    do 
	{
		while (!CheckRequest(&com)) 
		{
			IMediaSample *pSample;
			HRESULT hr = GetDeliveryBuffer(&pSample,NULL,NULL,0);
			if (FAILED(hr)) 
			{
                Sleep(1);
				continue;	
			}
			hr = FillBuffer(pSample);
			if (hr == S_OK) 
			{
				hr = Deliver(pSample);
                pSample->Release();
                if(hr != S_OK)
                {
                  DbgLog((LOG_TRACE, 2, TEXT("Deliver() returned %08x; stopping"), hr));
                  return S_OK;
                }
			} 
			else if (hr == S_FALSE) 
			{
				pSample->Release();
				DeliverEndOfStream();
				return S_OK;
			} 
			else 
			{
                pSample->Release();
				DbgLog((LOG_ERROR, 1, TEXT("Error %08lX from FillBuffer!!!"), hr));
                DeliverEndOfStream();
                m_pFilter->NotifyEvent(EC_ERRORABORT, hr, 0);
                return hr;
			}
		}//while
		if (com == CMD_RUN || com == CMD_PAUSE) 
		{
			Reply(NOERROR);
		} 
		else if (com != CMD_STOP) 
		{
			Reply((DWORD) E_UNEXPECTED);
			DbgLog((LOG_ERROR, 1, TEXT("Unexpected command!!!")));
		}
	} while (com != CMD_STOP);
    return S_FALSE;
}

下载由本文代码制作的DLL

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

h3974

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

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值