Вы находитесь на странице: 1из 34

CURSO DE DESARROLLO DE APLICACIONES ANDROID

Tema 15

Multimedia y Cámara
TEMA 15. MULTIMEDIA Y CÁMARA

Introducción

Android proporciona un conjunto de clases, un framework, que permite la reproducción de


distintos tipos de elementos multimedia en las aplicaciones (audio, vídeo e imágenes). Dichas
aplicaciones podrán reproducir vídeos y sonido almacenados en los recursos de la aplicación
(en la carpeta “res/raw/”), en otros directorios del dispositivo o, incluso, a través de una
conexión de red (por medio de streaming).

Para reproducir vídeos y sonido se utilizarán, básicamente, las clases MediaPlayer y


AudioManager. La primera clase es la clase principal de la API de reproducción de vídeo y
sonido, mientras que la segunda clase está encargada de gestionar las fuentes de audio así
como la salida de audio de los dispositivos.

Permisos necesarios

Para que una aplicación pueda reproducir elementos multimedia, debe solicitar los permisos
adecuados que le permitan acceder a las funcionalidades inherentes. Para ello, deberán
declararse los siguientes permisos en el manifiesto de la aplicación:

• Permiso INTERNET. Será necesario en caso de querer realizar streaming de elementos


multimedia a través de una conexión de red.

<uses-permission android:name="android.permission.INTERNET" />

• Permiso WAKE_LOCK. Será necesario en caso de querer mantener la pantalla


encendida y evitar que el procesador entre en modo de bajo consumo.

<uses-permission android:name="android.permission.WAKE_LOCK" />

Este permiso también será necesario en caso de querer usar los métodos:

 MediaPlayer.setScreenOnWhilePlaying()
 MediaPlayer.setWakeMode()

CURSO DE DESARROLLO DE APLICACIONES ANDROID 2


TEMA 15. MULTIMEDIA Y CÁMARA

Uso de MediaPlayer

El componente más importante del framework multimedia es MediaPlayer. Con esta clase se
podrá buscar, decodificar y reproducir tanto audio como vídeo de diversas fuentes de forma
sencilla (recursos locales, URIs internas obtenidas, por ejemplo, de proveedores de contenido,
URLs externas a través de streaming, etc.).

Por ejemplo, para reproducir un archivo de audio que se haya almacenado como recurso local
de la aplicación (en la carpeta “res/raw/”) bastarán dos líneas de código:

MediaPlayer mediaPlayer = MediaPlayer.create(getApplicationContext(),


R.raw.audio_local);
// No es necesario invocar prepare() ya que create() lo invoca
mediaPlayer.start();

Debido a que el archivo utilizado se ha añadido como un recurso raw (“crudo”, “en bruto”), el
sistema no lo tratará de ningún modo y no realizará ningún proceso de optimización sobre el
mismo. Los recursos multimedia almacenados como raw podrán ser de cualquiera de los tipos
multimedia que son soportados en Android (.3gp, .mp4, .aac, .flac, .mp3, .mid, .mkv, .wav, .jpg,
.gif, .bmp, .png, .webm)

Para reproducir audio de una URI obtenida de un proveedor de contenido local, se


implementará un código similar a este:

Uri uriInterna = Uri.withAppendedPath(


MediaStore.Audio.Media.INTERNAL_CONTENT_URI,"240");
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(getApplicationContext(), uriInterna);
mediaPlayer.prepare();
mediaPlayer.start();

Y para reproducir un elemento multimedia desde una URL externa, vía HTTP 1, únicamente se
deberá cambiar la “fuente” de datos:

String url = "http://www.sonidosmp3.com/sonidos/mas_usados/laser-01.mp3";


MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(url);
mediaPlayer.prepare();
mediaPlayer.start();

1
El servidor que alberga archivo accedido online deberá ser capaz de proporcionar descargas progresivas.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 3


TEMA 15. MULTIMEDIA Y CÁMARA

Se ha de tener en cuenta que, al usar setDataSource(), se corre el riesgo de acceder a


recursos que no existen, por lo que se deberán gestionar las excepciones
IllegalArgumentException e IOException.

Preparación asíncrona

Debido a que la ejecución del método prepare() puede tardar cierto tiempo (este método
está encargado de la búsqueda y decodificación del elemento multimedia), nunca se deberá
invocar desde el hilo principal de la aplicación, ya que esta se dejaría de responder hasta que la
ejecución dicho método finalizase. Esto podría provocar un error ANR (Application Not
Responding) además de implicar una mala experiencia de usuario quien podría llegar a adquirir
la impresión de que la aplicación es muy lenta.

Para evitar este comportamiento, se deberá crear otro hilo para preparar el MediaPlayer y se
notificará al hilo principal cuando este haya sido preparado. Se puede implementar un hilo en
segundo plano de forma convencional pero, afortunadamente, el framework multimedia
proporciona el método prepareAsync() que, internamente, genera un hilo en segundo plano
para preparar el elemento multimedia. Este método finaliza inmediatamente y no cuelga la
aplicación. Cuando el elemento multimedia esté preparado, se invocará al método callback
onPrepared() del listener MediaPlayer.OnPreparedListener, el cual será configurado a
través de setOnPreparedListener().

La forma más sencilla de utilizar prepareAsync() es hacer que la actividad implemente la


interfaz MediaPlayer.OnPreparedListener. Esta interfaz obliga la implementación del
método onPrepared(MediaPlayer mp). El método que inicialice MediaPlayer contendrá un
código similar al siguiente:

String url = "http://www.sonidosmp3.com/sonidos/mas_usados/sweep1.mp3";


MediaPlayer mediaPlayerAsync = new MediaPlayer();
mediaPlayerAsync.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayerAsync.setDataSource(url);
mediaPlayerAsync.setOnPreparedListener(this);
mediaPlayerAsync.prepareAsync();

Y el método onPrepared() podrá ser así de sencillo:

@Override
public void onPrepared(MediaPlayer mp) {

mp.start();
}

CURSO DE DESARROLLO DE APLICACIONES ANDROID 4


TEMA 15. MULTIMEDIA Y CÁMARA

Gestión del estado de MediaPlayer

MediaPlayer gestiona su estado de forma interna. El estado de MediaPlayer es importante y


deberá ser consultado antes de usar el reproductor debido a que existen ciertas operaciones
que solo serán posibles cuando el reproductor esté en ciertos estados concretos. Si se realizan
acciones sobre el reproductor cuando este está en un estado equivocado, el sistema lanzará
una excepción o provocará comportamientos extraños.

Por ejemplo, en el momento de crear un nuevo MediaPlayer, este se encontrará en el estado


Idle (desocupado). Cuando el reproductor se encuentre en dicho estado, deberá ser
inicializado utilizando el método setDataSource(), lo cual hará que pase al estado Initialized.
Entonces se deberá preparar el elemento multimedia que se va a reproducir utilizando el
método prepare() o bien el método prepareAsync(). Una vez finalice la preparación del
elemento multimedia, el reproductor adoptará el estado Prepared, lo cual implica que se
puede iniciar la reproducción por medio de start(). A continuación, se podrán invocar los
métodos start(), paused() y seekTo() que harán que el reproductor adopte los estados
Started, Paused y PlaybabackCompleted, entre otros. Por último, cuando se detiene la
reproducción invocando a stop(), no se puede volver a invocar start() hasta que no se
prepare de nuevo el reproductor (con prepare() o prepareAsync()).

