チュートリアル: MP4 ファイルのエンコード
このチュートリアルでは、ビデオ ストリームに H.264 を使用し、オーディオ ストリームに AAC を使用して、 トランスコード API を使用して MP4 ファイルをエンコードする方法について説明します。
ヘッダーとライブラリ ファイル
次のヘッダー ファイルを含めます。
#include <new>
#include <iostream>
#include <windows.h>
#include <mfapi.h>
#include <Mfidl.h>
#include <shlwapi.h>
次のライブラリ ファイルをリンクします。
#pragma comment(lib, "mfplat")
#pragma comment(lib, "mf")
#pragma comment(lib, "mfuuid")
#pragma comment(lib, "shlwapi")
エンコード プロファイルを定義する
エンコードの方法の 1 つは、事前に知られているターゲット エンコード プロファイルの一覧を定義することです。 このチュートリアルでは、比較的簡単なアプローチを取り、H.264 ビデオと AAC オーディオのエンコード形式の一覧を格納します。
H.264 の場合、最も重要な形式属性は、H.264 プロファイル、フレーム レート、フレーム サイズ、エンコードされたビット レートです。 次の配列には、H.264 エンコード形式の一覧が含まれています。
struct H264ProfileInfo
{
UINT32 profile;
MFRatio fps;
MFRatio frame_size;
UINT32 bitrate;
};
H264ProfileInfo h264_profiles[] =
{
{ eAVEncH264VProfile_Base, { 15, 1 }, { 176, 144 }, 128000 },
{ eAVEncH264VProfile_Base, { 15, 1 }, { 352, 288 }, 384000 },
{ eAVEncH264VProfile_Base, { 30, 1 }, { 352, 288 }, 384000 },
{ eAVEncH264VProfile_Base, { 29970, 1000 }, { 320, 240 }, 528560 },
{ eAVEncH264VProfile_Base, { 15, 1 }, { 720, 576 }, 4000000 },
{ eAVEncH264VProfile_Main, { 25, 1 }, { 720, 576 }, 10000000 },
{ eAVEncH264VProfile_Main, { 30, 1 }, { 352, 288 }, 10000000 },
};
H.264 プロファイルは、 eAVEncH264VProfile 列挙を使用して指定されます。 H.264 レベルを指定することもできますが、Microsoft Media Foundation H.264 Video Encoder は特定のビデオ ストリームに対して適切なレベルを派生できるため、エンコーダーの選択したレベルをオーバーライドしないことをお勧めします。 インターレース コンテンツの場合は、インターレース モードも指定します ( 「ビデオインターレース」を参照)。
AAC オーディオの場合、最も重要な形式属性は、オーディオ サンプル レート、チャネル数、サンプルあたりのビット数、エンコードされたビット レートです。 必要に応じて、AAC オーディオ プロファイル レベルの表示を設定できます。 詳細については、「 AAC エンコーダー」を参照してください。 次の配列には、AAC エンコード形式の一覧が含まれています。
struct AACProfileInfo
{
UINT32 samplesPerSec;
UINT32 numChannels;
UINT32 bitsPerSample;
UINT32 bytesPerSec;
UINT32 aacProfile;
};
AACProfileInfo aac_profiles[] =
{
{ 96000, 2, 16, 24000, 0x29},
{ 48000, 2, 16, 24000, 0x29},
{ 44100, 2, 16, 16000, 0x29},
{ 44100, 2, 16, 12000, 0x29},
};
Note
H264ProfileInfo
ここで定義されている 構造体と AACProfileInfo
構造体は、Media Foundation API の一部ではありません。
wmain 関数を記述する
次のコードは、コンソール アプリケーションのエントリ ポイントを示しています。
int video_profile = 0;
int audio_profile = 0;
int wmain(int argc, wchar_t* argv[])
{
HeapSetInformation(NULL, HeapEnableTerminationOnCorruption, NULL, 0);
if (argc < 3 || argc > 5)
{
std::cout << "Usage:" << std::endl;
std::cout << "input output [ audio_profile video_profile ]" << std::endl;
return 1;
}
if (argc > 3)
{
audio_profile = _wtoi(argv[3]);
}
if (argc > 4)
{
video_profile = _wtoi(argv[4]);
}
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
if (SUCCEEDED(hr))
{
hr = MFStartup(MF_VERSION);
if (SUCCEEDED(hr))
{
hr = EncodeFile(argv[1], argv[2]);
MFShutdown();
}
CoUninitialize();
}
if (SUCCEEDED(hr))
{
std::cout << "Done." << std::endl;
}
else
{
std::cout << "Error: " << std::hex << hr << std::endl;
}
return 0;
}
関数は wmain
次の処理を行います。
- CoInitializeEx 関数を呼び出して COM ライブラリを初期化します。
- MFStartup 関数を呼び出して Media Foundation を初期化します。
- アプリケーション定義
EncodeFile
関数を呼び出します。 この関数は、入力ファイルを出力ファイルにトランスコードし、次のセクションに示します。 - MFShutdown 関数を呼び出して Media Foundation をシャットダウンします。
- COM ライブラリを初期化解除するには、 CoUninitialize 関数を呼び出します。
ファイルをエンコードする
次のコードは、コード変換を実行する 関数を示しています EncodeFile
。 この関数は、主に、このトピックで後述する他のアプリケーション定義関数の呼び出しで構成されます。
HRESULT EncodeFile(PCWSTR pszInput, PCWSTR pszOutput)
{
IMFTranscodeProfile *pProfile = NULL;
IMFMediaSource *pSource = NULL;
IMFTopology *pTopology = NULL;
CSession *pSession = NULL;
MFTIME duration = 0;
HRESULT hr = CreateMediaSource(pszInput, &pSource);
if (FAILED(hr))
{
goto done;
}
hr = GetSourceDuration(pSource, &duration);
if (FAILED(hr))
{
goto done;
}
hr = CreateTranscodeProfile(&pProfile);
if (FAILED(hr))
{
goto done;
}
hr = MFCreateTranscodeTopology(pSource, pszOutput, pProfile, &pTopology);
if (FAILED(hr))
{
goto done;
}
hr = CSession::Create(&pSession);
if (FAILED(hr))
{
goto done;
}
hr = pSession->StartEncodingSession(pTopology);
if (FAILED(hr))
{
goto done;
}
hr = RunEncodingSession(pSession, duration);
done:
if (pSource)
{
pSource->Shutdown();
}
SafeRelease(&pSession);
SafeRelease(&pProfile);
SafeRelease(&pSource);
SafeRelease(&pTopology);
return hr;
}
関数は EncodeFile
、次の手順を実行します。
- 入力ファイルの URL またはファイル パスを使用して、入力ファイルのメディア ソースを作成します。 (「 メディア ソースの作成」を参照してください)。
- 入力ファイルの期間を取得します。 (「 ソースの期間を取得する」を参照してください)。
- トランスコード プロファイルを作成します。 (「 トランスコード プロファイルを作成する」を参照してください)。
- MFCreateTranscodeTopology を呼び出して、部分トランスコード トポロジを作成します。
- メディア セッションを管理するヘルパー オブジェクトを作成します。 (メディア セッション ヘルパーを参照)。
- エンコード セッションを実行し、完了するまで待ちます。 (「 エンコード セッションを実行する」を参照してください)。
- IMFMediaSource::Shutdown を呼び出して、メディア ソースをシャットダウンします。
- インターフェイス ポインターを解放します。 このコードでは、 SafeRelease 関数を使用してインターフェイス ポインターを解放します。 もう 1 つのオプションは、 CComPtr などの COM スマート ポインター クラスを使用することです。
メディア ソースを作成する
メディア ソースは、入力ファイルを読み取って解析するオブジェクトです。 メディア ソースを作成するには、入力ファイルの URL を ソース リゾルバーに渡します。 この方法を次のコードに示します。
HRESULT CreateMediaSource(PCWSTR pszURL, IMFMediaSource **ppSource)
{
MF_OBJECT_TYPE ObjectType = MF_OBJECT_INVALID;
IMFSourceResolver* pResolver = NULL;
IUnknown* pSource = NULL;
// Create the source resolver.
HRESULT hr = MFCreateSourceResolver(&pResolver);
if (FAILED(hr))
{
goto done;
}
// Use the source resolver to create the media source
hr = pResolver->CreateObjectFromURL(pszURL, MF_RESOLUTION_MEDIASOURCE,
NULL, &ObjectType, &pSource);
if (FAILED(hr))
{
goto done;
}
// Get the IMFMediaSource interface from the media source.
hr = pSource->QueryInterface(IID_PPV_ARGS(ppSource));
done:
SafeRelease(&pResolver);
SafeRelease(&pSource);
return hr;
}
詳細については、「 ソース リゾルバーの使用」を参照してください。
ソース期間を取得する
必須ではありませんが、入力ファイルの期間、メディア ソースに対してクエリを実行すると便利です。 この値は、エンコードの進行状況を追跡するために使用できます。 期間は、プレゼンテーション記述子の MF_PD_DURATION 属性に格納されます。 IMFMediaSource::CreatePresentationDescriptor を呼び出して、プレゼンテーション記述子を取得します。
HRESULT GetSourceDuration(IMFMediaSource *pSource, MFTIME *pDuration)
{
*pDuration = 0;
IMFPresentationDescriptor *pPD = NULL;
HRESULT hr = pSource->CreatePresentationDescriptor(&pPD);
if (SUCCEEDED(hr))
{
hr = pPD->GetUINT64(MF_PD_DURATION, (UINT64*)pDuration);
pPD->Release();
}
return hr;
}
トランスコード プロファイルを作成する
トランスコード プロファイルでは、エンコード パラメーターについて説明します。 トランスコード プロファイルの作成の詳細については、「 Transcode API の使用」を参照してください。 プロファイルを作成するには、次の手順を実行します。
- MFCreateTranscodeProfile を呼び出して、空のプロファイルを作成します。
- AAC オーディオ ストリームのメディアの種類を作成します。 それをプロファイルに追加するには、 IMFTranscodeProfile::SetAudioAttributes を呼び出します。
- H.264 ビデオ ストリームのメディアの種類を作成します。 それをプロファイルに追加するには、 IMFTranscodeProfile::SetVideoAttributes を呼び出します。
- MFCreateAttributes を呼び出して、コンテナー レベルの属性の属性ストアを作成します。
- MF_TRANSCODE_CONTAINERTYPE属性を設定します。 これが唯一必要なコンテナー レベルの属性です。 MP4 ファイル出力の場合は、この属性を MFTranscodeContainerType_MPEG4 に設定 します。
- IMFTranscodeProfile::SetContainerAttributes を呼び出して、コンテナー レベルの属性を設定します。
次のコードは、これらの手順を示しています。
HRESULT CreateTranscodeProfile(IMFTranscodeProfile **ppProfile)
{
IMFTranscodeProfile *pProfile = NULL;
IMFAttributes *pAudio = NULL;
IMFAttributes *pVideo = NULL;
IMFAttributes *pContainer = NULL;
HRESULT hr = MFCreateTranscodeProfile(&pProfile);
if (FAILED(hr))
{
goto done;
}
// Audio attributes.
hr = CreateAACProfile(audio_profile, &pAudio);
if (FAILED(hr))
{
goto done;
}
hr = pProfile->SetAudioAttributes(pAudio);
if (FAILED(hr))
{
goto done;
}
// Video attributes.
hr = CreateH264Profile(video_profile, &pVideo);
if (FAILED(hr))
{
goto done;
}
hr = pProfile->SetVideoAttributes(pVideo);
if (FAILED(hr))
{
goto done;
}
// Container attributes.
hr = MFCreateAttributes(&pContainer, 1);
if (FAILED(hr))
{
goto done;
}
hr = pContainer->SetGUID(MF_TRANSCODE_CONTAINERTYPE, MFTranscodeContainerType_MPEG4);
if (FAILED(hr))
{
goto done;
}
hr = pProfile->SetContainerAttributes(pContainer);
if (FAILED(hr))
{
goto done;
}
*ppProfile = pProfile;
(*ppProfile)->AddRef();
done:
SafeRelease(&pProfile);
SafeRelease(&pAudio);
SafeRelease(&pVideo);
SafeRelease(&pContainer);
return hr;
}
H.264 ビデオ ストリームの属性を指定するには、属性ストアを作成し、次の属性を設定します。
属性 | 説明 |
---|---|
MF_MT_SUBTYPE | [MFVideoFormat_H264] に設定します。 |
MF_MT_MPEG2_PROFILE | H.264 プロファイル。 |
MF_MT_FRAME_SIZE | フレーム サイズ。 |
MF_MT_FRAME_RATE | フレーム レート。 |
MF_MT_AVG_BITRATE | エンコードされたビット レート。 |
AAC オーディオ ストリームの属性を指定するには、属性ストアを作成し、次の属性を設定します。
属性 | 説明 |
---|---|
MF_MT_SUBTYPE | を MFAudioFormat_AAC に設定する |
MF_MT_AUDIO_SAMPLES_PER_SECOND | オーディオサンプルレート。 |
MF_MT_AUDIO_BITS_PER_SAMPLE | オーディオサンプルあたりのビット数。 |
MF_MT_AUDIO_NUM_CHANNELS | オーディオ チャネルの数。 |
MF_MT_AUDIO_AVG_BYTES_PER_SECOND | エンコードされたビット レート。 |
MF_MT_AUDIO_BLOCK_ALIGNMENT | 1 に設定します。 |
MF_MT_AAC_AUDIO_PROFILE_LEVEL_INDICATION | AAC プロファイル レベルの表示 (省略可能)。 |
次のコードでは、ビデオ ストリーム属性を作成します。
HRESULT CreateH264Profile(DWORD index, IMFAttributes **ppAttributes)
{
if (index >= ARRAYSIZE(h264_profiles))
{
return E_INVALIDARG;
}
IMFAttributes *pAttributes = NULL;
const H264ProfileInfo& profile = h264_profiles[index];
HRESULT hr = MFCreateAttributes(&pAttributes, 5);
if (SUCCEEDED(hr))
{
hr = pAttributes->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_H264);
}
if (SUCCEEDED(hr))
{
hr = pAttributes->SetUINT32(MF_MT_MPEG2_PROFILE, profile.profile);
}
if (SUCCEEDED(hr))
{
hr = MFSetAttributeSize(
pAttributes, MF_MT_FRAME_SIZE,
profile.frame_size.Numerator, profile.frame_size.Numerator);
}
if (SUCCEEDED(hr))
{
hr = MFSetAttributeRatio(
pAttributes, MF_MT_FRAME_RATE,
profile.fps.Numerator, profile.fps.Denominator);
}
if (SUCCEEDED(hr))
{
hr = pAttributes->SetUINT32(MF_MT_AVG_BITRATE, profile.bitrate);
}
if (SUCCEEDED(hr))
{
*ppAttributes = pAttributes;
(*ppAttributes)->AddRef();
}
SafeRelease(&pAttributes);
return hr;
}
次のコードでは、オーディオ ストリーム属性を作成します。
HRESULT CreateAACProfile(DWORD index, IMFAttributes **ppAttributes)
{
if (index >= ARRAYSIZE(aac_profiles))
{
return E_INVALIDARG;
}
const AACProfileInfo& profile = aac_profiles[index];
IMFAttributes *pAttributes = NULL;
HRESULT hr = MFCreateAttributes(&pAttributes, 7);
if (SUCCEEDED(hr))
{
hr = pAttributes->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_AAC);
}
if (SUCCEEDED(hr))
{
hr = pAttributes->SetUINT32(
MF_MT_AUDIO_BITS_PER_SAMPLE, profile.bitsPerSample);
}
if (SUCCEEDED(hr))
{
hr = pAttributes->SetUINT32(
MF_MT_AUDIO_SAMPLES_PER_SECOND, profile.samplesPerSec);
}
if (SUCCEEDED(hr))
{
hr = pAttributes->SetUINT32(
MF_MT_AUDIO_NUM_CHANNELS, profile.numChannels);
}
if (SUCCEEDED(hr))
{
hr = pAttributes->SetUINT32(
MF_MT_AUDIO_AVG_BYTES_PER_SECOND, profile.bytesPerSec);
}
if (SUCCEEDED(hr))
{
hr = pAttributes->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, 1);
}
if (SUCCEEDED(hr))
{
hr = pAttributes->SetUINT32(
MF_MT_AAC_AUDIO_PROFILE_LEVEL_INDICATION, profile.aacProfile);
}
if (SUCCEEDED(hr))
{
*ppAttributes = pAttributes;
(*ppAttributes)->AddRef();
}
SafeRelease(&pAttributes);
return hr;
}
トランスコード API では、メディアタイプの属性を使用しますが、真のメディアタイプは必要ありません。 特に、setVideoAttributes メソッドと SetAudioAttributes メソッドはメジャー型を意味するため、MF_MT_MAJOR_TYPE属性は必要ありません。 ただし、これらのメソッドに実際のメディアの種類を渡すこともできます。 ( IMFMediaType インターフェイスは IMFAttributes を継承します)。
エンコード セッションを実行する
次のコードは、エンコード セッションを実行します。 メディア セッション ヘルパー クラスを使用します。これは次のセクションで示します。
HRESULT RunEncodingSession(CSession *pSession, MFTIME duration)
{
const DWORD WAIT_PERIOD = 500;
const int UPDATE_INCR = 5;
HRESULT hr = S_OK;
MFTIME pos;
LONGLONG prev = 0;
while (1)
{
hr = pSession->Wait(WAIT_PERIOD);
if (hr == E_PENDING)
{
hr = pSession->GetEncodingPosition(&pos);
LONGLONG percent = (100 * pos) / duration ;
if (percent >= prev + UPDATE_INCR)
{
std::cout << percent << "% .. ";
prev = percent;
}
}
else
{
std::cout << std::endl;
break;
}
}
return hr;
}
メディア セッション ヘルパー
メディア セッションの詳細については、このドキュメントの「Media Foundation アーキテクチャ」セクションを参照してください。 メディア セッションでは、非同期イベント モデルが使用されます。 GUI アプリケーションでは、UI スレッドが次のイベントを待機するのをブロックせずにセッション イベントに応答する必要があります。 チュートリアル「 保護されていないメディア ファイルを再生する 方法」では、再生アプリケーションでこれを行う方法を示します。 エンコードの場合、原則は同じですが、関連するイベントは少なくなります。
Event | 説明 |
---|---|
MESessionEnded | エンコードが完了したときに発生します。 |
MESessionClosed | IMFMediaSession::Close メソッドが完了したときに発生します。 このイベントが発生した後は、メディア セッションをシャットダウンしても安全です。 |
コンソール アプリケーションの場合は、イベントをブロックして待機するのが妥当です。 ソース ファイルとエンコード設定によっては、エンコードが完了するまでに時間がかかる場合があります。 進行状況の更新は次のように取得できます。
- IMFMediaSession::GetClock を呼び出して、プレゼンテーション クロックを取得します。
- IMFPresentationClock インターフェイスのクロックに対してクエリを実行します。
- IMFPresentationClock::GetTime を呼び出して現在の位置を取得します。
- 位置は時間単位で指定されます。 完了率を取得するには、値
(100 * position) / duration
を使用します。
クラスの宣言を次に示 CSession
します。
class CSession : public IMFAsyncCallback
{
public:
static HRESULT Create(CSession **ppSession);
// IUnknown methods
STDMETHODIMP QueryInterface(REFIID riid, void** ppv);
STDMETHODIMP_(ULONG) AddRef();
STDMETHODIMP_(ULONG) Release();
// IMFAsyncCallback methods
STDMETHODIMP GetParameters(DWORD* pdwFlags, DWORD* pdwQueue)
{
// Implementation of this method is optional.
return E_NOTIMPL;
}
STDMETHODIMP Invoke(IMFAsyncResult *pResult);
// Other methods
HRESULT StartEncodingSession(IMFTopology *pTopology);
HRESULT GetEncodingPosition(MFTIME *pTime);
HRESULT Wait(DWORD dwMsec);
private:
CSession() : m_cRef(1), m_pSession(NULL), m_pClock(NULL), m_hrStatus(S_OK), m_hWaitEvent(NULL)
{
}
virtual ~CSession()
{
if (m_pSession)
{
m_pSession->Shutdown();
}
SafeRelease(&m_pClock);
SafeRelease(&m_pSession);
CloseHandle(m_hWaitEvent);
}
HRESULT Initialize();
private:
IMFMediaSession *m_pSession;
IMFPresentationClock *m_pClock;
HRESULT m_hrStatus;
HANDLE m_hWaitEvent;
long m_cRef;
};
次のコードは、 クラスの完全な実装を CSession
示しています。
HRESULT CSession::Create(CSession **ppSession)
{
*ppSession = NULL;
CSession *pSession = new (std::nothrow) CSession();
if (pSession == NULL)
{
return E_OUTOFMEMORY;
}
HRESULT hr = pSession->Initialize();
if (FAILED(hr))
{
pSession->Release();
return hr;
}
*ppSession = pSession;
return S_OK;
}
STDMETHODIMP CSession::QueryInterface(REFIID riid, void** ppv)
{
static const QITAB qit[] =
{
QITABENT(CSession, IMFAsyncCallback),
{ 0 }
};
return QISearch(this, qit, riid, ppv);
}
STDMETHODIMP_(ULONG) CSession::AddRef()
{
return InterlockedIncrement(&m_cRef);
}
STDMETHODIMP_(ULONG) CSession::Release()
{
long cRef = InterlockedDecrement(&m_cRef);
if (cRef == 0)
{
delete this;
}
return cRef;
}
HRESULT CSession::Initialize()
{
IMFClock *pClock = NULL;
HRESULT hr = MFCreateMediaSession(NULL, &m_pSession);
if (FAILED(hr))
{
goto done;
}
hr = m_pSession->GetClock(&pClock);
if (FAILED(hr))
{
goto done;
}
hr = pClock->QueryInterface(IID_PPV_ARGS(&m_pClock));
if (FAILED(hr))
{
goto done;
}
hr = m_pSession->BeginGetEvent(this, NULL);
if (FAILED(hr))
{
goto done;
}
m_hWaitEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
if (m_hWaitEvent == NULL)
{
hr = HRESULT_FROM_WIN32(GetLastError());
}
done:
SafeRelease(&pClock);
return hr;
}
// Implements IMFAsyncCallback::Invoke
STDMETHODIMP CSession::Invoke(IMFAsyncResult *pResult)
{
IMFMediaEvent* pEvent = NULL;
MediaEventType meType = MEUnknown;
HRESULT hrStatus = S_OK;
HRESULT hr = m_pSession->EndGetEvent(pResult, &pEvent);
if (FAILED(hr))
{
goto done;
}
hr = pEvent->GetType(&meType);
if (FAILED(hr))
{
goto done;
}
hr = pEvent->GetStatus(&hrStatus);
if (FAILED(hr))
{
goto done;
}
if (FAILED(hrStatus))
{
hr = hrStatus;
goto done;
}
switch (meType)
{
case MESessionEnded:
hr = m_pSession->Close();
if (FAILED(hr))
{
goto done;
}
break;
case MESessionClosed:
SetEvent(m_hWaitEvent);
break;
}
if (meType != MESessionClosed)
{
hr = m_pSession->BeginGetEvent(this, NULL);
}
done:
if (FAILED(hr))
{
m_hrStatus = hr;
m_pSession->Close();
}
SafeRelease(&pEvent);
return hr;
}
HRESULT CSession::StartEncodingSession(IMFTopology *pTopology)
{
HRESULT hr = m_pSession->SetTopology(0, pTopology);
if (SUCCEEDED(hr))
{
PROPVARIANT varStart;
PropVariantClear(&varStart);
hr = m_pSession->Start(&GUID_NULL, &varStart);
}
return hr;
}
HRESULT CSession::GetEncodingPosition(MFTIME *pTime)
{
return m_pClock->GetTime(pTime);
}
HRESULT CSession::Wait(DWORD dwMsec)
{
HRESULT hr = S_OK;
DWORD dwTimeoutStatus = WaitForSingleObject(m_hWaitEvent, dwMsec);
if (dwTimeoutStatus != WAIT_OBJECT_0)
{
hr = E_PENDING;
}
else
{
hr = m_hrStatus;
}
return hr;
}
関連トピック