Estudo de Caso: Fonte de Mídia MPEG-1

No Microsoft Media Foundation, o objeto que introduz dados de mídia no pipeline de dados é chamado de fonte de mídia. Este tópico analisa detalhadamente o Exemplo de SDK de Origem de Mídia MPEG-1 .

Pré-requisitos

Antes de ler este tópico, você deve entender os seguintes conceitos do Media Foundation:

Você também deve ter uma compreensão básica da arquitetura do Media Foundation, particularmente a função das fontes de mídia no pipeline. (Para obter mais informações, consulte Fontes de mídia.)

Além disso, talvez você queira ler o tópico Escrevendo uma fonte de mídia personalizada, que fornece uma visão geral mais geral das etapas descritas aqui.

Este tópico não reproduz todo o código do exemplo do SDK, pois o exemplo é bastante grande.

Classes C++ usadas na origem MPEG-1

A origem MPEG-1 de exemplo é implementada com as seguintes classes C++:

  • MPEG1ByteStreamHandler. Implementa o manipulador de fluxo de bytes para a origem da mídia. Dado um fluxo de bytes, o manipulador de fluxo de bytes cria uma instância da origem.
  • MPEG1Source. Implementa a fonte de mídia.
  • MPEG1Stream. Implementa os objetos de fluxo de mídia. A fonte de mídia cria um MPEG1Stream objeto para cada fluxo de áudio ou vídeo no bitstream MPEG-1.
  • Parser. Analisa o bitstream MPEG-1. Na maioria das vezes, os detalhes dessa classe não são relevantes para as APIs do Media Foundation.
  • SourceOp, OpQueue: essas duas classes gerenciam operações assíncronas na fonte de mídia. (Consulte Operações assíncronas).

Outras classes auxiliares diversas são descritas posteriormente no tópico.

Manipulador de Byte-Stream

O manipulador de fluxo de bytes é o objeto que cria a fonte de mídia. O manipulador de fluxo de bytes é criado pelo resolvedor de origem; os aplicativos não interagem diretamente com o manipulador de fluxo de bytes. O resolvedor de origem descobre o manipulador de fluxo de bytes procurando no registro. O manipulador é registrado por extensão de nome de arquivo ou tipo MIME. Para a origem MPEG-1, o manipulador de fluxo de bytes é registrado para a extensão de nome de arquivo ".mpg".

Observação

Se você quiser dar suporte a esquemas de URL personalizados, também poderá escrever um manipulador de esquema. A origem MPEG-1 foi projetada para arquivos locais e o Media Foundation já fornece um manipulador de esquema para URLs "file://".

 

O manipulador de fluxo de bytes implementa a interface IMFByteStreamHandler . Essa interface tem dois métodos mais importantes que devem ser implementados:

Dois outros métodos são opcionais e não são implementados no exemplo do SDK:

  • CancelObjectCreation. Cancela o método BeginCreateObject . Esse método é útil para uma fonte de rede que pode ter uma alta latência na inicialização.
  • GetMaxNumberOfBytesRequiredForResolution. Obtém o número máximo de bytes que o manipulador lerá do fluxo de origem. Implemente esse método se você souber quantos dados o manipulador de fluxo de bytes pode criar a fonte de mídia. Caso contrário, basta retornar E_NOTIMPL.

Aqui está a implementação do método 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;
}

O método executa as seguintes etapas:

  1. Cria uma nova instância do objeto MPEG1Source.
  2. Crie um objeto de resultado assíncrono. Esse objeto é usado posteriormente para invocar o método de retorno de chamada do resolvedor de origem.
  3. Chama MPEG1Source::BeginOpen, um método assíncrono definido na MPEG1Source classe .
  4. Define ppIUnknownCancelCookie como NULL, o que informa ao chamador que CancelObjectCreation não tem suporte.

O MPEG1Source::BeginOpen método faz o trabalho real de ler o fluxo de bytes e inicializar o MPEG1Source objeto. Esse método não faz parte da API pública. Você pode definir qualquer mecanismo entre o manipulador e a fonte de mídia que atenda às suas necessidades. Colocar a maior parte da lógica na fonte de mídia mantém o manipulador de fluxo de bytes relativamente simples.

Resumidamente, BeginOpen faz o seguinte:

  1. Chama IMFByteStream::GetCapabilities para verificar se o fluxo de bytes de origem é legível e viável.
  2. Chama IMFByteStream::BeginRead para iniciar uma solicitação de E/S assíncrona.