A la hora de interactuar con MediaPlayer se deberá prestar especial atención a su diagrama


de estados, ya que el invocar a sus métodos desde el estado incorrecto es una fuente de error
muy común.

En la siguiente página se muestra el diagrama completo de estados de MediaPlayer.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 5


TEMA 15. MULTIMEDIA Y CÁMARA

Figura 1 - Diagrama de estados de MediaPlayer

CURSO DE DESARROLLO DE APLICACIONES ANDROID 6


TEMA 15. MULTIMEDIA Y CÁMARA

Liberación de recursos

Cuando se finalice la reproducción del elemento multimedia será recomendable liberar los
recursos del sistema que estén asignados a MediaPlayer ya que pueden ser elevados, en
comparación con los recursos que haya disponibles en el dispositivo. Por lo tanto, se deberá
invocar siempre a release() y asignar a null la instancia del MediaPlayer en los siguientes
casos:

• Cuando la actividad pasa a segundo plano (salvo que se quiera mantener


expresamente la reproducción del elemento multimedia en segundo plano, tal y como
se verá más adelante), momento en el que se invocará a su método callback
onStop():

@Override
public void onStop() {

// Se ha finalizado la reproducción.
// Se liberan recursos
mediaPlayer.release();
mediaPlayer = null;
}

• Cuando el reproductor finalice la reproducción. La actividad deberá implementar


MediaPlayer.OnCompletionListener y el método callback onCompletion():

@Override
public void onCompletion(MediaPlayer mp) {

mp.release();
mp = null;
}

• En caso de error.

En el caso de que la actividad sea reiniciada o reactivada, se deberá crear una nueva instancia
de MediaPlayer y prepararla antes de poder reiniciar la reproducción.

Si no se liberan recursos tal y como se ha mencionado, se puede llegar a consumir


rápidamente todos los recursos disponibles en el dispositivo. Por ejemplo cuando se cambia la
orientación del dispositivo, el sistema, por defecto, reinicia la actividad (para reposicionar y
redimensionar los componentes del layout), por lo que, en cada nuevo cambio de orientación,
se creará una nueva instancia de MediaPlayer. Las instancias anteriores deberán haberse
asignado a null, previa liberación de recursos.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 7


TEMA 15. MULTIMEDIA Y CÁMARA

Uso de MediaPlayer con un servicio

Para reproducir contenido multimedia incluso si la aplicación no está en primer plano, se


deberá implementar un servicio que para controlar la instancia de MediaPlayer. Para realizar
dicha implementación de forma correcta, se deberá tener en cuenta lo siguiente.

Ejecución asíncrona

Por defecto, los servicios y actividades de una aplicación se ejecutan en el mismo hilo (el hilo
main, principal, de la aplicación). Por lo tanto, si un servicio va a realizar tareas largas, que
puedan bloquear el hilo principal, se deberán implementar dichas tareas de forma asíncrona,
bien creando un hilo, bien utilizando las funcionalidades que proporcione el framework. En el
caso de MediaPlayer, se podrá usar prepareAsync() tal y como ya se ha comentado,
implementando el listener MediaPlayer.OnPreparedListener.

Así mismo, se pueden implementar otros listeners en el servicio, de forma que reciba
notificaciones del MediaPlayer en caso, por ejemplo, de que finalice la reproducción o se
produzca un error 2, para que sean liberados los recursos.

Para invocar al servicio desde la actividad, se podrá lanzar un Intent explícito o bien uno
implícito cuya acción sea, por ejemplo, com.cursoandroid.action.PLAY. Para usar la
invocación implícita, se deberá definir un <intent-filter> en el manifiesto de la aplicación:

<service
android:name="com.cursoandroid.services.MediaService"
android:exported="false" >
<intent-filter>
<action android:name="com.cursoandroid.action.PLAY" />
</intent-filter>
</service>

En la actividad, se invocará al servicio así:

Intent intentService = new Intent(SERVICE_ACTION_PLAY);


startService(intentService);

Y, por último, el servicio contendrá un código similar a este:

2
Cuando se produzca un error, es importante recordar que el MediaPlayer adoptará el estado Error por lo que se
deberá reinicializar antes de volverlo a usar, invocando al método reset().

CURSO DE DESARROLLO DE APLICACIONES ANDROID 8


TEMA 15. MULTIMEDIA Y CÁMARA

Código fuente de MediaService.java:

public class MediaService extends Service implements


MediaPlayer.OnPreparedListener,
MediaPlayer.OnCompletionListener,
MediaPlayer.OnErrorListener {

private static final String ACTION_PLAY =


"com.cursoandroid.action.PLAY";
MediaPlayer mMediaPlayer = null;
String urlMedia = "http://www.mp3.com/sonido/audio.mp3";
WifiLock wifiLock = null;

@Override
public void onCreate() {
super.onCreate();
}

public int onStartCommand(Intent intent, int flags, int startId) {

if (intent!= null && intent.getAction() != null &&


intent.getAction().equals(ACTION_PLAY)) {

initMediaPlayer();
}
return super.onStartCommand(intent, flags, startId);
}

private void initMediaPlayer() {


Toast.makeText(this, R.string.loading, Toast.LENGTH_SHORT).show();

mMediaPlayer = new MediaPlayer();


mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);

try {
mMediaPlayer.setDataSource(urlMedia);
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

// Se obliga a que la CPU siga en funcionamiento, pese a que el


// dispositivo haya apagado la pantalla.
mMediaPlayer.setWakeMode(getApplicationContext(),
PowerManager.PARTIAL_WAKE_LOCK);

// Se bloquea la conexión WiFi para que no se apague cuando la


// pantalla sea apagada

CURSO DE DESARROLLO DE APLICACIONES ANDROID 9


TEMA 15. MULTIMEDIA Y CÁMARA

wifiLock = ((WifiManager)
getSystemService(Context.WIFI_SERVICE))
.createWifiLock(WifiManager.WIFI_MODE_FULL,
"bloqueo_wifi");
wifiLock.acquire();
// Importante: se añade este servicio como listener de diversos
// eventos del MediaPlayer
mMediaPlayer.setOnPreparedListener(this);
mMediaPlayer.setOnCompletionListener(this);
mMediaPlayer.setOnErrorListener(this);
mMediaPlayer.prepareAsync();
}

/** Invocado cuando MediaPlayer está listo */


public void onPrepared(MediaPlayer mp) {
Toast.makeText(this, R.string.loaded, Toast.LENGTH_SHORT).show();
mp.start();
}

@Override
/** Invocado cuando MediaPlayer finaliza la reproducción */
public void onCompletion(MediaPlayer mp) {
destroyMediaPlayer(mp);
}

@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
destroyMediaPlayer(mp);
Log.e("ERROR_MEDIA_PLAYER", "Error en la reproducción. What=" +
what + ". Extra=" + extra);
return false;
}

@Override
public void onDestroy() {
destroyMediaPlayer(mMediaPlayer);
super.onDestroy();
}

private void destroyMediaPlayer(MediaPlayer mp) {


// Liberación de recursos
if (mp != null) {
// Se invoca a reset() para evitar el warning:
// "mediaplayer went away with unhandled events"
mp.reset();
mp.release();
mp = null;
}
// Se libera el bloqueo de la conexión WiFi
if (wifiLock != null)
wifiLock.release();
}
}

CURSO DE DESARROLLO DE APLICACIONES ANDROID 10


