Efeitos de vídeo personalizados

Este artigo descreve como criar um componente do Windows Runtime que implemente a interface IBasicVideoEffect para criar efeitos personalizados para fluxos de vídeo. Efeitos personalizados podem ser usados com várias APIs do Windows Runtime diferentes, incluindo MediaCapture, que fornece acesso à câmera do dispositivo, e MediaComposition, que permite que você crie composições complexas de clipes de mídia.

Adicionar um efeito personalizado ao seu aplicativo

Um efeito de vídeo personalizado é definido em uma classe que implementa a interface IBasicVideoEffect. Essa classe não pode ser incluída diretamente no projeto do seu aplicativo. Em vez disso, você deve usar um componente do Tempo de Execução do Windows para hospedar sua classe de efeito de vídeo.

Adicionar um componente do Tempo de Execução do Windows para o efeito de vídeo

  1. No Microsoft Visual Studio, com sua solução aberta, vá para o menu Arquivo e selecione Adicionar> Novo Projeto.
  2. Selecione o tipo de projeto Componente do Tempo de Execução do Windows (Windows Universal).
  3. Para este exemplo, chame o projeto de VideoEffectComponent. Esse nome será referenciado no código posteriormente.
  4. Clique em OK.
  5. O modelo de projeto cria uma classe chamada Class1.cs. No Gerenciador de Soluções, clique com botão direito do mouse no ícone de Class1.cs e selecione Renomear.
  6. Renomeie o arquivo para ExampleVideoEffect.cs. O Visual Studio mostrará um aviso perguntando se você deseja atualizar todas as referências para o novo nome. Clique em Sim.
  7. Abra ExampleVideoEffect.cs e atualize a definição de classe para implementar a interface IBasicVideoEffect.
public sealed class ExampleVideoEffect : IBasicVideoEffect

Você precisa incluir os seguintes namespaces no arquivo de classe do efeito para acessar todos os tipos usados nos exemplos deste artigo.

using Windows.Media.Effects;
using Windows.Media.MediaProperties;
using Windows.Foundation.Collections;
using Windows.Graphics.DirectX.Direct3D11;
using Windows.Graphics.Imaging;

Implementar a interface IBasicVideoEffect usando o processamento de software

O efeito de vídeo deve implementar todos os métodos e propriedades da interface IBasicVideoEffect. Esta seção percorre uma implementação simples dessa interface que usa o processamento de software.

Método Close

O sistema chamará o método Close em sua classe quando o efeito for encerrado. Você deve usar esse método para descartar quaisquer recursos que você criou. O argumento para o método é um MediaEffectClosedReason que permite que você saiba se o efeito foi fechado normalmente, se ocorreu um erro ou se o efeito não é compatível com o formato de codificação necessário.

public void Close(MediaEffectClosedReason reason)
{
    // Dispose of effect resources
}

Método DiscardQueuedFrames

O método DiscardQueuedFrames é chamado quando o efeito deve ser redefinido. Um cenário típico para isso é se o efeito armazena quadros processados anteriormente para serem usados no processamento do quadro atual. Quando esse método é chamado, você deve descartar o conjunto de quadros anteriores que foram salvos. Esse método possa ser usado para restaurar qualquer estado relacionado ao quadros anteriores, não apenas quadros de vídeo acumulados.

private int frameCount;
public void DiscardQueuedFrames()
{
    frameCount = 0;
}

Propriedade IsReadOnly

A propriedade IsReadOnly permite que o sistema saiba se o efeito será gravado na saída. Se o seu aplicativo não modifica os quadros de vídeo (por exemplo, um efeito que só executa uma análise dos quadros do vídeo), você deve definir essa propriedade como true, o que fará com que o sistema copie de forma eficiente a entrada do quadro para a saída do quadro.

Dica

Quando a propriedade IsReadOnly é definida como true, o sistema copia o quadro de entrada para o quadro de saída antes que ProcessFrame seja chamada. Definir a propriedade IsReadOnly como true não impede que você grave nos quadros de saída do efeito em ProcessFrame.

