DirectShowを使った自前ソースフィルタの実装 その1

どうもプログラムネタに悩んでいたのですが、ちょっとGoogle Analysisなどを見ながら思ったことなどをあわせて。

MediaFoundationでIMFByteStreamを実装して自前読み出しを実装したのにDirectShowでそれをやっていなかったな~と思いまして。

ちょっと不公平感がありそうな感じなのでやってみます。

まず前提として

Windows SDKなどにあるDirecShowのbaseclassesをコンパイルしてライブラリを作成済みとします。

また、このディレクトリをインクルードディレクトリとして指定されているものとしてやりますので注意を。

追加して、baseclassesに実装されている各クラスをかなり使いますのでヘルプが手に入るようなら見ておいた方がいいです。

おすすめはDirectX9の初期ヘルプです。この時代まではDirectX系に結合されていたのである意味見やすいと思います。

ちなみに以下に出すコードもAsyncサンプルというDirectShowのソースフィルタを自分用に変更したものですのでサンプルがあるならそちらを見た方が早いかもしれません。

あと、このコードですが実装したときにはDirectShowの動きがわからない時代に組んでいたものなのでコメントが非常に怪しいです。

というわけで宣言部

class IDataStream;
class CDataRequestItem;
class CDataAsyncReader;
class CDataSourceOutputPin;
class CDataSourceReader;

//ユーザー定義ストリーム
class IDataStream{
public:
	IDataStream() { } //コンストラクタ
	virtual ~IDataStream() { }; //デストラクタ
	virtual HRESULT GetMediaType(CMediaType *lpMediaType,const AM_MEDIA_TYPE *lpDefaultType = NULL) = 0; //メディアタイプを取得する
	virtual HRESULT Seek(LONGLONG llPosition) = 0; //読み取り位置の移動
	virtual HRESULT Read(void *lpBuffer,DWORD dwToReadSize,DWORD *lpdwReadSize,BOOL bIsAligned) = 0; //現在の位置からデータを読み取る
	virtual BOOL GetSize(LONGLONG *lpllTotalSize,LONGLONG *lpllAvailableSize = NULL) const = 0; //データのサイズを取得する
	virtual DWORD GetAlignment(void) const = 0; //データにアライメントあわせの必要があるとき、そのサイズを返す
	virtual void Lock(void) = 0; //データを読み取るためにロックする
	virtual void Unlock(void) = 0; //データのロックを解除する
private:
	//オブジェクトのコピーを禁止する
	IDataStream(const IDataStream &); //コピーコンストラクタ(実装しない)
	void operator = (const IDataStream &); //コピーオペレータ(実装しない)
};

//ロック処理サポートクラス
class CLockableItem{
public:
	CLockableItem(){ ::InitializeCriticalSection(&m_cs); } //コンストラクタ
	virtual ~CLockableItem(){ ::DeleteCriticalSection(&m_cs); } //デストラクタ
	void Lock(void){ ::EnterCriticalSection(&m_cs); } //ロック
	void Unlock(void){ ::LeaveCriticalSection(&m_cs); } //ロック解除
protected:
	//オブジェクトの内部ロッククラス
	class CLockObject{
	public:
		CLockObject(CLockableItem *item) : m_lpLock(item) { item->Lock(); } //コンストラクタ
		~CLockObject() { m_lpLock->Unlock(); } //デストラクタ
	private:
		CLockableItem *m_lpLock; //ロックアイテム
	};
private:
	CRITICAL_SECTION m_cs; //クリティカルセクション
};

