教程:对 MP4 文件进行编码
本教程演示如何使用 转码 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 或文件路径为输入文件创建媒体源。 (请参阅 创建媒体源。)
- 获取输入文件的持续时间。 (请参阅 获取源持续时间。)
- 创建转码配置文件。 (请参阅 创建转码配置文件。)
- 调用 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;
}
创建转码配置文件
转码配置文件描述编码参数。 有关创建转码配置文件的详细信息,请参阅 使用转码 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 视频流的属性,请创建属性存储并设置以下属性:
Attribute | 描述 |
---|---|
MF_MT_SUBTYPE | 设置为 MFVideoFormat_H264。 |
MF_MT_MPEG2_PROFILE | H.264 配置文件。 |
MF_MT_FRAME_SIZE | 帧大小。 |
MF_MT_FRAME_RATE | 帧速率。 |
MF_MT_AVG_BITRATE | 编码比特率。 |
若要指定 AAC 音频流的属性,请创建属性存储并设置以下属性:
Attribute | 描述 |
---|---|
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 线程等待下一个事件。 教程 如何播放未受保护的媒体文件 介绍如何在播放应用程序中执行此操作。 对于编码,原则相同,但相关事件较少:
事件 | 描述 |
---|---|
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;
}
相关主题