TEMA 15. MULTIMEDIA Y CÁMARA

Wake Locks y Servicios en primer plano

Si se está reproduciendo audio en segundo plano, bien a través de un servicio o bien


directamente con una tarea asíncrona desde la propia actividad, incluso con la pantalla
apagada, será necesario evitar que el dispositivo entre en modo de ahorro de energía y que
apague la CPU o la conexión WiFi. Para ello, se deberá usar un wake lock, que indica al sistema
que la aplicación está utilizando alguna funcionalidad que debe permanecer activa incluso
cuando la pantalla está apagada o el dispositivo está en modo de ahorro de energía (idle). Los
wake lock deben ser utilizados con cuidado puesto que reducen significativamente el tiempo
de vida de la batería del dispositivo, y serán liberados en cuanto sea posible.

Para que MediaPlayer utilice esta funcionalidad, deberá invocar a setWakeMode() 3 durante
su inicialización. Cuando la reproducción sea pausada o parada, MediaPlayer liberará este
bloqueo automáticamente.

mMediaPlayer.setWakeMode(getApplicationContext(),
PowerManager.PARTIAL_WAKE_LOCK);

El bloqueo PARTIAL_WAKE_LOCK asegura que la CPU seguirá en funcionamiento, incluso con la


pantalla del dispositivo apagada.

En caso de estar realizando un streaming multimedia, será necesario bloquear también el


funcionamiento de la conexión WiFi 4. Dicho bloqueo deberá ser liberado manualmente,
puesto que MediaPlayer no lo gestiona.

WifiLock wifiLock =
((WifiManager) getSystemService(Context.WIFI_SERVICE))
.createWifiLock(WifiManager.WIFI_MODE_FULL, "bloqueo_wifi");
wifiLock.acquire();

Para liberar este bloqueo, se deberá invocar a wifiLock.release() cuando MediaPlayer sea
destruido.

En caso de que se quiera evitar que el servicio sea eliminado por el sistema, si los recursos
disponibles son escasos, se deberá pasar el servicio a primer plano. Para ello, se deberá crear
obligatoriamente una notificación que será mostrada mientras el servicio esté activo. Esta
notificación deberá ser mostrada, desde el servicio, a través del método
startForeground(NOTIFICATION_ID, notification) y será eliminada a través del método
stopForeground(true).

3
Para utilizar esta funcionalidad, la aplicación deberá solicitar el permiso WAKE_LOCK.
4
Ídem Nota al pie número 3

CURSO DE DESARROLLO DE APLICACIONES ANDROID 11


TEMA 15. MULTIMEDIA Y CÁMARA

Gestión del audio

Debido a que Android es un entorno multi-tarea real, las aplicaciones que usan audio deben
compartir la salida de audio de forma colaborativa, en vez de competir por el uso de la misma.
Antes de Android 2.2, no existía ningún mecanismo que resolviera este problema, de forma
que el sonido de una notificación importante podría ser inaudible si otra aplicación estaba
reproduciendo música con un volumen elevado. A partir de Android 2.2, la plataforma
introduce un mecanismo de negociación de la salida de audio llamado Audio Focus.

Las aplicaciones que utilicen la salida de audio deberán solicitar el foco del audio antes de
comenzar la reproducción. Una vez se consiga el foco, se podrá utilizar libremente la salida de
audio. Además, se deberá reaccionar ante los cambios de foco, a través de un listener, para, en
caso de perder el foco del audio, bajar el volumen (ducking) o parar la reproducción (y
posiblemente liberar recursos) y, posteriormente, subir el volumen o reanudar la reproducción
cuando se recupere el foco del audio.

Para solicitar el foco del audio, se utilizará requestAudioFocus(), de AudioManager, que


obligará a la implementación de un listener AudioManager.OnAudioFocusChangeListener
cuyo método onAudioFocusChange() será invocado cuando el foco del audio cambie.

AudioManager audioManager =
(AudioManager) getSystemService(Context.AUDIO_SERVICE);
int result = audioManager.requestAudioFocus(this,
AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);

if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
// No se ha podido obtener el foco del audio
Toast.makeText(this, R.string.no_focus, Toast.LENGTH_SHORT).show();
} else {
initMediaPlayer();
}

Se deberá implementar el método callback onAudioFocusChange(), que recibe, como


parámetro, un valor entero que informa cómo ha cambiado el foco del audio. Este parámetro
puede adoptar uno de los siguientes valores:

• AudioManager.AUDIO_FOCUS_GAIN: la aplicación obtiene el foco del audio.


• AudioManager.AUDIO_FOCUS_LOSS: la aplicación pierde el foco del audio durante un
periodo de tiempo sin especificar, posiblemente largo. Se deberá parar la
reproducción y liberar recursos.
• AudioManager.AUDIO_FOCUS_LOSS_TRANSIENT: la aplicación pierde el foco del audio
durante un periodo de tiempo que va a ser corto, por lo que se pausará la
reproducción pero se mantendrán los recursos.
• AudioManager.AUDIO_FOCUS_LOSS_TRANSIENT_CAN_DUCK: la aplicación pierde el foco
del audio, pero se le permite seguir reproduciendo a un volumen inferior.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 12


TEMA 15. MULTIMEDIA Y CÁMARA

La implementación del método onAudioFocusChange() será similar a la siguiente:

@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
// Se reanuda la reproducción
if (mMediaPlayer == null)
initMediaPlayer();
else if (!mMediaPlayer.isPlaying())
mMediaPlayer.start();

// Volumen al 100% (canal izquierdo, canal derecho)


mMediaPlayer.setVolume(1.0f, 1.0f);
break;

case AudioManager.AUDIOFOCUS_LOSS:
// Se ha perdido el foco del audio durante un periodo de tiempo
// sin especificar, por lo que se deberá parar la reproducción y
// liberar recursos
if (mMediaPlayer.isPlaying())
mMediaPlayer.stop();
destroyMediaPlayer(mMediaPlayer);
break;

case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
// Se ha perdido el foco del audio durante un periodo de tiempo
// que va a ser corto, por lo que simplemente se pausa la
// reproducción
if (mMediaPlayer.isPlaying())
mMediaPlayer.pause();
break;

case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// Se ha perdido el foco del audio durante un periodo de tiempo
// que va a ser corto y además es posible bajar el volumen y
// seguir reproduciendo
if (mMediaPlayer.isPlaying())
mMediaPlayer.setVolume(0.25f, 0.25f); // Volumen al 25%
break;
}
}

Debido a que AudioManager está disponible desde el nivel de API 8 (Android 2.2), para
crear aplicaciones compatibles con versiones anteriores se deberán adoptar estrategias
de compatibilidad “hacia atrás” (backward compatibility). Una de las estrategias de
compatibilidad se basa en el uso de reflection mientras que otra se basa en la
implementación de todas las funcionalidades relacionadas con el audio focus en una clase
separada (un helper). Se puede observar la implementación y uso de AudioFocusHelper
en la aplicación de ejemplo de este tema.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 13


TEMA 15. MULTIMEDIA Y CÁMARA

Gestión del Intent AUDIO_BECOMING_NOISY

Las aplicaciones Android tienen la posibilidad de parar la reproducción del audio cuando se
desencadena un evento que hace que dicho audio pueda ser molesto, debido a que comience
a emitirse a través de los altavoces externos del dispositivo porque se hayan desconectado los
auriculares.

