Efectos de vídeo personalizados

En este artículo se describe cómo crear un componente de Windows Runtime que implemente la interfaz IBasicVideoEffect para permitir la creación de efectos personalizados para las secuencias de vídeo. Se pueden usar efectos personalizados con distintas API de Windows Runtime, como MediaCapture, que proporciona acceso a la cámara del dispositivo, y MediaComposition, que permite crear composiciones complejas a partir de clips multimedia.

Agregar un efecto personalizado a la aplicación

Un efecto de vídeo personalizado se define en una clase que implementa la interfaz IBasicVideoEffect. Esta clase no se puede incluir directamente en el proyecto de la aplicación. En su lugar, debes usar un componente de Windows Runtime para hospedar la clase de efecto de vídeo.

Agregar un componente de Windows Runtime para el efecto de vídeo

  1. En Microsoft Visual Studio, con la solución abierta, vaya al menú Archivo y seleccione Agregar nuevo> proyecto.
  2. Selecciona el tipo de proyecto Componente de Windows Runtime (Windows Universal).
  3. En este ejemplo, asigne al proyecto el nombre VideoEffectComponent. Se hará referencia a este nombre en el código más adelante.
  4. Haga clic en Aceptar.
  5. La plantilla de proyecto crea una clase denominada Class1.cs. En el Explorador de soluciones, haz clic con el botón derecho en el icono de Class1.cs y selecciona Cambiar nombre.
  6. Cambie el nombre del archivo a ExampleVideoEffect.cs. Visual Studio mostrará un mensaje que pregunta si quieres actualizar todas las referencias con el nuevo nombre. Haga clic en .
  7. Abra ExampleVideoEffect.cs y actualice la definición de clase para implementar la interfaz IBasicVideoEffect .
public sealed class ExampleVideoEffect : IBasicVideoEffect

Debes incluir los siguientes espacios de nombres en el archivo de clase del efecto para tener acceso a todos los tipos usados en los ejemplos de este artículo.

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

Implementar la interfaz IBasicVideoEffect con el procesamiento de software

El efecto de vídeo debe implementar todos los métodos y propiedades de la interfaz IBasicVideoEffect. En esta sección te orientamos por una implementación sencilla de esta interfaz que usa procesamiento de software.

Método Close

El sistema llamará al método Close en la clase cuando se deba apagar el efecto. Debes usar este método para eliminar todos los recursos que hayas creado. El argumento del método es un MediaEffectClosedReason que le permite saber si el efecto se cerró normalmente, si se produjo un error o si el efecto no admite el formato de codificación necesario.

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

Método DiscardQueuedFrames

El método DiscardQueuedFrames se invoca cuando se debe restablecer el efecto. Un escenario típico se produce si el efecto almacena los fotogramas procesados anteriormente para usarlos en el procesamiento del fotograma actual. Cuando se llama a este método, se debe eliminar el conjunto de fotogramas anteriores guardados. Este método puede usarse para restablecer cualquier estado relacionado con fotogramas anteriores, no solo fotogramas de vídeo acumulados.

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

IsReadOnly, propiedad

La propiedad IsReadOnly permite que el sistema sepa si el efecto se escribe en la salida del efecto. Si la aplicación no modifica los fotogramas de vídeo (por ejemplo, un efecto que solo realiza análisis de fotogramas de vídeo), debes definir esta propiedad como true, lo que hará que el sistema copie eficazmente la entrada de fotograma a la salida de fotograma.

Sugerencia

Cuando la propiedad IsReadOnly se define como true, el sistema copia el fotograma de entrada al fotograma de salida antes de llamar a ProcessFrame. Definir la propiedad IsReadOnly como true no te impide escribir en los fotogramas de salida del efecto en ProcessFrame.

public bool IsReadOnly { get { return false; } }

Método SetEncodingProperties

El sistema llama a SetEncodingProperties en el efecto para comunicarte las propiedades de codificación de la secuencia de vídeo a que se aplica el efecto. Este método también proporciona una referencia al dispositivo Direct3D usado para la representación de hardware. El uso de este dispositivo se muestra en el ejemplo de procesamiento de hardware más adelante en este artículo.

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

Propiedad SupportedEncodingProperties

El sistema comprueba la propiedad SupportedEncodingProperties para determinar qué propiedades de codificación son compatibles con el efecto. Ten en cuenta que, si el consumidor del efecto no puede codificar vídeo mediante las propiedades especificadas, llamará a Close en el efecto y quitará el efecto de la canalización 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>();
    }
}

Nota:

Si se devuelve una lista vacía de objetos VideoEncodingProperties de SupportedEncodingProperties, el sistema tendrá como valor predeterminado la codificación ARGB32.

 

Propiedad SupportedMemoryTypes