public bool IsReadOnly { get { return false; } }

Método SetEncodingProperties

O sistema chama SetEncodingProperties o efeito para permitir que você conheça as propriedades de codificação do fluxo de vídeo no qual o efeito está operando. Esse método também fornece uma referência para o dispositivo Direct3D usado para renderização de hardware. O uso desse dispositivo é mostrado no exemplo de processamento de hardware mais adiante neste artigo.

private VideoEncodingProperties encodingProperties;
public void SetEncodingProperties(VideoEncodingProperties encodingProperties, IDirect3DDevice device)
{
    this.encodingProperties = encodingProperties;
}

Propriedade SupportedEncodingProperties

O sistema verifica a propriedade SupportedEncodingProperties para determinar quais propriedades de codificação são compatíveis com o efeito. Observe que se o consumidor do efeito não puder codificar o vídeo usando as propriedades que você especificar, ele chamará Close no efeito e o removerá do pipeline de vídeo.

public IReadOnlyList<VideoEncodingProperties> SupportedEncodingProperties
{            
    get
    {
        var encodingProperties = new VideoEncodingProperties();
        encodingProperties.Subtype = "ARGB32";
        return new List<VideoEncodingProperties>() { encodingProperties };

        // If the list is empty, the encoding type will be ARGB32.
        // return new List<VideoEncodingProperties>();
    }
}

Observação

Se você retornar uma lista vazia de objetos VideoEncodingProperties de SupportedEncodingProperties, o sistema usará o padrão de codificação ARGB32.

 

Propriedade SupportedMemoryTypes

O sistema verifica a propriedade SupportedMemoryTypes para determinar se o efeito acessará quadros de vídeo na memória do software ou na memória do hardware (GPU). Se você retornar MediaMemoryTypes.Cpu, serão passados para o efeito quadros de entrada e de saída que contêm dados de imagem em objetos SoftwareBitmap. Se você retornar MediaMemoryTypes.Gpu, serão passados para o efeito quadros de entrada e de saída que contêm dados de imagem em objetos IDirect3DSurface.

public MediaMemoryTypes SupportedMemoryTypes { get { return MediaMemoryTypes.Cpu; } }

Observação

Se você especificar MediaMemoryTypes.GpuAndCpu, o sistema usará a memória do sistema ou a GPU, o que for mais eficiente para o pipeline. Ao usar esse valor, você precisa verificar o método ProcessFrame para ver se SoftwareBitmap ou IDirect3DSurface passado para o método contém dados e processar o quadro de acordo.

 

Propriedade TimeIndependent

A propriedade TimeIndependent permite que o sistema saiba se o efeito não requer tempo uniforme. Quando definida como true, o sistema pode usar otimizações que aprimorem o desempenho do efeito.

public bool TimeIndependent { get { return true; } }

Método SetProperties

O método SetProperties permite que o aplicativo que está usando o efeito ajuste os parâmetros do efeito. As propriedades são passadas como um mapa de nomes e valores de propriedade IPropertySet.

private IPropertySet configuration;
public void SetProperties(IPropertySet configuration)
{
    this.configuration = configuration;
}

Este exemplo simples escurecerá os pixels em cada quadro de vídeo de acordo com um valor especificado. Uma propriedade é declarada e TryGetValue é usado para obter o valor definido pelo aplicativo de chamada. Se nenhum valor foi definido, um valor padrão de 0,5 será usado.

public double FadeValue
{
    get
    {
        object val;
        if (configuration != null && configuration.TryGetValue("FadeValue", out val))
        {
            return (double)val;
        }
        return .5;
    }
}

Método ProcessFrame

O método ProcessFrame é onde o efeito modifica os dados da imagem do vídeo. O método é chamado uma vez por quadro e um objeto ProcessVideoFrameContext é passado. Este objeto contém um objeto VideoFrame de entrada que contém o quadro de entrada a ser processado e um objeto VideoFrame de saída no qual você grava os dados da imagem que serão passados para o resto do pipeline do vídeo. Cada um desses objetos VideoFrame possui uma propriedade SoftwareBitmap e uma propriedade Direct3DSurface, mas o valor da propriedade SupportedMemoryTypes determina qual delas pode ser usada.

