教學課程:編碼 MP4 檔案
本教學課程示範如何使用 Transcode API 來編碼 MP4 檔案,使用 H.264 進行視訊串流,以及使用 AAC 進行音訊串流。
標頭和程式庫檔案
包含下列標頭檔。
#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")
定義編碼設定檔
編碼的其中一種方法是定義事先已知的目標編碼配置檔案清單。 在本教學課程中,我們會採用相對簡單的方法,並儲存 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 視訊編碼器 可以衍生指定視訊串流的適當層級,因此建議您不要覆寫編碼器選取的層級。 針對交錯內容,您也會指定交錯模式 (請參閱影片 交錯) 。
對於 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},
};
注意
H264ProfileInfo
這裡定義的 和 AACProfileInfo
結構不是媒體基礎 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。
- 呼叫 CoUninitialize 函式來取消初始化 COM 程式庫。
編碼檔案
下列程式碼顯示 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 或檔案路徑,建立輸入檔案的媒體來源。 (請參閱 建立媒體來源。)
- 取得輸入檔的持續時間。 (請參閱 取得來源持續時間。)
- 建立轉碼設定檔。 (請參閱 建立 Transcode Profile.)
- 呼叫 MFCreateTranscodeTopology 以建立部分轉碼拓撲。
- 建立管理媒體會話的協助程式物件。 (請參閱媒體會話協助程式) 。
- 執行編碼會話,並等候它完成。 (請參閱 執行編碼會話。)
- 呼叫 IMFMediaSource::Shutdown 以關閉媒體來源。
- 釋放介面指標。 此程式碼會使用 SafeRelease 函式來釋放介面指標。 另一個選項是使用 COM 智慧型指標類別,例如 CComPtr。
建立媒體來源
媒體來源是讀取和剖析輸入檔的物件。 若要建立媒體來源,請將輸入檔的 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 不需要真正的媒體類型,不過它使用媒體類型屬性。 特別是,它不需要 MF_MT_MAJOR_TYPE 屬性,因為 SetVideoAttributes 和 SetAudioAttributes 方法表示主要類型。 不過,將實際的媒體類型傳遞至這些方法也有效。 (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;
}
媒體會話協助程式
本檔的媒體基礎架構一節會更完整地說明媒體會話。 媒體會話會使用非同步事件模型。 在 GUI 應用程式中,您應該回應會話事件,而不會封鎖 UI 執行緒等候下一個事件。 本教學課程 How to Play Unprotected Media Files 示範如何在播放應用程式中執行這項操作。 針對編碼,原則相同,但與較少的事件相關:
事件 | 描述 |
---|---|
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;
}
相關主題