Fronteras de la UI

Fuentes de distribución de vídeo en Windows Phone 7

Charles Petzold

Descargar el ejemplo de código

Charles PetzoldEl smartphone moderno está repleto de órganos sensitivos electrónicos para obtener información del mundo exterior. Estos son la radio del teléfono celular, Wi-Fi, GPS, una pantalla táctil, detectores de movimiento, entre otros.

Para los programadores de aplicaciones, la utilidad de estos órganos está determinada por las API asociadas a ellos. Si las API son deficientes, las características del hardware se desvaloran y pueden incluso perder toda utilidad.

Desde el punto de vista del programador de aplicaciones, una de estas características ausentes en la versión inicial de Windows Phone era un buen globo ocular. Aunque Windows Phone siempre ha venido con una cámara, la única API disponible en la versión original era CameraCaptureTask. En esencia, esta clase genera un proceso secundario que permite que el usuario saque una foto y luego devuelve esa imagen a la aplicación. Eso es todo. La aplicación no tiene cómo controlar ninguna parte del proceso, ni puede obtener la fuente de distribución de vídeo en vivo que viene de la lente.

Esta deficiencia se corrigió con dos nuevos conjuntos de interfaces de programación.

Un conjunto de API se relaciona con las clases Camera y PhotoCamera. Estas clases permiten que las aplicaciones construyan una UI para sacar fotos, controlen las opciones del flash, realicen vistas previas en vivo de la fuente de distribución de vídeo, respondan al botón del obturador en forma completa y a medio camino; y puedan detectar el foco. Espero poder tratar esta interfaz en un próximo artículo.

Las API que analizaré en esta entrega se heredaron de la interfaz para la cámara web de Silverlight 4. Permiten que las aplicaciones obtengan las fuentes de distribución en vivo del vídeo y el audio de la cámara y el micrófono del teléfono. Luego pueden presentar estas fuentes de distribución al usuario, guardarlas en un archivo o (y esto es lo más interesante) manipularlas de alguna forma.

Dispositivos y orígenes

La interfaz para la cámara web de Silverlight 4 sólo se amplió un poco para Windows Phone 7 y está compuesta por una docena de clases definidas en el espacio de nombres System.Windows.Media. Siempre comenzamos con la clase estática CaptureDeviceConfiguration. Si el teléfono cuenta con varias cámaras o micrófonos, entonces estos están disponibles con los métodos Get­AvailableVideoCaptureDevices y GetAvailableAudioCapture­Devices. Podemos presentarlos al usuario en una lista para que pueda realizar una elección. De lo contrario, podemos llamar simplemente los métodos GetDefaultVideoCaptureDevice y GetDefaultAudioCaptureDevice.

La documentación menciona que estos métodos pueden devolver un valor nulo, lo que probablemente indica que el teléfono no cuenta con ninguna cámara. Aunque esto es poco probable, deberíamos revisar si el valor es nulo.

Estos métodos CaptureDeviceConfiguration devuelven instancias de las clases VideoCaptureDevice y AudioCaptureDevice o colecciones de instancias de estas. Estas clases proporcionan un espacio de nombres amigable para el dispositivo, una colección SupportedFormats y la propiedad DesiredFormat. En el caso del vídeo, los formatos contienen las dimensiones en píxeles de cada fotograma, el formato del color y la cantidad de fotogramas por segundo. Para el audio, el formato especifica la cantidad de canales, la resolución de muestreo y el formato WAVE, el cual es siempre Modulación por impulsos codificados (PCM).

Para obtener acceso a la cámara web, la aplicación Silverlight 4 debe llamar al método CaptureDevice­Configuration.RequestDeviceAccess para obtener el permiso del usuario. Esta llamada se debe realizar en respuesta a una intervención por parte del usuario, como por ejemplo el clic de un botón. Pero si la propiedad CaptureDevice­Configur­­ation.AllowedDeviceAccess es verdadera, entonces el usuario ya dio el permiso para este acceso y no hace falta que el programa vuelva a llamar RequestDeviceAccess.