O restante da inicialização ocorre de forma assíncrona. A fonte de mídia lê dados suficientes do fluxo para analisar os cabeçalhos de sequência MPEG-1. Em seguida, ele cria um descritor de apresentação, que é o objeto usado para descrever os fluxos de áudio e vídeo no arquivo. (Para obter mais informações, consulte Descritor de apresentação.) Quando a BeginOpen operação for concluída, o manipulador de fluxo de bytes invocará o método de retorno de chamada do resolvedor de origem. Nesse ponto, o resolvedor de origem chama IMFByteStreamHandler::EndCreateObject. O método EndCreateObject retorna o status da operação.

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;
}

Se ocorrer um erro a qualquer momento durante esse processo, o retorno de chamada será invocado com um erro status código.

Descritor de Apresentação

O descritor de apresentação descreve o conteúdo do arquivo MPEG-1, incluindo as seguintes informações:

  • O número dos fluxos.
  • O formato de cada fluxo.
  • Identificadores de fluxo.
  • A seleção status de cada fluxo (selecionado ou desmarcado).

Em termos da arquitetura do Media Foundation, o descritor de apresentação contém um ou mais descritores de fluxo. Cada descritor de fluxo contém um manipulador de tipo de mídia, que é usado para obter ou definir tipos de mídia no fluxo. O Media Foundation fornece implementações de estoque para o descritor de apresentação e o descritor de fluxo; eles são adequados para a maioria das fontes de mídia.

Para criar um descritor de apresentação, execute as seguintes etapas:

  1. Para cada fluxo:
    1. Forneça uma ID de fluxo e uma matriz de possíveis tipos de mídia. Se o fluxo der suporte a mais de um tipo de mídia, solicite a lista de tipos de mídia por preferência, se houver. (Coloque o tipo ideal em primeiro lugar e o tipo menos ideal por último.)
    2. Chame MFCreateStreamDescriptor para criar o descritor de fluxo.
    3. Chame IMFStreamDescriptor::GetMediaTypeHandler no descritor de fluxo recém-criado.
    4. Chame IMFMediaTypeHandler::SetCurrentMediaType para definir o formato padrão para o fluxo. Se houver mais de um tipo de mídia, você geralmente deverá definir o primeiro tipo na lista.
  2. Chame MFCreatePresentationDescriptor e passe a matriz de ponteiros do descritor de fluxo.
  3. Para cada fluxo, chame IMFPresentationDescriptor::SelectStream ou DeselectStream para definir o estado de seleção padrão. Se houver mais de um fluxo do mesmo tipo (áudio ou vídeo), apenas um deverá ser selecionado por padrão.

O MPEG1Source objeto cria o descritor de apresentação em seu InitPresentationDescriptor método:

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;
}

O aplicativo obtém o descritor de apresentação chamando IMFMediaSource::CreatePresentationDescriptor. Esse método cria uma cópia superficial do descritor de apresentação chamando IMFPresentationDescriptor::Clone. (A cópia contém ponteiros para os descritores de fluxo originais.) O aplicativo pode usar o descritor de apresentação para definir o tipo de mídia, selecionar um fluxo ou desmarcar um fluxo.

Opcionalmente, descritores de apresentação e descritores de fluxo podem conter atributos que fornecem informações adicionais sobre a origem. Para obter uma lista desses atributos, consulte os seguintes tópicos:

Um atributo merece menção especiais: o atributo MF_PD_DURATION contém a duração total da origem. Defina esse atributo se você souber a duração antecipadamente; por exemplo, a duração pode ser especificada nos cabeçalhos de arquivo, dependendo do formato do arquivo. O aplicativo pode exibir esse valor ou usá-lo para definir uma barra de progresso ou barra de busca.

Estados de streaming

Uma fonte de mídia define os seguintes estados:

Estado Descrição
Iniciado A origem aceita e processa solicitações de exemplo.
Em Pausa A origem aceita solicitações de exemplo, mas não as processa. As solicitações são enfileiradas até que a origem seja iniciada.
Stopped. A origem rejeita solicitações de exemplo.

 

Iniciar

O método IMFMediaSource::Start inicia a fonte de mídia. Ele usa os seguintes parâmetros:

  • Um descritor de apresentação.
  • Um GUID de formato de tempo.
  • Uma posição inicial.

O aplicativo deve obter o descritor de apresentação chamando CreatePresentationDescriptor na origem. Não há nenhum mecanismo definido para validar um descritor de apresentação. Se o aplicativo especificar o descritor de apresentação incorreto, os resultados serão indefinidos.

