사례 연구: MPEG-1 미디어 소스

Microsoft Media Foundation에서 데이터 파이프라인에 미디어 데이터를 도입하는 개체를 미디어 원본이라고 합니다. 이 항목에서는 MPEG-1 미디어 원본 SDK 샘플을 자세히 살펴봅니다.

사전 요구 사항

이 항목을 읽기 전에 다음 Media Foundation 개념을 이해해야 합니다.

또한 Media Foundation 아키텍처, 특히 파이프라인에서 미디어 원본의 역할에 대한 기본적인 이해가 있어야 합니다. (자세한 내용은 미디어 원본을 참조하세요.)

또한 여기에 설명된 단계에 대한 보다 일반적인 개요 를 제공하는 사용자 지정 미디어 원본 작성 항목을 읽을 수 있습니다.

이 항목은 샘플이 상당히 크므로 SDK 샘플에서 모든 코드를 재현하지는 않습니다.

MPEG-1 원본에서 사용되는 C++ 클래스

샘플 MPEG-1 원본은 다음 C++ 클래스를 사용하여 구현됩니다.

  • MPEG1ByteStreamHandler. 미디어 원본에 대한 바이트 스트림 처리기를 구현합니다. 바이트 스트림이 지정된 경우 바이트 스트림 처리기는 원본의 instance 만듭니다.
  • MPEG1Source. 미디어 원본을 구현합니다.
  • MPEG1Stream. 미디어 스트림 개체를 구현합니다. 미디어 원본은 MPEG-1 비트스트림의 모든 오디오 또는 비디오 스트림에 대해 하나의 MPEG1Stream 개체를 만듭니다.
  • Parser. MPEG-1 비트스트림을 구문 분석합니다. 대부분의 경우 이 클래스의 세부 정보는 Media Foundation API와 관련이 없습니다.
  • SourceOp, OpQueue: 이 두 클래스는 미디어 원본에서 비동기 작업을 관리합니다. ( 비동기 작업을 참조하세요.)

다른 기타 도우미 클래스는 항목의 뒷부분에서 설명합니다.

Byte-Stream 처리기

바이트 스트림 처리기는 미디어 원본을 만드는 개체입니다. 바이트 스트림 처리기는 원본 확인자에서 만듭니다. 애플리케이션은 바이트 스트림 처리기와 직접 상호 작용하지 않습니다. 원본 확인자는 레지스트리를 확인하여 바이트 스트림 처리기를 검색합니다. 처리기는 파일 이름 확장명 또는 MIME 형식으로 등록됩니다. MPEG-1 원본의 경우 바이트 스트림 처리기가 ".mpg" 파일 이름 확장명용으로 등록됩니다.

참고

사용자 지정 URL 체계를 지원하려는 경우 스키마 처리기를 작성할 수도 있습니다. MPEG-1 원본은 로컬 파일을 위해 설계되었으며 Media Foundation은 이미 "file://" URL에 대한 체계 처리기를 제공합니다.

 

바이트 스트림 처리기는 IMFByteStreamHandler 인터페이스를 구현합니다. 이 인터페이스에는 구현해야 하는 두 가지 가장 중요한 메서드가 있습니다.

다른 두 가지 메서드는 선택 사항이며 SDK 샘플에서 구현되지 않습니다.

  • CancelObjectCreation. BeginCreateObject 메서드를 취소합니다. 이 메서드는 시작 시 대기 시간이 긴 네트워크 원본에 유용합니다.
  • GetMaxNumberOfBytesRequiredForResolution. 처리기가 원본 스트림에서 읽을 최대 바이트 수를 가져옵니다. 바이트 스트림 처리기가 미디어 원본을 만들 수 있는 데이터의 양을 알고 있는 경우 이 메서드를 구현합니다. 그렇지 않으면 단순히 E_NOTIMPL 반환합니다.

BeginCreateObject 메서드의 구현은 다음과 같습니다.

HRESULT MPEG1ByteStreamHandler::BeginCreateObject(
        /* [in] */ IMFByteStream *pByteStream,
        /* [in] */ LPCWSTR pwszURL,
        /* [in] */ DWORD dwFlags,
        /* [in] */ IPropertyStore *pProps,
        /* [out] */ IUnknown **ppIUnknownCancelCookie,  // Can be NULL
        /* [in] */ IMFAsyncCallback *pCallback,
        /* [in] */ IUnknown *punkState                  // Can be NULL
        )
{
    if (pByteStream == NULL)
    {
        return E_POINTER;
    }

    if (pCallback == NULL)
    {
        return E_POINTER;
    }

    if ((dwFlags & MF_RESOLUTION_MEDIASOURCE) == 0)
    {
        return E_INVALIDARG;
    }

    HRESULT hr = S_OK;

    IMFAsyncResult *pResult = NULL;
    MPEG1Source    *pSource = NULL;

    // Create an instance of the media source.
    hr = MPEG1Source::CreateInstance(&pSource);

    // Create a result object for the caller's async callback.
    if (SUCCEEDED(hr))
    {
        hr = MFCreateAsyncResult(NULL, pCallback, punkState, &pResult);
    }

    // Start opening the source. This is an async operation.
    // When it completes, the source will invoke our callback
    // and then we will invoke the caller's callback.
    if (SUCCEEDED(hr))
    {
        hr = pSource->BeginOpen(pByteStream, this, NULL);
    }

    if (SUCCEEDED(hr))
    {
        if (ppIUnknownCancelCookie)
        {
            *ppIUnknownCancelCookie = NULL;
        }

        m_pSource = pSource;
        m_pSource->AddRef();

        m_pResult = pResult;
        m_pResult->AddRef();
    }

// cleanup
    SafeRelease(&pSource);
    SafeRelease(&pResult);
    return hr;
}