Sin duda, el método RequestDeviceAccess sirve para proteger la privacidad del usuario. Pero aparentemente Silverlight para la Web y Silverlight para Windows Phone 7 difieren un poco en este aspecto. La idea de que un sitio web comience a acceder clandestinamente a la cámara web definitivamente resulta espeluznante; esto no es tan así en un programa para el teléfono. En mi experiencia, AllowedDeviceAccess siempre devuelve verdadero en las aplicaciones de Windows Phone. A pesar de esto, en todos los programas descritos en esta columna, definí una UI para llamar RequestDeviceAccess.

La aplicación también debe crear un objeto CaptureSource, que combina un dispositivo de vídeo con uno de audio en una secuencia en vivo de vídeo y audio. CaptureSource tiene dos propiedades, llamadas VideoCaptureDevice y AudioCaptureDevice, que debemos establecer en instancias de VideoCaptureDevice y AudioCaptureDevice que obtenemos de la clase CaptureDeviceConfiguration. No hace falta que establezcamos ambas propiedades cuando sólo necesitamos vídeo o audio. En los programas de ejemplo de este artículo me concentraré exclusivamente en el vídeo.

Después de crear un objeto CaptureSource, podemos llamar los métodos Start y Stop del objeto. En un programa dedicado a obtener fuentes de distribución de vídeo y audio, es probable que queramos llamar al método Start en el reemplazo OnNavigatedTo, y el método Stop en el reemplazo OnNavigatedFrom.

Además, podemos usar el método CaptureImageAsync de CaptureSource para obtener fotogramas aislados en forma de objetos WriteableBitmap. No entregaré más detalles sobre esta característica.

Una vez que tenemos el objeto CaptureSource, tenemos dos alternativas: podemos crear un VideoBrush para mostrar la fuente de distribución de vídeo en vivo o podemos conectar CaptureSource a un objeto “receptor” para obtener acceso a los datos sin procesar o para guardarlos en un archivo en un almacenamiento aislado.

VideoBrush

Sin duda, la opción más fácil para CaptureSource es VideoBrush. Silverlight 3 introdujo la clase VideoBrush con un origen MediaElement y Silverlight 4 agregó la alternativa CaptureSource para VideoBrush. Como todos los pinceles, podemos usarlo para colorear el fondo o el primer plano de los elementos.

El código de descarga de este artículo contiene un programa llamado StraightVideo que usa VideoCaptureDevice, CaptureSource y VideoBrush para mostrar la fuente de distribución de vídeo en vivo que viene de la cámara predeterminada. En la Figura 1 vemos una buena parte del archivo MainPage.xaml. Preste atención al uso del modo horizontal (deseable en las fuentes de distribución de vídeo), la definición de VideoBrush en la propiedad Background de la cuadrícula del contenido y el botón para obtener el permiso del usuario para acceder a la cámara.

Figura 1 Archivo MainPage.xaml de StraightVideo

<phone:PhoneApplicationPage
  x:Class="StraightVideo.MainPage"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
  xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
  ...
  SupportedOrientations="Landscape" Orientation="LandscapeLeft"
  shell:SystemTray.IsVisible="True">
  <Grid x:Name="LayoutRoot" Background="Transparent">
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
      <TextBlock x:Name="ApplicationTitle" Text="STRAIGHT VIDEO"
        Style="{StaticResource PhoneTextNormalStyle}"/>
    </StackPanel>
    <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
      <Grid.Background>
        <VideoBrush x:Name="videoBrush" />
      </Grid.Background>
      <Button Name="startButton"
        Content="start"
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
        Click="OnStartButtonClick" />
    </Grid>
  </Grid>
</phone:PhoneApplicationPage>