O GUID de formato de tempo especifica como interpretar a posição inicial. O formato padrão é unidades de 100 nanossegundos (ns), indicadas por GUID_NULL. Cada fonte de mídia deve dar suporte a unidades de 100 ns. Opcionalmente, uma fonte pode dar suporte a outras unidades de tempo, como o número do quadro ou o código de tempo. No entanto, não há uma maneira padrão de consultar uma fonte de mídia para obter a lista de formatos de tempo compatíveis.

A posição inicial é fornecida como UM PROPVARIANT, permitindo diferentes tipos de dados, dependendo do formato de hora. Para 100-ns, o tipo PROPVARIANT é VT_I8 ou VT_EMPTY. Se VT_I8, o PROPVARIANT conterá a posição inicial em unidades de 100 ns. O valor VT_EMPTY tem o significado especial "iniciar na posição atual".

Implemente o método Start da seguinte maneira:

  1. Validar parâmetros e estado:
    • Verifique se há parâmetros NULL .
    • Verifique o GUID de formato de hora. Se o valor for inválido, retorne MF_E_UNSUPPORTED_TIME_FORMAT.
    • Verifique o tipo de dados do PROPVARIANT que mantém a posição inicial.
    • Valide a posição inicial. Se for inválido, retorne MF_E_INVALIDREQUEST.
    • Se a origem tiver sido desligada, retorne MF_E_SHUTDOWN.
  2. Se nenhum erro ocorrer na etapa 1, enfileira uma operação assíncrona. Tudo após essa etapa ocorre em um thread de fila de trabalho.
  3. Para cada fluxo:
    1. Verifique se o fluxo já está ativo em uma solicitação Inicial anterior.

    2. Chame IMFPresentationDescriptor::GetStreamDescriptorByIndex para marcar se o aplicativo selecionou ou desmarcada o fluxo.

    3. Se um fluxo selecionado anteriormente for desmarcado, libere todos os exemplos não entregues para esse fluxo.

    4. Se o fluxo estiver ativo, a fonte de mídia (não o fluxo) enviará um dos seguintes eventos:

      Para ambos os eventos, os dados do evento são o ponteiro IMFMediaStream para o fluxo.

    5. Se a origem estiver reiniciando do estado em pausa, pode haver solicitações de exemplo pendentes. Nesse caso, entregue-os agora.

    6. Se a origem estiver buscando uma nova posição, cada objeto de fluxo enviará um evento MEStreamSeeked . Caso contrário, cada fluxo enviará um evento MEStreamStarted .

  4. Se a origem estiver buscando uma nova posição, a fonte de mídia enviará um evento MESourceSeeked . Caso contrário, ele enviará um evento MESourceStarted .

Se ocorrer um erro a qualquer momento após a etapa 2, a origem enviará um evento MESourceStarted com um código de erro. Isso alerta o aplicativo de que o método Start falhou de forma assíncrona.

O código a seguir mostra as etapas 1 a 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;
}

As etapas restantes são mostradas no próximo exemplo:

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;
}

Pausar

O método IMFMediaSource::P ause pausa a fonte de mídia. Implemente esse método da seguinte maneira:

  1. Enfileirar uma operação assíncrona.
  2. Cada fluxo ativo envia um evento MEStreamPaused .
  3. A fonte de mídia envia um evento MESourcePaused .

Durante a pausa, a origem enfileira solicitações de exemplo sem processá-las. (Consulte Solicitações de exemplo.)

Stop

O método IMFMediaSource::Stop interrompe a fonte de mídia. Implemente esse método da seguinte maneira:

  1. Enfileirar uma operação assíncrona.
  2. Cada fluxo ativo envia um evento MEStreamStopped .
  3. Limpe todos os exemplos e solicitações de exemplo enfileirados.
  4. A fonte de mídia envia um evento MESourceStopped .

Enquanto está parada, a origem rejeita todas as solicitações de exemplos.

Se a origem for interrompida enquanto uma solicitação de E/S estiver em andamento, a solicitação de E/S poderá ser concluída depois que a origem entrar no estado parado. Nesse caso, a origem deve descartar o resultado dessa solicitação de E/S.

Solicitações de Amostra

O Media Foundation usa um modelo de pull , no qual o pipeline solicita exemplos da fonte de mídia. Isso difere do modelo usado pelo DirectShow, no qual as fontes "efetuam push" de exemplos.