메서드는 다음 단계를 수행합니다.

  1. MPEG1Source 개체의 새 인스턴스를 만듭니다.
  2. 비동기 결과 개체를 만듭니다. 이 개체는 나중에 소스 확인자의 콜백 메서드를 호출하는 데 사용됩니다.
  3. 클래스에 정의된 비동기 메서드인 를 호출 MPEG1Source::BeginOpen합니다 MPEG1Source .
  4. ppIUnknownCancelCookie를 NULL로 설정합니다. 이 NULLCancelObjectCreation이 지원되지 않는다는 것을 호출자에게 알릴 수 있습니다.

메서드는 MPEG1Source::BeginOpen 바이트 스트림을 읽고 개체를 초기화하는 MPEG1Source 실제 작업을 수행합니다. 이 메서드는 공용 API의 일부가 아닙니다. 처리기와 요구 사항에 맞는 미디어 원본 간에 메커니즘을 정의할 수 있습니다. 대부분의 논리를 미디어 원본에 배치하면 바이트 스트림 처리기가 비교적 간단해집니다.

간단히 BeginOpen 다음을 수행합니다.

  1. IMFByteStream::GetCapabilities를 호출하여 원본 바이트 스트림이 읽기 가능하고 검색 가능한지 확인합니다.
  2. IMFByteStream::BeginRead를 호출하여 비동기 I/O 요청을 시작합니다.

초기화의 나머지 는 비동기적으로 발생합니다. 미디어 원본은 스트림에서 충분한 데이터를 읽어 MPEG-1 시퀀스 헤더를 구문 분석합니다. 그런 다음 파일의 오디오 및 비디오 스트림을 설명하는 데 사용되는 개체인 프레젠테이션 설명자를 만듭니다. 자세한 내용은 프레젠테이션 설명자를 참조하세요. BeginOpen 작업이 완료되면 바이트 스트림 처리기는 원본 확인자의 콜백 메서드를 호출합니다. 이 시점에서 원본 확인자는 IMFByteStreamHandler::EndCreateObject를 호출합니다. EndCreateObject 메서드는 작업의 상태 반환합니다.

HRESULT MPEG1ByteStreamHandler::EndCreateObject(
        /* [in] */ IMFAsyncResult *pResult,
        /* [out] */ MF_OBJECT_TYPE *pObjectType,
        /* [out] */ IUnknown **ppObject)
{
    if (pResult == NULL || pObjectType == NULL || ppObject == NULL)
    {
        return E_POINTER;
    }

    HRESULT hr = S_OK;

    *pObjectType = MF_OBJECT_INVALID;
    *ppObject = NULL;

    hr = pResult->GetStatus();

    if (SUCCEEDED(hr))
    {
        *pObjectType = MF_OBJECT_MEDIASOURCE;

        assert(m_pSource != NULL);

        hr = m_pSource->QueryInterface(IID_PPV_ARGS(ppObject));
    }

    SafeRelease(&m_pSource);
    SafeRelease(&m_pResult);
    return hr;
}

이 프로세스 중에 언제든지 오류가 발생하면 코드에 오류 상태 콜백이 호출됩니다.

프레젠테이션 설명자

프레젠테이션 설명자는 다음 정보를 포함하여 MPEG-1 파일의 내용을 설명합니다.

  • 스트림의 수입니다.
  • 각 스트림의 형식입니다.
  • 스트림 식별자입니다.
  • 각 스트림의 선택 상태(선택되거나 선택 취소됨).

Media Foundation 아키텍처와 관련하여 프레젠테이션 설명자에는 하나 이상의 스트림 설명자가 포함되어 있습니다. 각 스트림 설명자에는 스트림에서 미디어 형식을 얻거나 설정하는 데 사용되는 미디어 형식 처리기가 포함되어 있습니다. Media Foundation은 프레젠테이션 설명자 및 스트림 설명자에 대한 스톡 구현을 제공합니다. 대부분의 미디어 원본에 적합합니다.

프레젠테이션 설명자를 만들려면 다음 단계를 수행합니다.

  1. 각 스트림에 대해 다음을 수행합니다.
    1. 스트림 ID와 가능한 미디어 형식의 배열을 제공합니다. 스트림이 둘 이상의 미디어 형식을 지원하는 경우 기본 설정에 따라 미디어 형식 목록을 정렬합니다(있는 경우). (최적 형식을 먼저 배치하고 최적이 가장 낮은 형식을 마지막에 배치합니다.)
    2. MFCreateStreamDescriptor를 호출하여 스트림 설명자를 만듭니다.
    3. 새로 만든 스트림 설명자에서 IMFStreamDescriptor::GetMediaTypeHandler 를 호출합니다.
    4. IMFMediaTypeHandler::SetCurrentMediaType을 호출하여 스트림의 기본 형식을 설정합니다. 두 개 이상의 미디어 형식이 있는 경우 일반적으로 목록에서 첫 번째 형식을 설정해야 합니다.
  2. MFCreatePresentationDescriptor를 호출하고 스트림 설명자 포인터의 배열을 전달합니다.
  3. 각 스트림에 대해 IMFPresentationDescriptor::SelectStream 또는 DeselectStream 을 호출하여 기본 선택 상태를 설정합니다. 동일한 형식(오디오 또는 비디오)의 스트림이 두 개 이상 있는 경우 기본적으로 하나만 선택해야 합니다.

개체는 MPEG1Source 메서드에 InitPresentationDescriptor 프레젠테이션 설명자를 만듭니다.