En la Figura 2 podemos ver gran parte del archivo de código subyacente. El objeto CaptureSource lo creo en el constructor de la página, pero lo inicio y detengo en los reemplazos de la navegación. También tuve que llamar SetSource en el VideoBrush en OnNavigatedTo; de lo contrario, la imagen se perdía después de una llamada Stop previa.

Figura 2 Archivo MainPage.xaml.cs principal de StraightVideo

public partial class MainPage : PhoneApplicationPage
{
  CaptureSource captureSource;
  public MainPage()
  {
    InitializeComponent();
    captureSource = new CaptureSource
    {
      VideoCaptureDevice =
        CaptureDeviceConfiguration.GetDefaultVideoCaptureDevice()
    };
  }
  protected override void OnNavigatedTo(NavigationEventArgs args)
  {
    if (captureSource !=
      null && CaptureDeviceConfiguration.AllowedDeviceAccess)
    {
      videoBrush.SetSource(captureSource);
      captureSource.Start();
      startButton.Visibility = Visibility.Collapsed;
    }
    base.OnNavigatedTo(args);
  }
  protected override void OnNavigatedFrom(NavigationEventArgs args)
  {
    if (captureSource != null && captureSource.State == CaptureState.Started)
    {
      captureSource.Stop();
      startButton.Visibility = Visibility.Visible;
    }
    base.OnNavigatedFrom(args);
  }
  void OnStartButtonClick(object sender, RoutedEventArgs args)
  {
    if (captureSource != null &&
        (CaptureDeviceConfiguration.AllowedDeviceAccess ||
        CaptureDeviceConfiguration.RequestDeviceAccess())
    {
      videoBrush.SetSource(captureSource);
      captureSource.Start();
      startButton.Visibility = Visibility.Collapsed;
    }
  }
}

Puede ejecutar este programa en Windows Phone Emulator, pero resulta mucho más interesante en un dispositivo verdadero. Se dará cuenta de que la presentación de la fuente de distribución de vídeo responde muy bien. Evidentemente, la fuente de distribución de vídeo va directamente al hardware de vídeo. (Otra evidencia para esta suposición es que mi método habitual para obtener capturas de pantallas del teléfono mediante una representación del objeto PhoneApplicationFrame en un WriteableBitmap no funciona con este programa.) También podrá observar que como uso un pincel para presentar el vídeo, el pincel se estira hasta las dimensiones de la cuadrícula del contenido, y la imagen se distorsiona.

Una de las características útiles de los pinceles es que podemos compartirlos entre varios elementos. Esta es la idea detrás de FlipXYVideo. Este programa crea un conjunto de objetos Rectangle en una cuadrícula Grid en forma dinámica. Siempre se usa el mismo VideoBrush, excepto que cada segundo Rectangle se invierte horizontal o verticalmente, o ambos a la vez, como vemos en la Figura 3. Los botones de ApplicationBar sirven para aumentar o disminuir la cantidad de filas y columnas.

Figura 3 Objetos VideoBrush compartidos en FlipXYVideo

void CreateRowsAndColumns()
{
  videoPanel.Children.Clear();
  videoPanel.RowDefinitions.Clear();
  videoPanel.ColumnDefinitions.Clear();
  for (int row = 0; row < numRowsCols; row++)
    videoPanel.RowDefinitions.Add(new RowDefinition
    {
      Height = new GridLength(1, GridUnitType.Star)
    });
  for (int col = 0; col < numRowsCols; col++)
    videoPanel.ColumnDefinitions.Add(new ColumnDefinition
  {
    Width = new GridLength(1, GridUnitType.Star)
  });
  for (int row = 0; row < numRowsCols; row++)
    for (int col = 0; col < numRowsCols; col++)
    {
      Rectangle rect = new Rectangle
      {
        Fill = videoBrush,
        RenderTransformOrigin = new Point(0.5, 0.5),
        RenderTransform = new ScaleTransform
        {
          ScaleX = 1 - 2 * (col % 2),
          ScaleY = 1 - 2 * (row % 2),
        },
      };
      Grid.SetRow(rect, row);
      Grid.SetColumn(rect, col);
      videoPanel.Children.Add(rect);
    }
  fewerButton.IsEnabled = numRowsCols > 1;
}

Resulta entretenido jugar con este programa, pero el programa del caleidoscopio que presentaré en breves instantes es mucho más divertido.

Origen y receptor

En vez de usar un objeto VideoBrush podemos conectar un objeto CaptureSource con un objeto AudioSink, VideoSink o FileSink. El uso de la palabra “sink” en los nombres de estas clases tiene el significado de “receptáculo” y es similar a las palabras que se emplean en la electrónica y la teoría de redes. (También podemos pensar en las palabras “heat source” y “heat sink”.)

La clase FileSink es el método preferido para guardar secuencias de transmisión de vídeo o audio en el almacenamiento aislado de la aplicación, sin que tengamos que intervenir en forma alguna. Cuando tenemos que tener acceso a los propios bits de vídeo o audio en tiempo real, entonces debemos usar VideoSink y AudioSink. Estas clases son abstractas. Derivamos una clase de una de estas clases abstractas (o ambas) y reemplazamos los métodos OnCaptureStarted, OnCaptureStopped, OnFormatChange y OnSample.

Las clases derivadas de VideoSink o AudioSink siempre recibirán una llamada a OnFormatChange antes de la primera llamada a OnSample. La información entregada con OnFormatChange indica cómo se deben interpretar los datos de muestreo. Tanto en VideoSink como en AudioSink, la llamada OnSample entrega información sobre el tiempo más una matriz de bytes. En el caso de AudioSink, estos bytes representan datos PCM. En el caso de VideoSink, estos bytes constituyen filas y columnas de píxeles para cada fotograma. Estos datos siempre se entregan sin procesar y sin comprimir.

Como tanto OnFormatChange como OnSample se llaman en procesos de ejecución secundarios, tendremos que usar un objeto Dispatcher para aquellas tareas dentro de estos métodos que tengamos que ejecutar dentro del subproceso de UI.

El programa StraightVideoSink es parecido a StraightVideo, excepto que los datos vienen a través de una clase derivada de VideoSink. Esta clase derivada (que vemos en la Figure 4) se llama SimpleVideoSink ya que simplemente toma la matriz de bytes OnSample y la transfiere a WriteableBitmap.

Figura 4 Clase SimpleVideoSink usada en StraightVideoSink

public class SimpleVideoSink : VideoSink
{
  VideoFormat videoFormat;
  WriteableBitmap writeableBitmap;
  Action<WriteableBitmap> action;
  public SimpleVideoSink(Action<WriteableBitmap> action)
  {
    this.action = action;
  }
  protected override void OnCaptureStarted() { }
  protected override void OnCaptureStopped() { }
  protected override void OnFormatChange(VideoFormat videoFormat)
  {
    this.videoFormat = videoFormat;
    System.Windows.Deployment.Current.Dispatcher.BeginInvoke(() =>
    {
      writeableBitmap = new WriteableBitmap(videoFormat.PixelWidth,
        videoFormat.PixelHeight);
      action(writeableBitmap);
    });
  }
  protected override void OnSample(long sampleTimeInHundredNanoseconds,
    long frameDurationInHundredNanoseconds, byte[] sampleData)
  {
    if (writeableBitmap == null)
      return;
    int baseIndex = 0;
    for (int row = 0; row < writeableBitmap.PixelHeight; row++)
    {
      for (int col = 0; col < writeableBitmap.PixelWidth; col++)
      {
        int pixel = 0;
        if (videoFormat.PixelFormat == PixelFormatType.Format8bppGrayscale)
        {
          byte grayShade = sampleData[baseIndex + col];
          pixel = (int)grayShade | (grayShade << 8) |
            (grayShade << 16) | (0xFF << 24);
        }
        else
        {
          int index = baseIndex + 4 * col;
          pixel = (int)sampleData[index + 0] | (sampleData[index + 1] << 8) |
            (sampleData[index + 2] << 16) | (sampleData[index + 3] << 24);
        }
        writeableBitmap.Pixels[row * writeableBitmap.PixelWidth + col] = pixel;
      }
      baseIndex += videoFormat.Stride;
    }
    writeableBitmap.Dispatcher.BeginInvoke(() =>
      {
        writeableBitmap.Invalidate();
      });
  }
}

MainPage usa ese WriteableBitmap con un elemento Image para presentar la fuente de distribución de vídeo resultante. (En forma alternativa, podríamos crear un pincel ImageBrush y establecerlo como el fondo o el primer plano de un elemento.)

Pero hay un problema: no podemos crear el objeto WriteableBitmap antes de llamar el método OnFormatChange en el derivado de VideoSink, ya que esta clase entrega el tamaño del fotograma. (En mi teléfono generalmente es 640x480 píxeles, pero podría ser diferente.) Aunque es la clase derivada de VideoSink la que crea el objeto WriteableBitmap, MainPage tiene que poder accederlo. Esta es la razón por la que defino un constructor para SimpleVideoSink que contiene un argumento Action que se puede llamar al crear WriteableBitmap.

Observe que como debemos crear WriteableBitmap en el subproceso de UI del programa, SimpleVideoSink emplea un objeto Dispatcher para poner en cola la creación del subproceso de UI. Por lo tanto, es posible que WriteableBitmap no se cree antes de la primera llamada de OnSample. Debe tener cuidado con esto. Aunque el método OnSample tiene acceso a la matriz Pixels de WriteableBitmap en un subproceso secundario, la llamada Invalidate para invalidar el mapa de bits se debe producir dentro del subproceso de UI, ya que esta llamada en definitiva afecta la presentación del mapa de bits del elemento Image.

La clase MainPage de StraightVideoSink incluye un botón Application­Bar para alternar entre las fuentes de distribución de vídeo en color y en escala de grises. Estas son las únicas dos opciones, y podemos establecer la propiedad DesiredFormat del objeto VideoCaptureDevice para alternar entre las dos. Las fuentes de distribución en color tienen 4 bytes por píxel en el orden verde, azul, rojo y alfa (que siempre es 255). Las fuentes de distribución en escala de grises siempre tienen un solo byte por píxel. En ambos casos, un WriteableBitmap siempre tiene 4 bytes por píxel, donde cada píxel se representa con un valor entero de 32 bits, y donde los 8 bits superiores representan el canal alfa, seguidos por rojo, verde y azul. Al cambiar los formatos hay que detener y reiniciar el objeto CaptureSource.

Aunque tanto StraightVideo como StraightVideoSink presentan la fuente de distribución de vídeo en vivo, probablemente se percatará de que StraightVideoSink es bastante más lento, debido al esfuerzo que el programa debe realizar para transferir los fotogramas a WriteableBitmap.

Creación de un caleidoscopio

Si sólo necesitamos una fuente de distribución de vídeo en tiempo real con algunas capturas aisladas de fotogramas, entonces podemos usar el método CaptureImageAsync de Capture­Source. Debido a las pérdidas en el rendimiento, es probable que restrinjamos el uso de VideoSink a las aplicaciones más especializadas que tengan que manipular los bits de los píxeles.

Escribamos un programa “especializado” como ese, que ordena la fuente de distribución de vídeo en un patrón caleidoscópico. El concepto es bastante sencillo: el derivado de VideoSink recibe una fuente de distribución de vídeo, con fotogramas que probablemente miden 640x480 píxeles o algo así. De estas imágenes tendremos que obtener una referencia en forma de triángulo equilátero, tal como vemos en la Figura 5.

Opté por un triángulo que apunta con la base hacia arriba y el ápice hacia abajo, para capturar mejor las caras.

The Source Triangle for a Kaleidoscope
Figura 5 Triángulo de origen para un caleidoscopio

Luego duplicamos varias veces la imagen de este triángulo en un WriteableBitmap con algunas rotaciones e inversiones, para que las imágenes queden alineadas y se agrupen en hexágonos sin ninguna discontinuidad, tal como podemos observar en la Figura 6. Sé que los hexágonos parecen flores bellas, pero en verdad sólo son varias imágenes de mi cara (puede que demasiadas imágenes de mi cara).

The Destination Bitmap for a Kaleidoscope
Figura 6 Mapa de bits de destino para un caleidoscopio

El patrón de las repeticiones se vuelve más evidente al delimitar los triángulos individuales, como podemos ver en la Figura 7. Al presentarlo en el teléfono, el alto del WriteableBitmap de destino será igual a la dimensión menor del teléfono, es decir, 480 píxeles. Por lo tanto, los lados de los triángulo equilátero miden 120 píxeles. Esto significa que el alto de un triángulo es 120 veces la raíz cuadrada de 0,75, aproximadamente 104 píxeles. En el programa uso 104 para los cálculos pero 105 para el tamaño del mapa de bits, para simplificar los bucles. La imagen resultante mide 630 píxeles de ancho.

The Destination Bitmap Showing the Source Triangles
Figura 7 Mapa de bits de destino con los triángulos

Me parece que resulta más conveniente tratar toda la imagen como tres bandas verticales con 210 píxeles de ancho. Como cada una de esas bandas verticales tiene un eje de simetría vertical que pasa por el centro, puedo reducir la imagen a un solo mapa de bits de 105x480 píxeles repetido seis veces, la mitad de ellas con una reflexión. La banda consiste en siete triángulos completos y dos triángulos parciales.

Pero aun así estaba nervioso con los cálculos necesarios para armar la imagen. Luego me percaté de que no tengo por qué repetirlos con la frecuencia de actualización del vídeo de 30 veces por segundo. Basta con hacer los cálculos una sola vez, cuando se obtiene el tamaño de la imagen de vídeo en el reemplazo OnFormatChange.

El programa resultante se llama Kaleidovideo. (Los tradicionalistas probablemente considerarán que este nombre es una abominación etimológica, ya que “kaleido” proviene del griego y significa “forma hermosa”, mientras que “vídeo” tiene una raíz griega, y al acuñar términos nuevos no se deben mezclar los dos idiomas.)

La clase KaleidoscopeVideoSink reemplaza a VideoSink. El método OnFormatChange es responsable de calcular los miembros de una matriz llamada indexMap. Esta matriz tiene tantos miembros como hay píxeles en WriteableBitmap (105 multiplicado por 480, o 50.400) y almacena un índice en el área rectangular de la imagen de vídeo. Con esta matriz indexMap la transferencia de los píxeles de la imagen de vídeo a WriteableBitmap en el método OnSample se realiza de la manera más rápida imaginable.

El programa es muy entretenido. Todo se ve mucho más bello a través de Kaleidovideo. En la Figura 8 vemos mis estantes de libros, por ejemplo. Incluso podemos usarlo para ver televisión.

A Typical Kaleidovideo Screen
Figura 8 Pantalla típica de Kaleidovideo

Y por si queremos guardar algo que aparece en pantalla, agregué el botón “capturar” en ApplicationBar. Las imágenes se guardan en la carpeta Saved Pictures de la biblioteca de fotografías del teléfono.           

Charles Petzold ha sido editor colaborador durante largo tiempo de MSDN Magazine. Su libro reciente Programming Windows Phone 7 (Microsoft Press, 2010) está disponible como una descarga gratuita en bit.ly/cpebookpdf.

Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: Mark Hopkins y Matt Stroshane