Tutorial: Mengodekan File MP4
Tutorial ini menunjukkan cara menggunakan API Transcode untuk mengodekan file MP4, menggunakan H.264 untuk aliran video dan AAC untuk aliran audio.
- Header dan File Pustaka
- Menentukan Profil Pengodean
- Menulis Fungsi wmain
- Mengodekan File
- Pembantu Sesi Media
- Topik terkait
Header dan File Pustaka
Sertakan file header berikut.
#include <new>
#include <iostream>
#include <windows.h>
#include <mfapi.h>
#include <Mfidl.h>
#include <shlwapi.h>
Tautkan file pustaka berikut.
#pragma comment(lib, "mfplat")
#pragma comment(lib, "mf")
#pragma comment(lib, "mfuuid")
#pragma comment(lib, "shlwapi")
Menentukan Profil Pengodean
Salah satu pendekatan untuk pengodean adalah menentukan daftar profil pengodean target yang diketahui sebelumnya. Untuk tutorial ini, kami mengambil pendekatan yang relatif sederhana, dan menyimpan daftar format pengodean untuk video H.264 dan audio AAC.
Untuk H.264, atribut format yang paling penting adalah profil H.264, kecepatan bingkai, ukuran bingkai, dan laju bit yang dikodekan. Array berikut berisi daftar format pengodean 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 },
};
Profil H.264 ditentukan menggunakan enumerasi eAVEncH264VProfile . Anda juga dapat menentukan tingkat H.264, tetapi Microsoft Media Foundation H.264 Video Encoder dapat memperoleh tingkat yang tepat untuk aliran video tertentu, sehingga disarankan untuk tidak mengambil alih tingkat yang dipilih encoder. Untuk konten yang terjalin, Anda juga akan menentukan mode interlace (lihat Video Interlacing).
Untuk audio AAC, atribut format yang paling penting adalah laju sampel audio, jumlah saluran, jumlah bit per sampel, dan laju bit yang dikodekan. Secara opsional, Anda dapat mengatur indikasi tingkat profil audio AAC. Untuk informasi selengkapnya, lihat Encoder AAC. Array berikut berisi daftar format pengodean 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},
};
Catatan
Struktur H264ProfileInfo
dan AACProfileInfo
yang ditentukan di sini bukan bagian dari MEDIA Foundation API.
Menulis Fungsi wmain
Kode berikut menunjukkan titik masuk untuk aplikasi konsol.
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;
}
Fungsi melakukan wmain
hal berikut:
- Memanggil fungsi CoInitializeEx untuk menginisialisasi pustaka COM.
- Memanggil fungsi MFStartup untuk menginisialisasi Media Foundation.
- Memanggil fungsi yang ditentukan
EncodeFile
aplikasi. Fungsi ini mentranskode file input ke file output, dan ditampilkan di bagian berikutnya. - Memanggil fungsi MFShutdown untuk mematikan Media Foundation.
- Panggil fungsi CoUninitialize untuk membatalkan inisialisasi pustaka COM.
Mengodekan File
Kode berikut menunjukkan EncodeFile
fungsi, yang melakukan transcoding. Fungsi ini sebagian besar terdiri dari panggilan ke fungsi lain yang ditentukan aplikasi, yang ditampilkan nanti dalam topik ini.
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;
}
Fungsi melakukan EncodeFile
langkah-langkah berikut.
- Membuat sumber media untuk file input, menggunakan URL atau jalur file file input. (Lihat Membuat Sumber Media.)
- Mendapatkan durasi file input. (Lihat Mendapatkan Durasi Sumber.)
- Buat profil transkode. (Lihat Membuat Profil Transkode.)
- Panggil MFCreateTranscodeTopology untuk membuat topologi transkode parsial.
- Buat objek pembantu yang mengelola Sesi Media. (Lihat Media Session Helper).
- Jalankan sesi pengodean dan tunggu hingga selesai. (Lihat Menjalankan Sesi Pengodean.)
- Panggil IMFMediaSource::Shutdown untuk mematikan sumber media.
- Lepaskan penunjuk antarmuka. Kode ini menggunakan fungsi SafeRelease untuk merilis penunjuk antarmuka. Opsi lain adalah menggunakan kelas pointer pintar COM, seperti CComPtr.
Membuat Sumber Media
Sumber media adalah objek yang membaca dan mengurai file input. Untuk membuat sumber media, teruskan URL file input ke Pemecah Masalah Sumber. Kode berikut menunjukkan cara melakukannya.
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;
}
Untuk informasi selengkapnya, lihat Menggunakan Pemecah Masalah Sumber.
Mendapatkan Durasi Sumber
Meskipun tidak diperlukan, akan berguna untuk mengkueri sumber media selama durasi file input. Nilai ini dapat digunakan untuk melacak kemajuan pengodean. Durasi disimpan dalam atribut MF_PD_DURATION deskriptor presentasi. Dapatkan deskriptor presentasi dengan memanggil 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;
}
Membuat Profil Transcode
Profil transkode menjelaskan parameter pengodean. Untuk informasi selengkapnya tentang membuat profil transkode, lihat Menggunakan API Transcode. Untuk membuat profil, lakukan langkah-langkah berikut.
- Panggil MFCreateTranscodeProfile untuk membuat profil kosong.
- Buat jenis media untuk aliran audio AAC. Tambahkan ke profil dengan memanggil IMFTranscodeProfile::SetAudioAttributes.
- Buat jenis media untuk aliran video H.264. Tambahkan ke profil dengan memanggil IMFTranscodeProfile::SetVideoAttributes.
- Panggil MFCreateAttributes untuk membuat penyimpanan atribut untuk atribut tingkat kontainer.
- Atur atribut MF_TRANSCODE_CONTAINERTYPE . Ini adalah satu-satunya atribut tingkat kontainer yang diperlukan. Untuk output file MP4, atur atribut ini ke MFTranscodeContainerType_MPEG4.
- Panggil IMFTranscodeProfile::SetContainerAttributes untuk mengatur atribut tingkat kontainer.
Kode berikut menunjukkan langkah-langkah ini.
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;
}
Untuk menentukan atribut untuk aliran video H.264, buat penyimpanan atribut dan atur atribut berikut:
Atribut | Desripsi |
---|---|
MF_MT_SUBTYPE | Atur ke MFVideoFormat_H264. |
MF_MT_MPEG2_PROFILE | Profil H.264. |
MF_MT_FRAME_SIZE | Ukuran bingkai. |
MF_MT_FRAME_RATE | Kecepatan bingkai. |
MF_MT_AVG_BITRATE | Laju bit yang dikodekan. |
Untuk menentukan atribut untuk aliran audio AAC, buat penyimpanan atribut dan atur atribut berikut:
Atribut | Desripsi |
---|---|
MF_MT_SUBTYPE | Atur ke MFAudioFormat_AAC |
MF_MT_AUDIO_SAMPLES_PER_SECOND | Laju sampel audio. |
MF_MT_AUDIO_BITS_PER_SAMPLE | Bit per sampel audio. |
MF_MT_AUDIO_NUM_CHANNELS | Jumlah saluran audio. |
MF_MT_AUDIO_AVG_BYTES_PER_SECOND | Laju bit yang dikodekan. |
MF_MT_AUDIO_BLOCK_ALIGNMENT | Atur ke 1. |
MF_MT_AAC_AUDIO_PROFILE_LEVEL_INDICATION | Indikasi tingkat profil AAC (opsional). |
Kode berikut membuat atribut streaming video.
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;
}
Kode berikut membuat atribut aliran audio.
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;
}
Perhatikan bahwa API transcode tidak memerlukan jenis media yang benar, meskipun menggunakan atribut jenis media. Secara khusus, atribut MF_MT_MAJOR_TYPE tidak diperlukan, karena metode SetVideoAttributes dan SetAudioAttributes menyiratkan jenis utama. Namun, juga valid untuk meneruskan jenis media aktual ke metode ini. (Antarmuka IMFMediaType mewarisi IMFAttributes.)
Jalankan Sesi Pengodean
Kode berikut menjalankan sesi pengodean. Ini menggunakan kelas pembantu Sesi Media, yang ditampilkan di bagian berikutnya.
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;
}
Pembantu Sesi Media
Sesi Media dijelaskan lebih lengkap di bagian Arsitektur Media Foundation dari dokumentasi ini. Sesi Media menggunakan model peristiwa asinkron. Dalam aplikasi GUI, Anda harus merespons peristiwa sesi tanpa memblokir utas UI untuk menunggu peristiwa berikutnya. Tutorial Cara Memutar File Media yang Tidak Terlindungi menunjukkan cara melakukan ini dalam aplikasi pemutaran. Untuk pengodean, prinsipnya sama, tetapi lebih sedikit peristiwa yang relevan:
Kejadian | Desripsi |
---|---|
MESessionEnded | Dimunculkan ketika pengodean selesai. |
MESessionClosed | Dimunculkan ketika metode IMFMediaSession::Close selesai. Setelah acara ini dinaikkan, aman untuk mematikan Sesi Media. |
Untuk aplikasi konsol, masuk akal untuk memblokir dan menunggu peristiwa. Bergantung pada file sumber dan pengaturan pengodean, mungkin perlu beberapa saat untuk menyelesaikan pengodean. Anda bisa mendapatkan pembaruan kemajuan sebagai berikut:
- Panggil IMFMediaSession::GetClock untuk mendapatkan jam presentasi.
- Kueri jam untuk antarmuka IMFPresentationClock .
- Panggil IMFPresentationClock::GetTime untuk mendapatkan posisi saat ini.
- Posisi diberikan dalam satuan waktu. Untuk menyelesaikan persentase, gunakan nilai
(100 * position) / duration
.
Berikut adalah deklarasi CSession
kelas .
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;
};
Kode berikut menunjukkan implementasi CSession
lengkap kelas .
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;
}
Topik terkait