[Windows Phone 8.1] Capturando la pantalla utilizando la API MediaCapture

MovieIntroducción

Entre la enorme cantidad de nuevas APIs, controles y herramientas disponibles con la llegada de Windows Phone 8.1, desde un principio llamo mucho la atención las nuevas APIs disponibles para grabar en video o tomar capturas de la actividad de la pantalla. Esta nueva característica la tenemos disponible dentro del namespace Windows.Media.Capture y sera nuestro centro de atención en este artículo.

¿Te apuntas?

Primeros pasos

Comenzamos creando un nuevo proyecto:

Nueva App Windows Phone 8.1 desde plantilla vacía

Nueva App Windows Phone 8.1 desde plantilla vacía

Añadimos las carpetas Views, ViewModels y Services además de las clases base necesarias para implementar el patrón MVVM de la misma forma que vimos en este artículo.

Nuestro objetivo sera muy sencillo. Nuestra aplicación de ejemplo contará con un botón para comenzar a grabar lo que ocurre en pantalla y otro para detener la grabación. Tras terminar la grabación podemos ver el video resultante.

Antes de comenzar, vamos a añadir la Webcam como capacidad en el archivo Package.manifiest:

Añadimos las capacidades necesarias

Añadimos las capacidades necesarias

Manos a la obra!

Tomar capturas de pantalla de la aplicación e incluso videos es una funcionalidad fantástica para segun que tipo de aplicaciones y sobretodo en juegos. En nuestro ejemplo, debíamos contar con “algo” interesante que grabar. Para ello, vamos a añadir un elipse con una animacion Easing de modo que otorgemos el efecto de una bola cayendo y rebotando.

Para gestionar la animación vamos a utilizar el SDK de Behaviors incluido dentro de las extensiones:

Behaviors SDK (XAML)

Behaviors SDK (XAML)

Nos centramos en la vista principal, MainView.xaml. Añadimos la elipse:

<Ellipse x:Name="Ball" Height="75" Width="75" Fill="Red" RenderTransformOrigin="0.5,0.5">
     <Ellipse.RenderTransform>
          <TranslateTransform/>
     </Ellipse.RenderTransform>
     <interactivity:Interaction.Behaviors>
          <core:EventTriggerBehavior EventName="Loaded">
               <media:ControlStoryboardAction ControlStoryboardOption="Play" Storyboard="{StaticResource MyStoryboard}"/>
          </core:EventTriggerBehavior>
     </interactivity:Interaction.Behaviors>
</Ellipse>

Al cargar lanzaremos una animacion llamada MyStoryBoard que tenemos definida en los recursos de la página:

<Storyboard x:Name="MyStoryboard">
    <DoubleAnimation From="0" To="250" Duration="00:00:10"
                     Storyboard.TargetName="Ball"
                     Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)">
         <DoubleAnimation.EasingFunction>
              <BounceEase Bounces="40" EasingMode="EaseOut"
                          Bounciness="1.2" />
         </DoubleAnimation.EasingFunction>
     </DoubleAnimation>
</Storyboard>

De este modo, al lanzar la aplicación se lanzará la animación con el siguiente resultado:

Animación

Animación

Ya tenemos listo el elemento visual a grabar. Necesitamos dos botones en nuetra aplicación. Uno para grabar la pantalla y otro que permita ver el resultado:

<Page.BottomAppBar>
     <CommandBar>
          <AppBarButton />
          <AppBarButton />
     </CommandBar>
</Page.BottomAppBar>

Creamos una CommandBar y añadimos los botones. El primer botón nos permitirá comenzar y detener la grabación del video. El segundo nos permitirá ver el video resultante. Para poder conseguir este resultado, en la viewmodel necesitaremos una propiedad bool que nos indique si se esta grabando el video o no, para permitir comenzar o detener la grabación y otra propiedad bool que nos indique si ya hay un video grabado:

private bool _recording;
private bool _result;

public bool Recording
{
     get { return _recording; }
     set
     {
          _recording = value;
          RaisePropertyChanged("Recording");
     }
}

public bool Result
{
     get { return _result; }
     set
     {
          _result = value;
          RaisePropertyChanged("Result");
     }
}

El segundo botón lo mostraremos cuando la propiedad Result sea cierta, necesitaremos un Converter para convertir la propiedad bool a Visibility:

public class BoolToVisibilityConverter : IValueConverter
{
     public object Convert(object value, Type targetType, object parameter, string language)
     {
          if (value is bool)
               return ((bool) value) ? Visibility.Visible : Visibility.Collapsed;

          return Visibility.Collapsed;
     }

     public object ConvertBack(object value, Type targetType, object parameter, string language)
     {
          return null;
     }
}

El primer botón nos permite controlar dos estados diferentes:

  • Comenzar a grabar.
  • Detener una grabación.

Si estamos grabando o no nos lo indica la propiedad Recording, necesitaremos dos converters para obtener el Label y el Icon del AppBarButton segun el valor de la propiedad:

public class RecordStateToIconElementConverter : IValueConverter
{
     public object Convert(object value, Type targetType, object parameter, string language)
     {
            var recording = value as bool?;

            if (recording != null)
                return recording == true ? new SymbolIcon(Symbol.Stop) : new SymbolIcon(Symbol.Play);
            return new SymbolIcon(Symbol.Stop);
     }

     public object ConvertBack(object value, Type targetType, object parameter, string language)
     {
            return null;
     }
}
public class RecordStateToStringConverter : IValueConverter
{
     public object Convert(object value, Type targetType, object parameter, string language)
     {
          var recording = value as bool?;

          if (recording != null)
              return recording == true ? "Stop" : "Play";
          return "Play";
     }

     public object ConvertBack(object value, Type targetType, object parameter, string language)
     {
          return null;
     }
}

Cada botón realizará una acción, en la viewmodel, la ejecución de un comando:

private ICommand _recordCommand;
private ICommand _resultCommand;

public ICommand RecordCommand
{
     get { return _recordCommand = _recordCommand ?? new DelegateCommandAsync(RecordCommandDelegate); }
}

public ICommand ResultCommand
{
     get { return _resultCommand = _resultCommand ?? new DelegateCommand(ResultCommandDelegate); }
}

public async Task RecordCommandDelegate()
{   

}

public void ResultCommandDelegate()
{

}

De modo que nuestros dos botones en la CommandBar quedaran:

<AppBarButton
     Label="{Binding Recording, Converter={StaticResource RecordStateToStringConverter}}"
     Icon="{Binding Recording, Converter={StaticResource RecordStateToIconElementConverter}}"
     Command="{Binding RecordCommand}" />
<AppBarButton
     Visibility="{Binding Result, Converter={StaticResource BoolToVisibilityConverter}}"
     Label="Result"
     Icon="Forward"
     Command="{Binding ResultCommand}" />

API Windows.Media.Capture

Llegamos a la parte fundamental del ejemplo y del artículo, el uso de las APIs disponibles en Windows.Media.Capture. Crearemos un servicio sencillo que nos permita grabar con facilidad lo que ocurre en la pantalla de nuestro dispositivo. El servicio lo llamaremos ScreenRecorederService y su definición sera la siguiente:

public interface IScreenRecorderService
{
     ScreenRecorderService.RecordingStatus Status { get; }
     Task Start(string recordName);
     void Stop();
}

Como podemos ver contamos con:

  • Una propieddad Status. Nos permitirá consultar en todo momento si el servicio esta grabando la pantalla, esta detenido, ha terminado la grabación con éxito o por el contrario ha ocurrido algun error.
  • Un evento Start que recibirá como parámetro el nombre del video resultante de la grabación. Este evento se encarga de comenzar la grabación del video.
  • Un evento Stop que detiene una grabación inciada de video.

Nos centramos en la implementación del servicio. Contaremos con una sencilla enumeración que nos permita determinar con facilidad el estado de la grabación:

public enum RecordingStatus
{
     Stopped,
     Recording,
     Failed,
     Sucessfull
};

Sencillo, ¿cierto?.

Comenzar la grabación del video

Nos centramos en el método Start de nuestro servicio. Comenzamos creando un objeto de clase ScreenCapture utilizando el método: GetForCurrentView:

//Inicializamos ScreenCapture.
ScreenCapture screenCapture = ScreenCapture.GetForCurrentView();

A continuación, creamos una instancia de la clase MediaCaptureInitializationSettings y establecemos la fuente de audio y vídeo con las propiedades AudioSource y VideoSource del objeto ScreenCapture previamente definido:

// Establecemos MediaCaptureInitializationSettings para que ScreenCapture capture audio y video.
var mediaCaptureInitializationSettings = new MediaCaptureInitializationSettings
{
     VideoSource = screenCapture.VideoSource,
     AudioSource = screenCapture.AudioSource,
     StreamingCaptureMode = StreamingCaptureMode.AudioAndVideo
};

El objeto MediaCaptureInitializationSettings nos permite establecer la configuración básica necesaria para crear un objeto de tipo MediaCapture. El objeto MediaCapture es el encargado de ofrecernos la posibilidad de capturar fotos, audio y vídeos.

// Inicializamos MediaCapture con los settings anteriores.
_mediaCapture = new MediaCapture();
await _mediaCapture.InitializeAsync(mediaCaptureInitializationSettings);

Inicializamos utilizando el método InitializeAsync pasándole como parámetro la configuración anterior.

NOTA: InitializeAsync iniciará una solicitud de consentimiento para obtener permisos de usuario.

Continuamos suscribiéndonos a los eventos Failed, RecordLimitationExceeded, y SourceSuspensionChanged:

_mediaCapture.Failed += (s, e) => { _recordingStatus = RecordingStatus.Failed; };
_mediaCapture.RecordLimitationExceeded += s => { _recordingStatus = RecordingStatus.Failed; };

screenCapture.SourceSuspensionChanged += (s, e) => Debug.WriteLine("IsAudioSuspended:" +
                                                                   e.IsAudioSuspended +
                                                                   " IsVideoSuspended:" +
                                                                   e.IsVideoSuspended);

Tenemos:

  • El evento Failed se lanzará cuando cualquier tipo de error ocurra durante la grabacion del video.
  • El evento RecordLimitationExceeded se lanzará cuando se supere el tiempo máximo de grabación. En Windows 8.1 el tiempo máximo de grabación es de 3 horas.
  • El evento SourceSuspensionChanged se lanzará cada vez que se cambie el estado de activo y suspensión de la aplicación durante la grabación del video. Profundizaremos en la gestión de la grabación del video y la suspensión de la aplicación algo más adelante en este mismo artículo.

A continuación, definimos el perfil de codificación para el archivo de video y audio. Para ello, utilizamos un objeto de la clase MediaEncondingProfile. Tenemos disponible los  formatos más habituales tanto de video como de audio:

  • MP3
  • MP4
  • Wav
  • Wma
  • Wmv
// Creamos un encondig a utilizar. Por defecto, MP4 1080P.                  
MediaEncodingProfile mediaEncodingProfile = MediaEncodingProfile.CreateMp4(VideoEncodingQuality.HD1080p);

Tras crear el perfil de codificación, creamos el archivo donde se guardarán los medios capturados:

// Creamos el fichero resultante de la grabación.            
StorageFile video =
                    await
                        ApplicationData.Current.LocalFolder.CreateFileAsync(string.Format("{0}.mp4", recordName),
                            CreationCollisionOption.ReplaceExisting);

Por último, utilizaremos el método StartRecordToStorageFileAsync para comenzar la grabación:

// Con el formato, calidad y archivo destino definidos, comenzamos a grabar.
IAsyncAction startAction = _mediaCapture.StartRecordToStorageFileAsync(mediaEncodingProfile, video);
startAction.Completed += (info, status) =>
{
     if (status == AsyncStatus.Completed)
          _recordingStatus = RecordingStatus.Recording;
     if (status == AsyncStatus.Error)
     {
          _recordingStatus = RecordingStatus.Failed;
          Debug.WriteLine(info.ErrorCode.Message);
     }
};

Detener la grabación del video

Para detener la grabación del video utilizamos el método StopRecordAsync.

public void Stop()
{
     //Detenemos la grabación.           
     IAsyncAction stopAction = _mediaCapture.StopRecordAsync();
     stopAction.Completed += (info, status) =>
     {
          if (status == AsyncStatus.Completed)
               if (_recordingStatus == RecordingStatus.Recording)
                    _recordingStatus = RecordingStatus.Sucessfull;
     };
}

Gestion de la suspensión