HRESULT MPEG1Source::InitPresentationDescriptor()
{
    HRESULT hr = S_OK;
    DWORD cStreams = 0;

    assert(m_pPresentationDescriptor == NULL);
    assert(m_state == STATE_OPENING);

    if (m_pHeader == NULL)
    {
        return E_FAIL;
    }

    // Get the number of streams, as declared in the MPEG-1 header, skipping
    // any streams with an unsupported format.
    for (DWORD i = 0; i < m_pHeader->cStreams; i++)
    {
        if (IsStreamTypeSupported(m_pHeader->streams[i].type))
        {
            cStreams++;
        }
    }

    // How many streams do we actually have?
    if (cStreams > m_streams.GetCount())
    {
        // Keep reading data until we have seen a packet for each stream.
        return S_OK;
    }

    // We should never create a stream we don't support.
    assert(cStreams == m_streams.GetCount());

    // Ready to create the presentation descriptor.

    // Create an array of IMFStreamDescriptor pointers.
    IMFStreamDescriptor **ppSD =
            new (std::nothrow) IMFStreamDescriptor*[cStreams];

    if (ppSD == NULL)
    {
        hr = E_OUTOFMEMORY;
        goto done;
    }

    ZeroMemory(ppSD, cStreams * sizeof(IMFStreamDescriptor*));

    // Fill the array by getting the stream descriptors from the streams.
    for (DWORD i = 0; i < cStreams; i++)
    {
        hr = m_streams[i]->GetStreamDescriptor(&ppSD[i]);
        if (FAILED(hr))
        {
            goto done;
        }
    }

    // Create the presentation descriptor.
    hr = MFCreatePresentationDescriptor(cStreams, ppSD,
        &m_pPresentationDescriptor);

    if (FAILED(hr))
    {
        goto done;
    }

    // Select the first video stream (if any).
    for (DWORD i = 0; i < cStreams; i++)
    {
        GUID majorType = GUID_NULL;

        hr = GetStreamMajorType(ppSD[i], &majorType);
        if (FAILED(hr))
        {
            goto done;
        }

        if (majorType == MFMediaType_Video)
        {
            hr = m_pPresentationDescriptor->SelectStream(i);
            if (FAILED(hr))
            {
                goto done;
            }
            break;
        }
    }

    // Switch state from "opening" to stopped.
    m_state = STATE_STOPPED;

    // Invoke the async callback to complete the BeginOpen operation.
    hr = CompleteOpen(S_OK);

done:
    // clean up:
    if (ppSD)
    {
        for (DWORD i = 0; i < cStreams; i++)
        {
            SafeRelease(&ppSD[i]);
        }
        delete [] ppSD;
    }
    return hr;
}

애플리케이션은 IMFMediaSource::CreatePresentationDescriptor를 호출하여 프레젠테이션 설명자를 가져옵니다. 이 메서드는 IMFPresentationDescriptor::Clone을 호출하여 프레젠테이션 설명자의 단순 복사본을 만듭니다. (복사본에는 원래 스트림 설명자에 대한 포인터가 포함되어 있습니다.) 애플리케이션은 프레젠테이션 설명자를 사용하여 미디어 유형을 설정하거나, 스트림을 선택하거나, 스트림을 선택 취소할 수 있습니다.

필요에 따라 프레젠테이션 설명자 및 스트림 설명자에는 원본에 대한 추가 정보를 제공하는 특성이 포함될 수 있습니다. 이러한 특성 목록은 다음 topics 참조하세요.

한 특성은 특별한 멘션 자격이 있습니다. MF_PD_DURATION 특성에는 원본의 총 기간이 포함됩니다. 기간을 미리 알고 있는 경우 이 특성을 설정합니다. 예를 들어 파일 형식에 따라 기간이 파일 헤더에 지정될 수 있습니다. 애플리케이션에서 이 값을 표시하거나 이를 사용하여 진행률 표시줄 또는 검색 막대를 설정할 수 있습니다.

스트리밍 상태

미디어 원본은 다음 상태를 정의합니다.

시스템 상태 설명
시작됨 원본은 샘플 요청을 수락하고 처리합니다.
일시 중지됨 원본은 샘플 요청을 수락하지만 처리하지는 않습니다. 요청은 원본이 시작될 때까지 큐에 대기됩니다.
중지됨. 원본은 샘플 요청을 거부합니다.

 

시작

IMFMediaSource::Start 메서드는 미디어 원본을 시작합니다. 사용되는 매개 변수는 다음과 같습니다.

  • 프레젠테이션 설명자입니다.
  • 시간 형식 GUID입니다.
  • 시작 위치입니다.

애플리케이션은 원본에서 CreatePresentationDescriptor 를 호출하여 프레젠테이션 설명자를 가져와야 합니다. 프레젠테이션 설명자의 유효성을 검사하기 위한 정의된 메커니즘은 없습니다. 애플리케이션에서 잘못된 프레젠테이션 설명자를 지정하면 결과가 정의되지 않습니다.

시간 형식 GUID는 시작 위치를 해석하는 방법을 지정합니다. 표준 형식은 GUID_NULL 나타내는 100나노초(ns) 단위입니다. 모든 미디어 원본은 100-ns 단위를 지원해야 합니다. 필요에 따라 원본은 프레임 번호 또는 시간 코드와 같은 다른 시간 단위를 지원할 수 있습니다. 그러나 미디어 원본에서 지원하는 시간 형식 목록을 쿼리하는 표준 방법은 없습니다.

시작 위치는 PROPVARIANT로 지정되어 시간 형식에 따라 다른 데이터 형식을 허용합니다. 100-ns의 경우 PROPVARIANT 형식은 VT_I8 또는 VT_EMPTY. VT_I8 경우 PROPVARIANT에는 100-ns 단위의 시작 위치가 포함됩니다. VT_EMPTY 값에는 "현재 위치에서 시작"이라는 특별한 의미가 있습니다.

