前編からの続きです。
これでMediaFoundation系はとりあえず終了です。IMFByteStreamを実装してみてそれを使ってみます。
コンストラクタ、デストラクタ
//コンストラクタ
CMFByteStream::CMFByteStream(HRESULT *lphr,const TCHAR *lpFileName)
: CUnknownBase(NULL), m_lpFileName(NULL), m_hFileStream(NULL), m_ullStreamPos(0), m_ullStreamSize(0), m_hAsyncThread(NULL), m_hThreadEvent(NULL), m_lpAttributes(NULL)
{
HRESULT hr;
::InitializeCriticalSection(&m_vCSStream);
::InitializeCriticalSection(&m_vCSQueue);
if(lpFileName != NULL){
m_lpFileName = new TCHAR[lstrlen(lpFileName) + 1];
_tcscpy(m_lpFileName,lpFileName);
}
hr = MFCreateAttributes(&m_lpAttributes,0);
NonDelegatingAddRef();
if(lphr != NULL){ *lphr = hr; }
}
//デストラクタ
CMFByteStream::~CMFByteStream()
{
Close();
if(m_lpAttributes != NULL){
m_lpAttributes->Release();
m_lpAttributes = NULL;
}
if(m_lpFileName != NULL){
delete[] m_lpFileName;
m_lpFileName = NULL;
}
::DeleteCriticalSection(&m_vCSQueue);
::DeleteCriticalSection(&m_vCSStream);
}
初期化シーケンスは基本そのままです。コンストラクタで内部オブジェクトを順次初期化、デストラクタでデータを閉じた上内部オブジェクトの解放を行っています。
ちなみにIMFAttributesだけはヘルパ関数でオブジェクトを作成しています。第二引数は初期データサイズなのですが、0でも大丈夫のようです。
IUnknown系動作の実装
//インターフェイスの要求
STDMETHODIMP CMFByteStream::NonDelegatingQueryInterface(REFIID riid,void **ppv)
{
if(IsEqualIID(riid,IID_IMFByteStream)) return GetInterface(static_cast<IMFByteStream *>(this),ppv);
else if(IsEqualIID(riid,IID_IMFAttributes)) return GetInterface(m_lpAttributes,ppv);
return CUnknownBase::NonDelegatingQueryInterface(riid,ppv);
}
IMFByteStreamを要求されたときは自分自身を渡します。
また、IMFAttributesを要求されたときには持っている内部オブジェクトを返しておきます。
これがないと使用時にいろいろと大変なことになります。内部では使わないんですけれどもね・・・。
ファイルオープン、クローズ
//ストリームを開く
HRESULT CMFByteStream::Open(void)
{
unsigned int threadid;
if(IsValidStream()) return E_ABORT;
m_hFileStream = ::CreateFile(m_lpFileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
if(m_hFileStream == INVALID_HANDLE_VALUE){ m_hFileStream = NULL; return E_FAIL; }
if(!::GetFileSizeEx(m_hFileStream,(LARGE_INTEGER *)&m_ullStreamSize)) return E_FAIL;
m_hThreadEvent = ::CreateEvent(NULL,FALSE,FALSE,NULL);
if(m_hThreadEvent == NULL) return E_FAIL;
m_hAsyncThread = (HANDLE)_beginthreadex(NULL,0,&CMFByteStream::AsyncThread,this,0,&threadid);
if(m_hAsyncThread == NULL){ ::CloseHandle(m_hThreadEvent); m_hThreadEvent = NULL; return E_FAIL; }
return S_OK;
}
//ストリームを閉じる
STDMETHODIMP CMFByteStream::Close(void)
{
dequeAsyncItem::iterator it,itend;
{
CLockQueue lock(this);
for(it = m_dequeWorkItem.begin(),itend = m_dequeWorkItem.end();it != itend;++it){ it->lpResult->Release(); }
for(it = m_dequeDoneItem.begin(),itend = m_dequeDoneItem.end();it != itend;++it){ it->lpResult->Release(); }
m_dequeWorkItem.clear(); m_dequeDoneItem.clear();
}
if(IsValidThread()){
::SetEvent(m_hThreadEvent);
::WaitForSingleObject(m_hAsyncThread,INFINITE);
::CloseHandle(m_hThreadEvent);
::CloseHandle(m_hAsyncThread);
m_hThreadEvent = NULL; m_hAsyncThread = NULL;
}
if(IsValidStream()){
CLockStream lock(this);
::CloseHandle(m_hFileStream);
m_ullStreamPos = m_ullStreamSize = 0;
m_hFileStream = NULL;
}
return S_OK;
}
ファイルの非同期処理の形を見たまま書いています。スレッドの作成手順も見たまま。
閉じる方も手順は見たままです。スレッドを止める手順も見たまま。
非同期アイテムキューの片付けだけが面倒なくらいですか。
属性値の取得、設定
//ストリームの能力を取得する
STDMETHODIMP CMFByteStream::GetCapabilities(DWORD *pdwCapabilities)
{
CheckPointer(pdwCapabilities,E_POINTER);
if(!IsValidStream()) return E_HANDLE;
*pdwCapabilities = MFBYTESTREAM_IS_READABLE | MFBYTESTREAM_IS_SEEKABLE;
return S_OK;
}
//ストリームのサイズを取得する
STDMETHODIMP CMFByteStream::GetLength(QWORD *pqwLength)
{
CheckPointer(pqwLength,E_POINTER);
*pqwLength = m_ullStreamSize;
return S_OK;
}
//ストリームのサイズを設定する
STDMETHODIMP CMFByteStream::SetLength(QWORD qwLength)
{
return E_NOTIMPL;
}
//ストリームの位置を取得する
STDMETHODIMP CMFByteStream::GetCurrentPosition(QWORD *pqwPosition)
{
CheckPointer(pqwPosition,E_POINTER);
*pqwPosition = m_ullStreamPos;
return S_OK;
}
//ストリームの位置を設定する
STDMETHODIMP CMFByteStream::SetCurrentPosition(QWORD qwPosition)
{
if(qwPosition > m_ullStreamSize) return E_INVALIDARG;
return Seek(msoBegin,qwPosition,0,NULL);
}
//ストリームの終端かどうか
STDMETHODIMP CMFByteStream::IsEndOfStream(BOOL *pfEndOfStream)
{
CheckPointer(pfEndOfStream,E_POINTER);
*pfEndOfStream = m_ullStreamPos >= m_ullStreamSize ? TRUE : FALSE;
return S_OK;
}
データ位置取得時にはロックを置いていませんが、置いても意味はないです。
この辺は単に内部の状態を返すだけなので持っている値を返して完了です。唯一SetLengthは書き込み時しか動作する必要がないでE_NOTIMPLを返しています。
書き込み処理
//同期書き込みを行う
STDMETHODIMP CMFByteStream::Write(const BYTE *pb,ULONG cb,ULONG *pcbWritten)
{
return E_NOTIMPL;
}
//非同期書き込みを開始する
STDMETHODIMP CMFByteStream::BeginWrite(const BYTE *pb,ULONG cb,IMFAsyncCallback *pCallback,IUnknown *punkState)
{
return E_NOTIMPL;
}
//非同期書き込みを終了する
STDMETHODIMP CMFByteStream::EndWrite(IMFAsyncResult *pResult,ULONG *pcbWritten)
{
return E_NOTIMPL;
}
//書き込みバッファのフラッシュを行う
STDMETHODIMP CMFByteStream::Flush(void)
{
return E_NOTIMPL;
}
実装する必要がないのでE_NOTIMPLで終わりです。IStreamの実装と考え方は同じです。
データカーソル位置の変更
//ストリーム位置を移動する
STDMETHODIMP CMFByteStream::Seek(MFBYTESTREAM_SEEK_ORIGIN SeekOrigin,LONGLONG llSeekOffset,DWORD dwSeekFlags,QWORD *pqwCurrentPosition)
{
int64_t llNewPos;
CLockStream lock(this);
switch(SeekOrigin){
case msoBegin: llNewPos = llSeekOffset; break;
case msoCurrent: llNewPos = (int64_t)m_ullStreamPos + llSeekOffset; break;
default: return E_INVALIDARG;
}
if(llNewPos < 0){ llNewPos = 0; } else if((uint64_t)llNewPos > m_ullStreamSize){ llNewPos = m_ullStreamSize; }
m_ullStreamPos = llNewPos;
if(pqwCurrentPosition != NULL){ *pqwCurrentPosition = m_ullStreamPos; }
return S_OK;
}
今回はさすがにロックが必要ですね。ほとんど見たまんまの実装です。
dwSeekFlagは今回の場合見る必要はないので無視しています。
読み込み処理
//同期読み取りを行う
STDMETHODIMP CMFByteStream::Read(BYTE *pb,ULONG cb,ULONG *pcbRead)
{
CheckPointer(pb,E_POINTER); CheckPointer(pcbRead,E_POINTER);
return InnerRead(pb,cb,pcbRead,m_ullStreamPos);
}
//非同期読み取りを開始する
STDMETHODIMP CMFByteStream::BeginRead(BYTE *pb,ULONG cb,IMFAsyncCallback *pCallback,IUnknown *punkState)
{
ASYNCITEM item; HRESULT hr;
CheckPointer(pb,E_POINTER); CheckPointer(pCallback,E_POINTER);
hr = MFCreateAsyncResult(NULL,pCallback,punkState,&item.lpResult);
if(FAILED(hr)) return hr;
item.pbBuffer = pb; item.cbSize = cb; item.cbRead = 0; item.nReadPos = m_ullStreamPos;
CLockQueue lock(this);
m_dequeWorkItem.push_back(item);
::SetEvent(m_hThreadEvent);
return S_OK;
}
//非同期読み取りを終了する
STDMETHODIMP CMFByteStream::EndRead(IMFAsyncResult *pResult,ULONG *pcbRead)
{
CheckPointer(pResult,E_POINTER); CheckPointer(pcbRead,E_POINTER);
dequeAsyncItem::iterator it,itend; HRESULT hr;
CLockQueue lock(this);
for(it = m_dequeDoneItem.begin(),itend = m_dequeDoneItem.end(),hr = E_FAIL;it != itend;++it){
if(it->lpResult == pResult){
*pcbRead = it->cbRead;
m_dequeDoneItem.erase(it);
pResult->Release(); hr = S_OK;
break;
}
}
return hr;
}
//内部読み込み処理
HRESULT CMFByteStream::InnerRead(BYTE *pb,ULONG cb,ULONG *pcbRead,uint64_t nReadPos)
{
if(!IsValidStream()) return E_HANDLE;
CLockStream lock(this);
if(!::SetFilePointerEx(m_hFileStream,*((LARGE_INTEGER *)&nReadPos),NULL,FILE_BEGIN)){ return E_FAIL; }
if(!::ReadFile(m_hFileStream,pb,cb,pcbRead,NULL)) return E_FAIL;
m_ullStreamPos = nReadPos + *pcbRead;
return S_OK;
}
同期読み取りは内部読み取りにそのまま渡しているだけです。
非同期読み取りの場合は、読み取りの開始側はIMFAsyncResultをヘルパ関数により生成してそれを読み取り要求アイテムとともに非同期処理スレッドに回しています。
終了側は終了したアイテム内から同じIMFAsyncResultを持つアイテムを探索して読み取られたバイト数を戻しています。
ちなみにdequeではなくlistにしたわけはこのEndReadの呼び出しが終了した順に呼び出されるかが不定のためです。
まあ、システムが実装しているMediaFoundationからの読み取りなら終了した順になるのですが・・・。
非同期処理スレッド
//非同期処理スレッド
unsigned int __stdcall CMFByteStream::AsyncThread(void *lpContext)
{
ASYNCITEM item; CMFByteStream *lpThis; HRESULT hr; DWORD dw;
lpThis = (CMFByteStream *)lpContext;
while(1){
dw = WaitForSingleObject(lpThis->m_hThreadEvent,INFINITE);
if(dw != WAIT_OBJECT_0 || lpThis->m_dequeWorkItem.empty()) break;
while(!lpThis->m_dequeWorkItem.empty()){
{
CLockQueue lock(lpThis);
if(lpThis->m_dequeWorkItem.empty()) break;
item = lpThis->m_dequeWorkItem.front();
lpThis->m_dequeWorkItem.pop_front();
}
hr = lpThis->InnerRead(item.pbBuffer,item.cbSize,&item.cbRead,item.nReadPos);
if(FAILED(hr)){ item.lpResult->Release(); break; }
{
CLockQueue lock(lpThis);
lpThis->m_dequeDoneItem.push_back(item);
}
hr = MFInvokeCallback(item.lpResult);
}
}
return 0;
}
非同期処理そのものは普通です。イベントが入ったときに未処理アイテムから状態を取得して読み取ります。
で、読み取り処理が完了すると、ヘルパ関数であるMFInvokeCallbackを使ってIMFAsyncCallback内のInvokeを呼び出します。
通常はInvokeを呼び出すときはIMFAsyncResultをMFCreateAsyncResultで作成してMFInvokeCallbackで呼び出す必要があります。
ちなみにヘルパだから単に呼び出しているだけかと思えばさにあらず。
このヘルパ関数はMediaFoundationが持っているWorkQueueにInvokeの発行を非同期で依頼する、という動作をとります。
これによりInvokeは実装側スレッドでもなくMediaFoundationのコアスレッドでもなく、非同期処理専用のスレッドから呼ばれるため安全な非同期となる、という形になります。
使用してみる
MediaSessionの初期化でByteStreamを使用しているコードがありましたが、これを修正します。
修正前は
//ByteStreamの作成
FAILED_BREAK(hr,MFCreateFile(MF_ACCESSMODE_READ,MF_OPENMODE_FAIL_IF_NOT_EXIST,MF_FILEFLAGS_NONE,wszFileName,&m_lpByteStream));
FAILED_BREAK(hr,SetByteStreamContentType(lpFileName,NULL));
としていましたが、これを
//ByteStreamの作成
CByteStream *lpByteStream = new CMFByteStream(&hr,lpFileName);
if(FAILED(hr)) break;
hr = lpByteStream->Open();
if(FAILED(hr)){ lpByteStream->Release(); break; }
hr = lpByteStream->QueryInterface(IID_PPV_ARGS(&m_lpByteStream)); lpByteStream->Release();
if(FAILED(hr)) break;
FAILED_BREAK(hr,SetByteStreamContentType(lpFileName,NULL));
とすることでこちらで実装したIMFByteStreamが使用できるようになります。
なお、IMFAttributesをQueryInterfaceで渡す実装を忘れるとSetByteStreamContentTypeが失敗します。
こういう見落としがたま~にあるので気をつけましょう。
というわけで、長々と書いてきたMediaFoundationネタもいったん終了
これでADVゲームでもまあ使えるかな~というところでしょうか。
なお、見てわかるとおりSource部分だけはMediaFoundationの方が実装しやすいです。
DirectShowだとSourceFilterの実装となるのでフィルタの実装、ピンの実装、IAsyncReaderの実装とコード量はかなり多くなります。
(DirectShowBaseClassesからCBaseFilterを引っ張ってくるなどすればまだましではあるが)
ただ残りの部分の実装は難しいです。こちらは暇があるときにでもまた紹介できるかな・・・?