Ya hemos mencionado el evento SourceSuspensionChanged. Ante ciertas circunstancias la grabación se suspende para ser reanudada tras finalizar la acción que provocó la suspensión. El video y el audio se pueden bloquear de manera independiente por lo que en ciertas circunstancias se detendrá la grabación de video, en otras las de audio y en ocasiones ambas. Entre alguna de las situaciones que provocan a detención de la grabación tenemos:

  • Una llamada entrante. Detiene la grabación de video y audio.
  • Notificaciones con información personal del usuario.
  • Cuando la aplicación no esta en primer plano.
  • Cuando se reproduce sonido en background se detiene la grabación de audio.

Con el evento SourceSuspensionChanged podemos detectar si se ha detenido la grabacion de video y audio gracias a las propiedades IsVideoSuspended y IsAudioSuspended respectivamente. Podemos utilizar las propiedades para notificar al usuario que se ha detenido la grabacion. Sin embargo, para reanudar la misma no tenemos que hacer nada.

Buenas prácticas

Para no interferir con otras aplicaciones que el usuario pueda utilizar cuando suspende nuestra aplicación, debemos limpiar los recursos de captura utilizados en la suspensión.

Para realizar esta tarea creamos una propiedad pública con un objeto de tipo MediaCapture en el archivo App.xaml.cs:

public MediaCapture MediaCapture { get; set; }

Establecemos la propiedad en nuestro servicio:

// Establecemos el MediaCapture de App.xaml.cs para gestionar la suspensión.
var app = Application.Current as App;
if (app != null) app.MediaCapture = _mediaCapture;

En el evento OnSuspending realizaremos la detención de la grabación y la liberación de recursos:

if (MediaCapture != null)
{
     await MediaCapture.StopRecordAsync(); 

     MediaCapture.Dispose();
}

Podéis descargar el ejemplo realizado a continuación:

Más información

4 pensamientos en “[Windows Phone 8.1] Capturando la pantalla utilizando la API MediaCapture

  1. Es un ejemplo muy interesante para aprender a manejar el “MediaCapture” con el patrón MVVM. Sólo tiene un problema: no funciona correctamente cuando se está grabando y se suspende la aplicación:

    1. En el evento “OnSuspending” de “App.xaml.cs” se elimina el “MediaCapture”. Es correcto, hay que liberar el recurso

    2. Error: al volver a la aplicación (resume) y pulsar el botón de “Stop” el “MediaCapture” no se puede utilizar porque ha sido eliminado de memoria y se muestra la excepción “System.ObjectDisposedException”.

    Todavía no he conseguido hacerlo funcionar correctamente. Supongo que habrá que crear de nuevo el “MediaCapture” en el evento “OnResuming” de “App.xaml.cs” pero no lo veo nada claro, sobre todo a la hora de mantener el vídeo grabando y que no se corte.

  2. Creo que en este caso lo más recomendable es gestionar los eventos “Suspending” y “Resuming” de “App.xaml” desde la página que utiliza la cámara.

    * Suspending:

    public sealed partial class ShowCamera : Page
    {
    private MediaCapture _mediaCapture;
    private bool _isPreviewing;

    public ShowCamera()
    {
    // …

    // Se ejecuta el evento “Suspending” de la clase “App.xaml.cs”
    Application.Current.Suspending += OnSuspending;
    }

    async void OnSuspending(object sender, Windows.ApplicationModel.SuspendingEventArgs e)
    {
    var deferral = e.SuspendingOperation.GetDeferral();

    await this.CleanupResources();

    deferral.Complete();
    }

    private async Task CleanupResources()
    {
    if(this._mediaCapture != null)
    {
    if(this._isPreviewing)
    {
    await this._mediaCapture.StopPreviewAsync();
    this._isPreviewing = false;
    }
    this._mediaCapture.Dispose();
    }
    }

    // …
    }

    * Resuming:

    public sealed partial class ShowCamera : Page
    {
    private MediaCapture _mediaCapture;
    private bool _isPreviewing;

    public ShowCamera()
    {
    // …

    Application.Current.Resuming += OnResuming;
    }

    void OnResuming(object sender, object e)
    {
    // “_mediaCapture” no es null. Ha ejecutado “Dispose()”
    // y sus propiedades no son accesibles, generan “System.ObjectDisposedException”.
    if(!this._isPreviewing)
    {
    this.InitializeCamera();
    }
    }

    // …
    }

    Este código no lo he probado en tu aplicación, Javier. Es sólo una prueba que he hecho para utilizar la cámara.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s