1. Introducción
La capacidad de guardar una experiencia de RA en un archivo MP4 y reproducirlo puede ser útil para los desarrolladores de apps y los usuarios finales.
Depura y prueba nuevas funciones desde tu escritorio
Los desarrolladores son los usuarios más directos de las API de Recording y Playback de ARCore. Ya no es necesario compilar y ejecutar una app en un dispositivo de prueba, desconectar el cable USB y caminar solo para probar un pequeño cambio de código. Ahora, solo debes grabar un archivo MP4 en el entorno de prueba con los movimientos previstos del teléfono y realizar las pruebas desde tu escritorio.
Graba y reproduce contenido desde diferentes dispositivos
Con las API de Recording y Playback, un usuario puede grabar una sesión con un dispositivo y otro reproducirla en un dispositivo diferente. Es posible compartir una experiencia de RA con otro usuario. Existen muchas posibilidades.
¿Es la primera vez que creas una app de ARCore?
¿Cómo usarás este codelab?
Qué compilarás
En este codelab, usarás las API de Recording y Playback para crear una app que registre una experiencia de RA en un archivo MP4 y reproduzca la experiencia desde el mismo archivo. Aprenderás a hacer lo siguiente:
- Usar la API de Recording para guardar una sesión de RA en un archivo MP4
- Usar la API de Playback para reproducir una sesión de RA desde un archivo MP4
- Grabar una sesión de RA en un dispositivo y volver a reproducirla en otro
Requisitos
En este codelab, modificarás la app de Hello AR Java, que se compiló mediante el SDK de ARCore para Android. Para continuar, necesitarás hardware y software específicos.
Requisitos de hardware
- Un dispositivo compatible con ARCore que tenga activadas las Opciones para desarrolladores y tenga habilitada la depuración por USB, conectado a través de un cable USB a la máquina de desarrollo
- Una máquina de desarrollo para ejecutar Android Studio
- Acceso a Internet para descargar bibliotecas durante el desarrollo
Requisitos de software
- Servicios de Google Play para RA (ARCore) 1.24 o una versión posterior en el dispositivo de desarrollo de ARCore (por lo general, se instalan automáticamente en el dispositivo a través de Play Store, pero también puedes instalarlos de forma manual en los dispositivos compatibles con ARCore)
- Android Studio (3.1 o una versión posterior) en la máquina de desarrollo
También debes tener conocimientos básicos de ARCore para obtener los mejores resultados.
2. Configura el entorno de desarrollo
Primero, configura el entorno de desarrollo.
Descarga el SDK de ARCore para Android
Haz clic en para descargar el SDK.
Descomprime el SDK de ARCore para Android
Una vez que hayas descargado el SDK para Android en la máquina, descomprime el archivo y navega hasta el directorio arcore-android-sdk-1.24/samples/hello_ar_java
. Este es el directorio raíz de la app con la que trabajarás.
Carga Hello AR Java en Android Studio
Inicia Android Studio y haz clic en Open an existing Android Studio project.
En la ventana de diálogo que aparece, selecciona arcore-android-sdk-1.24/samples/hello_ar_java
y haz clic en Open.
Espera a que Android Studio termine de sincronizar el proyecto. Si falta un componente, es posible que la importación del proyecto falle con mensajes de error. Soluciona estos problemas antes de continuar.
Ejecuta la app de muestra
- Conecta un dispositivo compatible con ARCore a la máquina de desarrollo.
- Si el dispositivo se reconoce correctamente, deberías ver su nombre en Android Studio.
- Haz clic en el botón Run o selecciona Run > Run “app” para que Android Studio instale y ejecute la app en el dispositivo.
- Se te pedirá permiso para tomar fotos y grabar video. Selecciona Mientras la app está en uso (While using the app) para otorgarle permiso a la app de Cámara. Luego, verás el entorno real en la pantalla del dispositivo.
- Mueve el dispositivo horizontalmente para buscar planos.
- Cuando la app detecta un plano, aparece una cuadrícula blanca. Presiónala para colocar un marcador en él.
Qué hiciste en este paso
- Configurar el proyecto Hello AR Java
- Compilar y ejecutar la app de muestra en un dispositivo compatible con ARCore
A continuación, grabarás una sesión de RA en un archivo MP4.
3. Graba una sesión de ARCore en un archivo MP4
En este paso, agregaremos la función de grabación, que consta de los siguientes elementos:
- Un botón para iniciar o detener la grabación
- Funciones de almacenamiento para guardar el archivo MP4 en el dispositivo
- Llamadas para iniciar o detener la grabación de la sesión de ARCore
Agrega a la IU un botón para grabar
Antes de implementar la grabación, agrega un botón a la IU para que el usuario pueda informarle a ARCore cuándo comenzar o detener la grabación.
En el panel Project, abre el archivo app/res/layout/activity_main.xml
.
De forma predeterminada, Android Studio usará la vista de diseño después de abrir el archivo app/res/layout/activity_main.xml
. Haz clic en el botón Code, ubicado en la esquina superior derecha de la pestaña, para cambiar a la vista de código.
En activity_main.xml
, agrega el siguiente código antes de la etiqueta de cierre para crear el nuevo botón Record y configurar su controlador de eventos en un método llamado onClickRecord()
:
<!--
Add a new "Record" button with those attributes:
text is "Record",
onClick event handler is "onClickRecord",
text color is "red".
-->
<Button
android:id="@+id/record_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@id/surfaceview"
android:layout_alignBottom="@id/surfaceview"
android:layout_marginBottom="100dp"
android:onClick="onClickRecord"
android:text="Record"
android:textColor="@android:color/holo_red_light" />
Después de agregar el código anterior, es posible que se muestre un error temporalmente: Corresponding method handler 'public void onClickRecord(android.view.View)' not found"
. Esta situación es esperable. Crearás la función onClickRecord()
en los próximos pasos para solucionar el error.
Cambia el texto del botón según el estado
El botón Record controla la grabación y la detención. Cuando la app no esté grabando datos, deberá mostrar la palabra “Record”. Cuando la app esté grabando datos, el botón debería cambiar para mostrar la palabra “Stop”.
Para implementar esta funcionalidad en el botón, la app debe conocer su estado actual. El siguiente código crea una nueva enum llamada AppState
para representar el estado de trabajo de la app y realiza un seguimiento de los cambios de estado específicos mediante una variable de miembro privada llamada appState
. Agrégalo a HelloArActivity.java
, al comienzo de la clase HelloArActivity
.
// Represents the app's working state.
public enum AppState {
Idle,
Recording
}
// Tracks app's specific state changes.
private AppState appState = AppState.Idle;
Ahora que puedes hacer un seguimiento del estado interno de la app, crea una función llamada updateRecordButton()
que cambie el texto del botón según el estado actual de la app. Agrega el siguiente código a la clase HelloArActivity
de HelloArActivity.java
.
// Add imports to the beginning of the file.
import android.widget.Button;
// Update the "Record" button based on app's internal state.
private void updateRecordButton() {
View buttonView = findViewById(R.id.record_button);
Button button = (Button) buttonView;
switch (appState) {
case Idle:
button.setText("Record");
break;
case Recording:
button.setText("Stop");
break;
}
}
A continuación, crea el método onClickRecord()
que verifique el estado de la app, cambie al siguiente y llame a updateRecordButton()
para modificar la IU del botón. Agrega el siguiente código a la clase HelloArActivity
de HelloArActivity.java
.
// Handle the "Record" button click event.
public void onClickRecord(View view) {
Log.d(TAG, "onClickRecord");
// Check the app's internal state and switch to the new state if needed.
switch (appState) {
// If the app is not recording, begin recording.
case Idle: {
boolean hasStarted = startRecording();
Log.d(TAG, String.format("onClickRecord start: hasStarted %b", hasStarted));
if (hasStarted)
appState = AppState.Recording;
break;
}
// If the app is recording, stop recording.
case Recording: {
boolean hasStopped = stopRecording();
Log.d(TAG, String.format("onClickRecord stop: hasStopped %b", hasStopped));
if (hasStopped)
appState = AppState.Idle;
break;
}
default:
// Do nothing.
break;
}
updateRecordButton();
}
Habilita la app para comenzar a grabar
Para comenzar a grabar en ARCore, solo debes realizar las siguientes dos acciones:
- Especifica el URI del archivo de grabación en un objeto
RecordingConfig
. - Llama a
session.startRecording
con el objetoRecordingConfig
.
El resto es solo código estándar: configuración, registro y verificación de la precisión.
Crea una nueva función llamada startRecording()
que registre datos y los guarde en un URI de MP4. Agrega el siguiente código a la clase HelloArActivity
de HelloArActivity.java
.
// Add imports to the beginning of the file.
import android.net.Uri;
import com.google.ar.core.RecordingConfig;
import com.google.ar.core.RecordingStatus;
import com.google.ar.core.exceptions.RecordingFailedException;
private boolean startRecording() {
Uri mp4FileUri = createMp4File();
if (mp4FileUri == null)
return false;
Log.d(TAG, "startRecording at: " + mp4FileUri);
pauseARCoreSession();
// Configure the ARCore session to start recording.
RecordingConfig recordingConfig = new RecordingConfig(session)
.setMp4DatasetUri(mp4FileUri)
.setAutoStopOnPause(true);
try {
// Prepare the session for recording, but do not start recording yet.
session.startRecording(recordingConfig);
} catch (RecordingFailedException e) {
Log.e(TAG, "startRecording - Failed to prepare to start recording", e);
return false;
}
boolean canResume = resumeARCoreSession();
if (!canResume)
return false;
// Correctness checking: check the ARCore session's RecordingState.
RecordingStatus recordingStatus = session.getRecordingStatus();
Log.d(TAG, String.format("startRecording - recordingStatus %s", recordingStatus));
return recordingStatus == RecordingStatus.OK;
}
Para pausar y reanudar una sesión de ARCore de forma segura, crea pauseARCoreSession()
y resumeARCoreSession()
en HelloArActivity.java
.
private void pauseARCoreSession() {
// Pause the GLSurfaceView so that it doesn't update the ARCore session.
// Pause the ARCore session so that we can update its configuration.
// If the GLSurfaceView is not paused,
// onDrawFrame() will try to update the ARCore session
// while it's paused, resulting in a crash.
surfaceView.onPause();
session.pause();
}
private boolean resumeARCoreSession() {
// We must resume the ARCore session before the GLSurfaceView.
// Otherwise, the GLSurfaceView will try to update the ARCore session.
try {
session.resume();
} catch (CameraNotAvailableException e) {
Log.e(TAG, "CameraNotAvailableException in resumeARCoreSession", e);
return false;
}
surfaceView.onResume();
return true;
}
Habilita la app para detener la grabación
Crea una función llamada stopRecording()
en HelloArActivity.java
para evitar que tu app grabe datos nuevos. Esta función llama a session.stopRecording()
y envía un error al registro de la consola si la app no puede detener la grabación.
private boolean stopRecording() {
try {
session.stopRecording();
} catch (RecordingFailedException e) {
Log.e(TAG, "stopRecording - Failed to stop recording", e);
return false;
}
// Correctness checking: check if the session stopped recording.
return session.getRecordingStatus() == RecordingStatus.NONE;
}
Diseña el almacenamiento de archivos según el almacenamiento específico de Android 11
Las funciones relacionadas con el almacenamiento de este codelab están diseñadas según los nuevos requisitos de almacenamiento específico de Android 11.
Realiza algunos cambios pequeños en el archivo app/build.gradle
para segmentar la app a Android 11. En el panel Project de Android Studio, este archivo se encuentra en el nodo Gradle Scripts, asociado con el módulo app.
Cambia compileSdkVersion
y targetSdkVersion
a 30.
compileSdkVersion 30
defaultConfig {
targetSdkVersion 30
}
Para grabar, usa la API de Android MediaStore a fin de crear el archivo MP4 en el directorio compartido Movie.
Crea una función llamada createMp4File()
en HelloArActivity.java
:
// Add imports to the beginning of the file.
import java.text.SimpleDateFormat;
import android.content.ContentResolver;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import android.content.ContentValues;
import java.io.File;
import android.content.CursorLoader;
import android.database.Cursor;
import java.util.Date;
private final String MP4_VIDEO_MIME_TYPE = "video/mp4";
private Uri createMp4File() {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
String mp4FileName = "arcore-" + dateFormat.format(new Date()) + ".mp4";
ContentResolver resolver = this.getContentResolver();
Uri videoCollection = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
videoCollection = MediaStore.Video.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL_PRIMARY);
} else {
videoCollection = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
}
// Create a new Media file record.
ContentValues newMp4FileDetails = new ContentValues();
newMp4FileDetails.put(MediaStore.Video.Media.DISPLAY_NAME, mp4FileName);
newMp4FileDetails.put(MediaStore.Video.Media.MIME_TYPE, MP4_VIDEO_MIME_TYPE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// The Relative_Path column is only available since API Level 29.
newMp4FileDetails.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES);
} else {
// Use the Data column to set path for API Level <= 28.
File mp4FileDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
String absoluteMp4FilePath = new File(mp4FileDir, mp4FileName).getAbsolutePath();
newMp4FileDetails.put(MediaStore.Video.Media.DATA, absoluteMp4FilePath);
}
Uri newMp4FileUri = resolver.insert(videoCollection, newMp4FileDetails);
// Ensure that this file exists and can be written.
if (newMp4FileUri == null) {
Log.e(TAG, String.format("Failed to insert Video entity in MediaStore. API Level = %d", Build.VERSION.SDK_INT));
return null;
}
// This call ensures the file exist before we pass it to the ARCore API.
if (!testFileWriteAccess(newMp4FileUri)) {
return null;
}
Log.d(TAG, String.format("createMp4File = %s, API Level = %d", newMp4FileUri, Build.VERSION.SDK_INT));
return newMp4FileUri;
}
// Test if the file represented by the content Uri can be open with write access.
private boolean testFileWriteAccess(Uri contentUri) {
try (java.io.OutputStream mp4File = this.getContentResolver().openOutputStream(contentUri)) {
Log.d(TAG, String.format("Success in testFileWriteAccess %s", contentUri.toString()));
return true;
} catch (java.io.FileNotFoundException e) {
Log.e(TAG, String.format("FileNotFoundException in testFileWriteAccess %s", contentUri.toString()), e);
} catch (java.io.IOException e) {
Log.e(TAG, String.format("IOException in testFileWriteAccess %s", contentUri.toString()), e);
}
return false;
}
Administra los permisos de almacenamiento
Si usas un dispositivo con Android 11, puedes comenzar a probar el código. Si quieres admitir dispositivos con Android 10 o versiones anteriores, deberás otorgarle a la app permisos de almacenamiento para guardar datos en el sistema de archivos del dispositivo de destino.
En AndroidManifest.xml
, declara que la app necesita permisos de lectura y escritura para el almacenamiento antes de Android 11 (nivel de API 30).
<!-- Inside the <manifest> tag, below the existing Camera permission -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
Agrega una función auxiliar llamada checkAndRequestStoragePermission()
a HelloArActivity.java
para solicitar el permiso WRITE_EXTERNAL_STORAGE
durante el tiempo de ejecución.
// Add imports to the beginning of the file.
import android.Manifest;
import android.content.pm.PackageManager;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
private final int REQUEST_WRITE_EXTERNAL_STORAGE = 1;
public boolean checkAndRequestStoragePermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
REQUEST_WRITE_EXTERNAL_STORAGE);
return false;
}
return true;
}
Si tienes el nivel de API 29 o inferior, agrega una verificación para los permisos de almacenamiento en la parte superior de createMp4File()
y sal de la función antes si la app no tiene los permisos correctos. El nivel de API 30 (Android 11) no requiere permisos de almacenamiento para acceder a los archivos de MediaStore.
private Uri createMp4File() {
// Since we use legacy external storage for Android 10,
// we still need to request for storage permission on Android 10.
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
if (!checkAndRequestStoragePermission()) {
Log.i(TAG, String.format(
"Didn't createMp4File. No storage permission, API Level = %d",
Build.VERSION.SDK_INT));
return null;
}
}
// ... omitted code ...
}
Graba desde el dispositivo de destino
Es hora de ver lo que compilaste hasta ahora. Conecta el dispositivo móvil a la máquina de desarrollo y haz clic en Run en Android Studio.
Deberías ver un botón rojo que indica Record en el lado inferior izquierdo de la pantalla. Al presionarlo, el texto debería cambiar a Stop. Mueve el dispositivo para grabar una sesión y haz clic en el botón Stop cuando quieras completar la grabación. Se debería guardar un archivo nuevo llamado arcore-xxxxxx_xxxxxx.mp4
en el almacenamiento externo del dispositivo.
Ahora, deberías tener un nuevo archivo arcore-xxxxxx_xxxxxx.mp4
en el almacenamiento externo del dispositivo. En dispositivos Pixel 5, la ruta es /storage/emulated/0/Movies/
. Puedes encontrar la ruta en la ventana Logcat después de iniciar una grabación.
com.google.ar.core.examples.java.helloar D/HelloArActivity: startRecording at:/storage/emulated/0/Movies/arcore-xxxxxxxx_xxxxxx.mp4 com.google.ar.core.examples.java.helloar D/HelloArActivity: startRecording - RecordingStatus OK
Ve la grabación
Puedes usar una app de sistema de archivos, como Files de Google, para ver la grabación o copiarla en la máquina de desarrollo. A continuación, se muestran los dos comandos de adb para enumerar y recuperar archivos del dispositivo Android:
adb shell ls '$EXTERNAL_STORAGE/Movies/*'
para mostrar los archivos del directorio Movies en el almacenamiento externo del dispositivoadb pull /absolute_path_from_previous_adb_shell_ls/arcore-xxxxxxxx_xxxxxx.mp4
para copiar el archivo del dispositivo a la máquina de desarrollo
Este es un resultado de ejemplo después de usar estos dos comandos (en macOS):
$ adb shell ls '$EXTERNAL_STORAGE/Movies/*' /sdcard/Movies/arcore-xxxxxxxx_xxxxxx.mp4 $ adb pull /sdcard/Movies/arcore-xxxxxxxx_xxxxxx.mp4 /sdcard/Movies/arcore-xxxxxxxx_xxxxxx.mp4: ... pulled
Qué hiciste en este paso
- Agregar un botón para iniciar y detener la grabación
- Implementar funciones para iniciar y detener la grabación
- Probar la app en el dispositivo
- Copiar el MP4 grabado en la máquina y verificarlo
A continuación, reproducirás una sesión de RA desde un archivo MP4.
4. Reproduce una sesión de ARCore desde un archivo MP4
Ahora, tienes el botón Record y algunos archivos MP4 con sesiones grabadas. Los reproducirás con la API de Playback de ARCore.
Agrega a la IU el botón para reproducir
Antes de implementar la reproducción, agrega un botón a la IU para que el usuario pueda informarle a ARCore cuándo debe comenzar y dejar de reproducir la sesión.
En el panel Project, abre el archivo app/res/layout/activity_main.xml
.
En activity_main.xml
, agrega el siguiente código antes de la etiqueta de cierre para crear el nuevo botón Playback y establecer su controlador de eventos en un método llamado onClickPlayback()
. Este botón será similar al botón Record y se mostrará en el lado derecho de la pantalla.
<!--
Add a new "Playback" button with those attributes:
text is "Playback",
onClick event handler is "onClickPlayback",
text color is "green".
-->
<Button
android:id="@+id/playback_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignEnd="@id/surfaceview"
android:layout_alignBottom="@id/surfaceview"
android:layout_marginBottom="100dp"
android:onClick="onClickPlayback"
android:text="Playback"
android:textColor="@android:color/holo_green_light" />
Actualiza los botones durante la reproducción
La app ahora tiene un estado nuevo llamado Playingback
. Actualiza la enum AppState
y todas las funciones existentes que reciben appState
como argumento para controlar el estado.
Agrega Playingback
a la enum AppState
en HelloArActivity.java
:
public enum AppState {
Idle,
Recording,
Playingback // New enum value.
}
Si el botón Record sigue en pantalla durante la reproducción, el usuario podría presionarlo por accidente. Para evitarlo, oculta el botón Record durante la reproducción. De esta manera, no necesitas controlar el estado de Playingback
en onClickRecord()
.
Modifica la función updateRecordButton()
en HelloArActivity.java
para ocultar el botón Record cuando la app esté en el estado Playingback
.
// Update the "Record" button based on app's internal state.
private void updateRecordButton() {
View buttonView = findViewById(R.id.record_button);
Button button = (Button)buttonView;
switch (appState) {
// The app is neither recording nor playing back. The "Record" button is visible.
case Idle:
button.setText("Record");
button.setVisibility(View.VISIBLE);
break;
// While recording, the "Record" button is visible and says "Stop".
case Recording:
button.setText("Stop");
button.setVisibility(View.VISIBLE);
break;
// During playback, the "Record" button is not visible.
case Playingback:
button.setVisibility(View.INVISIBLE);
break;
}
}
De manera similar, oculta el botón Playback cuando el usuario esté grabando una sesión y cámbialo para que diga “Stop” cuando el usuario esté reproduciendo una sesión de forma activa. De esta manera, puede detener una reproducción sin tener que esperar a que se complete por sí sola.
Agrega una función updatePlaybackButton()
a HelloArActivity.java
:
// Update the "Playback" button based on app's internal state.
private void updatePlaybackButton() {
View buttonView = findViewById(R.id.playback_button);
Button button = (Button)buttonView;
switch (appState) {
// The app is neither recording nor playing back. The "Playback" button is visible.
case Idle:
button.setText("Playback");
button.setVisibility(View.VISIBLE);
break;
// While playing back, the "Playback" button is visible and says "Stop".
case Playingback:
button.setText("Stop");
button.setVisibility(View.VISIBLE);
break;
// During recording, the "Playback" button is not visible.
case Recording:
button.setVisibility(View.INVISIBLE);
break;
}
}
Por último, actualiza onClickRecord()
para que llame a updatePlaybackButton()
. Agrega la siguiente línea a HelloArActivity.java
:
public void onClickRecord(View view) {
// ... omitted code ...
updatePlaybackButton(); // Add this line to the end of the function.
}
Selecciona un archivo con el botón Playback
Cuando se presiona, el botón Playback debe permitir que el usuario seleccione un archivo para reproducir. En Android, la selección de archivos se realiza mediante el selector de archivos del sistema en otra actividad a través del framework de acceso al almacenamiento (SAF). Una vez que el usuario selecciona un archivo, la app recibe una devolución de llamada denominada onActivityResult()
. Iniciarás la reproducción real en esta función de devolución de llamada.
En HelloArActivity.java
, crea una función onClickPlayback()
para seleccionar el archivo y detener la reproducción.
// Handle the click event of the "Playback" button.
public void onClickPlayback(View view) {
Log.d(TAG, "onClickPlayback");
switch (appState) {
// If the app is not playing back, open the file picker.
case Idle: {
boolean hasStarted = selectFileToPlayback();
Log.d(TAG, String.format("onClickPlayback start: selectFileToPlayback %b", hasStarted));
break;
}
// If the app is playing back, stop playing back.
case Playingback: {
boolean hasStopped = stopPlayingback();
Log.d(TAG, String.format("onClickPlayback stop: hasStopped %b", hasStopped));
break;
}
default:
// Recording - do nothing.
break;
}
// Update the UI for the "Record" and "Playback" buttons.
updateRecordButton();
updatePlaybackButton();
}
En HelloArActivity.java
, crea una función selectFileToPlayback()
que seleccione un archivo del dispositivo. Para seleccionar un archivo del sistema de archivos de Android, usa un intent ACTION_OPEN_DOCUMENT
.
// Add imports to the beginning of the file.
import android.content.Intent;
import android.provider.DocumentsContract;
private boolean selectFileToPlayback() {
// Start file selection from Movies directory.
// Android 10 and above requires VOLUME_EXTERNAL_PRIMARY to write to MediaStore.
Uri videoCollection;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
videoCollection = MediaStore.Video.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL_PRIMARY);
} else {
videoCollection = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
}
// Create an Intent to select a file.
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
// Add file filters such as the MIME type, the default directory and the file category.
intent.setType(MP4_VIDEO_MIME_TYPE); // Only select *.mp4 files
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, videoCollection); // Set default directory
intent.addCategory(Intent.CATEGORY_OPENABLE); // Must be files that can be opened
this.startActivityForResult(intent, REQUEST_MP4_SELECTOR);
return true;
}
REQUEST_MP4_SELECTOR
es una constante que permite identificar esta solicitud. Puedes definirla con cualquier valor de marcador de posición en la clase HelloArActivity
de HelloArActivity.java
:
private int REQUEST_MP4_SELECTOR = 1;
Anula la función onActivityResult()
de HelloArActivity.java
para administrar la devolución de llamada del selector de archivos.
// Begin playback once the user has selected the file.
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// Check request status. Log an error if the selection fails.
if (resultCode != android.app.Activity.RESULT_OK || requestCode != REQUEST_MP4_SELECTOR) {
Log.e(TAG, "onActivityResult select file failed");
return;
}
Uri mp4FileUri = data.getData();
Log.d(TAG, String.format("onActivityResult result is %s", mp4FileUri));
// Begin playback.
startPlayingback(mp4FileUri);
}
Habilita la app para iniciar la reproducción
Una sesión de ARCore requiere tres llamadas a la API para reproducir un archivo MP4:
session.pause()
session.setPlaybackDataset()
session.resume()
En HelloArActivity.java
, crea la función startPlayingback()
.
// Add imports to the beginning of the file.
import com.google.ar.core.PlaybackStatus;
import com.google.ar.core.exceptions.PlaybackFailedException;
private boolean startPlayingback(Uri mp4FileUri) {
if (mp4FileUri == null)
return false;
Log.d(TAG, "startPlayingback at:" + mp4FileUri);
pauseARCoreSession();
try {
session.setPlaybackDatasetUri(mp4FileUri);
} catch (PlaybackFailedException e) {
Log.e(TAG, "startPlayingback - setPlaybackDataset failed", e);
}
// The session's camera texture name becomes invalid when the
// ARCore session is set to play back.
// Workaround: Reset the Texture to start Playback
// so it doesn't crashes with AR_ERROR_TEXTURE_NOT_SET.
hasSetTextureNames = false;
boolean canResume = resumeARCoreSession();
if (!canResume)
return false;
PlaybackStatus playbackStatus = session.getPlaybackStatus();
Log.d(TAG, String.format("startPlayingback - playbackStatus %s", playbackStatus));
if (playbackStatus != PlaybackStatus.OK) { // Correctness check
return false;
}
appState = AppState.Playingback;
updateRecordButton();
updatePlaybackButton();
return true;
}
Habilita la app para detener la reproducción
Crea una función llamada stopPlayingback()
en HelloArActivity.java
para controlar los cambios de estado de la app después de las siguientes acciones:
- El usuario detuvo la reproducción de MP4.
- La reproducción de archivos MP4 se completó por sí sola.
Si el usuario detuvo la reproducción, la app debe volver al mismo estado que tenía cuando el usuario la inició por primera vez.
// Stop the current playback, and restore app status to Idle.
private boolean stopPlayingback() {
// Correctness check, only stop playing back when the app is playing back.
if (appState != AppState.Playingback)
return false;
pauseARCoreSession();
// Close the current session and create a new session.
session.close();
try {
session = new Session(this);
} catch (UnavailableArcoreNotInstalledException
|UnavailableApkTooOldException
|UnavailableSdkTooOldException
|UnavailableDeviceNotCompatibleException e) {
Log.e(TAG, "Error in return to Idle state. Cannot create new ARCore session", e);
return false;
}
configureSession();
boolean canResume = resumeARCoreSession();
if (!canResume)
return false;
// A new session will not have a camera texture name.
// Manually set hasSetTextureNames to false to trigger a reset.
hasSetTextureNames = false;
// Reset appState to Idle, and update the "Record" and "Playback" buttons.
appState = AppState.Idle;
updateRecordButton();
updatePlaybackButton();
return true;
}
Además, la reproducción puede detenerse naturalmente una vez que el reproductor llega al final del archivo MP4. Cuando esto sucede, stopPlayingback()
debe volver a cambiar el estado de la app a Idle
. En onDrawFrame()
, comprueba el PlaybackStatus
. Si es FINISHED
, llama a la función stopPlayingback()
en el subproceso de IU.
public void onDrawFrame(SampleRender render) {
// ... omitted code ...
// Insert before this line:
// frame = session.update();
// Check the playback status and return early if playback reaches the end.
if (appState == AppState.Playingback
&& session.getPlaybackStatus() == PlaybackStatus.FINISHED) {
this.runOnUiThread(this::stopPlayingback);
return;
}
// ... omitted code ...
}
Reproduce una sesión en el dispositivo de destino
Es hora de ver lo que compilaste hasta ahora. Conecta el dispositivo móvil a la máquina de desarrollo y haz clic en Run en Android Studio.
Cuando se inicie la app, deberías ver una pantalla con un botón rojo que indica Record a la izquierda y un botón verde que dice Playback a la derecha.
Presiona el botón Playback y selecciona uno de los archivos MP4 que acabas de grabar. Si no ves ningún nombre de archivo que comience con arcore-
, es posible que tu dispositivo no muestre la carpeta Movies. En este caso, navega a la carpeta Modelo del teléfono > Movies en el menú de la esquina superior izquierda. Es posible que también debas habilitar la opción Mostrar almacenamiento interno (Show internal storage) para ver la carpeta del modelo del teléfono.
Presiona un nombre de archivo en la pantalla para seleccionar el archivo MP4. La app debería reproducirlo.
La diferencia entre reproducir una sesión y reproducir un video común es que puedes interactuar con la sesión grabada. Presiona un plano detectado para colocar marcadores en la pantalla.
Qué hiciste en este paso
- Agregar un botón para iniciar y detener la reproducción
- Implementar una función para hacer que la app inicie y detenga la grabación
- Reproducir una sesión de ARCore grabada anteriormente en el dispositivo
5. Graba datos adicionales en el archivo MP4
Con ARCore 1.24, es posible registrar información adicional en el archivo MP4. Puedes registrar los atributos Pose
de las posiciones de los objetos de RA y, luego, durante la reproducción, crear los objetos en la misma ubicación.
Configura la pista nueva para grabar
Define un segmento nuevo con un UUID y una etiqueta MIME en HelloArActivity.java
.
// Add imports to the beginning of the file.
import java.util.UUID;
import com.google.ar.core.Track;
// Inside the HelloArActiity class.
private static final UUID ANCHOR_TRACK_ID = UUID.fromString("53069eb5-21ef-4946-b71c-6ac4979216a6");;
private static final String ANCHOR_TRACK_MIME_TYPE = "application/recording-playback-anchor";
private boolean startRecording() {
// ... omitted code ...
// Insert after line:
// pauseARCoreSession();
// Create a new Track, with an ID and MIME tag.
Track anchorTrack = new Track(session)
.setId(ANCHOR_TRACK_ID).
.setMimeType(ANCHOR_TRACK_MIME_TYPE);
// ... omitted code ...
}
Actualiza el código de salida para crear el objeto RecordingConfig
con una llamada a addTrack()
.
private boolean startRecording() {
// ... omitted code ...
// Update the lines below with a call to the addTrack() function:
// RecordingConfig recordingConfig = new RecordingConfig(session)
// .setMp4DatasetUri(mp4FileUri)
// .setAutoStopOnPause(true);
RecordingConfig recordingConfig = new RecordingConfig(session)
.setMp4DatasetUri(mp4FileUri)
.setAutoStopOnPause(true)
.addTrack(anchorTrack); // add the new track onto the recordingConfig
// ... omitted code ...
}
Guarda la pose del ancla durante la grabación
Cada vez que el usuario presiona un plano detectado, se coloca un marcador de RA en un objeto Anchor
, cuya posición se actualizará en ARCore.
Si aún estás grabando la sesión de ARCore, registra la pose de un Anchor
en el marco en el que se creó.
Modifica la función handleTap()
de HelloArActivity.java
.
// Add imports to the beginning of the file.
import com.google.ar.core.Pose;
import java.nio.FloatBuffer;
private void handleTap(Frame frame, Camera camera) {
// ... omitted code ...
// Insert after line:
// anchors.add(hit.createAnchor());
// If the app is recording a session,
// save the new Anchor pose (relative to the camera)
// into the ANCHOR_TRACK_ID track.
if (appState == AppState.Recording) {
// Get the pose relative to the camera pose.
Pose cameraRelativePose = camera.getPose().inverse().compose(hit.getHitPose());
float[] translation = cameraRelativePose.getTranslation();
float[] quaternion = cameraRelativePose.getRotationQuaternion();
ByteBuffer payload = ByteBuffer.allocate(4 * (translation.length + quaternion.length));
FloatBuffer floatBuffer = payload.asFloatBuffer();
floatBuffer.put(translation);
floatBuffer.put(quaternion);
try {
frame.recordTrackData(ANCHOR_TRACK_ID, payload);
} catch (IllegalStateException e) {
Log.e(TAG, "Error in recording anchor into external data track.", e);
}
}
// ... omitted code ...
}
El motivo por el que conservamos la Pose
relativa de la cámara en lugar de la Pose
relativa mundial es que el origen mundial de una sesión de grabación y el de una de reproducción no son los mismos. El origen mundial de una sesión de grabación comienza la primera vez que se reanuda la sesión, cuando se llama a Session.resume()
por primera vez. El origen mundial de una sesión de reproducción comienza cuando se registra el primer fotograma, cuando se llama a Session.resume()
por primera vez después de Session.startRecording()
.
Crea anclas de reproducción
Volver a crear un archivo Anchor
es sencillo. Agrega una función llamada createRecordedAnchors()
a HelloArActivity.java
.
// Add imports to the beginning of the file.
import com.google.ar.core.TrackData;
// Extract poses from the ANCHOR_TRACK_ID track, and create new anchors.
private void createRecordedAnchors(Frame frame, Camera camera) {
// Get all `ANCHOR_TRACK_ID` TrackData from the frame.
for (TrackData trackData : frame.getUpdatedTrackData(ANCHOR_TRACK_ID)) {
ByteBuffer payload = trackData.getData();
FloatBuffer floatBuffer = payload.asFloatBuffer();
// Extract translation and quaternion from TrackData payload.
float[] translation = new float[3];
float[] quaternion = new float[4];
floatBuffer.get(translation);
floatBuffer.get(quaternion);
// Transform the recorded anchor pose
// from the camera coordinate
// into world coordinates.
Pose worldPose = camera.getPose().compose(new Pose(translation, quaternion));
// Re-create an anchor at the recorded pose.
Anchor recordedAnchor = session.createAnchor(worldPose);
// Add the new anchor into the list of anchors so that
// the AR marker can be displayed on top.
anchors.add(recordedAnchor);
}
}
Llama a createRecordedAnchors()
en la función onDrawFrame()
en HelloArActivity.java
.
public void onDrawFrame(SampleRender render) {
// ... omitted code ...
// Insert after this line:
// handleTap(frame, camera);
// If the app is currently playing back a session, create recorded anchors.
if (appState == AppState.Playingback) {
createRecordedAnchors(frame, camera);
}
// ... omitted code ...
}
Haz pruebas en el dispositivo de destino
Conecta el dispositivo móvil a la máquina de desarrollo y haz clic en Run en Android Studio.
Primero, presiona el botón Record para grabar una sesión. Durante la grabación, presiona los planos detectados para colocar algunos marcadores de RA.
Después de que se detenga la grabación, presiona el botón Playback y selecciona el archivo que acabas de grabar. La reproducción debería comenzar. Observa cómo aparecen las ubicaciones anteriores de los marcadores de RA cuando presionas la app.
Eso es todo lo que tendrás que programar en este codelab.
6. Felicitaciones
Felicitaciones, llegaste al final de este codelab. En él, hiciste lo siguiente:
- Compilar y ejecutar la muestra de Hello AR Java de ARCore
- Agregar un botón de grabación a la app para guardar una sesión de RA en un archivo MP4
- Agregar un botón de reproducción a la app para mostrar una sesión de RA desde un archivo MP4
- Agregar una nueva función para guardar las anclas que creó el usuario en el archivo MP4 a fin de reproducirlas