#include "Stdafx.h"
#include "Cスケジューラ.h"
#include "共通\CopyComPointer.h"

//=========================================================
// !! Win32 と連携するので、例外を発生させてはならない。!!
//=========================================================

namespace FDK
{
// public:

	Cスケジューラ::Cスケジューラ()
	{
		this->m_p表示用コールバック = NULL;
		this->m_pプレゼンテーションクロック = NULL;
		this->m_nスレッドID = 0;
		this->m_hスケジューラスレッドハンドル = NULL;
		this->m_hスケジューラスレッド準備完了イベント = NULL;
		this->m_hフラッシュ完了イベント = NULL;
		this->m_f再生レート = 1.0f;
		this->m_最後のサンプル時刻 = 0;
		this->m_フレーム間隔 = 0;
		this->m_フレーム間隔の４分の１ = 0;
	}
	Cスケジューラ::~Cスケジューラ()
	{
		SAFE_COM_RELEASE( this->m_pプレゼンテーションクロック );
	}

	void Cスケジューラ::tコールバックを設定する( STスケジューラコールバック *pコールバック構造体 )
	{
		this->m_p表示用コールバック = pコールバック構造体;
	}

	HRESULT Cスケジューラ::tスケジューラをワーカスレッドで開始する( IMFClock *pClock )
	{
		if( this->m_hスケジューラスレッドハンドル )
			return E_UNEXPECTED;

		HRESULT hr = S_OK;
		DWORD dwID = 0;


		// クロックをCOMコピー。

		FDK::CopyComPointer( this->m_pプレゼンテーションクロック, pClock );


		// タイマを高解像度にする

		::timeBeginPeriod( 1 );


		// スレッドの開始を待つイベントを作成。

		if( ( this->m_hスケジューラスレッド準備完了イベント = ::CreateEvent( NULL, FALSE, FALSE, NULL ) ) == NULL )
			tFAILEDなら帰還( hr = HRESULT_FROM_WIN32( ::GetLastError() ) );


		// フラッシュ作業が完了するのを待つイベントを作成。
				
		if( ( this->m_hフラッシュ完了イベント = ::CreateEvent( NULL, FALSE, FALSE, NULL ) ) == NULL )
			tFAILEDなら帰還( hr = HRESULT_FROM_WIN32( ::GetLastError() ) );


		// スケジューラスレッドを作成。
				
		if( (this->m_hスケジューラスレッドハンドル = ::CreateThread( NULL, 0, SchedulerThreadProc, (LPVOID)this, 0, &dwID )) == NULL )
			tFAILEDなら帰還( hr = HRESULT_FROM_WIN32( ::GetLastError() ) );


		// スレッドが準備完了イベントを送ってくるのを待つ。

		HANDLE hObjects[] = {
			this->m_hスケジューラスレッド準備完了イベント, 
			this->m_hスケジューラスレッドハンドル
		};
		
		DWORD dwWait = ::WaitForMultipleObjects( 2, hObjects, FALSE, INFINITE ); 

		
		// 何らかの理由でスレッドが早期終了していたら、これはエラーである。

		if( dwWait != WAIT_OBJECT_0 )
		{
			SAFE_CLOSE_HANDLE( this->m_hスケジューラスレッドハンドル );
			return E_UNEXPECTED;
		}


		// スケジューラスレッドの ID を保存。

		this->m_nスレッドID = dwID;


		// 成功・失敗に関係無く、スケジューラスレッド準備完了イベントの利用は終了。

		if( this->m_hスケジューラスレッド準備完了イベント != NULL )
		{
			SAFE_CLOSE_HANDLE( this->m_hスケジューラスレッド準備完了イベント );
		}

		return S_OK;
	}
	HRESULT	Cスケジューラ::tスケジューラを停止しワーカスレッドを終了する()
	{
		if( this->m_hスケジューラスレッドハンドル == NULL )
			return S_OK;	// 既に止まっている

		// スケジューラスレッドに終了イベントを送信。
		::PostThreadMessage( this->m_nスレッドID, SCHEDULE_EVENT_終了, 0, 0 );
    
		// スレッドの終了を待つ。
		::WaitForSingleObject( this->m_hスケジューラスレッドハンドル, INFINITE );

		// スレッドを閉じる。
		SAFE_CLOSE_HANDLE( this->m_hスケジューラスレッドハンドル );

		// イベントを閉じる。
		SAFE_CLOSE_HANDLE( this->m_hフラッシュ完了イベント );

		// サンプルキューをクリアする。
		this->m_サンプルスケジュールキュー.tクリアする();

		// タイマの解像度を元に戻す。
		::timeEndPeriod( 1 );

		return S_OK;
	}
	HRESULT	Cスケジューラ::t新しいサンプルを表示するかスケジュールキューに入れる( IMFSample *pSample, BOOL bPresentNow )
	{
		if( this->m_p表示用コールバック == NULL || this->m_hスケジューラスレッドハンドル == NULL )
			return MF_E_NOT_INITIALIZED;

		HRESULT hr = S_OK;


		// スケジューラスレッドが既に終わってるならここで帰還。

		DWORD dwExitCode = 0;
		::GetExitCodeThread( this->m_hスケジューラスレッドハンドル, &dwExitCode );
		if( dwExitCode != STILL_ACTIVE )
			return E_FAIL;


		// 今すぐ表示すべきなら、キューに入れずにすぐ表示する。そうでないならキューに入れる。

		if( bPresentNow || ( m_pプレゼンテーションクロック == NULL ) )
		{
			this->m_p表示用コールバック->tサンプルを受け取る( pSample, 0 );	// プレゼンテーションクロックがない場合も、即座に表示される。
		}
		else
		{
			if( SUCCEEDED( hr = this->m_サンプルスケジュールキュー.tキューの末尾に入れる( pSample ) ) )		// キューに入れる。
				::PostThreadMessage( this->m_nスレッドID, SCHEDULE_EVENT_サンプルをキューに入れた, 0, 0 );	// 成功したらスレッドにも通知。
		}

		return S_OK;
	}
	HRESULT Cスケジューラ::tサンプルキューをフラッシュする()
	{
		if( this->m_hスケジューラスレッドハンドル == NULL )
			return S_OK;		// スケジューラスレッドが動いてないなら何もしない。

		// スケジューラスレッドに Flush イベントを送信。
		::PostThreadMessage( this->m_nスレッドID, SCHEDULE_EVENT_フラッシュ, 0 , 0 );

		// スケジューラスレッドがイベント（フラッシュ完了 or スレッド終了）を返すのを待つ。
		HANDLE objects[] = {
			this->m_hフラッシュ完了イベント,
			this->m_hスケジューラスレッドハンドル
		};
		::WaitForMultipleObjects( ARRAY_SIZE(objects), objects, FALSE, 5000 );	// 最大 5000 ms
	
		// 完了。
		return S_OK;
	}