Para implementar este comportamiento, será necesario gestionar el Intent con acción
ACTION_AUDIO_BECOMING_NOISY que es emitido por el sistema vía broadcast. En el manifiesto,
será necesario registrar un receptor de dicho broadcast:

<receiver
android:name="com.cursoandroid.broadcastreceiver.MediaIntentReceiver">
<intent-filter>
<action android:name="android.media.AUDIO_BECOMING_NOISY" />
</intent-filter>
</receiver>

El receptor de broadcast tendrá un código similar a este:

public class MediaIntentReceiver extends BroadcastReceiver {

private static final String


SERVICE_ACTION_STOP = "com.cursoandroid.action.STOP";

@Override
public void onReceive(Context ctx, Intent intent) {
if (intent.getAction().equals(
android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY)) {
// Intent al servicio para parar la reproducción
Intent intentService = new Intent(SERVICE_ACTION_STOP);
// Este método hará que el Intent sea pasado a
// MediaService.onStartCommand(), esté o no iniciado el servicio.
ctx.startService(intentService);
}
}
}

Y, finalmente, el servicio que reproduce el audio, deberá capturar el nuevo Intent, declarando
el correspondiente filtro en el manifiesto, y añadiendo el siguiente código en el método
onStartCommand():

CURSO DE DESARROLLO DE APLICACIONES ANDROID 14


TEMA 15. MULTIMEDIA Y CÁMARA

private static final String ACTION_STOP = "com.cursoandroid.action.STOP";

if (intent.getAction().equals(ACTION_STOP)) {
// Acción recibida desde MediaIntentReceiver
if (mMediaPlayer != null && mMediaPlayer.isPlaying())
mMediaPlayer.stop();
Toast.makeText(this, R.string.phones_unplugged,
Toast.LENGTH_SHORT).show();
destroyNotification(mNotificationID);
destroyMediaPlayer(mMediaPlayer);
}
}

CURSO DE DESARROLLO DE APLICACIONES ANDROID 15


TEMA 15. MULTIMEDIA Y CÁMARA

Uso de la cámara del dispositivo

Existen dos procedimientos distintos para incluir funcionalidades que permitan capturar
imágenes y vídeos en las aplicaciones. El primer procedimiento se basa en la delegación de
dicha funcionalidad en las aplicaciones tipo Cámara que ya estén instaladas en el dispositivo,
vía Intent, mientras que el segundo procedimiento, más complejo, permite la creación de
cámaras personalizadas en la propia aplicación a través de una SurfaceView.

En ambos procedimientos es necesario tener en cuenta una serie de consideraciones


generales. Si el uso de la cámara es imprescindible en la aplicación, de forma que esta no
pueda funcionar si el dispositivo no tiene una cámara, será necesario declarar en el manifiesto
como requisito la existencia de al menos una cámara en el dispositivo. Además, se deberá
decidir la ubicación de las imágenes y vídeos que genere la aplicación en función de si se desea
que sean visibles solo por dicha aplicación o si, por el contrario, se desea permitir el acceso a
dichas imágenes y vídeos a otras aplicaciones y hacer que estén disponibles incluso si se
desinstala la aplicación.

Android permite la captura de imágenes y vídeo a través de la API Camera o vía Intent. Las
clases fundamentales son Camera, que permite controlar las cámaras del dispositivo,
SurfaceView, que mostrará la previsualización en tiempo real de la imagen (el “visor”)
MediaRecorder, que será utilizada para grabar vídeo, e Intent que, mediante las acciones
MediaStore.ACTION_IMAGE_CAPTURE y MediaStore.ACTION_VIDEO_CAPTURE permite
capturar imagen y vídeo sin el uso directo de un objeto tipo Camera.

Para utilizar la API Camera será necesario declarar ciertos permisos y características en el
manifiesto de la aplicación:

• Permiso para usar la Cámara: solo en caso de usar Camera directamente. En caso de
usar la funcionalidad vía Intent, no será necesario.

<uses-permission android:name="android.permission.CAMERA" />

• Requisito de características del hardware Cámara: para que Google Play filtre
adecuadamente la aplicación y no la ofrezca a dispositivos que no tengan cámara u
otras características de hardware relacionadas, se deberá declarar el uso de las
siguientes características (features) según las funcionalidades que implemente la
aplicación.

<uses-feature android:name="android.hardware.camera" />


<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature android:name="android.hardware.camera.flash" />
<uses-feature android:name="android.hardware.camera.front" />

CURSO DE DESARROLLO DE APLICACIONES ANDROID 16


TEMA 15. MULTIMEDIA Y CÁMARA

En caso de que cierta característica no sea imprescindible en la aplicación, de forma


que la aplicación pueda usarla pero que, en caso de no existir en el dispositivo, no
impida su normal funcionamiento, se deberá añadir el atributo
android:required="false".

• Otros permisos: para almacenar las imágenes y vídeos en la memoria externa del
dispositivo, para grabar audio cuando se grabe un vídeo o para etiquetar con
coordenadas GPS las imágenes capturadas:

<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.RECORD_AUDIO" />
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION" />

Uso de las aplicaciones tipo Cámara existentes en el dispositivo

El procedimiento más rápido para añadir la funcionalidad de captura de imagen y vídeo en una
aplicación es vía Intent, delegando dicha funcionalidad en cualquiera de las aplicaciones que
ya estén instaladas en el dispositivo. Simplemente se lanzará un Intent con una acción
concreta con el método startActivityForResult() para, una vez capturada la imagen o el
vídeo con la aplicación adecuada, recibir el resultado de dicho Intent en el método callback
onActivityResult():

1. Se inicializará el Intent con una de las siguientes acciones:


• MediaStore.ACTION_IMAGE_CAPTURE
• MediaStore.ACTION_VIDEO_CAPTURE
2. Se lanzará el Intent a través del método callback startActivityForResult().
Aparecerá la interfaz de la aplicación de la cámara o bien un listado para elegir una
de las aplicaciones tipo cámara que estén instaladas en el dispositivo.
3. Se recibirá el resultado del Intent en la implementación del método callback
onActivityResult(). Dicho método será invocado cuando el usuario realice la
foto o el vídeo, o bien cancele la operación. Como parámetro, recibirá, entre otros,
un Intent que contendrá los datos de la Uri donde se haya almacenado la imagen
o el vídeo.

El Intent que lance la cámara también podrá contener información adicional añadida en el
método putExtra() como, por ejemplo:

• MediaStore.EXTRA_OUTPUT: clave para añadir un objeto tipo Uri que especificará la


ruta y nombre de archivo donde se almacenará la imagen o el vídeo. Añadir esta

CURSO DE DESARROLLO DE APLICACIONES ANDROID 17


TEMA 15. MULTIMEDIA Y CÁMARA

información es altamente recomendable ya que, si no se añade, la aplicación de la


cámara almacenará la imagen o el vídeo en la ubicación por defecto con un nombre
por defecto, información que será accesible en el campo Intent.getData() del intent
de resultado (que devolverá null en caso de no usar esta clave).
• MediaStore.EXTRA_VIDEO_QUALITY: clave que admitirá un float cuyo valor puede ser
0 (calidad más baja) o 1 (calidad más alta).
• MediaStore.EXTRA_DURATION_LIMIT: longitud máxima, en segundos, permitida para
el vídeo.
• MediaStore.EXTRA_SIZE_LIMIT: tamaño máximo, en bytes, para el archivo de vídeo.