El sistema comprueba la propiedad SupportedMemoryTypes para determinar si el efecto tendrá acceso a los fotogramas de vídeo en la memoria de software o hardware (GPU). Si devuelves MediaMemoryTypes.Cpu, se pasarán al efecto fotogramas de entrada y salida que contienen datos de imagen en objetos SoftwareBitmap. Si devuelves MediaMemoryTypes.Gpu, se pasarán al efecto fotogramas de entrada y salida que contienen datos de imagen en objetos IDirect3DSurface.

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

Nota

Si especificas MediaMemoryTypes.GpuAndCpu, el sistema usará la memoria del sistema o de la GPU, la que sea más eficiente para la canalización. Cuando uses este valor, debes comprobar el método ProcessFrame para ver si el valor de SoftwareBitmap o IDirect3DSurface pasado al método contiene datos y luego procesar el fotograma según corresponda.

 

Propiedad TimeIndependent

Gracias a la propiedad TimeIndependent, el sistema puede saber si el efecto no requiere un tiempo uniforme. Si se establece en true, el sistema puede usar optimizaciones que mejoren el rendimiento del efecto.

public bool TimeIndependent { get { return true; } }

Método SetProperties

El método SetProperties permite que la aplicación que está usando el efecto ajuste los parámetros de efecto. Las propiedades se pasan como mapa IPropertySet de nombres de propiedad y valores.

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

En este ejemplo sencillo, se oscurecerán los píxeles de cada fotograma de vídeo según un valor especificado. Se declara una propiedad y TryGetValue se usa para obtener el valor establecido por la aplicación que llama. Si no se ha establecido ningún valor, se usa un valor predeterminado de 0,5.

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

Método ProcessFrame

El método ProcessFrame es donde el efecto modifica los datos de imagen del vídeo. El método se llama una vez por fotograma y se pasa un objeto ProcessVideoFrameContext. Este objeto contiene un objeto de entrada VideoFrame que contiene el fotograma entrante para ser procesado y un resultado VideoFrame en el que se escriben los datos de imagen que se pasarán al resto de la canalización de vídeo. Cada uno de estos objetos VideoFrame tiene una propiedad SoftwareBitmap y una propiedad Direct3DSurface, pero cuál de ellos se debe usar se determina a partir del valor devuelto de la propiedad SupportedMemoryTypes.

Este ejemplo muestra una implementación sencilla del método ProcessFrame mediante el procesamiento de software. Para obtener más información sobre cómo trabajar con objetos SoftwareBitmap, consulta Imágenes. Más adelante se muestra un ejemplo de implementación de ProcessFrame mediante procesamiento de hardware.

El acceso al búfer de datos de SoftwareBitmap requiere interoperabilidad COM, por lo que debes incluir el espacio de nombres System.Runtime.InteropServices en el archivo de clase del efecto.

using System.Runtime.InteropServices;

Agrega el siguiente código dentro del espacio de nombres para que el efecto importe la interfaz para tener acceso al búfer de imagen.

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

Nota

Dado que esta técnica obtiene acceso a un búfer de imagen nativo sin administrar, tendrás que configurar el proyecto para permitir un código no seguro.

  1. En Explorador de soluciones, haga clic con el botón derecho en el proyecto VideoEffectComponent y seleccione Propiedades.
  2. Seleccione la pestaña Compilar .
  3. Active la casilla Permitir código no seguro .

 

Ahora puedes agregar la implementación del método ProcessFrame. Primero, este método obtiene un objeto BitmapBuffer de los mapas de bits de software de entrada y salida. Ten en cuenta que la trama de salida se abre para la escritura y, el de entrada, para la lectura. Luego, se obtiene un valor de IMemoryBufferReference para cada búfer llamando a CreateReference. A continuación, se obtiene el búfer de datos reales convirtiendo los objetos IMemoryBufferReference como la interfaz de interoperabilidad definida anteriormente, IMemoryByteAccess y, a continuación, se llama a GetBuffer.

Ahora que ha se han obtenido los búferes de datos, puedes leer del búfer de entrada y escribir en el búfer de salida. El diseño del búfer se obtiene llamando a GetPlaneDescription, que proporciona información sobre el ancho, intervalo y desplazamiento inicial del búfer. Los bits por píxel se determinan a partir de las propiedades de codificación establecidas previamente con el método SetEncodingProperties. La información de formato de búfer se usa para buscar el índice en el búfer para cada píxel. El valor del píxel en el búfer de origen se copia en el búfer de destino con los valores de color multiplicados por la propiedad FadeValue definida para que este efecto los atenúe según la cantidad especificada.

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 la interfaz IBasicVideoEffect con el procesamiento de hardware

Crear un efecto de vídeo personalizado mediante el procesamiento de hardware (GPU) es casi igual que usar el procesamiento de software como se describió anteriormente. En esta sección se muestran algunas diferencias en un efecto que usa el procesamiento de hardware. En este ejemplo se usa la API Win2D de Windows Runtime. Para obtener más información sobre el uso de Win2D, consulta la documentación de Win2D.