다음과 같이 Start 메서드를 구현합니다.

  1. 매개 변수 및 상태의 유효성을 검사합니다.
    • NULL 매개 변수를 확인합니다.
    • 시간 형식 GUID를 확인합니다. 값이 유효하지 않으면 MF_E_UNSUPPORTED_TIME_FORMAT 반환합니다.
    • 시작 위치를 보유하는 PROPVARIANT 의 데이터 형식을 확인합니다.
    • 시작 위치의 유효성을 검사합니다. 유효하지 않은 경우 MF_E_INVALIDREQUEST 반환합니다.
    • 원본이 종료된 경우 MF_E_SHUTDOWN 반환합니다.
  2. 1단계에서 오류가 발생하지 않으면 비동기 작업을 큐에 대기합니다. 이 단계 이후의 모든 작업은 작업 큐 스레드에서 발생합니다.
  3. 각 스트림에 대해 다음을 수행합니다.
    1. 스트림이 이전 시작 요청에서 이미 활성 상태인지 확인합니다.

    2. IMFPresentationDescriptor::GetStreamDescriptorByIndex를 호출하여 애플리케이션이 스트림을 선택하거나 선택 취소했는지 여부를 검사.

    3. 이전에 선택한 스트림의 선택을 취소한 경우 해당 스트림에 대해 배달되지 않은 샘플을 플러시합니다.

    4. 스트림이 활성 상태이면 미디어 원본(스트림이 아님)은 다음 이벤트 중 하나를 보냅니다.

      두 이벤트의 경우 이벤트 데이터는 스트림에 대한 IMFMediaStream 포인터입니다.

    5. 원본이 일시 중지된 상태에서 다시 시작되는 경우 보류 중인 샘플 요청이 있을 수 있습니다. 그렇다면 지금 배달합니다.

    6. 원본이 새 위치를 찾는 경우 각 스트림 개체는 MEStreamSeeked 이벤트를 보냅니다. 그렇지 않으면 각 스트림이 MEStreamStarted 이벤트를 보냅니다.

  4. 원본이 새 위치를 찾는 경우 미디어 원본은 MESourceSeeked 이벤트를 보냅니다. 그렇지 않으면 MESourceStarted 이벤트를 보냅니다.

2단계 이후 언제든지 오류가 발생하면 소스는 오류 코드와 함께 MESourceStarted 이벤트를 보냅니다. 그러면 Start 메서드가 비동기적으로 실패했음을 애플리케이션에 경고합니다.

다음 코드는 1~2단계를 보여줍니다.

HRESULT MPEG1Source::Start(
        IMFPresentationDescriptor* pPresentationDescriptor,
        const GUID* pguidTimeFormat,
        const PROPVARIANT* pvarStartPos
    )
{

    HRESULT hr = S_OK;
    SourceOp *pAsyncOp = NULL;

    // Check parameters.

    // Start position and presentation descriptor cannot be NULL.
    if (pvarStartPos == NULL || pPresentationDescriptor == NULL)
    {
        return E_INVALIDARG;
    }

    // Check the time format.
    if ((pguidTimeFormat != NULL) && (*pguidTimeFormat != GUID_NULL))
    {
        // Unrecognized time format GUID.
        return MF_E_UNSUPPORTED_TIME_FORMAT;
    }

    // Check the data type of the start position.
    if ((pvarStartPos->vt != VT_I8) && (pvarStartPos->vt != VT_EMPTY))
    {
        return MF_E_UNSUPPORTED_TIME_FORMAT;
    }

    EnterCriticalSection(&m_critSec);

    // Check if this is a seek request. This sample does not support seeking.

    if (pvarStartPos->vt == VT_I8)
    {
        // If the current state is STOPPED, then position 0 is valid.
        // Otherwise, the start position must be VT_EMPTY (current position).

        if ((m_state != STATE_STOPPED) || (pvarStartPos->hVal.QuadPart != 0))
        {
            hr = MF_E_INVALIDREQUEST;
            goto done;
        }
    }

    // Fail if the source is shut down.
    hr = CheckShutdown();
    if (FAILED(hr))
    {
        goto done;
    }

    // Fail if the source was not initialized yet.
    hr = IsInitialized();
    if (FAILED(hr))
    {
        goto done;
    }

    // Perform a basic check on the caller's presentation descriptor.
    hr = ValidatePresentationDescriptor(pPresentationDescriptor);
    if (FAILED(hr))
    {
        goto done;
    }

    // The operation looks OK. Complete the operation asynchronously.
    hr = SourceOp::CreateStartOp(pPresentationDescriptor, &pAsyncOp);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pAsyncOp->SetData(*pvarStartPos);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = QueueOperation(pAsyncOp);

done:
    SafeRelease(&pAsyncOp);
    LeaveCriticalSection(&m_critSec);
    return hr;
}

나머지 단계는 다음 예제에 나와 있습니다.