Los archivos multimedia que sean generados deberán ser almacenados en la memoria externa
del dispositivo (en general una tarjeta SD) para conservar el espacio reservado para el sistema
y para que los usuarios puedan acceder a dichos archivos desde otros dispositivos y
aplicaciones. Existen diversas localizaciones posibles para almacenar los archivos multimedia,
aunque se deberán considerar solo dos:

• Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)5.
Este método devolverá la localización compartida estándar y recomendada para
almacenar imágenes y vídeos. Este directorio es público de forma que cualquier otra
aplicación podrá leer, modificar o borrar los archivos que contenga. Al desinstalar
aplicaciones que hayan grabado archivos en esta localización, se conservarán dichos
archivos. Es recomendable crear subdirectorios para evitar mezclar las imágenes o
vídeos con los que almacenen otras aplicaciones.
• Environment.getExternalFilesDir(Environment.DIRECTORY_PICTURES). Este
método devolverá la localización estándar para almacenar imágenes y vídeos, asociada
a la aplicación. Si la aplicación es desinstalada, todos los archivos almacenados en este
directorio serán borrados. Los archivos almacenados en esta ubicación siguen siendo
públicos por lo que cualquier otra aplicación podrá acceder a ellos y los podrá leer,
modificar o borrar.

A continuación se muestra el código fuente de una actividad que utiliza la cámara del
dispositivo a través de Intent, para generar imágenes y vídeos.

5
Este método está dispobile desde el nivel de API 8 (Android 2.2). Para versiones anteriores, se deberá utilizar el
método Environment.getExternalStorageDirectory().

CURSO DE DESARROLLO DE APLICACIONES ANDROID 18


TEMA 15. MULTIMEDIA Y CÁMARA

Código fuente de ImageAndVideoActivity:

public class MainActivity extends Activity {

private static final int MEDIA_TYPE_IMAGE = 970;


private static final int MEDIA_TYPE_VIDEO = 971;
private static final int ACTIVITY_REQUEST_CODE_CAPTURE_PICTURE = 972;
private static final int ACTIVITY_REQUEST_CODE_CAPTURE_VIDEO = 973;
private Uri fileUri;
private boolean mExternalStorageAvailable = false;
private boolean mExternalStorageWriteable = false;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_image_and_video);
}

public void captureImage(View v) {

// Se crea un Intent para hacer la foto y devolver


// posteriormente el control a la aplicación que invoca
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

// Se crea el archivo donde se almacenará la imagen


fileUri = getExternalMediaFileUri(MEDIA_TYPE_IMAGE);

if (fileUri != null) {
// Se añade el nombre del archivo al Intent para que sea
// usado por la aplicación Cámara cuando almacene la
// imagen.
// CUIDADO: al utilizar EXTRA_OUTPUT en el result Intent,
// getData() = null
intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);

// Se lanza el Intent
startActivityForResult(intent,
ACTIVITY_REQUEST_CODE_CAPTURE_PICTURE);
} else
Toast.makeText(this, getString(R.string.error_file_dir),
Toast.LENGTH_LONG).show();
}

public void captureVideo(View v) {

// Se crea un Intent para hacer la foto y devolver


// posteriormente el control a la aplicación que invoca
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);

// Se crea el archivo donde se almacenará el vídeo


fileUri = getExternalMediaFileUri(MEDIA_TYPE_VIDEO);

if (fileUri != null) {

// Se añade el nombre del archivo al Intent para que sea


// usado por la aplicación Cámara cuando almacene el vídeo

CURSO DE DESARROLLO DE APLICACIONES ANDROID 19


TEMA 15. MULTIMEDIA Y CÁMARA

// CUIDADO: al utilizar EXTRA_OUTPUT en el result Intent,


// getData() = null
intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);

// ATENCIÓN. Existe un BUG en diversas implementaciones de


// Android que hace que EXTRA_OUTPUT sea ignorado en el
// caso de grabación de vídeo, almacenándose en el
// directorio (externo) por defecto de la Cámara (llamado,
// en general, DCIM).
// Se pueden crear diversos "workarouds" para conseguir
// ubicar el vídeo en el directorio deseado como, por
// ejemplo, moverlo a posteriori en caso de detectarlo en tal
// ubicación.
intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);

// Se establece la calidad del vídeo (la más alta)


intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1);

// Se lanza el Intent
startActivityForResult(intent,
ACTIVITY_REQUEST_CODE_CAPTURE_VIDEO);
} else
Toast.makeText(this, getString(R.string.error_file_dir),
Toast.LENGTH_LONG).show();
}

@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {

super.onActivityResult(requestCode, resultCode, data);

// Primero se comprueba qué Intent responde a través de


// requestCode
if (requestCode == ACTIVITY_REQUEST_CODE_CAPTURE_PICTURE) {
// Se comprueba si el resultado de la acción fue
// satisfactorio
if (resultCode == RESULT_OK) {
// La foto ha sido realizada y guardada en la Uri
// especificada en el Intent (fileUri)
// OJO: debido al uso de EXTRA_OUTPUT, data.getData() = null
Toast.makeText(this, getString(R.string.photo_saved_in) +
fileUri, Toast.LENGTH_LONG).show();
} else if (resultCode == RESULT_CANCELED) {
// El usuario canceló la operación.
Toast.makeText(this, getString(R.string.operation_cancelled),
Toast.LENGTH_SHORT).show();
} else {
// Se ha producido un error al realizar la foto
Toast.makeText(this, getString(R.string.photo_error_try_again),
Toast.LENGTH_LONG).show();
}
}

if (requestCode == ACTIVITY_REQUEST_CODE_CAPTURE_VIDEO) {
// Se comprueba si el resultado de la acción fue
// satisfactorio
if (resultCode == RESULT_OK) {

CURSO DE DESARROLLO DE APLICACIONES ANDROID 20


TEMA 15. MULTIMEDIA Y CÁMARA

// El vídeo ha sido grabado y guardado en la Uri


// especificada en el Intent (fileUri)
// OJO: debido al uso de EXTRA_OUTPUT, data.getData() = null
Toast.makeText(this, getString(R.string.video_saved_in) +
fileUri, Toast.LENGTH_LONG).show();

// La gestión de los archivos de vídeo deberá tratarse con


// mucho cuidado y plantearse alternativas...

} else if (resultCode == RESULT_CANCELED) {


// El usuario canceló la operación.
Toast.makeText(this, getString(R.string.operation_cancelled),
Toast.LENGTH_SHORT).show();
} else {
// Se ha producido un error al grabar el vídeo
Toast.makeText(this, getString(R.string.video_error_try_again),
Toast.LENGTH_LONG).show();
}
}
}

/** Creación de una Uri de archivo para almacenar imágenes o vídeos */


private Uri getExternalMediaFileUri(int type) {

// 1º: Creación de un objeto tipo File para almacenar imágenes o


// vídeos
// 2º: Creación de un objeto tipo Uri en caso de que File !=
// null

// Se comprueba si el almacenamiento externo está disponible


checkExternalStorageState();
if (!mExternalStorageAvailable || !mExternalStorageWriteable) {
Log.e(getString(R.string.app_name),
"La memoria externa no está disponible.");
return null;
}
// Se utiliza el directorio público para almacenar imágenes y
// vídeos de forma que se conserven en caso de desinstalar la
// aplicación
File mediaStorageDir = null;
if (type == MEDIA_TYPE_IMAGE) {
mediaStorageDir =
new File(Environment
.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES),
getString(R.string.app_name)
);
} else if (type == MEDIA_TYPE_VIDEO) {
mediaStorageDir =
new File(Environment
.getExternalStoragePublicDirectory(
Environment.DIRECTORY_MOVIES),
getString(R.string.app_name)
);
}

