Como gravar um apresentador para o EVR
O componente descrito nesta página, Renderizador de vídeo avançado, é um recurso herdado. Ele foi substituído pelo SVR (Simple Video Renderer) exposto por meio dos componentes MediaPlayer e IMFMediaEngine. Para reproduzir um conteúdo de vídeo, você deve enviar dados para um desses componentes e permitir que eles criem uma instância do novo renderizador de vídeo. Esses componentes foram otimizados para o Windows 10 e o Windows 11. A Microsoft recomenda fortemente que o novo código use o MediaPlayer ou as APIs do IMFMediaEngine de nível inferior para reproduzir uma mídia de vídeo no Windows em vez do EVR, quando possível. A Microsoft sugere que o código existente que usa as APIs herdadas seja reescrito para usar as novas APIs, se possível.]
Este artigo descreve como gravar um apresentador personalizado para o EVR (renderizador de vídeo avançado). Um apresentador personalizado pode ser usado com o DirectShow e o Media Foundation; as interfaces e o modelo de objeto são os mesmos para as duas tecnologias, embora a sequência exata de operações possa variar.
O código de amostra neste tópico é adaptado do Amostra de EVRPresenter, que é fornecido no SDK do Windows.
Este tópico contém as seguintes seções:
- Pré-requisitos
- Modelo de objeto do apresentador
- Formatos de negociação
- Gerenciar o dispositivo do Direct3D
- Processar saída
- Etapas do quadro
- Definir o apresentador no EVR
- Tópicos relacionados
Pré-requisitos
Para gravar um apresentador personalizado, você deve estar familiarizado com as seguintes tecnologias:
- O renderizador de vídeo avançado. Consulte Renderizador de vídeo avançado.
- Gráficos do Direct3D. Você não precisa entender os gráficos 3D para gravar um apresentador, mas deve saber como criar um dispositivo do Direct3D e gerenciar superfícies do Direct3D. Se você não estiver familiarizado com o Direct3D, leia as seções "Dispositivos do Direct3D" e "Recursos do Direct3D" na documentação do SDK de gráficos do DirectX.
- Gráficos de filtro do DirectShow ou o pipeline do Media Foundation, dependendo da tecnologia que o aplicativo usará para renderizar vídeo.
- Media Foundation Transforms. O mixer EVR é uma transformação do Media Foundation, e o apresentador chama métodos diretamente no mixer.
- Implementação de objetos COM. O apresentador é um objeto COM em processo e livre de threads.
Modelo de objeto do apresentador
Esta seção contém uma visão geral do modelo de objeto e das interfaces do apresentador.
Fluxo de dados dentro do EVR
O EVR usa dois componentes de plug-in para renderizar vídeo: o mixer e o apresentador. O mixer mistura os fluxos de vídeo e desentrelaça o vídeo, se necessário. O apresentador desenha (ou apresenta) o vídeo na tela e agenda quando cada quadro é desenhado. Os aplicativos podem substituir qualquer um desses objetos por uma implementação personalizada.
O EVR tem um ou mais fluxos de entrada, e o mixer tem um número correspondente de fluxos de entrada. O fluxo 0 é sempre o fluxo de referência. Os outros fluxos são subfluxos, dos quais o mixer faz uma combinação alfa no fluxo de referência. O fluxo de referência determina a taxa de quadros mestre para o vídeo composto. Para cada quadro de referência, o mixer usa o quadro mais recente de cada subfluxo, faz a combinação alfa deles no quadro de referência e produz um único quadro composto. O mixer também realiza desentrelaçamento e conversão de cores de YUV para RGB, se necessário. O EVR sempre insere o mixer no pipeline de vídeo, independentemente do número de fluxos de entrada ou do formato de vídeo. O imagem a seguir ilustra esse processo.
O apresentador realiza as seguintes tarefas:
- Define o formato de saída no mixer. Antes do início do streaming, o apresentador define um tipo de mídia no fluxo de saída do mixer. Esse tipo de mídia define o formato da imagem composta.
- Cria o dispositivo do Direct3D.
- Aloca superfícies do Direct3D. O mixer faz a transferência de bits dos quadros compostos nessas superfícies.
- Obtém a saída do mixer.
- Agenda quando os quadros são apresentados. O EVR fornece o relógio de apresentação, e o apresentador agenda quadros de acordo com esse relógio.
- Apresenta cada quadro usando o Direct3D.
- Executa as etapas do quadro e a depuração.
Estados do apresentador
A qualquer momento, o apresentador está em um dos seguintes estados:
- Iniciado. O relógio de apresentação do EVR está funcionando. O apresentador agenda quadros de vídeo para apresentação à medida que eles chegam.
- Pausado. O relógio de apresentação está suspenso. O apresentador não exibe novas amostras, mas mantém sua fila de amostras agendadas. Se novas amostras forem recebidas, o apresentador vai adicioná-las à fila.
- Parado. O relógio de apresentação está interrompido. O apresentador descarta todas as amostras agendadas.
- Desligado. O apresentador libera todos os recursos relacionados ao streaming, como superfícies do Direct3D. Este é o estado inicial do apresentador e o estado final antes que o apresentador seja destruído.
No código de exemplo neste tópico, o estado do apresentador é representado por uma enumeração:
enum RENDER_STATE
{
RENDER_STATE_STARTED = 1,
RENDER_STATE_STOPPED,
RENDER_STATE_PAUSED,
RENDER_STATE_SHUTDOWN, // Initial state.
};
Algumas operações não serão válidas enquanto o apresentador estiver no estado desligado. Para verificar esse estado, o código de exemplo chama um método auxiliar:
HRESULT CheckShutdown() const
{
if (m_RenderState == RENDER_STATE_SHUTDOWN)
{
return MF_E_SHUTDOWN;
}
else
{
return S_OK;
}
}
Interfaces do apresentador
Um apresentador é necessário para implementar as seguintes interfaces:
Interface | Descrição |
---|---|
IMFClockStateSink | Notifica o apresentador quando o relógio do EVR muda de estado. Consulte Implementar IMFClockStateSink. |
IMFGetService | Fornece uma maneira para que o aplicativo e outros componentes no pipeline obtenham interfaces do apresentador. |
IMFTopologyServiceLookupClient | Permite que o apresentador obtenha interfaces do EVR ou do mixer. Consulte Implementar IMFTopologyServiceLookupClient. |
IMFVideoDeviceID | Garante que o apresentador e o mixer usem tecnologias compatíveis. Consulte Implementar IMFVideoDeviceID. |
IMFVideoPresenter | Processa mensagens do EVR. Consulte Implementar IMFVideoPresenter. |
As seguintes interfaces são opcionais:
Interface | Descrição |
---|---|
IEVRTrustedVideoPlugin | Permite que o apresentador trabalhe com mídia protegida. Implemente essa interface se o apresentador for um componente confiável projetado para funcionar no PMP (caminho de mídia protegido). |
IMFRateSupport | Informa o intervalo de taxas de reprodução que o aceita. Consulte Implementar IMFRateSupport. |
IMFVideoPositionMapper | Mapeia coordenadas no quadro de vídeo de saída para coordenadas no quadro de vídeo de entrada. |
IQualProp | Relata informações de desempenho. O EVR usa essas informações para o gerenciamento do controle de qualidade. Essa interface está documentada no SDK do DirectShow. |
Você também pode fornecer interfaces para que o aplicativo se comunique com o apresentador. O apresentador padrão implementa a interface IMFVideoDisplayControl para essa finalidade. Você pode implementar essa interface ou definir a sua própria. O aplicativo obtém interfaces do apresentador chamando IMFGetService::GetService no EVR. Quando o GUID do serviço é MR_VIDEO_RENDER_SERVICE, o EVR passa a solicitação GetService para o apresentador.
Implementar IMFVideoDeviceID
A interface IMFVideoDeviceID contém um método, GetDeviceID, que retorna um GUID de dispositivo. O GUID de dispositivo garante que o apresentador e o mixer usem tecnologias compatíveis. Se os GUIDs de dispositivo não corresponderem, o EVR não será inicializado.
O mixer e o apresentador padrão usam Direct3D 9, com o GUID de dispositivo igual a IID_IDirect3DDevice9. Se você pretende usar seu apresentador personalizado com o mixer padrão, o GUID de dispositivo do apresentador deve ser IID_IDirect3DDevice9. Se você substituir os dois componentes, poderá definir um novo GUID de dispositivo. No restante deste artigo, presume-se que o apresentador usa Direct3D 9. Aqui está a implementação padrão de GetDeviceID:
HRESULT EVRCustomPresenter::GetDeviceID(IID* pDeviceID)
{
if (pDeviceID == NULL)
{
return E_POINTER;
}
*pDeviceID = __uuidof(IDirect3DDevice9);
return S_OK;
}
O método deve ser bem-sucedido mesmo quando o apresentador está desligado.
Implementar IMFTopologyServiceLookupClient
A interface IMFTopologyServiceLookupClient permite que o apresentador obtenha ponteiros de interface do EVR e do mixer da seguinte maneira:
- Quando o EVR inicializa o apresentador, ele chama o método IMFTopologyServiceLookupClient::InitServicePointers do apresentador. O argumento é um ponteiro para a interface IMFTopologyServiceLookup do EVR.
- O apresentador chama IMFTopologyServiceLookup::LookupService para obter ponteiros de interface do EVR ou do mixer.
O método LookupService é semelhante ao método IMFGetService::GetService. Ambos os métodos usam um GUID de serviço e um IID (identificador de interface) como entrada, mas LookupService retorna uma matriz de ponteiros de interface, enquanto GetService retorna um único ponteiro. Na prática, no entanto, você pode definir sempre o tamanho da matriz como 1. O objeto consultado depende do GUID de serviço:
- Se o GUID de serviço for MR_VIDEO_RENDER_SERVICE, o EVR será consultado.
- Se o GUID de serviço for MR_VIDEO_MIXER_SERVICE, o mixer será consultado.
Em sua implementação de InitServicePointers, obtenha as seguintes interfaces do EVR:
Interface do EVR | Descrição |
---|---|
IMediaEventSink | Fornece uma maneira para que o apresentador envie mensagens para o EVR. Essa interface é definida no SDK do DirectShow. Portanto, as mensagens seguem o padrão para eventos do DirectShow, não para eventos do Media Foundation. |
IMFClock | Representa o relógio do EVR. O apresentador usa essa interface para agendar amostras para apresentação. O EVR pode ser executado sem um relógio, portanto, essa interface talvez não esteja disponível. Caso contrário, ignore o código de erro de LookupService. O relógio também implementa a interface IMFTimer. No pipeline do Media Foundation, o relógio implementa a interface IMFPresentationClock. Ele não implementa essa interface no DirectShow. |
Obtenha as seguintes interfaces do mixer:
Interface do mixer | Descrição |
---|---|
IMFTransform | Permite que o apresentador se comunique com o mixer. |
IMFVideoDeviceID | Permite que o apresentador valide o GUID de dispositivo do mixer. |
O seguinte código implementa o método InitServicePointers:
HRESULT EVRCustomPresenter::InitServicePointers(
IMFTopologyServiceLookup *pLookup
)
{
if (pLookup == NULL)
{
return E_POINTER;
}
HRESULT hr = S_OK;
DWORD dwObjectCount = 0;
EnterCriticalSection(&m_ObjectLock);
// Do not allow initializing when playing or paused.
if (IsActive())
{
hr = MF_E_INVALIDREQUEST;
goto done;
}
SafeRelease(&m_pClock);
SafeRelease(&m_pMixer);
SafeRelease(&m_pMediaEventSink);
// Ask for the clock. Optional, because the EVR might not have a clock.
dwObjectCount = 1;
(void)pLookup->LookupService(
MF_SERVICE_LOOKUP_GLOBAL, // Not used.
0, // Reserved.
MR_VIDEO_RENDER_SERVICE, // Service to look up.
IID_PPV_ARGS(&m_pClock), // Interface to retrieve.
&dwObjectCount // Number of elements retrieved.
);
// Ask for the mixer. (Required.)
dwObjectCount = 1;
hr = pLookup->LookupService(
MF_SERVICE_LOOKUP_GLOBAL, 0,
MR_VIDEO_MIXER_SERVICE, IID_PPV_ARGS(&m_pMixer), &dwObjectCount
);
if (FAILED(hr))
{
goto done;
}
// Make sure that we can work with this mixer.
hr = ConfigureMixer(m_pMixer);
if (FAILED(hr))
{
goto done;
}
// Ask for the EVR's event-sink interface. (Required.)
dwObjectCount = 1;
hr = pLookup->LookupService(
MF_SERVICE_LOOKUP_GLOBAL, 0,
MR_VIDEO_RENDER_SERVICE, IID_PPV_ARGS(&m_pMediaEventSink),
&dwObjectCount
);
if (FAILED(hr))
{
goto done;
}
// Successfully initialized. Set the state to "stopped."
m_RenderState = RENDER_STATE_STOPPED;
done:
LeaveCriticalSection(&m_ObjectLock);
return hr;
}
Quando os ponteiros de interface obtidos de LookupService não são mais válidos, o EVR chama IMFTopologyServiceLookupClient::ReleaseServicePointers. Dentro desse método, libere todos os ponteiros de interface e defina o estado do apresentador como desligado:
HRESULT EVRCustomPresenter::ReleaseServicePointers()
{
// Enter the shut-down state.
EnterCriticalSection(&m_ObjectLock);
m_RenderState = RENDER_STATE_SHUTDOWN;
LeaveCriticalSection(&m_ObjectLock);
// Flush any samples that were scheduled.
Flush();
// Clear the media type and release related resources.
SetMediaType(NULL);
// Release all services that were acquired from InitServicePointers.
SafeRelease(&m_pClock);
SafeRelease(&m_pMixer);
SafeRelease(&m_pMediaEventSink);
return S_OK;
}
O EVR chama ReleaseServicePointers por vários motivos, incluindo:
- Desconexão ou reconexão de pinos (DirectShow); ou adição ou remoção de coletores de fluxo (Media Foundation).
- Mudança de formato.
- Definição de um novo relógio.
- Desligamento final do EVR.
Durante a vida útil do apresentador, o EVR pode chamar InitServicePointers e ReleaseServicePointers várias vezes.
Implementar IMFVideoPresenter
A interface IMFVideoPresenter herda IMFClockStateSink e adiciona dois métodos:
Método | Descrição |
---|---|
GetCurrentMediaType | Retorna o tipo de mídia dos quadros de vídeo compostos. |
ProcessMessage | Sinaliza para que o apresentador realize várias ações. |
O método GetCurrentMediaType retorna o tipo de mídia do apresentador. (Para obter detalhes sobre como definir o tipo de mídia, consulte Formatos de negociação.) O tipo de mídia é retornado como um ponteiro de interface IMFVideoMediaType. O exemplo a seguir pressupõe que o apresentador armazena o tipo de mídia como um ponteiro IMFMediaType. Para obter a interface IMFVideoMediaType do tipo de mídia, chame QueryInterface:
HRESULT EVRCustomPresenter::GetCurrentMediaType(
IMFVideoMediaType** ppMediaType
)
{
HRESULT hr = S_OK;
if (ppMediaType == NULL)
{
return E_POINTER;
}
*ppMediaType = NULL;
EnterCriticalSection(&m_ObjectLock);
hr = CheckShutdown();
if (FAILED(hr))
{
goto done;
}
if (m_pMediaType == NULL)
{
hr = MF_E_NOT_INITIALIZED;
goto done;
}
hr = m_pMediaType->QueryInterface(IID_PPV_ARGS(ppMediaType));
done:
LeaveCriticalSection(&m_ObjectLock);
return hr;
}
O método ProcessMessage é o mecanismo principal para o EVR se comunicar com o apresentador. As mensagens a seguir estão definidas. Os detalhes da implementação de cada mensagem são fornecidos no restante deste tópico.
Mensagem | Descrição |
---|---|
MFVP_MESSAGE_INVALIDATEMEDIATYPE | O tipo de mídia de saída do mixer é inválido. O apresentador deve negociar um novo tipo de mídia com o mixer. Consulte Formatos de negociação. |
MFVP_MESSAGE_BEGINSTREAMING | O streaming começou. Nenhuma ação específica é exigida por essa mensagem, mas você pode usá-la para alocar recursos. |
MFVP_MESSAGE_ENDSTREAMING | O streaming terminou. Libere todos os recursos alocados em resposta à mensagem MFVP_MESSAGE_BEGINSTREAMING. |
MFVP_MESSAGE_PROCESSINPUTNOTIFY | O mixer recebeu uma nova amostra de entrada e pode gerar um novo quadro de saída. O apresentador deve chamar IMFTransform::ProcessOutput no mixer. Consulte Processar saída. |
MFVP_MESSAGE_ENDOFSTREAM | A apresentação terminou. Consulte Fim do fluxo. |
MFVP_MESSAGE_FLUSH | O EVR está liberando os dados em seu pipeline de renderização. O apresentador deve descartar todos os quadros de vídeo agendados para apresentação. |
MFVP_MESSAGE_STEP | Solicita que o apresentador avance N quadros. O apresentador deve descartar os próximos quadros N-1 e exibir o quadro N. Consulte Etapas do quadro. |
MFVP_MESSAGE_CANCELSTEP | Cancela as etapas do quadro. |
Implementar IMFClockStateSink
O apresentador deve implementar a interface IMFClockStateSink como parte de sua implementação de IMFVideoPresenter, que herda IMFClockStateSink. O EVR usa essa interface para notificar o apresentador sempre que o relógio do EVR muda de estado. Para obter mais informações sobre os estados do relógio, consulte Relógio de apresentação.
Aqui estão algumas diretrizes para implementar os métodos nessa interface. Todos os métodos devem falhar caso o apresentador esteja desligado.
Método | Descrição |
---|---|
OnClockStart |
|
OnClockStop |
|
OnClockPause | Defina o estado do apresentador como pausado. |
OnClockRestart | Trate o mesmo que OnClockStart,, mas não libere a fila de amostras. |
OnClockSetRate |
|
Implementar IMFRateSupport
Para oferecer suporte a taxas de reprodução diferentes de velocidade 1×, o apresentador deve implementar a interface IMFRateSupport. Aqui estão algumas diretrizes para implementar os métodos nessa interface. Todos os métodos devem falhar depois que o apresentador é desligado. Para obter mais informações sobre essa interface, consulte Controle de taxa.
Valor | Descrição |
---|---|
GetSlowestRate | Retorne zero para indicar que não há taxa mínima de reprodução. |
GetFastestRate | Para reprodução não reduzida, a taxa de reprodução não deve exceder a taxa de atualização do monitor: taxa máxima = taxa de atualização (Hz) / taxa de quadros de vídeo (fps). A taxa de quadros do vídeo é especificada no tipo de mídia do apresentador. Para reprodução reduzida, a taxa de reprodução é ilimitada; retorne o valor FLT_MAX. Na prática, a origem e o decodificador serão os fatores limitadores durante a reprodução reduzida. Para reprodução reversa, retorne o negativo da taxa máxima. |
IsRateSupported | Retorne MF_E_UNSUPPORTED_RATE se o valor absoluto de flRate exceder a taxa máxima de reprodução do apresentador. Calcule a taxa máxima de reprodução conforme descrito para GetFastestRate. |
O seguinte exemplo mostra como implementar o método GetFastestRate:
float EVRCustomPresenter::GetMaxRate(BOOL bThin)
{
// Non-thinned:
// If we have a valid frame rate and a monitor refresh rate, the maximum
// playback rate is equal to the refresh rate. Otherwise, the maximum rate
// is unbounded (FLT_MAX).
// Thinned: The maximum rate is unbounded.
float fMaxRate = FLT_MAX;
MFRatio fps = { 0, 0 };
UINT MonitorRateHz = 0;
if (!bThin && (m_pMediaType != NULL))
{
GetFrameRate(m_pMediaType, &fps);
MonitorRateHz = m_pD3DPresentEngine->RefreshRate();
if (fps.Denominator && fps.Numerator && MonitorRateHz)
{
// Max Rate = Refresh Rate / Frame Rate
fMaxRate = (float)MulDiv(
MonitorRateHz, fps.Denominator, fps.Numerator);
}
}
return fMaxRate;
}
O exemplo anterior chama um método auxiliar, GetMaxRate, para calcular a taxa máxima de reprodução direta:
O seguinte exemplo mostra como implementar o método IsRateSupported:
HRESULT EVRCustomPresenter::IsRateSupported(
BOOL bThin,
float fRate,
float *pfNearestSupportedRate
)
{
EnterCriticalSection(&m_ObjectLock);
float fMaxRate = 0.0f;
float fNearestRate = fRate; // If we support fRate, that is the nearest.
HRESULT hr = CheckShutdown();
if (FAILED(hr))
{
goto done;
}
// Find the maximum forward rate.
// Note: We have no minimum rate (that is, we support anything down to 0).
fMaxRate = GetMaxRate(bThin);
if (fabsf(fRate) > fMaxRate)
{
// The (absolute) requested rate exceeds the maximum rate.
hr = MF_E_UNSUPPORTED_RATE;
// The nearest supported rate is fMaxRate.
fNearestRate = fMaxRate;
if (fRate < 0)
{
// Negative for reverse playback.
fNearestRate = -fNearestRate;
}
}
// Return the nearest supported rate.
if (pfNearestSupportedRate != NULL)
{
*pfNearestSupportedRate = fNearestRate;
}
done:
LeaveCriticalSection(&m_ObjectLock);
return hr;
}
Enviar eventos para o EVR
O apresentador deve notificar o EVR de vários eventos. Para fazer isso, ele usa a interface IMediaEventSink do EVR, obtida quando o EVR chama o método IMFTopologyServiceLookupClient::InitServicePointers do apresentador. (A interface IMediaEventSink é originalmente uma interface do DirectShow, mas é usada tanto no EVR do DirectShow quanto no Media Foundation.) O seguinte código mostra como enviar um evento para o EVR:
// NotifyEvent: Send an event to the EVR through its IMediaEventSink interface.
void NotifyEvent(long EventCode, LONG_PTR Param1, LONG_PTR Param2)
{
if (m_pMediaEventSink)
{
m_pMediaEventSink->Notify(EventCode, Param1, Param2);
}
}
A tabela a seguir lista os eventos que o apresentador envia, juntamente com os parâmetros de evento.
Evento | Descrição |
---|---|
EC_COMPLETE | O apresentador terminou de renderizar todos os quadros após a mensagem MFVP_MESSAGE_ENDOFSTREAM.
|
EC_DISPLAY_CHANGED | O dispositivo do Direct3D foi alterado.
|
EC_ERRORABORT | Ocorreu um erro que requer a interrupção do streaming.
|
EC_PROCESSING_LATENCY | Especifica a quantidade de tempo que o apresentador está levando para renderizar cada quadro. (Opcional).
|
EC_SAMPLE_LATENCY | Especifica o tempo de atraso atual em amostras de renderização. Se o valor é positivo, as amostras estão atrasadas. Se o valor é negativo, as amostras estão adiantadas. (Opcional).
|
EC_SCRUB_TIME | Enviado imediatamente após EC_STEP_COMPLETE se a taxa de reprodução for zero. Esse evento contém o carimbo de data/hora do quadro exibido.
|
EC_STEP_COMPLETE | O apresentador concluiu ou cancelou uma etapa do quadro. - Param1: não utilizado. - Param2: não utilizado. Para obter mais informações, consulte Etapas do quadro. Observação: uma versão anterior da documentação descreveu o Param1 incorretamente. Esse parâmetro não é usado para esse evento. |
Formatos de negociação
Sempre que o apresentador receber uma mensagem MFVP_MESSAGE_INVALIDATEMEDIATYPE do EVR, ele deverá definir o formato de saída no mixer, da seguinte maneira:
Chame IMFTransform::GetOutputAvailableType no mixer para obter um possível tipo de saída. Esse tipo descreve um formato que o mixer pode produzir, dados os fluxos de entrada e os recursos de processamento de vídeo do dispositivo gráfico.
Verifique se o apresentador pode usar esse tipo de mídia como seu formato de renderização. Aqui estão alguns itens a serem verificados, embora a implementação possa ter seus próprios requisitos:
- O vídeo deve estar descompactado.
- O vídeo deve ter apenas quadros progressivos. Verifique se o atributo MF_MT_INTERLACE_MODE é igual a MFVideoInterlace_Progressive.
- O formato deve ser compatível com o dispositivo do Direct3D.
Se o tipo não for aceitável, volte para a etapa 1 e obtenha o próximo tipo proposto do mixer.
Crie um novo tipo de mídia que seja um clone do tipo original e altere os seguintes atributos:
- Defina o atributo MF_MT_FRAME_SIZE igual à largura e altura desejadas para as superfícies do Direct3D que você alocará.
- Defina o atributo MF_MT_PAN_SCAN_ENABLED como FALSE.
- Defina o atributo MF_MT_PIXEL_ASPECT_RATIO igual ao PAR da exibição (normalmente 1:1).
- Defina a abertura geométrica (atributo MF_MT_GEOMETRIC_APERTURE) igual a um retângulo na superfície do Direct3D. Quando o mixer gera um quadro de saída, ele faz a transferência de bits da imagem de origem nesse retângulo. A abertura geométrica pode ser tão grande quanto a superfície, ou pode ser um subretângulo na superfície. Para obter mais informações, consulte Retângulos de origem e destino.
Para testar se o mixer aceitará o tipo de saída modificado, chame IMFTransform::SetOutputType com o sinalizador MFT_SET_TYPE_TEST_ONLY. Se o mixer rejeitar o tipo, volte para a etapa 1 e obtenha o próximo tipo.
Aloque um pool de superfícies do Direct3D, conforme descrito em Alocar superfícies do Direct3D. O mixer usará essas superfícies quando desenhar os quadros de vídeo compostos.
Para definir o tipo de saída no mixer, chame SetOutputType sem sinalizadores. Se a primeira chamada para SetOutputType teve êxito na etapa 4, o método deve ser bem-sucedido novamente.
Se o mixer ficar sem tipos, o método GetOutputAvailableType retornará MF_E_NO_MORE_TYPES. Se o apresentador não conseguir encontrar um tipo de saída adequado para o mixer, o fluxo não poderá ser renderizado. Nesse caso, o DirectShow ou o Media Foundation pode tentar outro formato de fluxo. Portanto, o apresentador pode receber várias mensagens MFVP_MESSAGE_INVALIDATEMEDIATYPE em uma linha até que um tipo válido seja encontrado.
O mixer coloca o vídeo no formato letterbox automaticamente, levando em conta a PAR (taxa de proporção de pixels) da origem e do destino. Para obter melhores resultados, a largura e a altura da superfície e a abertura geométrica devem ser iguais ao tamanho real que você deseja que o vídeo apareça na tela. O imagem a seguir ilustra esse processo.
O código a seguir mostra a estrutura de tópicos do processo. Algumas das etapas são colocadas em funções auxiliares, cujos detalhes exatos dependerão dos requisitos do seu apresentador.
HRESULT EVRCustomPresenter::RenegotiateMediaType()
{
HRESULT hr = S_OK;
BOOL bFoundMediaType = FALSE;
IMFMediaType *pMixerType = NULL;
IMFMediaType *pOptimalType = NULL;
IMFVideoMediaType *pVideoType = NULL;
if (!m_pMixer)
{
return MF_E_INVALIDREQUEST;
}
// Loop through all of the mixer's proposed output types.
DWORD iTypeIndex = 0;
while (!bFoundMediaType && (hr != MF_E_NO_MORE_TYPES))
{
SafeRelease(&pMixerType);
SafeRelease(&pOptimalType);
// Step 1. Get the next media type supported by mixer.
hr = m_pMixer->GetOutputAvailableType(0, iTypeIndex++, &pMixerType);
if (FAILED(hr))
{
break;
}
// From now on, if anything in this loop fails, try the next type,
// until we succeed or the mixer runs out of types.
// Step 2. Check if we support this media type.
if (SUCCEEDED(hr))
{
// Note: None of the modifications that we make later in CreateOptimalVideoType
// will affect the suitability of the type, at least for us. (Possibly for the mixer.)
hr = IsMediaTypeSupported(pMixerType);
}
// Step 3. Adjust the mixer's type to match our requirements.
if (SUCCEEDED(hr))
{
hr = CreateOptimalVideoType(pMixerType, &pOptimalType);
}
// Step 4. Check if the mixer will accept this media type.
if (SUCCEEDED(hr))
{
hr = m_pMixer->SetOutputType(0, pOptimalType, MFT_SET_TYPE_TEST_ONLY);
}
// Step 5. Try to set the media type on ourselves.
if (SUCCEEDED(hr))
{
hr = SetMediaType(pOptimalType);
}
// Step 6. Set output media type on mixer.
if (SUCCEEDED(hr))
{
hr = m_pMixer->SetOutputType(0, pOptimalType, 0);
assert(SUCCEEDED(hr)); // This should succeed unless the MFT lied in the previous call.
// If something went wrong, clear the media type.
if (FAILED(hr))
{
SetMediaType(NULL);
}
}
if (SUCCEEDED(hr))
{
bFoundMediaType = TRUE;
}
}
SafeRelease(&pMixerType);
SafeRelease(&pOptimalType);
SafeRelease(&pVideoType);
return hr;
}
Para obter mais informações sobre tipos de mídia de vídeo, consulte Tipos de mídia de vídeo.
Gerenciar o dispositivo do Direct3D
O apresentador cria o dispositivo do Direct3D e identifica qualquer perda de dispositivo durante o streaming. O apresentador também hospeda o gerenciador de dispositivos do Direct3D, que fornece uma maneira para que outros componentes usem o mesmo dispositivo. Por exemplo, o mixer usa o dispositivo do Direct3D para misturar subfluxos, desentrelaçar e executar ajustes de cor. Os decodificadores podem usar o dispositivo do Direct3D para decodificação acelerada por vídeo. (Para obter mais informações sobre aceleração de vídeo, consulte Aceleração de vídeo do DirectX 2.0.)
Para configurar o dispositivo do Direct3D, execute as seguintes etapas:
- Crie o objeto do Direct3D chamando Direct3DCreate9 ou Direct3DCreate9Ex.
- Crie o dispositivo chamando IDirect3D9::CreateDevice ou IDirect3D9Ex::CreateDevice.
- Crie o gerenciador de dispositivos chamando DXVA2CreateDirect3DDeviceManager9.
- Defina o dispositivo no gerenciador de dispositivos chamando IDirect3DDeviceManager9::ResetDevice.
Se outro componente de pipeline precisar do gerenciador de dispositivos, ele chamará IMFGetService::GetService no EVR, especificando MR_VIDEO_ACCELERATION_SERVICE para o GUID do serviço. O EVR passa a solicitação para o apresentador. Depois que o objeto obtém o ponteiro IDirect3DDeviceManager9, ele pode obter um identificador para o dispositivo chamando IDirect3DDeviceManager9::OpenDeviceHandle. Quando o objeto precisa usar o dispositivo, ele passa o identificador do dispositivo para o método IDirect3DDeviceManager9::LockDevice, que retorna um IDirect3DDevice9.
Depois que o dispositivo for criado, se o apresentador destruir o dispositivo e criar um novo, o apresentador deverá chamar ResetDevice mais uma vez. O método ResetDevice invalida quaisquer identificadores de dispositivos existentes, o que faz com que LockDevice retorne DXVA2_E_NEW_VIDEO_DEVICE. Esse código de erro sinaliza para outros objetos usando o dispositivo que eles devem abrir um novo identificador de dispositivo. Para obter mais informações sobre como usar o gerenciador de dispositivos, consulte Gerenciador de dispositivos do Direct3D.
O apresentador pode criar o dispositivo no modo de janela ou no modo exclusivo de tela inteira. No modo de janela, você deve fornecer uma maneira para o aplicativo especificar a janela de vídeo. O apresentador padrão implementa o método IMFVideoDisplayControl::SetVideoWindow para essa finalidade. Crie o dispositivo quando o apresentador for criado pela primeira vez. Normalmente, você não saberá todos os parâmetros do dispositivo nesse momento, como a janela ou o formato de buffer de fundo. É possível criar um dispositivo temporário e substituí-lo posteriormente. Apenas lembre-se de chamar ResetDevice no gerenciador de dispositivos.
Se você criar um novo dispositivo ou chamar IDirect3DDevice9::Reset ou IDirect3DDevice9Ex::ResetEx em um dispositivo existente, envie um evento EC_DISPLAY_CHANGED para o EVR. Esse evento notifica o EVR para renegociar o tipo de mídia. O EVR ignora os parâmetros desse evento.
Alocar superfícies do Direct3D
Depois que o apresentador define o tipo de mídia, ele pode alocar as superfícies do Direct3D, que o mixer usará para gravar os quadros de vídeo. A superfície deve corresponder ao tipo de mídia do apresentador:
- O formato de superfície deve corresponder ao subtipo de mídia. Por exemplo, se o subtipo for MFVideoFormat_RGB24, o formato de superfície deverá ser D3DFMT_X8R8G8B8. Para obter mais informações sobre subtipos e formatos do Direct3D, consulte GUIDs de subtipo de vídeo.
- A largura e a altura da superfície devem corresponder às dimensões fornecidas no atributo MF_MT_FRAME_SIZE do tipo de mídia.
A maneira recomendada de alocar superfícies depende se o apresentador executa na janela ou na tela inteira.
Se o dispositivo do Direct3D estiver na janela, você poderá criar várias cadeias de troca, cada uma com um único buffer de fundo. Usando essa abordagem, é possível apresentar cada superfície de forma independente, porque a apresentação de uma cadeia de troca não vai interferir com as outras cadeias de troca. O mixer pode gravar dados em uma superfície enquanto outra superfície está agendada para apresentação.
Primeiro, decida quantas cadeias de troca você vai criar. Um mínimo de três é recomendado. Para cada cadeia de troca, faça o seguinte:
- Chame IDirect3DDevice9::CreateAdditionalSwapChain para criar a cadeia de troca.
- Chame IDirect3DSwapChain9::GetBackBuffer para obter um ponteiro para a superfície do buffer de fundo da cadeia de troca.
- Chame MFCreateVideoSampleFromSurface e passe um ponteiro para a superfície. Essa função retorna um ponteiro para um objeto de amostra de vídeo. O objeto de amostra de vídeo implementa a interface IMFSample, e o apresentador usa essa interface para entregar a superfície ao mixer quando o apresentador chama o método IMFTransform::ProcessOutput do mixer. Para obter mais informações sobre o objeto de amostra de vídeo, consulte Amostras de vídeo.
- Armazene o ponteiro IMFSample em uma fila. O apresentador vai extrair amostras dessa fila durante o processamento, conforme descrito em Processar saída.
- Mantenha uma referência ao ponteiro IDirect3DSwapChain9 para que a cadeia de troca não seja liberada.
No modo exclusivo de tela inteira, o dispositivo não pode ter mais de uma cadeia de troca. Essa cadeia de troca é criada implicitamente quando você cria o dispositivo de tela inteira. A cadeia de troca pode ter mais de um buffer de fundo. Infelizmente, no entanto, caso você apresente um buffer de fundo enquanto grava em outro buffer de fundo na mesma cadeia de troca, não há uma maneira fácil de coordenar as duas operações. Isso ocorre devido à maneira como o Direct3D implementa a inversão de superfície. Quando você chama Present, o driver gráfico atualiza os ponteiros de superfície na memória gráfica. Se você estiver retendo ponteiros IDirect3DSurface9 quando chamar Present, eles apontarão para buffers diferentes após o retorno da chamada de Present.
A opção mais simples é criar uma amostra de vídeo para a cadeia de troca. Se você escolher essa opção, siga as mesmas etapas fornecidas para o modo de janela. A única diferença é que a fila de amostra contém uma única amostra de vídeo. Outra opção é criar superfícies fora da tela e, depois, enviá-las para o buffer de fundo. As superfícies criadas devem oferecer suporte ao método IDirectXVideoProcessor::VideoProcessBlt, que o mixer usa para compor os quadros de saída.
Amostras de rastreamento
Quando o apresentador aloca as amostras de vídeo pela primeira vez, ele as coloca em uma fila. O apresentador usa essa fila sempre que precisa obter um novo quadro do mixer. Depois que o mixer produz o quadro, o apresentador move a amostra para uma segunda fila. A segunda fila é para amostras que estão aguardando seus horários de apresentação agendados.
Para facilitar o controle do status de cada amostra, o objeto de amostra de vídeo implementa a interface IMFTrackedSample. Você pode usar essa interface da seguinte maneira:
Implemente a interface IMFAsyncCallback no apresentador.
Antes de colocar uma amostra na fila agendada, consulte o objeto de amostra de vídeo para a interface IMFTrackedSample.
Chame IMFTrackedSample::SetAllocator com um ponteiro para sua interface de retorno de chamada.
Quando a amostra estiver pronta para apresentação, remova-a da fila agendada, apresente-a e libere todas as referências à amostra.
O exemplo invoca o retorno de chamada. (O objeto de amostra não é excluído nesse caso porque mantém uma contagem de referência até que o retorno de chamada seja invocado.)
Dentro do retorno de chamada, retorne a amostra para a fila disponível.
Um apresentador não é obrigado a usar IMFTrackedSample para rastrear amostras; você pode implementar qualquer técnica que funcione melhor para o seu design. Uma vantagem de IMFTrackedSample é que você pode mover as funções de agendamento e renderização do apresentador para objetos auxiliares, e esses objetos não precisam de nenhum mecanismo especial para chamar de volta ao apresentador quando liberam amostras de vídeo, porque o objeto de amostra fornece esse mecanismo.
O seguinte código mostra como definir o retorno de chamada:
HRESULT EVRCustomPresenter::TrackSample(IMFSample *pSample)
{
IMFTrackedSample *pTracked = NULL;
HRESULT hr = pSample->QueryInterface(IID_PPV_ARGS(&pTracked));
if (SUCCEEDED(hr))
{
hr = pTracked->SetAllocator(&m_SampleFreeCB, NULL);
}
SafeRelease(&pTracked);
return hr;
}
No retorno de chamada, chame IMFAsyncResult::GetObject no objeto de resultado assíncrono para recuperar um ponteiro para a amostra:
HRESULT EVRCustomPresenter::OnSampleFree(IMFAsyncResult *pResult)
{
IUnknown *pObject = NULL;
IMFSample *pSample = NULL;
IUnknown *pUnk = NULL;
// Get the sample from the async result object.
HRESULT hr = pResult->GetObject(&pObject);
if (FAILED(hr))
{
goto done;
}
hr = pObject->QueryInterface(IID_PPV_ARGS(&pSample));
if (FAILED(hr))
{
goto done;
}
// If this sample was submitted for a frame-step, the frame step operation
// is complete.
if (m_FrameStep.state == FRAMESTEP_SCHEDULED)
{
// Query the sample for IUnknown and compare it to our cached value.
hr = pSample->QueryInterface(IID_PPV_ARGS(&pUnk));
if (FAILED(hr))
{
goto done;
}
if (m_FrameStep.pSampleNoRef == (DWORD_PTR)pUnk)
{
// Notify the EVR.
hr = CompleteFrameStep(pSample);
if (FAILED(hr))
{
goto done;
}
}
// Note: Although pObject is also an IUnknown pointer, it is not
// guaranteed to be the exact pointer value returned through
// QueryInterface. Therefore, the second QueryInterface call is
// required.
}
/*** Begin lock ***/
EnterCriticalSection(&m_ObjectLock);
UINT32 token = MFGetAttributeUINT32(
pSample, MFSamplePresenter_SampleCounter, (UINT32)-1);
if (token == m_TokenCounter)
{
// Return the sample to the sample pool.
hr = m_SamplePool.ReturnSample(pSample);
if (SUCCEEDED(hr))
{
// A free sample is available. Process more data if possible.
(void)ProcessOutputLoop();
}
}
LeaveCriticalSection(&m_ObjectLock);
/*** End lock ***/
done:
if (FAILED(hr))
{
NotifyEvent(EC_ERRORABORT, hr, 0);
}
SafeRelease(&pObject);
SafeRelease(&pSample);
SafeRelease(&pUnk);
return hr;
}
Processar saída
Sempre que o mixer recebe uma nova amostra de entrada, o EVR envia uma mensagem MFVP_MESSAGE_PROCESSINPUTNOTIFY para o apresentador. Essa mensagem indica que o mixer pode ter um novo quadro de vídeo para entregar. Em resposta, o apresentador chama IMFTransform::ProcessOutput no mixer. Se o método for bem-sucedido, o apresentador agendará a amostra para apresentação.
Para obter a saída do misturador, execute as seguintes etapas:
Verifique o estado do relógio. Se o relógio estiver pausado, ignore a mensagem MFVP_MESSAGE_PROCESSINPUTNOTIFY, a menos que ele seja o primeiro quadro de vídeo. Se o relógio estiver em execução ou se for o primeiro quadro de vídeo, continue.
Obtenha uma amostra da fila de amostras disponíveis. Caso a fila esteja vazia, isso significa que todas as amostras alocadas estão atualmente agendadas para apresentação. Nesse caso, ignore a mensagem MFVP_MESSAGE_PROCESSINPUTNOTIFY neste momento. Quando a próxima amostra estiver disponível, repita as etapas listadas aqui.
Opcional. Se o relógio estiver disponível, obtenha a hora atual do relógio (T1) chamando IMFClock::GetCorrelatedTime.
Chame IMFTransform::ProcessOutput no mixer. Se ProcessOutput for bem-sucedido, a amostra conterá um quadro de vídeo. Se o método falhar, verifique o código de retorno. Os seguintes códigos de erro de ProcessOutput não são falhas críticas:
Código do erro Descrição MF_E_TRANSFORM_NEED_MORE_INPUT O mixer precisa de mais entrada para produzir um novo quadro de saída.
Se você receber esse código de erro, verifique se o EVR atingiu o final do fluxo e responda de acordo, conforme descrito em Fim do fluxo. Caso contrário, ignore essa mensagem MF_E_TRANSFORM_NEED_MORE_INPUT. O EVR enviará outro quando o mixer receber mais entrada.MF_E_TRANSFORM_STREAM_CHANGE O tipo de saída do mixer tornou-se inválido, possivelmente devido a um upstream de mudança de formato.
Se você receber esse código de erro, defina o tipo de mídia do apresentador como NULL. O EVR solicitará um novo formato.MF_E_TRANSFORM_TYPE_NOT_SET O mixer requer um novo tipo de mídia.
Se você receber esse código de erro, renegocie o tipo de saída do mixer, conforme descrito em Formatos de negociação.Se ProcessOutput for bem-sucedido, continue.
Opcional. Se o relógio estiver disponível, obtenha a hora atual do relógio (T2). A quantidade de latência introduzida pelo mixer é (T2 - T1). Envie um evento EC_PROCESSING_LATENCY com esse valor para o EVR. O EVR usa esse valor para controle de qualidade. Caso não exista relógio disponível, não há razão para enviar o evento EC_PROCESSING_LATENCY.
Opcional. Consulte a amostra para IMFTrackedSample e chame IMFTrackedSample::SetAllocator, conforme descrito em Amostras de rastreamento.
Agende a amostra da apresentação.
Essa sequência de etapas pode terminar antes que o apresentador obtenha qualquer saída do mixer. Para garantir que nenhuma solicitação seja descartada, repita as mesmas etapas quando ocorrer o seguinte:
- O método IMFClockStateSink::OnClockStart ou IMFClockStateSink::OnClockStart do apresentador é chamado. Isso identifica o caso em que o mixer ignora a entrada porque o relógio está pausado (etapa 1).
- O retorno de chamada IMFTrackedSample é invocado. Isso identifica o caso em que o mixer recebe entrada, mas todas as amostras de vídeo do apresentador estão em uso (etapa 2).
Os próximos exemplos de código mostram essas etapas com mais detalhes. O apresentador chama o método ProcessInputNotify
(que aparece no exemplo a seguir) quando recebe a mensagem MFVP_MESSAGE_PROCESSINPUTNOTIFY.
//-----------------------------------------------------------------------------
// ProcessInputNotify
//
// Attempts to get a new output sample from the mixer.
//
// This method is called when the EVR sends an MFVP_MESSAGE_PROCESSINPUTNOTIFY
// message, which indicates that the mixer has a new input sample.
//
// Note: If there are multiple input streams, the mixer might not deliver an
// output sample for every input sample.
//-----------------------------------------------------------------------------
HRESULT EVRCustomPresenter::ProcessInputNotify()
{
HRESULT hr = S_OK;
// Set the flag that says the mixer has a new sample.
m_bSampleNotify = TRUE;
if (m_pMediaType == NULL)
{
// We don't have a valid media type yet.
hr = MF_E_TRANSFORM_TYPE_NOT_SET;
}
else
{
// Try to process an output sample.
ProcessOutputLoop();
}
return hr;
}
Esse método ProcessInputNotify
define um sinalizador booliano para registrar o fato de que o mixer tem nova entrada. Em seguida, ele chama o método ProcessOutputLoop
, que aparece no próximo exemplo. Esse método tenta extrair o maior número possível de amostras do mixer:
void EVRCustomPresenter::ProcessOutputLoop()
{
HRESULT hr = S_OK;
// Process as many samples as possible.
while (hr == S_OK)
{
// If the mixer doesn't have a new input sample, break from the loop.
if (!m_bSampleNotify)
{
hr = MF_E_TRANSFORM_NEED_MORE_INPUT;
break;
}
// Try to process a sample.
hr = ProcessOutput();
// NOTE: ProcessOutput can return S_FALSE to indicate it did not
// process a sample. If so, break out of the loop.
}
if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT)
{
// The mixer has run out of input data. Check for end-of-stream.
CheckEndOfStream();
}
}
O método ProcessOutput
, que aparece no próximo exemplo, tenta obter um único quadro de vídeo do mixer. Se nenhum quadro de vídeo estiver disponível, ProcessSample
retornará S_FALSE ou um código de erro, o que interrompe o loop em ProcessOutputLoop
. A maior parte do trabalho é feito dentro do método ProcessOutput
:
//-----------------------------------------------------------------------------
// ProcessOutput
//
// Attempts to get a new output sample from the mixer.
//
// Called in two situations:
// (1) ProcessOutputLoop, if the mixer has a new input sample.
// (2) Repainting the last frame.
//-----------------------------------------------------------------------------
HRESULT EVRCustomPresenter::ProcessOutput()
{
assert(m_bSampleNotify || m_bRepaint); // See note above.
HRESULT hr = S_OK;
DWORD dwStatus = 0;
LONGLONG mixerStartTime = 0, mixerEndTime = 0;
MFTIME systemTime = 0;
BOOL bRepaint = m_bRepaint; // Temporarily store this state flag.
MFT_OUTPUT_DATA_BUFFER dataBuffer;
ZeroMemory(&dataBuffer, sizeof(dataBuffer));
IMFSample *pSample = NULL;
// If the clock is not running, we present the first sample,
// and then don't present any more until the clock starts.
if ((m_RenderState != RENDER_STATE_STARTED) && // Not running.
!m_bRepaint && // Not a repaint request.
m_bPrerolled // At least one sample has been presented.
)
{
return S_FALSE;
}
// Make sure we have a pointer to the mixer.
if (m_pMixer == NULL)
{
return MF_E_INVALIDREQUEST;
}
// Try to get a free sample from the video sample pool.
hr = m_SamplePool.GetSample(&pSample);
if (hr == MF_E_SAMPLEALLOCATOR_EMPTY)
{
// No free samples. Try again when a sample is released.
return S_FALSE;
}
else if (FAILED(hr))
{
return hr;
}
// From now on, we have a valid video sample pointer, where the mixer will
// write the video data.
assert(pSample != NULL);
// (If the following assertion fires, it means we are not managing the sample pool correctly.)
assert(MFGetAttributeUINT32(pSample, MFSamplePresenter_SampleCounter, (UINT32)-1) == m_TokenCounter);
if (m_bRepaint)
{
// Repaint request. Ask the mixer for the most recent sample.
SetDesiredSampleTime(
pSample,
m_scheduler.LastSampleTime(),
m_scheduler.FrameDuration()
);
m_bRepaint = FALSE; // OK to clear this flag now.
}
else
{
// Not a repaint request. Clear the desired sample time; the mixer will
// give us the next frame in the stream.
ClearDesiredSampleTime(pSample);
if (m_pClock)
{
// Latency: Record the starting time for ProcessOutput.
(void)m_pClock->GetCorrelatedTime(0, &mixerStartTime, &systemTime);
}
}
// Now we are ready to get an output sample from the mixer.
dataBuffer.dwStreamID = 0;
dataBuffer.pSample = pSample;
dataBuffer.dwStatus = 0;
hr = m_pMixer->ProcessOutput(0, 1, &dataBuffer, &dwStatus);
if (FAILED(hr))
{
// Return the sample to the pool.
HRESULT hr2 = m_SamplePool.ReturnSample(pSample);
if (FAILED(hr2))
{
hr = hr2;
goto done;
}
// Handle some known error codes from ProcessOutput.
if (hr == MF_E_TRANSFORM_TYPE_NOT_SET)
{
// The mixer's format is not set. Negotiate a new format.
hr = RenegotiateMediaType();
}
else if (hr == MF_E_TRANSFORM_STREAM_CHANGE)
{
// There was a dynamic media type change. Clear our media type.
SetMediaType(NULL);
}
else if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT)
{
// The mixer needs more input.
// We have to wait for the mixer to get more input.
m_bSampleNotify = FALSE;
}
}
else
{
// We got an output sample from the mixer.
if (m_pClock && !bRepaint)
{
// Latency: Record the ending time for the ProcessOutput operation,
// and notify the EVR of the latency.
(void)m_pClock->GetCorrelatedTime(0, &mixerEndTime, &systemTime);
LONGLONG latencyTime = mixerEndTime - mixerStartTime;
NotifyEvent(EC_PROCESSING_LATENCY, (LONG_PTR)&latencyTime, 0);
}
// Set up notification for when the sample is released.
hr = TrackSample(pSample);
if (FAILED(hr))
{
goto done;
}
// Schedule the sample.
if ((m_FrameStep.state == FRAMESTEP_NONE) || bRepaint)
{
hr = DeliverSample(pSample, bRepaint);
if (FAILED(hr))
{
goto done;
}
}
else
{
// We are frame-stepping (and this is not a repaint request).
hr = DeliverFrameStepSample(pSample);
if (FAILED(hr))
{
goto done;
}
}
m_bPrerolled = TRUE; // We have presented at least one sample now.
}
done:
SafeRelease(&pSample);
// Important: Release any events returned from the ProcessOutput method.
SafeRelease(&dataBuffer.pEvents);
return hr;
}
Algumas observações sobre este exemplo:
- Presume-se que a variável m_SamplePool seja um objeto de coleção que retém a fila de amostras de vídeo disponíveis. O método
GetSample
do objeto retornará MF_E_SAMPLEALLOCATOR_EMPTY se a fila estiver vazia. - Se o método ProcessOutput do mixer retorna MF_E_TRANSFORM_NEED_MORE_INPUT, isso significa que o mixer não pode produzir mais saída, portanto, o apresentador limpa o sinalizador m_fSampleNotify.
- O método
TrackSample
, que define o retorno de chamada de IMFTrackedSample, aparece na seção Amostras de rastreamento.
Redesenhar quadros
Ocasionalmente, o apresentador pode precisar redesenhar o quadro de vídeo mais recente. Por exemplo, o apresentador padrão redesenha o quadro nas seguintes situações:
- No modo de janela, em resposta a mensagens WM_PAINT recebidas pela janela do aplicativo. Consulte IMFVideoDisplayControl::RepaintVideo.
- Se o retângulo de destino for alterado quando o aplicativo chamar IMFVideoDisplayControl::SetVideoPosition.
Use as seguintes etapas para solicitar que o mixer recrie o quadro mais recente:
- Obtenha uma amostra de vídeo da fila.
- Consulte a amostra para a interface IMFDesiredSample.
- Chame IMFDesiredSample::SetDesiredSampleTimeAndDuration. Especifique o carimbo de data/hora do quadro de vídeo mais recente. (Você precisará armazenar esse valor em cache e atualizá-lo para cada quadro.)
- Chame ProcessOutput no mixer.
Ao redesenhar um quadro, você pode ignorar o relógio de apresentação e apresentá-lo imediatamente.
Amostras de agendamento
Os quadros de vídeo podem alcançar o EVR a qualquer momento. O apresentador é responsável por apresentar cada quadro no horário correto, de acordo com o carimbo de data/hora do quadro. Quando o apresentador obtém uma nova amostra do mixer, ele coloca a amostra na fila agendada. Em um thread separado, o apresentador obtém continuamente a primeira amostra do início da fila e determina se deve:
- Apresentar a amostra.
- Manter a amostra na fila porque é cedo.
- Descartar a amostra porque está atrasada. Embora você deva evitar a perda de quadros, se possível, talvez isso seja necessário se o apresentador ficar para trás continuamente.
Para obter o carimbo de data/hora de um quadro de vídeo, chame IMFSample::GetSampleTime na amostra de vídeo. O carimbo de data/hora é relativo ao relógio de apresentação do EVR. Para obter a hora atual do relógio, chame IMFClock::GetCorrelatedTime. Se o EVR não tiver um relógio de apresentação ou se uma amostra não tiver um carimbo de data/hora, você poderá apresentar a amostra imediatamente após obtê-la.
Para obter a duração de cada amostra, chame IMFSample::GetSampleDuration. Se a amostra não tiver uma duração, você poderá usar a função MFFrameRateToAverageTimePerFrame para calcular a duração da taxa de quadros.
Ao agendar amostras, lembre-se do seguinte:
- Se a taxa de reprodução for mais rápida ou mais lenta do que a velocidade normal, o relógio funcionará a uma taxa mais rápida ou mais lenta. Isso significa que o carimbo de data/hora em uma amostra sempre fornece a hora de destino correta em relação ao relógio de apresentação. No entanto, se você converter os horários de apresentação em algum outro horário de relógio (por exemplo, o contador de desempenho de alta resolução), deverá dimensionar os horários de acordo com a velocidade do relógio. Se a velocidade do relógio mudar, o EVR chamará o método IMFClockStateSink::OnClockSetRate do apresentador.
- A taxa de reprodução pode ser negativa para reprodução reversa. Quando a taxa de reprodução é negativa, o relógio da apresentação é atrasado. Em outras palavras, o horário N + 1 ocorre antes do horário N.
O seguinte exemplo calcula o quanto uma amostra está adiantada ou atrasada em relação ao relógio de apresentação:
LONGLONG hnsPresentationTime = 0;
LONGLONG hnsTimeNow = 0;
MFTIME hnsSystemTime = 0;
BOOL bPresentNow = TRUE;
LONG lNextSleep = 0;
if (m_pClock)
{
// Get the sample's time stamp. It is valid for a sample to
// have no time stamp.
hr = pSample->GetSampleTime(&hnsPresentationTime);
// Get the clock time. (But if the sample does not have a time stamp,
// we don't need the clock time.)
if (SUCCEEDED(hr))
{
hr = m_pClock->GetCorrelatedTime(0, &hnsTimeNow, &hnsSystemTime);
}
// Calculate the time until the sample's presentation time.
// A negative value means the sample is late.
LONGLONG hnsDelta = hnsPresentationTime - hnsTimeNow;
if (m_fRate < 0)
{
// For reverse playback, the clock runs backward. Therefore, the
// delta is reversed.
hnsDelta = - hnsDelta;
}
Geralmente, o relógio da apresentação é acionado pelo relógio do sistema ou pelo renderizador de áudio. (O renderizador de áudio deriva o tempo da taxa na qual a placa de som consome áudio.) Em geral, o relógio da apresentação não é sincronizado com a taxa de atualização do monitor.
Se os parâmetros de apresentação do Direct3D especificarem D3DPRESENT_INTERVAL_DEFAULT ou D3DPRESENT_INTERVAL_ONE para o intervalo da apresentação, a operação Present aguardará o retraço vertical do monitor. Essa é uma maneira fácil de evitar rupturas, mas reduz a precisão do seu algoritmo de agendamento. Por outro lado, se o intervalo da apresentação for D3DPRESENT_INTERVAL_IMMEDIATE, o método Present será executado imediatamente, o que causará rupturas, a menos que seu algoritmo de agendamento seja preciso o suficiente para que você chame Present somente durante o período de retraço vertical.
As seguintes funções podem ajudar você a obter informações precisas sobre o tempo:
- IDirect3DDevice9::GetRasterStatus retorna informações sobre a varredura, incluindo a linha de verificação atual e se a varredura está no período em branco vertical.
- DwmGetCompositionTimingInfo retorna informações de tempo para o gerenciador de janelas da área de trabalho. Essas informações serão úteis se a composição da área de trabalho estiver habilitada.
Amostras de apresentação
Nesta seção, pressupõe-se que você criou uma cadeia de troca separada para cada superfície, conforme descrito em Alocar superfícies do Direct3D. Para apresentar uma amostra, obtenha a cadeia de troca da amostra de vídeo da seguinte maneira:
- Chame IMFSample::GetBufferByIndex na amostra de vídeo para obter o buffer.
- Consulte o buffer da interface IMFGetService.
- Chame IMFGetService::GetService para obter a interface IDirect3DSurface9 da superfície do Direct3D. (É possível combinar esta etapa e a etapa anterior em uma chamando MFGetService.)
- Chame IDirect3DSurface9::GetContainer na superfície para obter um ponteiro para a cadeia de troca.
- Chame IDirect3DSwapChain9::Present na cadeia de troca.
O código a seguir mostra essas etapas:
HRESULT D3DPresentEngine::PresentSample(IMFSample* pSample, LONGLONG llTarget)
{
HRESULT hr = S_OK;
IMFMediaBuffer* pBuffer = NULL;
IDirect3DSurface9* pSurface = NULL;
IDirect3DSwapChain9* pSwapChain = NULL;
if (pSample)
{
// Get the buffer from the sample.
hr = pSample->GetBufferByIndex(0, &pBuffer);
if (FAILED(hr))
{
goto done;
}
// Get the surface from the buffer.
hr = MFGetService(pBuffer, MR_BUFFER_SERVICE, IID_PPV_ARGS(&pSurface));
if (FAILED(hr))
{
goto done;
}
}
else if (m_pSurfaceRepaint)
{
// Redraw from the last surface.
pSurface = m_pSurfaceRepaint;
pSurface->AddRef();
}
if (pSurface)
{
// Get the swap chain from the surface.
hr = pSurface->GetContainer(IID_PPV_ARGS(&pSwapChain));
if (FAILED(hr))
{
goto done;
}
// Present the swap chain.
hr = PresentSwapChain(pSwapChain, pSurface);
if (FAILED(hr))
{
goto done;
}
// Store this pointer in case we need to repaint the surface.
CopyComPointer(m_pSurfaceRepaint, pSurface);
}
else
{
// No surface. All we can do is paint a black rectangle.
PaintFrameWithGDI();
}
done:
SafeRelease(&pSwapChain);
SafeRelease(&pSurface);
SafeRelease(&pBuffer);
if (FAILED(hr))
{
if (hr == D3DERR_DEVICELOST || hr == D3DERR_DEVICENOTRESET || hr == D3DERR_DEVICEHUNG)
{
// We failed because the device was lost. Fill the destination rectangle.
PaintFrameWithGDI();
// Ignore. We need to reset or re-create the device, but this method
// is probably being called from the scheduler thread, which is not the
// same thread that created the device. The Reset(Ex) method must be
// called from the thread that created the device.
// The presenter will detect the state when it calls CheckDeviceState()
// on the next sample.
hr = S_OK;
}
}
return hr;
}
Retângulos de origem e destino
O retângulo de origem é a parte do quadro de vídeo a ser exibida. Ele é definido em relação a um sistema de coordenadas normalizado, no qual todo o quadro de vídeo ocupa um retângulo com coordenadas {0, 0, 1, 1}. O retângulo de destino é a área dentro da superfície de destino onde o quadro de vídeo é desenhado. O apresentador padrão permite que um aplicativo defina esses retângulos chamando IMFVideoDisplayControl::SetVideoPosition.
Há várias opções para aplicar retângulos de origem e destino. A primeira opção é deixar o mixer aplicá-los:
- Defina o retângulo de origem usando o atributo VIDEO_ZOOM_RECT. O mixer aplicará o retângulo de origem quando ele fizer a transferência de bits do vídeo para a superfície de destino. O retângulo de origem padrão do mixer é o quadro inteiro.
- Defina o retângulo de destino como a abertura geométrica no tipo de saída do mixer. Para obter mais informações, consulte Formatos de negociação.
A segunda opção é aplicar os retângulos quando você chama IDirect3DSwapChain9::Present especificando os parâmetros pSourceRect e pDestRect no método Present. É possível combinar essas opções. Por exemplo, você pode definir o retângulo de origem no mixer, mas aplicar o retângulo de destino no método Present.
Se o aplicativo alterar o retângulo de destino ou redimensionar a janela, talvez seja necessário alocar novas superfícies. Em caso afirmativo, tome cuidado ao sincronizar essa operação com seu thread de agendamento. Libere a fila de agendamento e descarte as amostras antigas antes de alocar novas superfícies.
Fim do fluxo
Quando todos os fluxos de entrada no EVR terminam, o EVR envia uma mensagem MFVP_MESSAGE_ENDOFSTREAM para o apresentador. No momento em que você recebe a mensagem, no entanto, pode haver alguns quadros de vídeo restantes a ser processados. Antes de responder à mensagem de fim de fluxo, você deve esvaziar toda a saída do mixer e apresentar todos os quadros restantes. Depois que o último quadro for apresentado, envie um evento EC_COMPLETE para o EVR.
O próximo exemplo mostra um método que envia o evento EC_COMPLETE caso várias condições sejam atendidas. Caso contrário, ele retorna S_OK sem enviar o evento:
HRESULT EVRCustomPresenter::CheckEndOfStream()
{
if (!m_bEndStreaming)
{
// The EVR did not send the MFVP_MESSAGE_ENDOFSTREAM message.
return S_OK;
}
if (m_bSampleNotify)
{
// The mixer still has input.
return S_OK;
}
if (m_SamplePool.AreSamplesPending())
{
// Samples are still scheduled for rendering.
return S_OK;
}
// Everything is complete. Now we can tell the EVR that we are done.
NotifyEvent(EC_COMPLETE, (LONG_PTR)S_OK, 0);
m_bEndStreaming = FALSE;
return S_OK;
}
Esse método verifica os seguintes estados:
- Se a variável m_fSampleNotify é TRUE, isso significa que o mixer tem um ou mais quadros que ainda não foram processados. (Para obter detalhes, consulte Processar saída.)
- A variável m_fEndStreaming é um sinalizador booliano cujo valor inicial é FALSE. O apresentador define o sinalizador como TRUE quando o EVR envia a mensagem MFVP_MESSAGE_ENDOFSTREAM.
- Presume-se que o método
AreSamplesPending
retorne TRUE, desde que um ou mais quadros estejam aguardando na fila agendada.
No método IMFVideoPresenter::ProcessMessage, defina m_fEndStreaming como TRUE e chame CheckEndOfStream
quando o EVR envia a mensagem MFVP_MESSAGE_ENDOFSTREAM:
HRESULT EVRCustomPresenter::ProcessMessage(
MFVP_MESSAGE_TYPE eMessage,
ULONG_PTR ulParam
)
{
HRESULT hr = S_OK;
EnterCriticalSection(&m_ObjectLock);
hr = CheckShutdown();
if (FAILED(hr))
{
goto done;
}
switch (eMessage)
{
// Flush all pending samples.
case MFVP_MESSAGE_FLUSH:
hr = Flush();
break;
// Renegotiate the media type with the mixer.
case MFVP_MESSAGE_INVALIDATEMEDIATYPE:
hr = RenegotiateMediaType();
break;
// The mixer received a new input sample.
case MFVP_MESSAGE_PROCESSINPUTNOTIFY:
hr = ProcessInputNotify();
break;
// Streaming is about to start.
case MFVP_MESSAGE_BEGINSTREAMING:
hr = BeginStreaming();
break;
// Streaming has ended. (The EVR has stopped.)
case MFVP_MESSAGE_ENDSTREAMING:
hr = EndStreaming();
break;
// All input streams have ended.
case MFVP_MESSAGE_ENDOFSTREAM:
// Set the EOS flag.
m_bEndStreaming = TRUE;
// Check if it's time to send the EC_COMPLETE event to the EVR.
hr = CheckEndOfStream();
break;
// Frame-stepping is starting.
case MFVP_MESSAGE_STEP:
hr = PrepareFrameStep(LODWORD(ulParam));
break;
// Cancels frame-stepping.
case MFVP_MESSAGE_CANCELSTEP:
hr = CancelFrameStep();
break;
default:
hr = E_INVALIDARG; // Unknown message. This case should never occur.
break;
}
done:
LeaveCriticalSection(&m_ObjectLock);
return hr;
}
Além disso, chame CheckEndOfStream
se o método IMFTransform::ProcessOutput do mixer retornar MF_E_TRANSFORM_NEED_MORE_INPUT. Esse código de erro indica que o mixer não tem mais amostras de entrada (consulte Processar saída).
Etapas do quadro
O EVR foi projetado para oferecer suporte às etapas do quadro no DirectShow e à depuração no Media Foundation. As etapas do quadro e a depuração são conceitualmente semelhantes. Em ambos os casos, o aplicativo solicita um quadro de vídeo por vez. Internamente, o apresentador usa o mesmo mecanismo para implementar ambos os recursos.
As etapas do quadro no DirectShow funcionam da seguinte maneira:
- O aplicativo chama IVideoFrameStep::Step. O número de etapas é fornecido no parâmetro dwSteps. O EVR envia uma mensagem MFVP_MESSAGE_STEP para o apresentador, onde o parâmetro de mensagem (ulParam) é o número de etapas.
- Se o aplicativo chamar IVideoFrameStep::CancelStep ou alterar o estado do gráfico (em execução, pausado ou interrompido), o EVR enviará uma mensagem MFVP_MESSAGE_CANCELSTEP.
A depuração no Media Foundation funciona da seguinte maneira:
- O aplicativo define a taxa de reprodução como zero chamando IMFRateControl::SetRate.
- Para renderizar um novo quadro, o aplicativo chama IMFMediaSession::Start com a posição desejada. O EVR envia uma mensagem MFVP_MESSAGE_STEP com ulParam igual a 1.
- Para interromper a depuração, o aplicativo define a taxa de reprodução como um valor diferente de zero. O EVR envia a mensagem MFVP_MESSAGE_CANCELSTEP.
Depois de receber a mensagem MFVP_MESSAGE_STEP, o apresentador aguarda a chegada do quadro de destino. Se o número de etapas for N, o apresentador descartará as próximas amostras (N - 1) e apresentará a amostra N. Quando o apresentador conclui a etapa de quadro, ele envia um evento EC_STEP_COMPLETE para o EVR com lParam1 definido como FALSE. Além disso, se a taxa de reprodução for zero, o apresentador enviará um evento EC_SCRUB_TIME. Se o EVR cancelar as etapas do quadro enquanto uma operação de etapa de quadro ainda estiver pendente, o apresentador enviará um evento EC_STEP_COMPLETE com lParam1 definido como TRUE.
O aplicativo pode executar a etapa do quadro ou a depuração várias vezes, para que o apresentador possa receber várias mensagens MFVP_MESSAGE_STEP antes de receber uma mensagem MFVP_MESSAGE_CANCELSTEP. Além disso, o apresentador pode receber a mensagem MFVP_MESSAGE_STEP antes de o relógio ser iniciado ou enquanto o relógio está em execução.
Implementar as etapas do quadro
Esta seção descreve um algoritmo para implementar as etapas do quadro. O algoritmo das etapas do quadro usa as seguintes variáveis:
step_count. Um inteiro sem sinal que especifica o número de etapas na operação de etapas do quadro atual.
step_queue. Uma fila de ponteiros IMFSample.
step_state. A qualquer momento, o apresentador pode estar em um dos seguintes estados em relação à etapa do quadro:
Estadual Descrição NOT_STEPPING Sem executar as etapas do quadro. WAITING O apresentador recebeu a mensagem MFVP_MESSAGE_STEP, mas o relógio não foi iniciado. PENDING O apresentador recebeu a mensagem MFVP_MESSAGE_STEP e o relógio foi iniciado, mas o apresentador está aguardando o recebimento do quadro de destino. SCHEDULED O apresentador recebeu o quadro de destino e o agendou para a apresentação, mas o quadro não foi apresentado. CONCLUÍDO O apresentador mostrou o quadro de destino, enviou o evento EC_STEP_COMPLETE e está aguardando a próxima mensagem MFVP_MESSAGE_STEP ou MFVP_MESSAGE_CANCELSTEP. Esses estados são independentes dos estados do apresentador listados na seção Estados do apresentador.
Os seguintes procedimentos são definidos para o algoritmo de etapas do quadro:
Procedimento PrepareFrameStep
- Incremente step_count.
- Defina step_state como WAITING.
- Se o relógio estiver em execução, chame StartFrameStep.
Procedimento StartFrameStep
- Se step_state for igual a WAITING, defina step_state como PENDING. Para cada amostra em step_queue, chame DeliverFrameStepSample.
- Se step_state for igual a NOT_STEPPING, remova todas as amostras de step_queue e agende-as para a apresentação.
Procedimento CompleteFrameStep
- Defina step_state como COMPLETE.
- Envie o evento EC_STEP_COMPLETE com lParam1 = FALSE.
- Se a velocidade do clock for zero, envie o evento EC_SCRUB_TIME com o horário da amostra.
DeliverFrameStepSample Procedure
- Se a velocidade do clock for zero e horário da amostra + duração da amostra<horário do relógio, descarte a amostra. Sair.
- Se step_state for igual a SCHEDULED ou COMPLETE, adicione a amostra a step_queue. Sair.
- Reduza step_count.
- Se step_count> 0, descarte a amostra. Sair.
- Se step_state for igual a WAITING, adicione a amostra a step_queue. Sair.
- Agende a amostra da apresentação.
- Defina step_state como SCHEDULED.
Procedimento CancelFrameStep
- Defina step_state como NOT_STEPPING
- Redefina step_count como zero.
- Se o valor anterior de step_state era WAITING, PENDING, ou SCHEDULED, envie EC_STEP_COMPLETE com lParam1 = TRUE.
Chame esses procedimentos da seguinte maneira:
Mensagem ou método do apresentador | Procedimento |
---|---|
MFVP_MESSAGE_STEP message | PrepareFrameStep |
MFVP_MESSAGE_STEP message | CancelStep |
IMFClockStateSink::OnClockStart | StartFrameStep |
IMFClockStateSink::OnClockRestart | StartFrameStep |
Retorno de chamada IMFTrackedSample | CompleteFrameStep |
IMFClockStateSink::OnClockStop | CancelFrameStep |
IMFClockStateSink::OnClockSetRate | CancelFrameStep |
O fluxograma a seguir mostra os procedimentos de etapas do quadro.
Definir o apresentador no EVR
Depois de implementar o apresentador, a próxima etapa é configurar o EVR para usá-lo.
Definir o apresentador no DirectShow
Em um aplicativo do DirectShow, defina o apresentador no EVR da seguinte maneira:
- Crie o filtro do EVR chamando CoCreateInstance. O CLSID é CLSID_EnhancedVideoRenderer.
- Adicione o EVR ao gráfico de filtro.
- Crie uma instância do seu apresentador. Seu apresentador pode oferecer suporte à criação de objetos COM padrão por meio de IClassFactory, mas isso não é obrigatório.
- Consulte o filtro do EVR para a interface IMFVideoRenderer.
- Chame IMFVideoRenderer::InitializeRenderer.
Definir o apresentador no Media Foundation
No Media Foundation, há várias opções, dependendo se você cria o coletor de mídia do EVR ou o objeto de ativação do EVR. Para obter mais informações sobre objetos de ativação, consulte Objetos de ativação.
Para o coletor de mídia do EVR, faça o seguinte:
- Chame MFCreateVideoRenderer para criar o coletor de mídia.
- Crie uma instância do seu apresentador.
- Consulte o coletor de mídia do EVR para a interface IMFVideoRenderer.
- Chame IMFVideoRenderer::InitializeRenderer.
Para o objeto de ativação do EVR, faça o seguinte:
Chame MFCreateVideoRendererActivate para criar o objeto de ativação.
Defina um dos seguintes atributos no objeto de ativação:
Atributo Descrição MF_ACTIVATE_CUSTOM_VIDEO_PRESENTER_ACTIVATE Ponteiro para um objeto de ativação do apresentador.
Com esse sinalizador, você deve fornecer um objeto de ativação para seu apresentador. O objeto de ativação deve implementar a interface IMFActivate.MF_ACTIVATE_CUSTOM_VIDEO_PRESENTER_CLSID CLSID do apresentador.
Com esse sinalizador, o apresentador deve oferecer suporte à criação de objetos COM padrão por meio do IClassFactory.Opcionalmente, defina o atributo MF_ACTIVATE_CUSTOM_VIDEO_PRESENTER_FLAGS no objeto de ativação.
Tópicos relacionados