//非同期読み取りアイテム
class CDataRequestItem{
public:
	CDataRequestItem(); //通常コンストラクタ
	CDataRequestItem(const CDataRequestItem &item); //コピーコンストラクタ
	CDataRequestItem & operator = (const CDataRequestItem &item); //コピー演算子
	BOOL SetItem(CDataAsyncReader *lpAsyncReader,IDataStream *lpDataStream,LONGLONG llReadPos,BOOL bIsAligned,long lLength,void *lpBuffer,void *lpContext,DWORD_PTR dwUser); //アイテムを設定する
	HRESULT CompleteRequest(void); //要求を完了する
	HRESULT CancelRequest(void); //要求をキャンセルする
	//以下は取得インターフェイス
	inline void *GetContextData(void) const { return m_lpContext; }
	inline DWORD_PTR GetUserData(void) const { return m_dwUser; }
	inline HRESULT GetResult(void) const { return m_hr; }
	inline LONGLONG GetStart(void) const { return m_llReadPos; }
	inline long GetLength(void) const { return m_lLength; }
	
private:
	CDataAsyncReader *m_lpAsyncReader; //非同期読み取りインターフェイス
	IDataStream *m_lpDataStream; //データストリーム
	void *m_lpBuffer; //読み取りバッファ
	void *m_lpContext; //コンテキスト
	LONGLONG m_llReadPos; //読み取り開始位置
	DWORD_PTR m_dwUser; //ユーザーデータ
	long m_lLength; //読み取り長
	BOOL m_bIsAligned; //アライメントあわせを行うか
	HRESULT m_hr; //インターフェイス戻り値
};

//非同期読み取り用インターフェイス
//IAsyncReaderの機能をここで受け持つ
class CDataAsyncReader : private CLockableItem{
public:
	CDataAsyncReader(IDataStream *lpDataStream); //コンストラクタ
	~CDataAsyncReader(); //デストラクタ
	BOOL EnableAsyncRead(void); //非同期動作を有効にする
	BOOL DisableAsyncRead(void); //非同期動作を無効にする
	//以降IAsyncReaderの機能を受け持つ
	HRESULT Request(LONGLONG llPos,long lLength,BOOL bIsAligned,void *lpBuffer,void *lpContext,DWORD_PTR dwUser); //要求を投げる
	HRESULT WaitForNext(DWORD dwTimeOut,void **lplpContext,DWORD_PTR *lpdwUser,long *lplActualSize); //次の要求が終わるまで待つ
	HRESULT SyncReadAligned(LONGLONG llPos,long lLength,void *lpBuffer,long *lplActualSize,void *lpContext); //アライメントを合わせて同期で読み込む
	HRESULT SyncRead(LONGLONG llPos,long lLength,void *lpBuffer); //同期で読み込む
	HRESULT Length(LONGLONG *lpllTotalLength,LONGLONG *lpllAvailableLength); //サイズを取得する
	HRESULT Alignment(long *lplAlignment); //アライメントを合わせる
	HRESULT BeginFlush(void); //フラッシュ処理を開始する
	HRESULT EndFlush(void); //フラッシュ処理を終了する
	BOOL IsAligned(LONG_PTR lSize); //アライメントがあっているかどうか
#ifndef _WIN64
	BOOL IsAligned(LONGLONG llSize); //アライメントがあっているかどうか(LONGLONGバージョン)
#endif //_WIN64
protected:
	static unsigned int __stdcall StartAsyncReadThread(void *lpContext); //非同期読み込み開始
	unsigned long ThreadProc(void); //スレッドルーチン
	BOOL ProcessWork(void); //作業を行う
	HRESULT PutWorkItem(const CDataRequestItem &item); //作業待ちアイテムを設定する
	HRESULT PutDoneItem(const CDataRequestItem &item); //作業終了アイテムを設定する
	HRESULT GetWorkItem(CDataRequestItem &item); //作業待ちアイテムを取得する
	HRESULT GetDoneItem(CDataRequestItem &item); //作業終了アイテムを取得する
private:
	//オブジェクトのコピーを禁止する
	CDataAsyncReader(const CDataAsyncReader &); //コピーコンストラクタ(実装しない)
	void operator = (const CDataAsyncReader &); //コピーオペレータ(実装しない)
private:
	//キューアイテムの定義
	typedef deque<CDataRequestItem> dequeRequestItem;
	CEvent m_vEventInvalidThread; //スレッドが有効かどうかを示すイベント
	CEvent m_vEventSetWorkItem; //仕事があるかどうか
	CEvent m_vEventSetWorkDone; //終了した仕事があるかどうか
	CEvent m_vEventThreadWorkEnd; //スレッド上での作業が終了したかどうか
		