// En caso de que no exista el directorio, será creado


if (mediaStorageDir != null) {

CURSO DE DESARROLLO DE APLICACIONES ANDROID 21


TEMA 15. MULTIMEDIA Y CÁMARA

if (!mediaStorageDir.exists()) {
if (!mediaStorageDir.mkdirs()) {
Log.e(getString(R.string.app_name),
"No se ha podido crear el directorio.");
return null;
}
}
} else
return null;

// Se crea el nombre de archivo incluyendo marca de tiempo.


String timeStamp =
new SimpleDateFormat("ddMMyyyy_HHmmss").format(new Date());
File mediaFile;
if (type == MEDIA_TYPE_IMAGE) {
mediaFile = new File(mediaStorageDir.getPath() +
File.separator + "IMG_" +
timeStamp + ".jpg");
} else if (type == MEDIA_TYPE_VIDEO) {
mediaFile = new File(mediaStorageDir.getPath() +
File.separator + "VID_" +
timeStamp + ".mp4");
} else
return null;

return Uri.fromFile(mediaFile);
}

private void checkExternalStorageState() {


String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state))
mExternalStorageAvailable = mExternalStorageWriteable = true;
else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
mExternalStorageAvailable = true;
mExternalStorageWriteable = false;
} else
mExternalStorageAvailable = mExternalStorageWriteable = false;
}
}

CURSO DE DESARROLLO DE APLICACIONES ANDROID 22


TEMA 15. MULTIMEDIA Y CÁMARA

Creación de una interfaz tipo Cámara

Para crear una interfaz personalizada para la cámara, dentro de una aplicación, será necesario:

1. Detectar y acceder a la cámara, creando el código necesario que compruebe que


existen cámaras en el dispositivo y que solicite acceso a las mismas.
2. Crear una clase de previsualización, que extenderá SurfaceView, que implementará
la interfaz SurfaceHolder y cuya funcionalidad será mostrar la imagen en tiempo real
que captura el hardware de la cámara.
3. Construir un layout de previsualización, que incorporará la clase de previsualización
así como los controles necesarios para la interfaz (botones, etc.).
4. Establecer los listeners, para controlar la captura de imágenes o grabación de vídeo.
5. Guardar los archivos generados.
6. Desbloquear (liberar) la cámara después de usarla, para que otras aplicaciones
puedan usarla, puesto que el hardware de la cámara es un recurso compartido.

Detección y acceso al hardware de la cámara

Para detectar en tiempo de ejecución si el dispositivo tiene cámara o no, así como si tiene
autofocus, flash o cámara frontal, se accederá a gestor de paquetes del contexto 6 o,
directamente, a la Cámara:

// Se comprueba que el dispositivo tiene cámara


boolean mHasCamera =
getPackageManager().hasSystemFeature(
PackageManager.FEATURE_CAMERA);
// Se comprueba que el dispositivo tiene autofocus
boolean mHasAutofocus =
getPackageManager().hasSystemFeature(
PackageManager.FEATURE_CAMERA_AUTOFOCUS);
// Se comprueba que el dispositivo tiene flash
boolean mHasFlash =
getPackageManager().hasSystemFeature(
PackageManager.FEATURE_CAMERA_FLASH);
// Se comprueba que el dispositivo tiene cámara frontal
boolean mHasFrontCamera =
getPackageManager().hasSystemFeature(
PackageManager.FEATURE_CAMERA_FRONT);
// Número de cámaras del dispositivo (solo API Level >= 9)
mNumberOfCameras = Camera.getNumberOfCameras();

6
PackageManager es una clase que devuelve distintos tipos de información relacionados con los paquetes de
aplicación instalados en el dispositivo, muchos de los cuales están dedicados a gestionar diversos componentes de
hardware del dispositivo.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 23


TEMA 15. MULTIMEDIA Y CÁMARA

Una vez que se ha comprobado que el dispositivo tiene cámara(s), se deberá acceder a la
misma a través de una instancia de la clase Camera. La generación de la instancia (a través de
Camera.open()) puede lanzar una excepción que deberá ser capturada para evitar un FC
(force close) de la aplicación:

Camera c = null;
try {
// Se intenta obtener una instancia de la clase Camera
c = Camera.open();
}
catch (Exception e){
// La cámara no está disponible (está en uso o no existe)
// Toast(…);
}

Después de obtener el acceso a la cámara, se podrá conseguir más información sobre sus
funcionalidades y capacidades por medio del objeto de tipo Camera.Parametes que devuelve
el método Camera.getParameters(). A partir de Android 2.3 se puede averiguar si la cámara
es frontal o no, así como la orientación de la imagen, a través del método
Camera.getCameraInfo().

Creación de una clase de previsualización

Para mostrar la imagen en tiempo real que captura la cámara, es necesario crear una clase de
previsualización que extienda de SurfaceView y que será incluida en un layout. La clase
deberá implementar la interfaz SurfaceHolder.Callback para capturar los eventos de
creación, modificación o destrucción de la View contenida en el layout, ya que es necesario
para iniciar o reiniciar la previsualización de la cámara.

/** Clase para previsualizar la imagen de la cámara */


public class CameraPreview extends SurfaceView implements
SurfaceHolder.Callback {
private static final String TAG_ERROR = "CameraPreview-ERROR";
private SurfaceHolder mHolder;
private Camera mCamera;
private Camera.Size mSize;

public CameraPreview(Context context, Camera camera) {


super(context);
// Se recibe una implementación de Camera no nula
mCamera = camera;
// Se añade esta clase, que implementa SurfaceHolder.Callback,
// al SurfaceHolder para que notifique a esta clase cuándo
// cambia la superficie (surface).
mHolder = getHolder();
mHolder.addCallback(this);

CURSO DE DESARROLLO DE APLICACIONES ANDROID 24


TEMA 15. MULTIMEDIA Y CÁMARA

// Deprecado, pero necesario para versiones anteriores a


// Android 3.0
mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}

@Override
public void surfaceCreated(SurfaceHolder holder) {
// La superficie se ha creado, se dice a la cámara dónde mostrar
// la previsualización
try {
mCamera.setPreviewDisplay(holder);
mCamera.startPreview();
} catch (IOException e) {
Log.d(TAG_ERROR,
"Error estableciendo la previsualización de la cámara: " +
e.getMessage());
}
}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
// Es mejor liberar la previsualización de la cámara en la
// actividad.
// mCamera.stopPreview();
// mCamera.release();
// mCamera = null;
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int w,
int h) {
// Se capturarán eventos de cambio de tamaño o rotación de la
// superficie de la previsualización de la cámara.
// Es necesario parar la previsualización antes de redimensionar
// o cambiar sus proporciones.

// Este control ayuda a mostrar la previsualización en modo


// "portrait" (vertical). Solo en caso de que la orientación sea
// vertical, height > weight, y solo en ese caso se rota.
// (Esto es debido a la orientación por defecto que espera la
// cámara)
if (h > w)
mCamera.setDisplayOrientation(90);

if (mHolder.getSurface() == null)
// No existe superficie de previsualización
return;

// Se para la previsualización antes de hacer ningún cambio


try {
mCamera.stopPreview();
} catch (Exception e) {
// Excepción ignorada. Se ha intentado parar la
// previsualización pero no existe.
}

// Se establece el tamaño de la previsualización y sus proporciones.


// La cámara es accesible (ya que se construye esta clase

CURSO DE DESARROLLO DE APLICACIONES ANDROID 25


TEMA 15. MULTIMEDIA Y CÁMARA

// pasando una cámara accesible como parámetro en el


// constructor)
Camera.Parameters parameters = mCamera.getParameters();

// Se elige el tamaño de previsualización óptimo que soporta la


// cámara
Size optimalSize = getPreviewSize(sizes, w, h);
if (optimalSize != null) {
parameters.setPreviewSize(optimalSize.width, optimalSize.height);
mCamera.setParameters(parameters);
}

// Se inicia la previsualización de nuevo


try {
mCamera.setPreviewDisplay(mHolder);
mCamera.startPreview();
} catch (Exception e) {
Log.d(TAG_ERROR,
"Error iniciando la previsualzación de la cámara: " +
e.getMessage());
}
}