	void	Cスケジューラ::tフレームレートを設定する( const MFRatio& fps )
	{
		// FPS をフレーム間隔に変換する。

		UINT64 AvgTimePerFrame = 0;
		::MFFrameRateToAverageTimePerFrame( fps.Numerator, fps.Denominator, &AvgTimePerFrame );
		this->m_フレーム間隔 = (MFTIME) AvgTimePerFrame;

		// よく使うので、フレーム間隔の４分の１を計算しておく。
		this->m_フレーム間隔の４分の１ = this->m_フレーム間隔 / 4;
	}
	void	Cスケジューラ::tクロックレートを設定する( float fRate )
	{
		this->m_f再生レート = fRate;
	}

	const LONGLONG& Cスケジューラ::ll最後のサンプル時刻() const
	{
		return this->m_最後のサンプル時刻;
	}
	const LONGLONG& Cスケジューラ::llフレーム間隔() const
	{
		return this->m_フレーム間隔;
	}

// private:

	DWORD	Cスケジューラ::procスケジューラスレッドエントリ()
	{
		HRESULT hr = S_OK;
		MSG     msg;
		LONG    lWait = INFINITE;
		BOOL    bExitThread = FALSE;

		// このスレッドにメッセージキューを強制的に作らせる。(MSDN の PostThreadMessage を参照。)
		::PeekMessage( &msg, NULL, WM_USER, WM_USER, PM_NOREMOVE );

		// スレッドの準備が整ったことをスケジューラに告げる。
		::SetEvent( this->m_hスケジューラスレッド準備完了イベント );

		// スレッドメッセージループ
		while( !bExitThread )
		{
			// スレッドメッセージが来るか、lWait がタイムアウトするまで待つ。
			DWORD dwResult = ::MsgWaitForMultipleObjects( 0, NULL, FALSE, lWait, QS_POSTMESSAGE );

			if( dwResult == WAIT_TIMEOUT )
			{
				if( FAILED( hr = this->tキュー内のサンプルを可能な限り表示する( &lWait ) ) )
					bExitThread = TRUE;		// スレッドエラー終了
			}

			while( ::PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )
			{
				BOOL bProcessSamples = TRUE;

				switch( msg.message )
				{

				case SCHEDULE_EVENT_終了:

					bExitThread = TRUE;			// スレッド正常終了
					break;

				case SCHEDULE_EVENT_フラッシュ:

					this->m_サンプルスケジュールキュー.tクリアする();
					lWait = INFINITE;
					::SetEvent( this->m_hフラッシュ完了イベント );
					break;

				case SCHEDULE_EVENT_サンプルをキューに入れた:

					if( bProcessSamples )
					{
						if( FAILED( hr = this->tキュー内のサンプルを可能な限り表示する( &lWait ) ) )
							bExitThread = TRUE;		// スレッドエラー終了

						// 上記処理の結果、キューが空じゃないなら TRUE。
						bProcessSamples = ( lWait != INFINITE );
					}
					break;
				}
			}
		}
		return ( SUCCEEDED( hr ) ? 0 : 1 );
	}
	HRESULT	Cスケジューラ::tキュー内のサンプルを可能な限り表示する( LONG *plNextSleep )
	{
		HRESULT hr = S_OK;
		LONG lWait = 0;
		IMFSample *pSample = NULL;

		try
		{
			// キューが空になるか、またはサンプルが表示されている間、サンプルを処理する。
			while( this->m_サンプルスケジュールキュー.tキューの先頭から取り出す( &pSample ) == S_OK )	// 注意：キューが空なら S_FALSE が返される。なので、SUCCEDED() では判別できない。
			{
				if( FAILED( hr = this->tサンプルの表示時刻判定と表示を行う・表示しないならキューに戻す( pSample, &lWait ) ) )
					break;			// エラーなら抜ける。

				if( lWait > 0 )
					break;			// さっきのサンプル（次のサンプル）までの sleep 時間があるなら抜ける。

				// さっきのサンプルが表示されたので、次のサンプルに移る。
				SAFE_COM_RELEASE( pSample );
			}
		}
		finally
		{
			SAFE_COM_RELEASE( pSample );
		}

		if( lWait == 0 )
			lWait = INFINITE;	// キューが空になった（またはエラーが起きた）ので、sleep 時間は無限。→ 次のスレッドメッセージが来るまで起きない。

		*plNextSleep = lWait;
		return S_OK;
	}
	HRESULT	Cスケジューラ::tサンプルの表示時刻判定と表示を行う・表示しないならキューに戻す( IMFSample *pSample, LONG *plNextSleep )
	{
		HRESULT hr = S_OK;
		LONGLONG llサンプルのタイムスタンプ = 0;
		LONGLONG ll現在時刻 = 0;
		MFTIME mftmシステム時刻 = 0;
		LONG lNextSleep = 0;

	
		// プレゼンテーションクロックが存在する場合のみ、サンプルのタイムスタンプを考慮する。
		// 存在しない場合は即座に表示。
    
		BOOL b即座に表示する = TRUE;

		if( this->m_pプレゼンテーションクロック )
		{
			// サンプルのタイムスタンプを取得する。
			hr = pSample->GetSampleTime( &llサンプルのタイムスタンプ );		// タイムスタンプを持っていなくてもエラーではない。

			// サンプルがタイムスタンプを持っていた場合、プレゼンテーションクロックから現在時刻を取得する。
			if( SUCCEEDED( hr ) )
				hr = m_pプレゼンテーションクロック->GetCorrelatedTime( 0, &ll現在時刻, &mftmシステム時刻 );

			// サンプルのタイムスタンプとの時刻差を計算する。負数ならサンプルの表示が遅れていることを意味する。
			LONGLONG llサンプルのタイムスタンプとの時刻差 = llサンプルのタイムスタンプ - ll現在時刻;

			// 逆再生中なら、クロックは後方に走る。そのため、時刻差も反転する。（負数でサンプル表示が遅れていることを意味するようにするため。）
			if( this->m_f再生レート < 0 )
				llサンプルのタイムスタンプとの時刻差 = -llサンプルのタイムスタンプとの時刻差;


			// 以下、表示時刻判定！！


			// (A) 表示が遅れてはいるが、フレーム間隔の４分の１以内の遅れであれば、即座に表示する。

			if( llサンプルのタイムスタンプとの時刻差 < -this->m_フレーム間隔の４分の１ )
				b即座に表示する = TRUE;


			// (B) 表示時刻までまだ フレーム間隔の４分の３ 以上ある場合は、表示せずに次の sleep に入る。

			else if( llサンプルのタイムスタンプとの時刻差 > ( 3 * this->m_フレーム間隔の４分の１ ) )
			{
				// スリープ時間を計算する。
				lNextSleep = this->TimeToMsec( llサンプルのタイムスタンプとの時刻差 - ( 3 * this->m_フレーム間隔の４分の１ ) );

				// sleep 時間をクロックレートで調整する。（sleep はプレゼンテーションクロックではなくシステムクロックで行われるため。）
				lNextSleep = (LONG)( lNextSleep / ::fabsf( this->m_f再生レート ) );

				// 表示はしない。
				b即座に表示する = FALSE;
			}

			// (C) その他の場合（表示時刻まで残りフレーム間隔の４分の３以下になった場合）；即座に表示する。

			else
			{
				b即座に表示する = TRUE;
			}
		}


		// 表示？

		if( b即座に表示する )
		{
			hr = this->m_p表示用コールバック->tサンプルを受け取る( pSample, llサンプルのタイムスタンプ );
		}
		else
		{
			hr = this->m_サンプルスケジュールキュー.tキューの先頭に戻す( pSample );
		}

		*plNextSleep = lNextSleep;
		return hr;
	}

	DWORD WINAPI Cスケジューラ::SchedulerThreadProc( LPVOID lpParameter )
	{
		Cスケジューラ* pScheduler = reinterpret_cast<Cスケジューラ*>( lpParameter );

		if( pScheduler == NULL )
			return -1;

		return pScheduler->procスケジューラスレッドエントリ();
	}
}