Este exemplo mostra uma implementação simples do método ProcessFrame usando processamento de software. Para obter informações sobre como trabalhar com objetos SoftwareBitmap, consulte Geração de imagens. Uma implementação de ProcessFrame de exemplo usando o processamento de hardware é mostrada mais adiante neste artigo.

Acessar o buffer de dados de um SoftwareBitmap requer interoperabilidade COM, portanto, você deve incluir o namespace System.Runtime.InteropServices no arquivo de classe do efeito.

using System.Runtime.InteropServices;

Adicione o código a seguir ao namespace para que o efeito importe a interface para acessar o buffer de imagem.

[ComImport]
[Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
unsafe interface IMemoryBufferByteAccess
{
    void GetBuffer(out byte* buffer, out uint capacity);
}

Observação

Como essa técnica acessa um buffer de imagem nativo não gerenciado, você precisará configurar seu projeto para permitir código não seguro.

  1. No Gerenciador de Soluções, clique com o botão direito do mouse no projeto VideoEffectComponent e selecione Propriedades.
  2. Selecione a guia Compilar .
  3. Selecione a caixa Permitir marcar de código não seguro.

 

Agora você pode adicionar a implementação do método ProcessFrame. Primeiro, esse método obtém um objeto BitmapBuffer a partir dos bitmaps de software de entrada e de saída. Observe que o quadro de saída é aberto para gravação e o de entrada para leitura. Em seguida, um IMemoryBufferReference é obtido para cada buffer, chamando-se CreateReference. Em seguida, o buffer de dados real é obtido, transmitindo os objetos IMemoryBufferReference como a interface de interoperabilidade COM definida acima, IMemoryByteAccess, e chamando GetBuffer.

Agora que os buffers de dados foram obtidos, você pode ler o buffer de entrada e gravar no buffer de saída. O layout do buffer é obtido chamando GetPlaneDescription, que fornece informações sobre a largura, a distância e o deslocamento inicial do buffer. Os bits por pixel são determinados pelas propriedades de codificação definidas anteriormente com o método SetEncodingProperties. As informações de formato do buffer são usadas para encontrar o índice de cada pixel no buffer. O valor de pixel do buffer de origem é copiado para o buffer de destino, com os valores de cores sendo multiplicados pela propriedade FadeValue definida para esse efeito escurecê-las pelo valor especificado.

public unsafe void ProcessFrame(ProcessVideoFrameContext context)
{
    using (BitmapBuffer buffer = context.InputFrame.SoftwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))
    using (BitmapBuffer targetBuffer = context.OutputFrame.SoftwareBitmap.LockBuffer(BitmapBufferAccessMode.Write))
    {
        using (var reference = buffer.CreateReference())
        using (var targetReference = targetBuffer.CreateReference())
        {
            byte* dataInBytes;
            uint capacity;
            ((IMemoryBufferByteAccess)reference).GetBuffer(out dataInBytes, out capacity);

            byte* targetDataInBytes;
            uint targetCapacity;
            ((IMemoryBufferByteAccess)targetReference).GetBuffer(out targetDataInBytes, out targetCapacity);

            var fadeValue = FadeValue;

            // Fill-in the BGRA plane
            BitmapPlaneDescription bufferLayout = buffer.GetPlaneDescription(0);
            for (int i = 0; i < bufferLayout.Height; i++)
            {
                for (int j = 0; j < bufferLayout.Width; j++)
                {

                    byte value = (byte)((float)j / bufferLayout.Width * 255);

                    int bytesPerPixel = 4; 
                    if (encodingProperties.Subtype != "ARGB32")
                    {
                        // If you support other encodings, adjust index into the buffer accordingly
                    }
                    

                    int idx = bufferLayout.StartIndex + bufferLayout.Stride * i + bytesPerPixel * j;

                    targetDataInBytes[idx + 0] = (byte)(fadeValue * (float)dataInBytes[idx + 0]);
                    targetDataInBytes[idx + 1] = (byte)(fadeValue * (float)dataInBytes[idx + 1]);
                    targetDataInBytes[idx + 2] = (byte)(fadeValue * (float)dataInBytes[idx + 2]);
                    targetDataInBytes[idx + 3] = dataInBytes[idx + 3];
                }
            }
        }
    }
}