private Size getPreviewSize(int width, int height) {


Camera.Size result = null;
Camera.Parameters p = mCamera.getParameters();
for (Camera.Size size : p.getSupportedPreviewSizes()) {
if (size.width <= width && size.height <= height) {
if (result == null) {
result = size;
} else {
int resultArea = result.width * result.height;
int newArea = size.width * size.height;

if (newArea > resultArea) {


result = size;
}
}
}
}
return result;
}
}

CURSO DE DESARROLLO DE APLICACIONES ANDROID 26


TEMA 15. MULTIMEDIA Y CÁMARA

Construcción del layout de previsualización

Para usar la clase CameraPreview implementada en la sección anterior se podrá diseñar por
ejemplo un layout que incluya un FrameLayout el cual actuará como contenedor de la clase de
previsualización. Al utilizar un FrameLayout se podrán superponer los botones necesarios para
controlar la cámara, así como información adicional.

Por ejemplo, se puede crear un sencillo layout similar a este:

<?xml version="1.0" encoding="utf-8"?>


<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:baselineAligned="false" >

<FrameLayout
android:id="@+id/cameraPreview"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
</FrameLayout>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:orientation="horizontal"
android:padding="10dp" >

<Button
android:id="@+id/buttonCancel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_weight="1"
android:onClick="cancel"
android:text="@string/cancel" />

<Button
android:id="@+id/buttonCapture"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:layout_marginRight="20dp"
android:layout_weight="1"
android:onClick="shoot"
android:text="@string/shoot" />
</LinearLayout>

</RelativeLayout>

CURSO DE DESARROLLO DE APLICACIONES ANDROID 27


TEMA 15. MULTIMEDIA Y CÁMARA

La actividad que controlará CameraPreview tendrá un método onCreate() similar a este:

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.camera_preview_layout);

// Creación de una instancia de la cámara


Camera c = null;
try {
mCamera = Camera.open(); // Se intenta obtener una instancia de
// la clase Camera
}
catch (Exception e){
// La cámara no está disponible (está en uso o no existe)
// TODO: Avisar al usuario (Toast)
}

if (mCamera != null) {
// Se crea la previsualización y se asocia al FrameLayout
try {
mCameraPreview = new CameraPreview(this, mCamera);
} catch (InstantiationException e) {
Log.d(TAG_ERROR,
"Error asociando previsualización de cámara al layout: " +
e.getMessage());
}

FrameLayout preview =
(FrameLayout) findViewById(R.id.cameraPreview);
preview.addView(mCameraPreview);
}
}

También es importante que el método onPause() de la actividad libere el recurso Camera para
que otras aplicaciones puedan usarla:

@Override
public void onPause() {
if (mCamera != null) {
mCamera.release();
mCamera = null;
super.onPause(); // Es importante invocar a super() al final
}
}

CURSO DE DESARROLLO DE APLICACIONES ANDROID 28


TEMA 15. MULTIMEDIA Y CÁMARA

Grabación de imágenes

Una vez construida la clase que previsualiza la imagen que captura la cámara, así como el
layout que contiene dicha previsualización, se deberán añadir los listeners necesarios en la
interfaz de usuario para realizar las fotos.

Para obtener la imagen que capture la cámara, será necesario invocar al método
Camera.takePicture(). Este método recibe diversos parámetros siendo el último de ellos el
más importante ya que ha de ser una instancia de la interfaz Camera.PictureCallback. El
método callback que implementa esta interfaz, onPictureTaken() es el método que será
invocado cuando se realice una foto, y será el método donde se implemente la grabación de la
foto en una ubicación del dispositivo.

public void shoot(View v) {


mCamera.takePicture(null, null, mPictureCallback);
// Se reinicia la previsualización de la cámara. Si no se reiniciara,
// se mostraría la foto realizada en la previsualización.
mCamera.startPreview();
}

// Interfaz para grabar la foto en JPEG