HRESULT MPEG1Source::DoStart(StartOp *pOp)
{
    assert(pOp->Op() == SourceOp::OP_START);

    IMFPresentationDescriptor *pPD = NULL;
    IMFMediaEvent  *pEvent = NULL;

    HRESULT     hr = S_OK;
    LONGLONG    llStartOffset = 0;
    BOOL        bRestartFromCurrentPosition = FALSE;
    BOOL        bSentEvents = FALSE;

    hr = BeginAsyncOp(pOp);

    // Get the presentation descriptor from the SourceOp object.
    // This is the PD that the caller passed into the Start() method.
    // The PD has already been validated.
    if (SUCCEEDED(hr))
    {
        hr = pOp->GetPresentationDescriptor(&pPD);
    }

    // Because this sample does not support seeking, the start
    // position must be 0 (from stopped) or "current position."

    // If the sample supported seeking, we would need to get the
    // start position from the PROPVARIANT data contained in pOp.

    if (SUCCEEDED(hr))
    {
        // Select/deselect streams, based on what the caller set in the PD.
        // This method also sends the MENewStream/MEUpdatedStream events.
        hr = SelectStreams(pPD, pOp->Data());
    }

    if (SUCCEEDED(hr))
    {
        m_state = STATE_STARTED;

        // Queue the "started" event. The event data is the start position.
        hr = m_pEventQueue->QueueEventParamVar(
            MESourceStarted,
            GUID_NULL,
            S_OK,
            &pOp->Data()
            );
    }

    if (FAILED(hr))
    {
        // Failure. Send the error code to the application.

        // Note: It's possible that QueueEvent itself failed, in which case it
        // is likely to fail again. But there is no good way to recover in
        // that case.

        (void)m_pEventQueue->QueueEventParamVar(
            MESourceStarted, GUID_NULL, hr, NULL);
    }

    CompleteAsyncOp(pOp);

    SafeRelease(&pEvent);
    SafeRelease(&pPD);
    return hr;
}

일시 중지

IMFMediaSource::P ause 메서드는 미디어 원본을 일시 중지합니다. 다음과 같이 이 메서드를 구현합니다.

  1. 비동기 작업을 큐에 추가합니다.
  2. 각 활성 스트림은 MEStreamPaused 이벤트를 보냅니다.
  3. 미디어 원본은 MESourcePaused 이벤트를 보냅니다.

일시 중지된 동안 원본은 샘플 요청을 처리하지 않고 큐에 대기합니다. ( 샘플 요청을 참조하세요.)

Stop

IMFMediaSource::Stop 메서드는 미디어 원본을 중지합니다. 다음과 같이 이 메서드를 구현합니다.

  1. 비동기 작업을 큐에 추가합니다.
  2. 각 활성 스트림은 MEStreamStopped 이벤트를 보냅니다.
  3. 큐에 대기된 모든 샘플 및 샘플 요청을 지웁니다.
  4. 미디어 원본은 MESourceStopped 이벤트를 보냅니다.

중지된 동안 원본은 샘플에 대한 모든 요청을 거부합니다.

I/O 요청이 진행 중인 동안 원본이 중지된 경우 원본이 중지된 상태로 전환된 후 I/O 요청이 완료될 수 있습니다. 이 경우 원본은 해당 I/O 요청의 결과를 삭제해야 합니다.

샘플 요청

Media Foundation은 파이프라인이 미디어 원본에서 샘플을 요청하는 끌어오기 모델을 사용합니다. 이는 원본이 샘플을 "푸시"하는 DirectShow에서 사용하는 모델과 다릅니다.

새 샘플을 요청하기 위해 Media Foundation 파이프라인은 IMFMediaStream::RequestSample을 호출합니다. 이 메서드는 토큰 개체를 나타내는 IUnknown 포인터를 사용합니다. 토큰 개체의 구현은 호출자에게 달려 있습니다. 단순히 호출자가 샘플 요청을 추적하는 방법을 제공합니다. 토큰 매개 변수는 NULL일 수도 있습니다.

원본이 비동기 I/O 요청을 사용하여 데이터를 읽는 경우 샘플 생성은 샘플 요청과 동기화되지 않습니다. 샘플 요청을 샘플 생성과 동기화하기 위해 미디어 원본은 다음을 수행합니다.

  1. 요청 토큰은 큐에 배치됩니다.
  2. 샘플이 생성되면 두 번째 큐에 배치됩니다.
  3. 미디어 원본은 첫 번째 큐에서 요청 토큰을 끌어와 두 번째 큐에서 샘플을 가져와서 샘플 요청을 완료합니다.
  4. 미디어 원본은 MEMediaSample 이벤트를 보냅니다. 이벤트에는 샘플에 대한 포인터가 포함되고 샘플에는 토큰에 대한 포인터가 포함됩니다.

다음 다이어그램은 MEMediaSample 이벤트, 샘플 및 요청 토큰 간의 관계를 보여 줍니다.

memediasample 및 imfsample을 가리키는 샘플 큐를 보여 주는 다이어그램 imfsample 및 요청 큐가 iunknown을 가리킵니다.

예제 MPEG-1 원본은 다음과 같이 이 프로세스를 구현합니다.

  1. RequestSample 메서드는 FIFO 큐에 요청을 배치합니다.
  2. I/O 요청이 완료되면 미디어 원본은 새 샘플을 만들어 두 번째 FIFO 큐에 배치합니다. (이 큐의 최대 크기는 원본이 너무 앞서 읽지 못하도록 합니다.)
  3. 이러한 큐에 하나 이상의 항목(하나의 요청 및 하나의 샘플)이 있을 때마다 미디어 원본은 샘플 큐에서 첫 번째 샘플을 전송하여 요청 큐에서 첫 번째 요청을 완료합니다.
  4. 샘플을 제공하기 위해 원본 개체가 아닌 스트림 개체는 MEMediaSample 이벤트를 보냅니다.
    • 이벤트 데이터는 샘플의 IMFSample 인터페이스에 대한 포인터입니다.
    • 요청에 토큰이 포함된 경우 샘플에서 MFSampleExtension_Token 특성을 설정하여 토큰을 샘플에 연결합니다.

이 시점에서 다음과 같은 세 가지 가능성이 있습니다.

  • 샘플 큐에는 다른 샘플이 있지만 일치하는 요청은 없습니다.
  • 요청이 있지만 샘플은 없습니다.
  • 두 큐가 모두 비어 있습니다. 샘플이 없고 요청이 없습니다.

