Partilhar via


Estudo de caso: MPEG-1 Media Source

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 examina detalhadamente o exemplo de MPEG-1 Media Source SDK.

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 o papel das fontes de mídia no pipeline. (Para obter mais informações, consulte Fontes de mídia.)

Além disso, você pode querer 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 SDK, porque o exemplo é bastante grande.

Classes C++ usadas na fonte MPEG-1

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

  • MPEG1ByteStreamHandler. Implementa o manipulador de fluxo de bytes para a fonte de 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 objeto MPEG1Stream para cada fluxo de áudio ou vídeo no fluxo de bits MPEG-1.
  • Parser. Analisa o fluxo de bits 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 mais adiante no tópico.

Manipulador 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 quiser oferecer suporte a esquemas de URL personalizados, você também pode escrever um manipulador de esquema . A fonte 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 o IMFByteStreamHandler interface. Esta interface tem dois métodos mais importantes que devem ser implementados:

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

  • CancelarCriaçãoDeObjeto. 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 este método se souber quantos dados o gestor de fluxo de bytes necessita antes que possa criar a fonte de mídia. Caso contrário, simplesmente devolva E_NOTIMPL.

Aqui está a implementação do BeginCreateObject método:

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. Este 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 classe MPEG1Source.
  4. Define ppIUnknownCancelCookie como NULL, o que indica ao chamador que CancelObjectCreation não é suportado.

O método MPEG1Source::BeginOpen faz o trabalho real de ler o fluxo de bytes e inicializar o objeto MPEG1Source. Este 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 dentro da fonte de média mantém o manipulador de fluxo de bytes bastante simples.

Resumidamente, BeginOpen faz o seguinte:

  1. Chama IMFByteStream::GetCapabilities para verificar se o fluxo de bytes de origem é legível e passível de procura.
  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 operação BeginOpen é concluída, o manipulador de fluxo de bytes invoca 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 este processo, o callback será ativado com um código de status de erro.

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 de transmissões.
  • O formato de cada fluxo.
  • Identificadores de fluxo.
  • O status de seleção de cada fluxo (selecionado ou não selecionado).

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. Media Foundation fornece implementações de estoque para o descritor de apresentação e descritor de fluxo; estes 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 um ID de fluxo e uma matriz de possíveis tipos de mídia. Se o fluxo suportar mais de um tipo de mídia, ordene a lista de tipos de mídia por preferência, se houver. (Coloque o tipo ideal em primeiro lugar e o tipo menos ótimo 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 deve 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 deve ser selecionado por padrão.

O objeto MPEG1Source cria o descritor de apresentação em seu método 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;
}

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, os descritores de apresentação e os descritores de fluxo podem conter atributos que fornecem informações adicionais sobre a fonte. Para obter uma lista desses atributos, consulte os seguintes tópicos:

Um atributo merece menção especial: O atributo MF_PD_DURATION contém a duração total da fonte. Defina este atributo se souber a duração antecipadamente; Por exemplo, a duração pode ser especificada nos cabeçalhos do 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
Início A fonte aceita e processa solicitações de exemplo.
Em pausa A fonte aceita solicitações de amostra, mas não as processa. As solicitações são enfileiradas até que a fonte seja iniciada.
Parou. A fonte rejeita solicitações de amostra.

 

Início

O método IMFMediaSource::Start inicia a fonte de mídia. Aceita 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 fonte. Não existe um mecanismo definido para validar um descritor de apresentação. Se o aplicativo especificar o descritor de apresentação errado, os resultados serão indefinidos.

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

A posição inicial é fornecida como um PROPVARIANT, permitindo diferentes tipos de dados dependendo do formato de tempo. Para 100 nanosegundos, o tipo de PROPVARIANT é VT_I8 ou VT_EMPTY. Se VT_I8, o PROPVARIANT contém a posição inicial em unidades de 100 ns. O valor VT_EMPTY tem o significado especial "começar na posição atual".

Implemente o método Start da seguinte maneira:

  1. Validar parâmetros e estado:
    • Verifique os parâmetros NULL entre e.
    • Verifique o GUID do formato de hora. Se o valor for inválido, retorne MF_E_UNSUPPORTED_TIME_FORMAT.
    • Verifique o tipo de dados do PROPVARIANT que contém a posição inicial.
    • Valide a posição inicial. Se não for válido, devolva MF_E_INVALIDREQUEST.
    • Se a fonte tiver sido desligada, retorne MF_E_SHUTDOWN.
  2. Se não ocorrer nenhum erro na etapa 1, enfileire 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 de uma solicitação anterior Iniciar.

    2. Ligue IMFPresentationDescriptor::GetStreamDescriptorByIndex para verificar se a aplicação selecionou ou desselecionou o stream.

    3. Se um fluxo previamente selecionado estiver agora deselecionado, descarte todas as amostras 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 de IMFMediaStream para o fluxo.

    5. Se a origem estiver reiniciando a partir do estado pausado, pode haver solicitações de amostra pendentes. Em caso afirmativo, entregue-os agora.

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

  4. Se a fonte está à procura de uma nova posição, a fonte de mídia envia um evento MESourceSeeked. Caso contrário, ele envia um evento MESourceStarted.

Se ocorrer um erro a qualquer momento após a etapa 2, a fonte 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;
}

Pausa

O método IMFMediaSource::Pause pausa a fonte de mídia. Implemente este método da seguinte forma:

  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.

Quando está em pausa, a origem enfileira solicitações de demonstração sem processá-las. (Ver Solicitações de Amostra.)