	IDataStream *m_lpDataStream; //データストリーム
	dequeRequestItem m_dequeWorkItem; //作業待ちキュー
	dequeRequestItem m_dequeDoneItem; //作業終了キュー
	BOOL m_bIsFlushing; //フラッシュ作業中かどうか
	BOOL m_bIsWaitingWorkEnd; //スレッド処理待ちを行っているかどうか
	int m_nItemExecCount; //処理中アイテム数
	HANDLE m_hAsyncReadThread; //非同期読み込みスレッド
};

//データ出力ピン
class CDataSourceOutputPin : public CBasePin, public IAsyncReader{
public:
	CDataSourceOutputPin(HRESULT *lphr,CDataSourceReader *lpSourceReader,CDataAsyncReader *lpAsyncReader,CCritSec *lpLockCritical); //コンストラクタ
	virtual ~CDataSourceOutputPin(); //デストラクタ

	//IAsyncReaderを継承するために必要な関数宣言
	DECLARE_IUNKNOWN;
	STDMETHODIMP NonDelegatingQueryInterface(REFIID refiid,void **lplpInterface); //QueryInterfaceの実装

	//IPinを継承するために必要な関数群
	STDMETHODIMP Connect(IPin *lpReceivePin,const AM_MEDIA_TYPE *lpMediaType); //ピンを接続する
	virtual HRESULT ConnectOnBasePin(IPin *lpReceivePin,const AM_MEDIA_TYPE *lpMediaType); //CBasePinを使ってピンを接続する

	//CBasePinから継承する関数
	virtual HRESULT GetMediaType(int nPosition,CMediaType *lpMediaType); //このピンから出力されるメディアタイプを取得する
	virtual HRESULT CheckMediaType(const CMediaType *lpMediaType); //指定されたメディアタイプがサポートされているかどうかをチェックする
	virtual HRESULT CheckConnect(IPin *lpPin); //指定されたピント適切に接続されているかどうか
	virtual HRESULT CompleteConnect(IPin *lpReceivePin); //ピンへの接続を確立する
	virtual HRESULT BreakConnect(void); //ピンへの接続を解放する

	//以下IAsyncReaderのために必要な関数
	STDMETHODIMP RequestAllocator(IMemAllocator *lpPreferred,ALLOCATOR_PROPERTIES *lpProperties,IMemAllocator **lplpActualAllocator); //メモリ確保インターフェイスを取得する
	STDMETHODIMP Request(IMediaSample *lpSample,DWORD_PTR dwUser); //リクエストキューにデータ要求を入れる
	STDMETHODIMP WaitForNext(DWORD dwTimeOut,IMediaSample **lplpSample,DWORD_PTR *lpdwUser); //次のデータ読み取りまで待機する
	STDMETHODIMP SyncReadAligned(IMediaSample *lpSample); //アライメントを合わせた同期読み取りを行う
	STDMETHODIMP SyncRead(LONGLONG llPosition,long lLength,BYTE *lpBuffer); //同期読み取りを行う
	STDMETHODIMP Length(LONGLONG *lpllTotalLength,LONGLONG *lpllAvailableLength); //メディアストリームの長さを取得する
	STDMETHODIMP BeginFlush(void); //フラッシュ処理を開始する
	STDMETHODIMP EndFlush(void); //フラッシュ処理を終了する
protected:
	HRESULT InitializeAllocator(IMemAllocator **lplpAllocator); //メモリ確保インターフェイスを初期化する
private:
	CDataSourceReader *m_lpSourceReader; //リードフィルタ
	CDataAsyncReader *m_lpAsyncReader; //非同期読み取り
	BOOL m_bIsQueriedForAsyncReader; //AsyncReaderでQueryInterfaceが発行されたかどうか
};