Implementar a interface IBasicVideoEffect usando o processamento de hardware

Criar um efeito de vídeo personalizado usando o processamento de hardware (GPU) é quase idêntico a usar o processamento de software conforme descrito anteriormente. Esta seção mostrará as algumas diferenças em um efeito que usa o processamento de hardware. Este exemplo usa a API do Windows Runtime Win2D. Para obter mais informações sobre como usar Win2D, consulte a Documentação do Win2D.

Use as etapas a seguir para adicionar o pacote NuGet Win2D ao projeto que você criou, conforme descrito na seção Adicionar um efeito personalizado ao seu aplicativo no início deste artigo.

Para adicionar o pacote NuGet Win2D ao seu projeto de efeito

  1. No Gerenciador de Soluções, clique com o botão direito do mouse no projeto VideoEffectComponent e selecione Gerenciar Pacotes NuGet.
  2. Na parte superior da janela, selecione a guia Procurar.
  3. Na caixa de pesquisa, digite Win2D.
  4. Selecione Win2D.uwp e, em seguida, selecione Instalar no painel à direita.
  5. A caixa de diálogo Revisar Alterações mostra o pacote a ser instalado. Clique em OK.
  6. Aceite a licença do pacote.

Além dos namespaces incluídos na instalação do projeto básico, você precisará incluir os namespaces a seguir fornecidos pelo Win2D.

using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas;

Como esse efeito usará a memória GPU para operar nos dados da imagem, você deve retornar MediaMemoryTypes.Gpu da propriedade SupportedMemoryTypes.

public MediaMemoryTypes SupportedMemoryTypes { get { return MediaMemoryTypes.Gpu; } }

Defina as propriedades de codificação com as quais o efeito será compatível com a propriedade SupportedEncodingProperties. Ao trabalhar com Win2D, você deve usar a codificação ARGB32.

public IReadOnlyList<VideoEncodingProperties> SupportedEncodingProperties {
    get
    {
        var encodingProperties = new VideoEncodingProperties();
        encodingProperties.Subtype = "ARGB32";
        return new List<VideoEncodingProperties>() { encodingProperties };
    }
}

Use o método SetEncodingProperties para criar um novo objeto CanvasDevice Win2D do IDirect3DDevice passado para o método.

private CanvasDevice canvasDevice;
public void SetEncodingProperties(VideoEncodingProperties encodingProperties, IDirect3DDevice device)
{
    canvasDevice = CanvasDevice.CreateFromDirect3D11Device(device);
}

A implementação SetProperties é idêntica ao exemplo anterior de processamento de software. Este exemplo usa uma propriedade BlurAmount para configurar um efeito de desfoque Win2D.

private IPropertySet configuration;
public void SetProperties(IPropertySet configuration)
{
    this.configuration = configuration;
}
public double BlurAmount
{
    get
    {
        object val;
        if (configuration != null && configuration.TryGetValue("BlurAmount", out val))
        {
            return (double)val;
        }
        return 3;
    }
}

A última etapa é implementar o método ProcessFrame que realmente processa os dados da imagem.

Usando as APIs Win2D, um CanvasBitmap é criado a partir da propriedade Direct3DSurface do quadro de entrada. Um CanvasRenderTarget é criado a partir do Direct3DSurface do quadro de saída e um CanvasDrawingSession é criado a partir desse destino de renderização. Um novo GaussianBlurEffect Win2D é inicializado, usando a propriedade BlurAmount que o nosso efeito expõe via SetProperties. Por fim, o método CanvasDrawingSession.DrawImage é chamado para desenhar o bitmap de entrada para o destino de renderização usando o efeito de desfoque.