Parar

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

  1. Enfileirar uma operação assíncrona.
  2. Cada fluxo ativo envia um evento MEStreamStopped.
  3. Apague todas as amostras e solicitações de amostras enfileiradas.
  4. A fonte de mídia envia um evento MESourceStoped.

Quando está parado, a fonte rejeita todas as solicitações de amostras.

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 fonte entrar no estado parado. Nesse caso, a fonte 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 amostras da fonte de mídia. Isso difere do modelo usado pelo DirectShow, no qual as fontes "empurram" amostras.

Para solicitar uma nova amostra, o pipeline do Media Foundation chama IMFMediaStream::RequestSample. Este método aceita um ponteiro IUnknown que representa um objeto token. A implementação do objeto token cabe ao chamador; ele simplesmente fornece uma maneira para o chamador rastrear solicitações de amostra. O parâmetro 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 amostras não será sincronizada com solicitações de amostra. Para sincronizar solicitações de exemplo com a geração de amostras, uma fonte de mídia faz o seguinte:

  1. Os tokens de solicitação são colocados numa fila.
  2. À medida que as amostras são geradas, elas são colocadas 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 uma amostra 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, a amostra e o token de solicitação.

diagrama que mostra memediasample e uma fila de exemplo apontando para imfsample; imfsample e a fila de solicitações apontam para iunknown

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

  1. O método RequestSample coloca o pedido numa fila FIFO.
  2. À medida que as solicitações de E/S são concluídas, a fonte de mídia cria novas amostras e as coloca em uma segunda fila FIFO. (Esta fila tem um tamanho máximo, para evitar que a fonte 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 a primeira amostra da fila de amostra.
  4. Para entregar uma amostra, o objeto de fluxo (não o objeto de origem) envia um evento MEMediaSample.
    • Os dados do evento são um ponteiro para a interface da amostra IMFSample.
    • Se a solicitação incluía um token, anexe-o ao exemplo definindo o atributo MFSampleExtension_Token no exemplo.

Neste ponto, existem três possibilidades:

  • Há outro exemplo na fila de exemplo, mas nenhuma solicitação correspondente.
  • Há um pedido, mas nenhuma amostra.
  • Ambas as filas estão vazias; Não há amostras nem pedidos.

Se a fila de amostras estiver vazia, a origem verifica o fim do fluxo (consulte Fim do fluxo). Caso contrário, ele inicia outra solicitação de E/S para dados. Se ocorrer algum erro durante este processo, o fluxo envia um evento MEError.

O código a seguir implementa o IMFMediaStream::RequestSample método:

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 método DispatchSamples extrai amostras da fila de amostras, faz a correspondência com solicitações de amostra pendentes e coloca na fila eventos de 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 método DispatchSamples é chamado nas seguintes circunstâncias:

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

Fim do fluxo

Quando um fluxo não tem mais dados e todas as amostras desse fluxo foram entregues, o objeto de fluxo envia um evento MEEndOfStream.

Quando todos os fluxos ativos são concluídos, a fonte multimé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 do 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 a verificação de parâmetros. Em seguida, a origem despacha o restante do trabalho para uma fila de tarefas. Após a conclusão da operação, a fonte de mídia envia um evento de volta para o chamador através da interface IMFMediaEventGenerator da própria fonte de mídia. Por isso, é importante entender as filas de trabalho.

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

  • Um DWORD valor que identifica a fila de trabalhos. Você pode criar uma fila de trabalho privada ou usar MFASYNC_CALLBACK_QUEUE_STANDARD.
  • Um ponteiro para a interface IMFAsyncCallback. A interface de callback é 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 IMFAsyncCallback::Invoke da interface de retorno de chamada.

Não é garantido 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, então as chamadas Invoke podem 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 correta. Somente quando a operação anterior estiver concluída é que a fonte inicia a próxima operação.

Para representar operações pendentes, a fonte 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 enumeração Operation 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 objetos SourceOp. 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 classe OpQueue é 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 — neste caso, OP_TYPE será SourceOp. A classe OpQueue implementa os seguintes métodos:

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

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 Start na fila e chama ProcessQueue (na forma de um objeto SourceOp).
  3. ProcessQueue chama MFPutWorkItem.
  4. O thread da fila de trabalho chama ProcessQueueAsync.
  5. O método ProcessQueueAsync 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 método ValidateOperation verifica se a fonte MPEG-1 pode despachar 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 ativa 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;
}

Resumindo:

  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 objeto SourceOp.
  3. O método QueueOperation coloca a operação na fila de m_OpQueue e chama OpQueue::ProcessQueue.
  4. O método ProcessQueue chama MFPutWorkItem. A partir deste 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 método OpQueue::ProcessQueueAsync.
  6. O método ProcessQueueAsync chama MPEG1Source:ValidateOperation para validar a operação.
  7. O método ProcessQueueAsync chama MPEG1Source::DispatchOperation para processar a operação.

Existem vários benefícios neste design:

  • Os métodos são assíncronos, portanto, não bloqueiam o thread do aplicativo chamador.
  • As operações são despachadas num thread da fila de trabalho do Media Foundation, que é partilhado entre os componentes da pipeline. Portanto, a fonte de mídia não cria seu próprio thread, reduzindo o número total de threads que são 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 impasse e ajuda a reduzir a troca 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 bloquear enquanto aguarda a conclusão da rotina de E/S.

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

Fontes de mídia

Escrevendo uma fonte de mídia personalizada