Introducción a las API de Recording y Playback de ARCore

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?

No

¿Cómo usarás este codelab?

Solo lo leeré Lo leeré y completaré los ejercicios

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

Requisitos de software

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.

hello-ar-java-extracted

Carga Hello AR Java en Android Studio

Inicia Android Studio y haz clic en Open an existing Android Studio project.

android-studio-open-projects

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

  1. Conecta un dispositivo compatible con ARCore a la máquina de desarrollo.
  2. Si el dispositivo se reconoce correctamente, deberías ver su nombre en Android Studio. android-studio-pixel-5.png
  3. Haz clic en el botón Run o selecciona Run > Run “app” para que Android Studio instale y ejecute la app en el dispositivo. android-studio-run-button.png
  4. 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. hello-ar-java-permission
  5. Mueve el dispositivo horizontalmente para buscar planos.
  6. Cuando la app detecta un plano, aparece una cuadrícula blanca. Presiónala para colocar un marcador en él. Ubicación de RA en Hello

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.

activity_main-xml-location-in-project

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.

swith-to-the-code-view.png

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:

  1. Especifica el URI del archivo de grabación en un objeto RecordingConfig.
  2. Llama a session.startRecording con el objeto RecordingConfig.

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.

app-build.gradle.png

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.

record-button.png

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 dispositivo
  • adb 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.

activity_main-xml-location-in-project

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:

  1. session.pause()
  2. session.setPlaybackDataset()
  3. 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:

  1. El usuario detuvo la reproducción de MP4.
  2. 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.

playback-button.png

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.

show-internal-storage-button.png

nativate-to-movies-file-picker.jpg

Presiona un nombre de archivo en la pantalla para seleccionar el archivo MP4. La app debería reproducirlo.

playback-stop-button.png

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.

playback-placement

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

¿Te pareció divertido este codelab?

No

¿Aprendiste algo útil en este codelab?

No

¿Completaste la creación de la app en este codelab?

No