Usa los siguientes pasos para agregar el paquete de NuGet Win2D en el proyecto creado como se describe en la sección Agregar un efecto personalizado a la aplicación al principio de este artículo.

Para agregar el paquete de NuGet Win2D al proyecto de efecto

  1. En el Explorador de soluciones, haz clic con el botón derecho en el proyecto VideoEffectComponent y selecciona Administrar paquetes NuGet.
  2. En la parte superior de la ventana, selecciona la pestaña Examinar.
  3. En el cuadro de búsqueda, escribe Win2D.
  4. Selecciona Win2D.uwpy luego selecciona Instalar en el panel derecho.
  5. En el cuadro de diálogo Revisar cambios se muestra el paquete que se instalará. Haga clic en Aceptar.
  6. Acepta la licencia del paquete.

Además de los espacios de nombres incluidos en la configuración básica del proyecto, debes incluir los siguientes espacios de nombres proporcionados por Win2D.

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

Dado que este efecto usa la memoria de GPU para el funcionamiento de los datos de imagen, debes devolver MediaMemoryTypes.Gpu de la propiedad SupportedMemoryTypes.

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

Establece las propiedades de codificación que admitirá el efecto con la propiedad SupportedEncodingProperties. Al trabajar con Win2D, debes usar la codificación ARGB32.

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

Usa el método SetEncodingProperties para crear un nuevo objeto CanvasDevice de Win2D desde el valor de IDirect3DDevice pasado al método.

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

La implementación de SetProperties es idéntica al ejemplo de procesamiento de software anterior. En este ejemplo se usa una propiedad BlurAmount para configurar un efecto de desenfoque de 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;
    }
}

El último paso es implementar el método ProcessFrame que realmente procesa los datos de imagen.

Mediante las API de Win2D, se crea CanvasBitmap a partir de la propiedad Direct3DSurface del fotograma de entrada. Se crea CanvasRenderTarget a partir del fotograma de salida Direct3DSurface, y CanvasDrawingSession a partir de este destino de representación. Se inicializa un nuevo efecto Win2D GaussianBlurEffect de Win2D mediante la propiedad BlurAmount que expone nuestro efecto a través de SetProperties. Por último, el método CanvasDrawingSession.DrawImage se llama para dibujar el mapa de bits de entrada en el destino de representación con el efecto de desenfoque.

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

    }
}

Agregar el efecto personalizado a tu aplicación

Para usar el efecto de vídeo desde la aplicación, debes agregar una referencia al proyecto de efecto a la aplicación.

  1. En el Explorador de soluciones, en el proyecto de la aplicación, haz clic con el botón derecho en Referencias y selecciona Agregar referencia.
  2. Expande la pestaña Proyectos, selecciona Solución y activa la casilla para el nombre del proyecto de efecto. Para este ejemplo, el nombre es VideoEffectComponent.
  3. Haga clic en Aceptar.

Agregar el efecto personalizado a una secuencia de vídeo de la cámara

Puedes configurar una secuencia simple de vista previa de la cámara siguiendo los pasos descritos en el artículo Acceso fácil a la vista previa de cámara. Sigue estos pasos que te proporcionarán un objeto MediaCapture inicializado que se usa para obtener acceso a la secuencia de vídeo de la cámara.

Para agregar el efecto de vídeo personalizado en una secuencia de cámara, primero crea un nuevo objeto VideoEffectDefinition, pasando el espacio de nombres y el nombre de clase para el efecto. A continuación, llame al método AddVideoEffect del objeto MediaCapture para agregar el efecto a la secuencia especificada. Este ejemplo usa el valor MediaStreamType.VideoPreview para especificar que se debe agregar el efecto a la secuencia de vista previa. Si la aplicación admite la captura de vídeo, también podrías usar MediaStreamType.VideoRecord para agregar el efecto a la secuencia de captura. AddVideoEffect devuelve un objeto IMediaExtension que representa el efecto personalizado. Puedes usar el método SetProperties para establecer la configuración del efecto.

Una vez que se haya agregado el efecto, se llama a StartPreviewAsync para iniciar la secuencia de vista previa.

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

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

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

await mediaCapture.StartPreviewAsync();

Agregar el efecto personalizado a un clip en MediaComposition

Para obtener instrucciones generales sobre la creación de composiciones multimedia a partir de clips de vídeo, consulta Composiciones y edición multimedia. En el siguiente fragmento de código se muestra la creación de una composición de contenido multimedia sencilla que usa un efecto de vídeo personalizado. Un objeto MediaClip se crea llamando a CreateFromFileAsync, pasando un archivo de vídeo que seleccionó el usuario con FileOpenPicker y agregando el vídeo a una nueva clase MediaComposition. A continuación, se crea un nuevo objeto VideoEffectDefinition y se pasa el nombre del espacio de nombres y la clase para el efecto al constructor. Por último, se agrega la definición del efecto a la colección VideoEffectDefinitions del 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);