//データリーダー
class CDataSourceReader : public CBaseFilter{
public:
	CDataSourceReader(const TCHAR *lpFilterName,IUnknown *lpOwner,HRESULT *lphr,IDataStream *lpDataStream); //コンストラクタ
	virtual ~CDataSourceReader(); //デストラクタ
	//CBaseFilterから継承する関数
	virtual int GetPinCount(void); //接続できるピンの数を取得する
	virtual CBasePin *GetPin(int n); //ピンを取得する
	virtual const CMediaType *GetMediaType() const; //保持しているメディア形式を取得する
	virtual HRESULT Connect(IPin *lpReceivePin,const AM_MEDIA_TYPE *lpMediaType); //ピンを接続する
	
protected:
	CDataSourceOutputPin *m_lpOutputPin; //出力ピン
	CDataAsyncReader *m_lpAsyncReader; //非同期読み取りオブジェクト
	CMediaType m_vMediaType; //保持しているメディア形式
	CCritSec m_vCSFilter; //フィルタクリティカルセクション
};

さすがにコードを出しただけではわかりづらいですね。ちょっと解説してみましょうか。

IDataStream

データストリームを扱います。これは使用者側が実装する必要があるもので、このblog上で出てきた例としてはIStreamやIMFByteStreamなどに相当します。

ちょっと変わっているのが

  • GetMediaTypeというストリームのメディア種別を返す必要がある
  • 自身にLock、Unlockをサポートする機能が必要だがSeekやReadそのものに同期は必要がない

という性質を持ちます。なぜかはこの後やります。

CLockableItem

単に同期処理を簡単にするためのサポートクラスです。LockとUnlockを持ち、継承を行うことで対象のクラスに同期機能を持たせます。

と同時にコードを実装するときにロックサポート用のクラスを使えるようにするという優れものです。

LockやUnlockを公開したくないときはprivate継承を使うこともできます。今回はprivate継承を使っています。

CDataRequestItem

データの読み出し要求があったときにそれに関する情報を管理するクラスです。事実上のデータパケットですね。

一応読み出しの為の補助関数はありますが事実上は単なるデータの集合体です。

CDataAsyncReader

IAsyncReaderの実コード部です。ただしIAsyncReaderは継承していません。

IAsyncReaderはCOMとして実装しなければならないのでIUnknownの実装などと言う面倒なことになるからです。

そのため、このクラスで機能だけを実装してIAsyncReaderの機能を使うときはこのクラスに処理を委譲する、という形をとります。

CDataSourceOutputPin

実装するフィルタの出力ピンです。

通常のデータを扱うためのピンではないため、継承元はCBasePinになっています。

また、DirectShow側がフィルタ間接続を行うときにIAsyncReaderを要求するのはフィルタではなくピンになるためこちらにIAsyncReaderを継承します。

なお、IUnknown系の実装についてはCBasePinがCUnknownなどを継承して実装しているのでそこの実装を借りる形で実装されています。

CBasePinとIAsyncReaderを継承しているので必要なコードを実装しています。

CDataSourceReader

実装する本体フィルタです。

なお、実際にはこのフィルタをグラフに登録すれば動くことは動きますがある程度補助コードがあった方がいいことが多いので実際にはこれに実装が追加されます。

この辺は実装コードおよびを見てください。

DirectShowのbaseclassesで実装されている為使っているコードについて

登場している中では以下があります。

クラス名 機能
CMediaType AM_MEDIA_TYPE構造体にサポート関数およびメモリ管理機能をつけたもの。構造体の一部のメンバがメモリを使った可変となるため必須
CEvent イベントを使った同期処理のWrapper
CCritSec クリティカルセクションを使った同期処理のWrapper

どこまでこのネタ引っ張るのかな・・・

書いている当の本人が気になってきました。

一応軽く検索した範囲では余り書いている人がいなかったので価値はあるかな~と思っています。

せめて何か仕事などで有利になるような材料にならないかな~と思いながら。


コメントを残す

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

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