샘플 큐가 비어 있는 경우 원본은 스트림의 끝을 확인합니다( 스트림 끝 참조). 그렇지 않으면 데이터에 대한 다른 I/O 요청을 시작합니다. 이 프로세스 중에 오류가 발생하면 스트림은 MEError 이벤트를 보냅니다.

다음 코드는 IMFMediaStream::RequestSample 메서드를 구현합니다.

HRESULT MPEG1Stream::RequestSample(IUnknown* pToken)
{
    HRESULT hr = S_OK;
    IMFMediaSource *pSource = NULL;

    // Hold the media source object's critical section.
    SourceLock lock(m_pSource);

    hr = CheckShutdown();
    if (FAILED(hr))
    {
        goto done;
    }

    if (m_state == STATE_STOPPED)
    {
        hr = MF_E_INVALIDREQUEST;
        goto done;
    }

    if (!m_bActive)
    {
        // If the stream is not active, it should not get sample requests.
        hr = MF_E_INVALIDREQUEST;
        goto done;
    }

    if (m_bEOS && m_Samples.IsEmpty())
    {
        // This stream has already reached the end of the stream, and the
        // sample queue is empty.
        hr = MF_E_END_OF_STREAM;
        goto done;
    }

    hr = m_Requests.InsertBack(pToken);
    if (FAILED(hr))
    {
        goto done;
    }

    // Dispatch the request.
    hr = DispatchSamples();
    if (FAILED(hr))
    {
        goto done;
    }

done:
    if (FAILED(hr) && (m_state != STATE_SHUTDOWN))
    {
        // An error occurred. Send an MEError even from the source,
        // unless the source is already shut down.
        hr = m_pSource->QueueEvent(MEError, GUID_NULL, hr, NULL);
    }
    return hr;
}

메서드는 DispatchSamples 샘플 큐에서 샘플을 가져오고 보류 중인 샘플 요청과 일치시키고 MEMediaSample 이벤트를 큐에 넣습니다.

HRESULT MPEG1Stream::DispatchSamples()
{
    HRESULT hr = S_OK;
    BOOL bNeedData = FALSE;
    BOOL bEOS = FALSE;

    SourceLock lock(m_pSource);

    // An I/O request can complete after the source is paused, stopped, or
    // shut down. Do not deliver samples unless the source is running.
    if (m_state != STATE_STARTED)
    {
        return S_OK;
    }

    IMFSample *pSample = NULL;
    IUnknown  *pToken = NULL;

    // Deliver as many samples as we can.
    while (!m_Samples.IsEmpty() && !m_Requests.IsEmpty())
    {
        // Pull the next sample from the queue.
        hr = m_Samples.RemoveFront(&pSample);
        if (FAILED(hr))
        {
            goto done;
        }

        // Pull the next request token from the queue. Tokens can be NULL.
        hr = m_Requests.RemoveFront(&pToken);
        if (FAILED(hr))
        {
            goto done;
        }

        if (pToken)
        {
            // Set the token on the sample.
            hr = pSample->SetUnknown(MFSampleExtension_Token, pToken);
            if (FAILED(hr))
            {
                goto done;
            }
        }

        // Send an MEMediaSample event with the sample.
        hr = m_pEventQueue->QueueEventParamUnk(
            MEMediaSample, GUID_NULL, S_OK, pSample);

        if (FAILED(hr))
        {
            goto done;
        }

        SafeRelease(&pSample);
        SafeRelease(&pToken);
    }

    if (m_Samples.IsEmpty() && m_bEOS)
    {
        // The sample queue is empty AND we have reached the end of the source
        // stream. Notify the pipeline by sending the end-of-stream event.

        hr = m_pEventQueue->QueueEventParamVar(
            MEEndOfStream, GUID_NULL, S_OK, NULL);

        if (FAILED(hr))
        {
            goto done;
        }

        // Notify the source. It will send the end-of-presentation event.
        hr = m_pSource->QueueAsyncOperation(SourceOp::OP_END_OF_STREAM);
        if (FAILED(hr))
        {
            goto done;
        }
    }
    else if (NeedsData())
    {
        // The sample queue is empty; the request queue is not empty; and we
        // have not reached the end of the stream. Ask for more data.
        hr = m_pSource->QueueAsyncOperation(SourceOp::OP_REQUEST_DATA);
        if (FAILED(hr))
        {
            goto done;
        }
    }

done:
    if (FAILED(hr) && (m_state != STATE_SHUTDOWN))
    {
        // An error occurred. Send an MEError even from the source,
        // unless the source is already shut down.
        m_pSource->QueueEvent(MEError, GUID_NULL, hr, NULL);
    }

    SafeRelease(&pSample);
    SafeRelease(&pToken);
    return S_OK;
}

메서드는 DispatchSamples 다음과 같은 상황에서 호출됩니다.

  • RequestSample 메서드 내부.
  • 미디어 원본이 일시 중지된 상태에서 다시 시작되는 경우
  • I/O 요청이 완료되는 경우

스트림의 끝

스트림에 더 이상 데이터가 없고 해당 스트림에 대한 모든 샘플이 전달되면 스트림 개체는 MEEndOfStream 이벤트를 보냅니다.

모든 활성 스트림이 완료되면 미디어 원본은 MEEndOfPresentation 이벤트를 보냅니다.

비동기 작업

미디어 원본을 작성하는 가장 어려운 부분은 Media Foundation 비동기 모델을 이해하는 것입니다.

