本教程演示如何使用 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 },
};
使用eAVEncH264VProfile枚举来指定 H.264 格式。 还可以指定 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 配置文件。)
 - 调用 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 配置文件
转码配置文件描述编码参数。 有关创建转码配置文件的详细信息,请参阅 使用 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 视频流的属性,请创建属性存储并设置以下属性:
| 特征 | DESCRIPTION | 
|---|---|
| MF_MT_SUBTYPE | 设置为 MFVideoFormat_H264。 | 
| MF_MT_MPEG2_PROFILE | H.264 规格。 | 
| MF_MT_FRAME_SIZE | 帧大小。 | 
| MF_MT_FRAME_RATE | 帧速率。 | 
| MF_MT_AVG_BITRATE | 编码的比特率。 | 
若要指定 AAC 音频流的属性,请创建属性存储并设置以下属性:
| 特征 | DESCRIPTION | 
|---|---|
| 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_音频配置级别指示 | 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 线程等待下一个事件。 本教程 介绍如何在播放应用程序中播放未受保护的媒体文件 。 对于编码,原则相同,但相关事件较少:
| 事件 / 活动 | DESCRIPTION | 
|---|---|
| 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;
}
相关主题