Para solicitar um novo exemplo, o pipeline do Media Foundation chama IMFMediaStream::RequestSample. Esse método usa um ponteiro IUnknown que representa um objeto de token . A implementação do objeto de token cabe ao chamador; ele simplesmente fornece uma maneira de o chamador acompanhar solicitações de exemplo. O parâmetro de token também pode ser NULL.

Supondo que a origem use solicitações de E/S assíncronas para ler dados, a geração de exemplo não será sincronizada com solicitações de exemplo. Para sincronizar solicitações de exemplo com a geração de exemplo, uma fonte de mídia faz o seguinte:

  1. Os tokens de solicitação são colocados em uma fila.
  2. À medida que os exemplos são gerados, eles são colocados em uma segunda fila.
  3. A fonte de mídia conclui uma solicitação de exemplo extraindo um token de solicitação da primeira fila e um exemplo da segunda fila.
  4. A fonte de mídia envia um evento MEMediaSample. O evento contém um ponteiro para o exemplo e o exemplo contém um ponteiro para o token.

O diagrama a seguir mostra a relação entre o evento MEMediaSample , o exemplo e o token de solicitação.

diagrama mostrando memediasample e uma fila de exemplo apontando para imfsample; imfsample e a fila de solicitação apontam para iunknown

A origem MPEG-1 de exemplo implementa esse processo da seguinte maneira:

  1. O método RequestSample coloca a solicitação em uma fila FIFO.
  2. À medida que as solicitações de E/S são concluídas, a fonte de mídia cria novos exemplos e as coloca em uma segunda fila FIFO. (Essa fila tem um tamanho máximo, para impedir que a origem leia muito à frente.)
  3. Sempre que ambas as filas tiverem pelo menos um item (uma solicitação e uma amostra), a fonte de mídia concluirá a primeira solicitação da fila de solicitação enviando o primeiro exemplo da fila de exemplo.
  4. Para entregar um exemplo, o objeto stream (não o objeto de origem) envia um evento MEMediaSample .

Neste ponto, há três possibilidades:

  • Há outro exemplo na fila de exemplo, mas nenhuma solicitação correspondente.
  • Há uma solicitação, mas nenhuma amostra.
  • Ambas as filas estão vazias; não há exemplos nem solicitações.

Se a fila de exemplo estiver vazia, a origem verificará o final do fluxo (consulte Fim do Fluxo). Caso contrário, ele iniciará outra solicitação de E/S para dados. Se ocorrer algum erro durante esse processo, o fluxo enviará um evento MEError .

O código a seguir implementa o método 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;
}

O DispatchSamples método extrai exemplos da fila de exemplo, corresponde a eles com solicitações de exemplo pendentes e enfileira eventos 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;
}

O DispatchSamples método é chamado nas seguintes circunstâncias:

  • Dentro do método RequestSample .
  • Quando a fonte de mídia é reiniciada do estado em pausa.
  • Quando uma solicitação de E/S é concluída.

Fim do Fluxo

Quando um fluxo não tem mais dados e todos os exemplos desse fluxo foram entregues, os objetos de fluxo enviam um evento MEEndOfStream .

Quando todos os fluxos ativos são concluídos, a fonte de mídia envia um evento MEEndOfPresentation .

Operações assíncronas

Talvez a parte mais difícil de escrever uma fonte de mídia seja entender o modelo assíncrono da Media Foundation.

Todos os métodos em uma fonte de mídia que controlam o streaming são assíncronos. Em cada caso, o método faz alguma validação inicial, como verificar parâmetros. Em seguida, a origem envia o restante do trabalho para uma fila de trabalho. Após a conclusão da operação, a fonte de mídia envia um evento de volta ao chamador, por meio da interface IMFMediaEventGenerator da fonte de mídia. Portanto, é importante entender as filas de trabalho.

Para colocar um item em uma fila de trabalho, você pode chamar MFPutWorkItem ou MFPutWorkItemEx. A origem MPEG-1 usa MFPutWorkItem, mas as duas funções fazem a mesma coisa. A função MFPutWorkItem usa os seguintes parâmetros:

  • Um valor DWORD que identifica a fila de trabalho. Você pode criar uma fila de trabalho privada ou usar MFASYNC_CALLBACK_QUEUE_STANDARD.
  • Um ponteiro para a interface IMFAsyncCallback . Essa interface de retorno de chamada é invocada para executar o trabalho.
  • Um objeto de estado opcional, que deve implementar IUnknown.

A fila de trabalho é atendida por um ou mais threads de trabalho que extraem continuamente o próximo item de trabalho da fila e invocam o método IMFAsyncCallback::Invoke da interface de retorno de chamada.

