共用方式為


教學課程:編碼 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 式會執行下列動作:

  1. 呼叫 CoInitializeEx 函式來初始化 COM 程式庫。
  2. 呼叫 MFStartup 函式來初始化 Media Foundation。
  3. 呼叫應用程式定義的 EncodeFile 函式。 此函式會將輸入檔轉碼為輸出檔案,並顯示在下一節中。
  4. 呼叫 MFShutdown 函式以關閉 Media Foundation。
  5. 呼叫 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 式會執行下列步驟。

  1. 使用輸入檔的 URL 或檔案路徑,建立輸入檔案的媒體來源。 (請參閱 建立媒體來源。)
  2. 取得輸入檔的持續時間。 (請參閱 取得來源持續時間。)
  3. 建立轉碼設定檔。 (請參閱 建立 Transcode Profile.)
  4. 呼叫 MFCreateTranscodeTopology 以建立部分轉碼拓撲。
  5. 建立管理媒體會話的協助程式物件。 (請參閱媒體會話協助程式) 。
  6. 執行編碼會話,並等候它完成。 (請參閱 執行編碼會話。)
  7. 呼叫 IMFMediaSource::Shutdown 以關閉媒體來源。
  8. 釋放介面指標。 此程式碼會使用 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。 若要建立設定檔,請執行下列步驟。

  1. 呼叫 MFCreateTranscodeProfile 以建立空的設定檔。
  2. 建立 AAC 音訊資料流程的媒體類型。 藉由呼叫 IMFTranscodeProfile::SetAudioAttributes將其新增至設定檔。
  3. 建立 H.264 視訊串流的媒體類型。 呼叫 IMFTranscodeProfile::SetVideoAttributes將其新增至設定檔。
  4. 呼叫 MFCreateAttributes 以建立容器層級屬性的屬性存放區。
  5. 設定 MF_TRANSCODE_CONTAINERTYPE 屬性。 這是唯一必要的容器層級屬性。 針對 MP4 檔案輸出,請將此屬性設定為 MFTranscodeContainerType_MPEG4
  6. 呼叫 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 屬性,因為 SetVideoAttributesSetAudioAttributes 方法表示主要類型。 不過,將實際的媒體類型傳遞至這些方法也有效。 (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方法完成時引發。 引發此事件之後,安全關閉媒體會話。

 

針對主控台應用程式,合理地封鎖和等候事件。 視來源檔案和編碼設定而定,可能需要一些時間才能完成編碼。 您可以取得進度更新,如下所示:

  1. 呼叫 IMFMediaSession::GetClock 以取得簡報時鐘。
  2. 查詢 IMFPresentationClock 介面的時鐘。
  3. 呼叫 IMFPresentationClock::GetTime 以取得目前的位置。
  4. 位置會以時間單位來指定。 若要取得完成百分比,請使用 值 (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;
}

AAC 編碼器

H.264 視訊編碼器

媒體會話

媒體類型

轉碼 API