MediaFoundationを使う (8) IMFByteStreamを実装してみる 後編

前編からの続きです。

これで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を引っ張ってくるなどすればまだましではあるが)

ただ残りの部分の実装は難しいです。こちらは暇があるときにでもまた紹介できるかな・・・?


3 thoughts on “MediaFoundationを使う (8) IMFByteStreamを実装してみる 後編

  1. tanaka

    以前、Media Foudation関連で質問させて頂きました、tanakaと申します。
    以前の問題はご指摘の通りDirectXを更新することで解決することができました。ありがとうございます。
    今回、また行き詰まってしまったのでご助言頂けないかと思いコメントしました。よろしくお願いします。

    独自アーカイブから動画(wmv)を読み出し、再生したいのですが、
    下記の部分で「MF_E_UNSUPPORTED_BYTESTREAM_TYPE」を返してしまいます。

    > FAILED_BREAK(hr, lpSourceResolver->CreateObjectFromByteStream(m_lpByteStream, NULL, MF_RESOLUTION_MEDIASOURCE, NULL, &objtype, &lpSource));

    リターン内容から、CMFByteStreamを改造すれば行ける?かと思いましたが、
    どのように改造すればよいか検討がつかず困っています。

    解決方法について、何か心当たりがあればご助言頂けないでしょうか?
    よろしくお願いします。

    ※独自アーカイブに動画を内臓していない場合は正常に動きました。
    ※SetByteStreamContentType()は正常に設定できていました。

    返信
    1. 音宮 志久 投稿作成者

      ご質問ありがとうございます。
      私側でもこの現象について少々調べてみましたが、CreateObjectFromByteStreamがMF_E_UNSUPPORTED_BYTESTREAM_TYPEを返すのは基本的にはByteStreamに対してContentTypeを正しく設定できていない場合でこのような動作になることが多いようです。
      tanakaさんのコメントには「SetByteStreamContentType()は正常に設定できていました」とありましたが、デバッガなどで再度確認すると良いかもしれません。(注意する点としましては拡張子をwmvではなく独自アーカイブのもので認識しておりMIMEが設定できていない、といったことやMIME設定ルーチンにミスなどがあり本来のファイルタイプと異なるMIMEが設定されてしまっているため読めないということもあるかもしれません)
      そのほか独自アーカイブから読み出すときにIMFByteStreamが必要とする機能が実装できていないパターンも考えられます。(独自アーカイブから読み出すためにシーク処理に不具合があるパターンや非同期読み込み時に非同期読み込みができていない、非同期読み込みとシーク処理との兼ね合いでバグがあるなど)

      参考になれば幸いです。

      返信
  2. tanaka

    ご回答ありがとうございます。
    お返事が遅くなり申し訳ありません。
    結果として問題を解決することができました。ありがとうございます。

    ご指摘を頼りに色々調べてみたところ、
    バイトストリームの読込開始位置を変更する必要があることが分かりました。

    具体的には、CMFByteStream::InnerReadの下記の部分を変更したところ、
    正常に動作するようになりました。

    > if(!::SetFilePointerEx(m_hFileStream,*((LARGE_INTEGER *)&nReadPos),NULL,FILE_BEGIN)){ return E_FAIL; }

    LARGE_INTEGER d;
    d.QuadPart = m_originPos + nReadPos;
    if(!::SetFilePointerEx(m_hFileStream,d,NULL,FILE_BEGIN)){ return E_FAIL; }

    ※m_originPosは、読込対象動画ファイルのファイルポインタの位置

    おそらく、SetFilePointerExで読込開始位置が独自アーカイブファイル自体の位置
    からになっていた?ので、おかしくなったのかなと思います。

    返信

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

この記事のトラックバック用URL