Não há garantia de que os itens de trabalho sejam executados na mesma ordem em que você os colocou na fila. Lembre-se de que mais de um thread pode atender à mesma fila de trabalho, portanto, invocar chamadas pode se sobrepor ou ocorrer fora de ordem. Portanto, cabe à fonte de mídia manter o estado interno correto enviando itens da fila de trabalho na ordem certa. Somente quando a operação anterior for concluída a origem iniciará a próxima operação.

Para representar operações pendentes, a origem MPEG-1 define uma classe chamada 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.
};

A Operation enumeração identifica qual operação está pendente. A classe também contém um PROPVARIANT para transmitir quaisquer dados adicionais para a operação.

Fila de Operações

Para serializar operações, a fonte de mídia mantém uma fila de SourceOp objetos. Ele usa uma classe auxiliar para gerenciar a fila:

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.
};

A OpQueue classe foi projetada para ser herdada pelo componente que executa itens de trabalho assíncronos. O parâmetro de modelo OP_TYPE é o tipo de objeto usado para representar itens de trabalho na fila— nesse caso, OP_TYPE será SourceOp. A OpQueue classe implementa os seguintes métodos:

  • QueueOperation coloca um novo item na fila.
  • ProcessQueue envia a próxima operação da fila. Esse método é assíncrono.
  • ProcessQueueAsync conclui o método assíncrono ProcessQueue .

Outros dois métodos devem ser implementados pela classe derivada:

  • ValidateOperation verifica se é válido executar uma operação especificada, dado o estado atual da fonte de mídia.
  • DispatchOperation executa o item de trabalho assíncrono.

A fila de operações é usada da seguinte maneira:

  1. O pipeline do Media Foundation chama um método assíncrono na fonte de mídia, como IMFMediaSource::Start.
  2. O método assíncrono chama QueueOperation, que coloca a operação Iniciar na fila e chama ProcessQueue (na forma de um SourceOp objeto).
  3. ProcessQueue chama MFPutWorkItem.
  4. O thread da fila de trabalho chama ProcessQueueAsync.
  5. O ProcessQueueAsync método chama ValidateOperation e DispatchOperation.

O código a seguir enfileira uma nova operação na fonte 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;
}

O código a seguir processa a fila:

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;
}

O ValidateOperation método verifica se a origem MPEG-1 pode expedir a próxima operação na fila. Se outra operação estiver em andamento, ValidateOperation retornará MF_E_NOTACCEPTING. Isso garante que DispatchOperation não seja chamado enquanto houver outra operação pendente.

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

O método DispatchOperation alterna o tipo de operação:

//-------------------------------------------------------------------
// 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;
}

Para resumir:

  1. O pipeline chama um método assíncrono, como IMFMediaSource::Start.
  2. O método assíncrono chama OpQueue::QueueOperation, passando um ponteiro para um SourceOp objeto .
  3. O QueueOperation método coloca a operação na fila m_OpQueue e chama OpQueue::ProcessQueue.
  4. O ProcessQueue método chama MFPutWorkItem. A partir desse ponto, tudo acontece em um thread de fila de trabalho do Media Foundation. O método assíncrono retorna ao chamador.
  5. O thread da fila de trabalho chama o OpQueue::ProcessQueueAsync método .
  6. O ProcessQueueAsync método chama MPEG1Source:ValidateOperation para validar a operação.
  7. O ProcessQueueAsync método chama MPEG1Source::DispatchOperation para processar a operação.

Há vários benefícios para esse design:

  • Os métodos são assíncronos, portanto, eles não bloqueiam o thread do aplicativo de chamada.
  • As operações são expedidas em um thread de fila de trabalho do Media Foundation, que é compartilhado entre componentes de pipeline. Portanto, a fonte de mídia não cria seu próprio thread, reduzindo o número total de threads criados.
  • A fonte de mídia não bloqueia enquanto aguarda a conclusão das operações. Isso reduz a chance de uma fonte de mídia causar acidentalmente um deadlock e ajuda a reduzir a alternância de contexto.
  • A fonte de mídia pode usar E/S assíncrona para ler o arquivo de origem (chamando IMFByteStream::BeginRead). A fonte de mídia não precisa ser bloqueada enquanto aguarda a conclusão da rotina de E/S.

Se você seguir o padrão mostrado no exemplo do SDK, poderá se concentrar nos detalhes específicos da fonte de mídia.

Fontes de mídia

Escrevendo uma fonte de mídia personalizada