public void ProcessFrame(ProcessVideoFrameContext context)
{

    using (CanvasBitmap inputBitmap = CanvasBitmap.CreateFromDirect3D11Surface(canvasDevice, context.InputFrame.Direct3DSurface))
    using (CanvasRenderTarget renderTarget = CanvasRenderTarget.CreateFromDirect3D11Surface(canvasDevice, context.OutputFrame.Direct3DSurface))
    using (CanvasDrawingSession ds = renderTarget.CreateDrawingSession())
    {


        var gaussianBlurEffect = new GaussianBlurEffect
        {
            Source = inputBitmap,
            BlurAmount = (float)BlurAmount,
            Optimization = EffectOptimization.Speed
        };

        ds.DrawImage(gaussianBlurEffect);

    }
}

Adicionando efeito personalizado ao seu aplicativo

Para usar o efeito de vídeo do seu aplicativo, você deve adicionar uma referência ao projeto do efeito ao seu aplicativo.

  1. No Gerenciador de Soluções, no projeto do seu aplicativo, clique com o botão direito do mouse em Referências e selecione Adicionar referência.
  2. Expanda a guia Projetos, selecione Solução e marque a caixa de seleção com o nome do projeto do efeito. Neste exemplo, o nome é VideoEffectComponent.
  3. Clique em OK.

Adicionar o efeito personalizado a um fluxo de vídeo da câmera

Você pode configurar um fluxo de visualização simples da câmera seguindo as etapas do artigo Acesso de visualização de câmera simples. As etapas a seguir fornecerão um objeto MediaCapture inicializado que é usado para acessar o fluxo de vídeo da câmera.

Para adicionar o efeito de vídeo personalizado ao fluxo de uma câmera, primeiro crie um novo objeto VideoEffectDefinition, passando o namespace e o nome de classe do efeito. Em seguida, do objeto MediaCapture, chame o método AddVideoEffect para adicionar o efeito ao fluxo especificado. Este exemplo usa o valor MediaStreamType.VideoPreview para especificar se esse efeito deve ser adicionado ao fluxo de inicialização. Se o seu aplicativo der suporte à captura de vídeo, você também poderia usar MediaStreamType.VideoRecord para adicionar o efeito ao fluxo de captura. AddVideoEffect retorna um objeto IMediaExtension que representa o efeito personalizado. Você pode usar o método SetProperties para definir a configuração do efeito.

Depois que o efeito tiver sido adicionado, StartPreviewAsync é chamado para iniciar o fluxo de visualização.

var videoEffectDefinition = new VideoEffectDefinition("VideoEffectComponent.ExampleVideoEffect");

IMediaExtension videoEffect =
   await mediaCapture.AddVideoEffectAsync(videoEffectDefinition, MediaStreamType.VideoPreview);

videoEffect.SetProperties(new PropertySet() { { "FadeValue", .25 } });

await mediaCapture.StartPreviewAsync();

Adicionar o efeito personalizado a um clipe em um MediaComposition

Para obter orientações gerais sobre como criar composições de mídia a partir de clipes de vídeo, consulte Composições e edição de mídia. O trecho de código a seguir mostra a criação de uma composição de mídia simples com um efeito de vídeo personalizado. Um objeto MediaClip é criado, chamando-se CreateFromFileAsync, passando um arquivo de vídeo que foi selecionado pelo usuário com um FileOpenPicker e o clipe é adicionado a um novo MediaComposition. Em seguida, um novo objeto VideoEffectDefinition é criado, passando o namespace e nome de classe do efeito para o construtor. Por fim, a definição do efeito é adicionada à coleção VideoEffectDefinitions do objeto MediaClip.

MediaComposition composition = new MediaComposition();
var clip = await MediaClip.CreateFromFileAsync(pickedFile);
composition.Clips.Add(clip);

var videoEffectDefinition = new VideoEffectDefinition("VideoEffectComponent.ExampleVideoEffect", new PropertySet() { { "FadeValue", .5 } });

clip.VideoEffectDefinitions.Add(videoEffectDefinition);