스트리밍을 제어하는 미디어 소스의 모든 메서드는 비동기적입니다. 각 경우에 메서드는 매개 변수 검사와 같은 몇 가지 초기 유효성 검사를 수행합니다. 그런 다음 원본은 나머지 작업을 작업 큐로 디스패치합니다. 작업이 완료되면 미디어 원본은 미디어 원본의 IMFMediaEventGenerator 인터페이스를 통해 이벤트를 호출자에게 다시 보냅니다. 따라서 작업 큐를 이해하는 것이 중요합니다.

작업 큐에 항목을 배치하려면 MFPutWorkItem 또는 MFPutWorkItemEx를 호출할 수 있습니다. MPEG-1 원본은 MFPutWorkItem을 사용하지만 두 함수는 동일한 작업을 수행합니다. MFPutWorkItem 함수는 다음 매개 변수를 사용합니다.

  • 작업 큐를 식별하는 DWORD 값입니다. 프라이빗 작업 큐를 만들거나 MFASYNC_CALLBACK_QUEUE_STANDARD 사용할 수 있습니다.
  • IMFAsyncCallback 인터페이스에 대한 포인터입니다. 이 콜백 인터페이스는 작업을 수행하기 위해 호출됩니다.
  • IUnknown을 구현해야 하는 선택적 상태 개체입니다.

작업 큐는 큐에서 다음 작업 항목을 지속적으로 끌어오고 콜백 인터페이스의 IMFAsyncCallback::Invoke 메서드를 호출하는 하나 이상의 작업자 스레드에서 서비스됩니다.

작업 항목은 큐에 배치한 순서와 동일한 순서로 실행되도록 보장되지 않습니다. 둘 이상의 스레드가 동일한 작업 큐를 서비스할 수 있으므로 호출 호출 이 겹치거나 순서가 다를 수 있습니다. 따라서 올바른 순서로 작업 큐 항목을 제출하여 올바른 내부 상태를 유지하는 것은 미디어 원본의 입니다. 이전 작업이 완료된 경우에만 원본이 다음 작업을 시작합니다.

보류 중인 작업을 나타내기 위해 MPEG-1 소스는 라는 SourceOp클래스를 정의합니다.

// Represents a request for an asynchronous operation.

class SourceOp : public IUnknown
{
public:

    enum Operation
    {
        OP_START,
        OP_PAUSE,
        OP_STOP,
        OP_REQUEST_DATA,
        OP_END_OF_STREAM
    };

    static HRESULT CreateOp(Operation op, SourceOp **ppOp);
    static HRESULT CreateStartOp(IMFPresentationDescriptor *pPD, SourceOp **ppOp);

    // IUnknown
    STDMETHODIMP QueryInterface(REFIID iid, void** ppv);
    STDMETHODIMP_(ULONG) AddRef();
    STDMETHODIMP_(ULONG) Release();

    SourceOp(Operation op);
    virtual ~SourceOp();

    HRESULT SetData(const PROPVARIANT& var);

    Operation Op() const { return m_op; }
    const PROPVARIANT& Data() { return m_data;}

protected:
    long        m_cRef;     // Reference count.
    Operation   m_op;
    PROPVARIANT m_data;     // Data for the operation.
};

열거형은 Operation 보류 중인 작업을 식별합니다. 클래스에는 작업에 대한 추가 데이터를 전달하는 PROPVARIANT 도 포함되어 있습니다.

작업 큐

작업을 직렬화하기 위해 미디어 소스는 개체 큐 SourceOp 를 유지 관리합니다. 도우미 클래스를 사용하여 큐를 관리합니다.

template <class OP_TYPE>
class OpQueue : public IUnknown
{
public:

    typedef ComPtrList<OP_TYPE>   OpList;

    HRESULT QueueOperation(OP_TYPE *pOp);

protected:

    HRESULT ProcessQueue();
    HRESULT ProcessQueueAsync(IMFAsyncResult *pResult);

    virtual HRESULT DispatchOperation(OP_TYPE *pOp) = 0;
    virtual HRESULT ValidateOperation(OP_TYPE *pOp) = 0;

    OpQueue(CRITICAL_SECTION& critsec)
        : m_OnProcessQueue(this, &OpQueue::ProcessQueueAsync),
          m_critsec(critsec)
    {
    }

    virtual ~OpQueue()
    {
    }

protected:
    OpList                  m_OpQueue;         // Queue of operations.
    CRITICAL_SECTION&       m_critsec;         // Protects the queue state.
    AsyncCallback<OpQueue>  m_OnProcessQueue;  // ProcessQueueAsync callback.
};

클래스는 OpQueue 비동기 작업 항목을 수행하는 구성 요소에서 상속되도록 설계되었습니다. OP_TYPE 템플릿 매개 변수는 큐의 작업 항목을 나타내는 데 사용되는 개체 형식입니다. 이 경우 OP_TYPE 입니다SourceOp. 클래스는 OpQueue 다음 메서드를 구현합니다.

  • QueueOperation 는 새 항목을 큐에 배치합니다.
  • ProcessQueue 는 큐에서 다음 작업을 디스패치합니다. 이 메서드는 비동기적이며,
  • ProcessQueueAsync 는 비동 ProcessQueue 기 메서드를 완료합니다.

파생 클래스에서 다른 두 메서드를 구현해야 합니다.

  • ValidateOperation 미디어 원본의 현재 상태를 고려할 때 지정된 작업을 수행하는 것이 유효한지 여부를 확인합니다.
  • DispatchOperation 는 비동기 작업 항목을 수행합니다.

작업 큐는 다음과 같이 사용됩니다.

  1. Media Foundation 파이프라인은 IMFMediaSource::Start와 같은 미디어 원본에서 비동기 메서드를 호출합니다.
  2. 비동기 메서드는 를 호출 QueueOperation합니다. 이 메서드는 시작 작업을 큐에 두고 를 호출 ProcessQueue 합니다(개체 형식 SourceOp ).
  3. ProcessQueueMFPutWorkItem을 호출합니다.
  4. 작업 큐 스레드는 를 호출합니다 ProcessQueueAsync.
  5. 메서드는 ProcessQueueAsync 및 를 호출 ValidateOperation 합니다 DispatchOperation.

