[Windows Phone 8.1] Reproducir Audio en Background

Media-PlayIntroducción

Una tarea habitual a realizar en aplicaciones es la reproducción de archivos de audio ya sean podcasts, música, sonidos, etc. En muchas de las situaciones la reproducción debe continuar cuando la aplicación pasa a background.

En este artículo vamos a crear un reproductor de audio con continuidad al pasar a background.

¿Te apuntas?

Un poco de teoría antes de comenzar…

En Windows Phone 8.0 ya podíamos realizar esta acción, reproducir audio en background. Con la llegada de las aplicaciones Universales con Windows Phone 8.1, la forma de crear la tarea en background es diferente, algo más similar a la forma ya disponible en WinRT aunque tampoco igual. Esto nos permite crear la tarea en background de audio compartiendo  código aunque no sería exactamente el mismo, hay diferencias entre la implementación del agente en background para Windows Phone y Windows Store. Además tendremos acceso a nuevas características previamente no disponibles como trabajar con la velocidad de reproducción por ejemplo.

NOTA: Al actualizar una Aplicación Windows Phone 8.0 a Silverlight 8.1 que implementase una tarea en background de audio hay que tener en cuenta que el AudioPlayerAgent no esta soportado.

Queremos reproducir audio cuando nuestra interfaz de usuario no este en primer plano. Para ello, utilizaremos una tarea en background capaz de reproducir audio.En el espacio de nombres Windows.Media.Playback contamos con un conjunto de APIs destinadas a ofrecernos la posibilidad de reproducir audio en segundo plano (incluso en primer plano en caso necesario). Usando esta API utilizaremos un MediaPlayer global encargado de llevar a cabo la reproducción.

La reprodución del audio se realizará desde background mientras que la App accederá a la información del MediaPlayer vía objeto proxy. Concretamente la comunicación se realizará por un sencillo sistema de mensajería. Se pueden enviar mensajes desde primer plano a segundo plano y viceversa.

NOTA: Un mensaje puede ser desde una simple cadena a un conjunto de valores.

Veamos el diagrama de como sería el sistema:

Diagrama de Background Audio

Arquitectura Background Audio

Cuando queremos reproducir audio en una tarea de fondo en Windows Phone estamos tratando con dos procesos. Por un lado contamos con un proceso en primer plano, nuestra App con nuestra interfaz de usuario y por otro lado una tarea en segundo plano que contará con la lógica para reproducir el audio. Esto es asi ya que si el sistema o el usuario suspende o finaliza el primer proceso, el audio seguiría reproduciendose desde el segundo.

Nuestra UI

Comenzamos creando un nuevo proyecto:

Nuevo proyecto

Nuevo proyecto

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.

En este ejemplo tendremos eventos del //BUILD 2014 de Channel 9. Nuestro objetivo será crear una App capaz de reproducir el audio de los eventos funcionando por supuesto en segundo plano.

Comenzamos creand el modelo:

public class Event
{
     public string Name { get; set; }

     public string Image { get; set; }

     public string Duration { get; set; }

     public string Url { get; set; }
}

Una sencilla clase que nos permita almacenar toda la información relacionada con un evento. Los valores principales será la Url donde tendremos el acceso al audio y la propiedad Name que nos indicará que se esta reproduciendo.

En nuestra viewmodel, cargaremos la información de un evento:

private Event LoadEvent()
{
     return new Event
     {
          Name = "What’s New for Windows and Windows Phone Developers",
          Image = "ms-appx:///Assets/Build.jpg",
          Duration = "27 minutes, 43 seconds",
          Url = "http://video.ch9.ms/ch9/b544/1a759a52-7309-40c6-aa63-0ef77b38b544/C9Live9001.mp3"
     };
}

Lo llamaremos cuando la vista pase a ser la activa, es decir, al entrar en la vista, sobreescritura del método OnNavigatedTo:

public override Task OnNavigatedTo(NavigationEventArgs args)
{
     // Cargamos los datos del evento
     Event = LoadEvent();

     return null;
}

Una vez cargada la información del evento contaremos en nuestra interfaz con un botón para controlar la reproducción (PlayStop). Definimos una pequeña enumeración para que la gestión del estado sea sencilla:

public enum PlayerState
{
     Play,
     Pause
};

private PlayerState _state;
public PlayerState State
{
     get { return _state; }
     set
     {
          _state = value;
          RaisePropertyChanged();
     }
}

También necesitaremos el comando a ejecutar en la viewmodel al pulsar sobre el botón:

private ICommand _playerCommand;

public ICommand PlayerCommand
{
     get { return _playerCommand = _playerCommand ?? new DelegateCommand(PlayerCommandExecute); }
}

private void PlayerCommandExecute()
{
     if(State == PlayerState.Play)
          // Play
     else
          // Stop
}

De modo que la definición del botón en nuestra interfaz sería:

<Button VerticalAlignment="Stretch"
        BorderBrush="{x:Null}" Width="50"
        Command="{Binding PlayerCommand}">
     <Image Source="{Binding State, Converter={StaticResource StateToIconConverter}}"/>
</Button>

Donde la imagen la gestiona un Converter. El Converter devuelve un icono de Play o Stop segun el estado:

public class StateToIconConverter : IValueConverter
{
     private const string Play = "ms-appx:///Assets/Play.png";
     private const string Stop = "ms-appx:///Assets/Stop.png";

     public object Convert(object value, Type targetType, object parameter, string language)
     {
          var state = value as PlayerViewModel.PlayerState?;

          if (state == null)
              return string.Empty;

          return state == PlayerViewModel.PlayerState.Play ? Play : Stop;
     }

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

El resultado de nuestra UI es el siguiente:

Nuestra interfaz de usuario

Nuestra interfaz de usuario

NOTA: Para simplificar el ejemplo se han suprimido ciertas partes de código no necesarias para el objetivo principal del artículo, el audio en background. Hablamos de código como el estilo del botón de reproducción o el XAML general de la vista. En la parte inferior del artículo esta disponible todo el código fuente del ejemplo.

Creando la tarea en background

Teniendo una aplicación Windows Phone 8.1 nos centramos en añadir la background task. Para añadir la background task debemos añadir un componente WinRT.

Crear nuevo componente de Windows

Crear nuevo componente de Windows

Una vez creado el componente WinRT renombraremos la clase a UpdateTask. La clase BackgoundAudioTask implementa la interfaz IBackgroundTask. Esta interfaz cuenta con un único método llamado Run.

public sealed class BackgroundAudioTask : IBackgroundTask
{
    public void Run(IBackgroundTaskInstance taskInstance)
    {
             
    }
}

NOTA:  La clase de la tarea en segundo plano debe ser una clase public y sealed.

Comenzamos a escribir el código necesario en la tarea en background para realizar la reproducción de audio:

private BackgroundTaskDeferral _deferral;
private SystemMediaTransportControls _systemMediaTransportControl;
private MediaPlayer _mediaPlayer;

Creamos las variables globales necesarias. Antes de continuar vamos a determinar su cometido. Comenzamos hablando de la variable de tipo BackgroundTaskDeferral. La tarea en segundo plano es iniciada por el proceso en primer plano haciendo una llamada aBackgroundMediaPlayer.Current. Tras esa llamada se lanza el método IBackgroundTask.Run donde se inicia la variable _deferral con el objetivo de completar el aplazamiento, la reproducción en los eventos Canceled o Completed.

SystemMediaTransportControls representa los controles multimedia del sistema.

SystemMediaTransportControls

SystemMediaTransportControls

Los utilizaremos para gestionar el audio cuando nuestra Aplicación no se encuentre en primer plano (Ejemplo: Pantalla de bloqueo).

Por último, la variable de tipo MediaPlayer será la que nos exponga los métodos necesarios para comenzar y detener la reproducción del audio.

Continuamos. Vamos a definir el código del método Run:

public void Run(IBackgroundTaskInstance taskInstance)
{
     // La clase SystemMediaTransportControls permite a tu aplicación usar los controles de
     // transporte multimedia del sistema proporcionados por Windows y actualizar la información
     // multimedia que se muestra.
     _systemMediaTransportControl = SystemMediaTransportControls.GetForCurrentView();
     _systemMediaTransportControl.IsEnabled = true;

     BackgroundMediaPlayer.MessageReceivedFromForeground += MessageReceivedFromForeground;
     BackgroundMediaPlayer.Current.CurrentStateChanged += BackgroundMediaPlayerCurrentStateChanged;

     taskInstance.Canceled += OnCanceled;
     taskInstance.Task.Completed += Taskcompleted;

     _deferral = taskInstance.GetDeferral();
}

Aparte de instanciar las variables globales vistas previamente cabe destacar la suscripción a dos eventos fundamentales:

  • MessageReceivedFromForeground: Este evento se lanzará cada vez que un mensaje desde la UI sea enviado.
  • CurrentStateChanged: Este evento se lanzará cada vez que el estado del MediaPlayer cambie entre Playing, Paused o Stopped.

Vemos la definición de los eventos recibidos desde la aplicación:

private void MessageReceivedFromForeground(object sender, MediaPlayerDataReceivedEventArgs e)
{
     ValueSet valueSet = e.Data;
     foreach (string key in valueSet.Keys)
     {
          switch (key)
          {
               case "Play":
                    Play(valueSet[key].ToString(), valueSet["Title"].ToString());
                    break;
               case "Pause":
                    Pause();
                    break;
          }
     }
}

Sencillo. Recordad que la UI se comunica con la tarea en segundo plano vía mensajes. En este evento capturamos los mensajes y los interpretamos. Podemos recibir dos tipos de mensajes desde la UI, comenzar la reproducción detenerla. Asi que en función de la Key recibida lanzamos un método Play o un método Pause.

Veamos por lo tanto la definición de los métodos base, Play y Pause:

private void Play(string url, string title)
{
     _mediaPlayer = BackgroundMediaPlayer.Current;
     _mediaPlayer.AutoPlay = true;
     _mediaPlayer.SetUriSource(new Uri(url));

     _systemMediaTransportControl.ButtonPressed += MediaTransportControlButtonPressed;
     _systemMediaTransportControl.IsPauseEnabled = true;
     _systemMediaTransportControl.IsPlayEnabled = true;
     _systemMediaTransportControl.DisplayUpdater.Type = MediaPlaybackType.Music;
     _systemMediaTransportControl.DisplayUpdater.MusicProperties.Title = title;
     _systemMediaTransportControl.DisplayUpdater.Update();
}

El método Play define la fuente del audio en el objeto MediaPlayer y actualiza toda la información del reproductor SystemMediaTransportControl.

El método Pause:

private void Pause()
{
     if (_mediaPlayer == null)
          _mediaPlayer = BackgroundMediaPlayer.Current;

     _mediaPlayer.Pause();
}

Lanza el método Pause del MediaPlayer.

Debemos controlar que el PlaybackStatus del control SystemMediaTransportControl se ve reflejado en el estado de nuestro MediaPlayer:

private void BackgroundMediaPlayerCurrentStateChanged(MediaPlayer sender, object args)
{
     switch (sender.CurrentState)
     {
          case MediaPlayerState.Playing:
               _systemMediaTransportControl.PlaybackStatus = MediaPlaybackStatus.Playing;
               break;
          case MediaPlayerState.Paused:
               _systemMediaTransportControl.PlaybackStatus = MediaPlaybackStatus.Paused;
               break;
     }
}

También debemos gestionar la pulsación de botones en el control SystemMediaTransportControl:

private void MediaTransportControlButtonPressed(SystemMediaTransportControls sender,
            SystemMediaTransportControlsButtonPressedEventArgs args)
{
     switch (args.Button)
     {
          case SystemMediaTransportControlsButton.Play:
               BackgroundMediaPlayer.Current.Play();
               break;
          case SystemMediaTransportControlsButton.Pause:
               BackgroundMediaPlayer.Current.Pause();
               break;
     }
}

Y por supuesto, debemos cerrar correctamente la tarea de fondo. En caso de finalización o cancelación detenemos la reproducción:

private void Taskcompleted(BackgroundTaskRegistration sender, BackgroundTaskCompletedEventArgs args)
{
     BackgroundMediaPlayer.Shutdown();
     _deferral.Complete();
}

private void OnCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
{
     BackgroundMediaPlayer.Shutdown();
     _deferral.Complete();
}

Cerramos aqui el código de nuestra tarea en background. Continuamos viendo como vincular nuestro proyecto Windows Phone 8.1, la UI, con la tarea.

En nuestro proyecto Windows Phone 8.1 hacemos clic derecho, opción “Add references”:

Añadir la referencia a la tarea en background

Añadir la referencia a la tarea en background

Tras añadir la referencia al componente WinRT debemos realizar algunos cambios en el archivo Package.appxmanifiest. Nos dirigimos a la pestaña “Capabilites”. Añadimos una nueva capacidad de tipo Background Task:

Añadimos la tarea en segundo plano

Añadimos la tarea en segundo plano

En las propiedades debemos definir el tipo a Audio la propiedad Entry Point, es decir, el nombre completo de la clase de nuestra background task incluido namespace:

Definición de la tarea en background

Definición de la tarea en background

Y todo listo!

Integrándolo todo

Con la tarea en background definida y referenciada en nuestro proyecto Windows Phone es hora de integrarlo todo. Vamos a utilizar la tarea en segundo plano en el comando que gestiona la reproducción en la viewmodel, lo recordamos:

private void PlayerCommandExecute()
{
     if(State == PlayerState.Play)
          // Play
     else
          // Stop
}

En los comentarios realizaremos llamadas a metodos Play() Y Stop() respectivamente. Definimos el método de reproducción:

private void Play()
{
     State = PlayerState.Pause;
     BackgroundMediaPlayer.SendMessageToBackground(new ValueSet
     {
          {
               "Play",
               _event.Url
          },
          {
               "Title",
               _event.Name
          }
     });
}

Enviamos un mensaje a nuestra tarea en segundo plano. En la Key le indicamos la acción a ejecutar, la reproducción, pasando en el valor la Url con el audio a reproducir. Podemos pasar tantos parámetros como necesitemos. No estamos limitados a una sencilla cadena. En este ejemplo también se pasa el nombre del audio a reproducir aunque también podría ser interesante la portada del evento/album, el autor o artista, etc.

Ahora pasamos al método de detención de la reproducción.

private void Pause()
{
     State = PlayerState.Play;
     BackgroundMediaPlayer.SendMessageToBackground(new ValueSet
     {
          {
               "Pause",
               string.Empty
          }
     });
}

De esta forma, nuestro comando quedara de la siguiente forma:

private void PlayerCommandExecute()
{
     if(State == PlayerState.Play)
          Play();
     else
          Pause();
}

Y hasta aqui ya lo tenemos todo listo!. El resultado final es el siguiente:

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

Y hasta aquí llega el artículo de hoy. Como siempre espero que os resulte interesante. Recordar que cualquier tipo de duda o sugerencia la podéis dejar reflejada en los comentarios.

En próximos artículos veremos como realizar la misma operación en una Aplicaicón Windows Store para Windows 8.1 entre otras novedades.

Más información

Un pensamiento en “[Windows Phone 8.1] Reproducir Audio en Background

  1. Buenas,

    Estupendo artículo, como siempre. Buenos también las crhistmas sessions (tengo pendiente ver la última tuya).
    El tema de este articulo coincide con algo a lo que le estoy dando vueltas y no doy con ello. A ver si me puedes dar una pista donde buscar y/o orientarme un poco.
    En una universal app tengo que reproducir sonidos pero debo enterarme cuando finalizan el sonido para habilitar ciertos botones en el UI. Uso MVVM con servicios que se encargan de hacer las cosas. En el SoundService tengo un MediaElement, pero no logro capturar el evento MediaEnded. Imagino que si pudiera capturarlo sería capaz de usarlo como si fuera awaitable, aunque no tengo claro como.
    A lo mejor estoy usando un enfoque equivocado y debería usar otra cosa distinta al media element. Yo lo que quiero realmente es reproducir un sonido de longitud variable (con el TextToSpeech y saber cuando este finaliza para poder desbloquear ciertos botones)

    muchas gracias y felices fiestas

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