private PictureCallback mPictureCallback = new PictureCallback() {

@Override
public void onPictureTaken(byte[] data, Camera camera) {

File pictureFile =
FileUtils.getExternalMediaFile(FileUtils.MEDIA_TYPE_IMAGE);
if (pictureFile == null) {
Log.d(TAG_ERROR, "Error creando el archivo de imagen.
Comprueba que se tiene permiso de acceso al
almacenamiento.");
return;
}

try {
FileOutputStream fos = new FileOutputStream(pictureFile);
fos.write(data);
fos.close();
} catch (FileNotFoundException e) {
Log.d(TAG_ERROR, "Archivo no encontrado: " + e.getMessage());
} catch (IOException e) {
Log.d(TAG_ERROR, "Error al acceder al archivo: " +
e.getMessage());
}
}
};

(La clase FileUtils contiene los métodos de gestión de archivos ya utilizados en un tema
anterior.)

CURSO DE DESARROLLO DE APLICACIONES ANDROID 29


TEMA 15. MULTIMEDIA Y CÁMARA

Grabación de vídeo

La grabación de vídeo requiere una gestión cuidadosa y ordenada de la instancia de Camera así
como una coordinación con la clase MediaRecorder, ya que se deberá permitir a dicha clase
acceder al hardware de la cámara, lo cual implicará la gestión de los métodos Camera.lock(),
Camera.unlock() 7, Camera.open() y Camera.release().

Para grabar vídeo se deberán realizar los siguientes pasos.

1. Abrir la cámara, con el método Camera.open(), obteniendo una instancia de Camera.


2. Conectar la previsualización, de tipo SurfaceView a la cámara, a través del método
Camera.setPreviewDisplay().
3. Iniciar la previsualización, invocando a Camera.startPreview().
4. Iniciar la grabación de vídeo, realizando los siguientes pasos en orden:
4.1. Desbloquear la cámara, para que pueda ser usada por MediaRecorder, invocando al
método Camera.unlock().
4.2. Configurar MediaRecorder, invocando los siguientes métodos en el orden
especificado:
4.2.1. setCamera(): se establece la cámara que se usará, y que será la instancia
previamente obtenida.
4.2.2. setAudioSource(): se establece la fuente de audio, que será
MediaRecorder.AudioSource.CAMCORDER.
4.2.3. setVideoSource(): se establece la fuente de vídeo, que será
MediaRecorder.VideoSource.CAMERA.
4.2.4. Se establece el formato de salida, así como la codificación.
4.2.4.1. Para nivel de API 8 o superior, se usará el método
MediaRecorder.setProfile() que recibirá como parámetro un
perfil obtenido vía CamcorderProfile.get().
4.2.4.2. Para niveles de API previos a 8, se deberán establecer los
parámetros de codificación manualmente a sus valores por defecto
o a los siguientes valores:
 setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
 setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
 setVideoEncoder(MediaRecorder.VideoEncoder.MPEG_4_SP)
4.2.5. setOutputFile(): se establece el archivo que contendrá el vídeo grabado.
Deberá ser un objeto File.toString().
4.2.6. setPreviewDisplay(): se establece la superficie (SurfaceView) que se
usará para previsualizar el vídeo y que coincidirá con la instancia utilizada en
el punto 2.
4.3. Preparar MediaRecorder, con los parámetros que se han configurado, invocando a
MediaRecorder.prepare().
4.4. Iniciar MediaRecorder, invocando el método MediaRecorder.start().

7
A partir de Android 4.0, las llamadas a Camera.lock() y Camera.unlock() son gestionadas automáticamente por
el sistema, por lo que, a la hora de grabar vídeo, se podrán obviar los pasos 4.1 y 5.4, en caso de que
MediaRecorder.prepare() no falle.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 30


TEMA 15. MULTIMEDIA Y CÁMARA

5. Parar la grabación de vídeo, invocando a los siguientes métodos en el orden especificado:


5.1. Parar MediaRecorder, invocando MediaRecorder.stop().
5.2. Reinicializar MediaRecorder, invocando MediaRecorder.reset() que eliminará
los parámetros que se hayan configurado.
5.3. Liberar MediaRecorder, invocando MediaRecorder.release().
5.4. Bloquear la cámara, invocando Camera.lock() para que pueda ser usada por
MediaRecorder posteriormente. A partir de Android 4.0 no es necesario bloquear la
cámara a no ser que la llamada a MediaRecorder.prepare() falle.
6. Finalizar la previsualización, invocando a Camera.stopPreview().
7. Liberar la cámara, invocando a Camera.release().

En caso de que la aplicación esté dedicada a grabar vídeo, se podrá invocar a


setRecordingHint(true) justo antes de inicializar la previsualización (punto 3) para reducir
el tiempo necesario para comenzar la grabación.

El siguiente método muestra cómo configurar y preparar correctamente MediaRecorder. Se


trata de un método que se implementaría en una clase de previsualización
MediaRecorderPreview, que extenderá de SurfaceView e implementará
SurfaceHolder.Callback, del mismo modo que la clase que CameraPreview, mostrada
anteriormente.

private void prepareMediaRecorder() {

mCamera = getCameraInstance();
state = MediaRecorderState.INITIAL;

try {
// Se activa la opción de Autofocus en la cámara, en caso de que
// esté disponible
Camera.Parameters params = mCamera.getParameters();
List<String> focusModes = params.getSupportedFocusModes();
if (focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
// La cámara tiene Autofocus, se establece.
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
// Se añade a los parámetros de la cámara
mCamera.setParameters(params);
}

// 2: Se conecta la previsualización
mCamera.setPreviewDisplay(mSurfaceHolder);
// 3: Se inicia la previsualización
mCamera.startPreview();
} catch (IOException ioe) {
Log.e(TAG_ERROR, "Error al establecer la previsualización
para el vídeo: " + ioe.getMessage());
ioe.printStackTrace();
}

mMediaRecorder = new MediaRecorder();

CURSO DE DESARROLLO DE APLICACIONES ANDROID 31


TEMA 15. MULTIMEDIA Y CÁMARA

// Se pueden añadir listener de errores e info a MediaRecorder

// 4.1 y 4.2.1: Se desbloquea la cámara y se asocia a MediaRecorder


mCamera.unlock();
mMediaRecorder.setCamera(mCamera);

// 4.2.2 y 4.2.3: Se establecen las fuentes que se usarán


mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);

state = MediaRecorderState.INITIALIZED;

// 4.2.4: Se establece perfil para la cámara de vídeo (para nivel de


// API inferior a 8, de forma manual)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO)
mMediaRecorder.setProfile(CamcorderProfile.get(
CamcorderProfile.QUALITY_HIGH));
else {
// Formato de salida MPEG_4 y codificadores de audio y vídeo por
// defecto
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
mMediaRecorder.setVideoEncoder(
MediaRecorder.VideoEncoder.MPEG_4_SP);
}

state = MediaRecorderState.DATA_SOURCE_CONFIGURED;

// 4.2.5: Se asocia el archivo que contendrá el vídeo


mOutputFile = FileUtils.getExternalMediaFile(
FileUtils.MEDIA_TYPE_VIDEO).toString();
mMediaRecorder.setOutputFile(mOutputFile);

// (Opcional)
mMediaRecorder.setVideoFrameRate(25);
}

Una vez configurado MediaRecorder será necesario esperar a que se cree la superficie que
muestra la previsualización para asignársela y finalizar así la preparación de la videocámara. Se
deberá implementar, por lo tanto, el método callback surfaceCreated() con un código
similar al siguiente:

CURSO DE DESARROLLO DE APLICACIONES ANDROID 32


TEMA 15. MULTIMEDIA Y CÁMARA

public void surfaceCreated(SurfaceHolder holder) {

if (mMediaRecorder != null) {
try {
// 4.2.6: Se establece la Surface para previsualizar el vídeo.
// No se asocia el display hasta que la superficie es creada.
mMediaRecorder.setPreviewDisplay(holder.getSurface());

// 4.3: Se prepara MediaRecorder para comprobar e implementar las


// configuraciones anteriores:
mMediaRecorder.prepare();

state = MediaRecorderState.PREPARED;

} catch (IllegalStateException e) {
Log.d(TAG_DEBUG, "IllegalStateException preparando MediaRecorder:
" + e.getMessage());
releaseMediaRecorder();
} catch (IOException e) {
Log.d(TAG_DEBUG, "IOException preparando MediaRecorder: "
+ e.getMessage());
releaseMediaRecorder();
}
}
}

Cuando MediaRecorder está preparado correctamente, sólo quedará invocar a su método


start() a través de algún evento que desencadene la interfaz de usuario.

Para finalizar la grabación del vídeo, bastará invocar a su método stop() y liberar la cámara,
así como otros recursos, en caso de que no se vayan a grabar más vídeos:

public void releaseMediaRecorder() {

if (mMediaRecorder != null) {
// Se elimina la configuración de la cámara de vídeo
mMediaRecorder.reset();
state = MediaRecorderState.INITIAL;
mMediaRecorder.release(); // Se libera el objeto
state = MediaRecorderState.RELEASED;
mMediaRecorder = null;
state = MediaRecorderState.NULL;
mCamera.lock(); // Se bloquea la cámara para futuros usos
}
}

Por último, es importante tener en cuenta que los métodos de MediaRecorder hacen que este
adopte uno u otro estado y que, por lo tanto, solo se pueden invocar ciertos métodos cuando
MediaRecorder adopta dichos estados, según el siguiente diagrama:

CURSO DE DESARROLLO DE APLICACIONES ANDROID 33


TEMA 15. MULTIMEDIA Y CÁMARA

CURSO DE DESARROLLO DE APLICACIONES ANDROID 34

Вам также может понравиться