다음 코드는 MPEG-1 원본에서 새 작업을 큐에 대기합니다.

template <class OP_TYPE>
HRESULT OpQueue<OP_TYPE>::QueueOperation(OP_TYPE *pOp)
{
    HRESULT hr = S_OK;

    EnterCriticalSection(&m_critsec);

    hr = m_OpQueue.InsertBack(pOp);
    if (SUCCEEDED(hr))
    {
        hr = ProcessQueue();
    }

    LeaveCriticalSection(&m_critsec);
    return hr;
}

다음 코드는 큐를 처리합니다.

template <class OP_TYPE>
HRESULT OpQueue<OP_TYPE>::ProcessQueue()
{
    HRESULT hr = S_OK;
    if (m_OpQueue.GetCount() > 0)
    {
        hr = MFPutWorkItem(
            MFASYNC_CALLBACK_QUEUE_STANDARD,    // Use the standard work queue.
            &m_OnProcessQueue,                  // Callback method.
            NULL                                // State object.
            );
    }
    return hr;
}

메서드는 ValidateOperation MPEG-1 원본이 큐에서 다음 작업을 디스패치할 수 있는지 여부를 확인합니다. 다른 작업이 진행 중인 경우 는 ValidateOperationMF_E_NOTACCEPTING 반환합니다. 이렇게 하면 DispatchOperation 보류 중인 다른 작업이 있는 동안 이 호출되지 않습니다.

HRESULT MPEG1Source::ValidateOperation(SourceOp *pOp)
{
    if (m_pCurrentOp != NULL)
    {
        return MF_E_NOTACCEPTING;
    }
    return S_OK;
}

DispatchOperation 메서드는 작업 유형에서 전환됩니다.

//-------------------------------------------------------------------
// DispatchOperation
//
// Performs the asynchronous operation indicated by pOp.
//
// NOTE:
// This method implements the pure-virtual OpQueue::DispatchOperation
// method. It is always called from a work-queue thread.
//-------------------------------------------------------------------

HRESULT MPEG1Source::DispatchOperation(SourceOp *pOp)
{
    EnterCriticalSection(&m_critSec);

    HRESULT hr = S_OK;

    if (m_state == STATE_SHUTDOWN)
    {
        LeaveCriticalSection(&m_critSec);

        return S_OK; // Already shut down, ignore the request.
    }

    switch (pOp->Op())
    {

    // IMFMediaSource methods:

    case SourceOp::OP_START:
        hr = DoStart((StartOp*)pOp);
        break;

    case SourceOp::OP_STOP:
        hr = DoStop(pOp);
        break;

    case SourceOp::OP_PAUSE:
        hr = DoPause(pOp);
        break;

    // Operations requested by the streams:

    case SourceOp::OP_REQUEST_DATA:
        hr = OnStreamRequestSample(pOp);
        break;

    case SourceOp::OP_END_OF_STREAM:
        hr = OnEndOfStream(pOp);
        break;

    default:
        hr = E_UNEXPECTED;
    }

    if (FAILED(hr))
    {
        StreamingError(hr);
    }

    LeaveCriticalSection(&m_critSec);
    return hr;
}

요약:

  1. 파이프라인은 IMFMediaSource::Start와 같은 비동기 메서드를 호출합니다.
  2. 비동기 메서드는 를 호출 OpQueue::QueueOperation하고 개체에 대한 포인터를 SourceOp 전달합니다.
  3. 메서드는 QueueOperationm_OpQueue 큐에 작업을 배치하고 를 호출합니다 OpQueue::ProcessQueue.
  4. 메서드는 ProcessQueueMFPutWorkItem을 호출합니다. 이 시점에서 모든 작업은 Media Foundation 작업 큐 스레드에서 발생합니다. 비동기 메서드는 호출자에게 반환됩니다.
  5. 작업 큐 스레드는 메서드를 호출합니다 OpQueue::ProcessQueueAsync .
  6. 메서드는 ProcessQueueAsync 를 호출 MPEG1Source:ValidateOperation 하여 작업의 유효성을 검사합니다.
  7. 메서드는 ProcessQueueAsync 를 호출 MPEG1Source::DispatchOperation 하여 작업을 처리합니다.

이 디자인에는 다음과 같은 몇 가지 이점이 있습니다.

  • 메서드는 비동기적이므로 호출 애플리케이션의 스레드를 차단하지 않습니다.
  • 작업은 파이프라인 구성 요소 간에 공유되는 Media Foundation 작업 큐 스레드에서 디스패치됩니다. 따라서 미디어 원본은 자체 스레드를 만들지 않으므로 생성된 총 스레드 수가 줄어듭니다.
  • 미디어 원본은 작업이 완료 될 때까지 기다리는 동안 차단되지 않습니다. 이렇게 하면 미디어 원본이 실수로 교착 상태를 일으킬 가능성이 줄어들고 컨텍스트 전환을 줄이는 데 도움이 됩니다.
  • 미디어 원본은 비동기 I/O를 사용하여 원본 파일을 읽을 수 있습니다( IMFByteStream::BeginRead 호출). 미디어 원본은 I/O 루틴이 완료 될 때까지 기다리는 동안 차단할 필요가 없습니다.

SDK 샘플에 표시된 패턴을 따르는 경우 미디어 원본의 특정 세부 정보에 집중할 수 있습니다.

미디어 원본

사용자 지정 미디어 원본 작성