מבוא ל-ARCore Recording and Playback API

1. מבוא

היכולת לשמור חוויית AR בקובץ MP4 ולהשמיע קובצי MP4 יכולה להועיל גם למפתחי האפליקציות וגם למשתמשי הקצה.

ניפוי באגים ובדיקה של תכונות חדשות בשולחן העבודה

השימוש הישיר ביותר ברשומת ARCore ממשק ה-API להפעלה מיועד למפתחים. חלפו הימים שבהם הייתם צריכים לבנות ולהפעיל את האפליקציה במכשיר בדיקה, לנתק את כבל ה-USB ולהסתובב כדי לבדוק שינוי קטן בקוד. עכשיו צריך להקליט רק MP4 בסביבת הבדיקה עם התזוזה הצפויה של הטלפון, ולבדוק את זה ישירות מהמחשב.

הקלטה והפעלה ממכשירים שונים

באמצעות ממשקי ה-API להקלטה ולהפעלה, משתמש אחד יכול להקליט סשן באמצעות מכשיר אחד, ומשתמש אחר יכול להפעיל את אותו סשן במכשיר אחר. אפשר לשתף חוויית AR עם משתמש אחר. אפשרויות רבות.

זאת הפעם הראשונה שיצרת אפליקציית ARCore?

לא. כן.

איך בכוונתך להשתמש ב-Codelab הזה?

לקריאה בלבד לקרוא אותו ולבצע את התרגילים

מה תפַתחו

ב-Codelab הזה תשתמשו בהקלטה ממשק API להפעלה כדי ליצור אפליקציה שגם מקליטה חוויית AR בקובץ MP4 וגם מפעילה את החוויה מאותו קובץ. תוכלו ללמוד:

  • איך משתמשים ב- Recording API כדי לשמור סשן AR בקובץ MP4.
  • איך להשתמש ב-Activate API כדי להפעיל מחדש סשן AR מקובץ MP4.
  • איך להקליט סשן ב-AR במכשיר אחד ולהפעיל אותו מחדש במכשיר אחר.

למה תזדקק?

ב-Codelab הזה, משנים את האפליקציה Hello AR Java, שמבוססת על ARCore Android SDK. יש צורך בחומרה ותוכנה ספציפיות כדי לעקוב אחריהן.

דרישות החומרה

דרישות תוכנה

בנוסף, כדי לקבל את התוצאות הכי טובות, צריכה להיות לכם הבנה בסיסית של ARCore.

2. הגדרת סביבת הפיתוח

מתחילים בהגדרה של סביבת הפיתוח.

הורדת ARCore Android SDK

לוחצים על כדי להוריד את ה-SDK.

ביטול קובץ ZIP של Android SDK ב-ARCore

אחרי שמורידים את Android SDK למחשב, מחלצים את הקובץ ועוברים לספרייה arcore-android-sdk-1.24/samples/hello_ar_java. זאת תיקיית השורש של האפליקציה שאיתה רוצים לעבוד.

hello-ar-java-extracted

טעינה של Hello AR Java ל-Android Studio

מפעילים את Android Studio ולוחצים על פתיחת פרויקט קיים של Android Studio.

android-studio-open-projects

בחלון שנפתח, בוחרים את arcore-android-sdk-1.24/samples/hello_ar_java ולוחצים על Open.

ממתינים עד שסנכרון הפרויקט יסתיים ב-Android Studio. אם חסר רכיב, ייתכן שייבוא הפרויקט ייכשל ויופיעו הודעות שגיאה. צריך לפתור את הבעיות האלה לפני שממשיכים.

הרצת האפליקציה לדוגמה

  1. חיבור מכשיר שנתמך ב-ARCore למכונת הפיתוח.
  2. אם המכשיר מזוהה כראוי, שם המכשיר אמור להופיע ב-Android Studio. android-studio-pixel-5.png
  3. לחצו על הלחצן 'הפעלה' או בחרו באפשרות הפעלה > מריצים את 'אפליקציה' כדי להתקין את Android Studio ולהפעיל את האפליקציה במכשיר. android-studio-run-button.png
  4. תוצג לכם הודעה שמבקשת מכם הרשאה לצלם תמונות וסרטונים. בוחרים באפשרות בזמן השימוש באפליקציה הזו כדי להעניק לאפליקציה הרשאות גישה למצלמה. לאחר מכן תופיע במסך המכשיר את הסביבה בעולם האמיתי. hello-ar-java-permission
  5. צריך להזיז את המכשיר לרוחב כדי לחפש מטוסים.
  6. כשהאפליקציה מזהה מטוס מופיעה רשת לבנה. מקישים עליה כדי להציב סמן במטוס הזה. שלום, מיקום מודעה ב-AR

מה עשית בשלב הזה

  • הגדרת פרויקט Hello AR Java
  • יצירה והרצה של האפליקציה לדוגמה במכשיר שנתמך ב-ARCore

בשלב הבא, המערכת תקליט סשן ב-AR לקובץ MP4.

3. הקלטה של סשן ARCore לקובץ MP4

אנחנו נוסיף את תכונת ההקלטה בשלב הזה. הוא מורכב מ:

  • לחצן להפעלה או להפסקה של ההקלטה.
  • פונקציות אחסון לשמירת קובץ ה-MP4 במכשיר.
  • שיחות להפעלה או להפסקה של הקלטת סשן ARCore.

הוספת ממשק משתמש ללחצן ההקלטה

לפני שמטמיעים את ההקלטה, צריך להוסיף לחצן לממשק המשתמש כדי שהמשתמש יוכל להודיע ל-ARCore מתי להתחיל או להפסיק את ההקלטה.

בחלונית הפרויקט, פותחים את הקובץ app/res/layout/activity_main.xml.

activity_main-xml-location-in-project

כברירת מחדל, מערכת Android Studio תשתמש בתצוגת העיצוב אחרי שפותחים את הקובץ app/res/layout/activity_main.xml. לוחצים על הלחצן Code (קוד) בפינה השמאלית העליונה של הכרטיסייה כדי לעבור לתצוגת הקוד.

swith-to-the-code-view.png

ב-activity_main.xml, צריך להוסיף את הקוד הבא לפני תג הסגירה כדי ליצור את הלחצן Record החדש ולהגדיר את הגורם המטפל באירועים שלו לשיטה שנקראת 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" />

אחרי הוספת הקוד שלמעלה, יכול להיות שתוצג באופן זמני הודעת שגיאה: Corresponding method handler 'public void onClickRecord(android.view.View)' not found". זו תופעה נורמלית. כדי לפתור את השגיאה, צריך ליצור את הפונקציה onClickRecord() בשלבים הבאים.

שינוי הטקסט בלחצן בהתאם למצב

הלחצן הקלטה מטפל בפועל גם בהקלטה וגם בעצירה. כשהאפליקציה לא מתעדת נתונים, היא צריכה להציג את המילה 'הקלטה'. כשהאפליקציה מקליטה נתונים, הלחצן אמור להשתנות כדי להציג את המילה 'עצירה'.

כדי להפעיל את הלחצן הזה, האפליקציה צריכה לדעת מה המצב הנוכחי שלה. הקוד הבא יוצר enum חדש בשם AppState כדי לייצג את מצב הפעילות של האפליקציה, ועוקב אחר שינויים ספציפיים במצב באמצעות משתנה חבר פרטי בשם appState. צריך להוסיף אותו אל HelloArActivity.java, בתחילת הכיתה HelloArActivity.

  // Represents the app's working state.
  public enum AppState {
    Idle,
    Recording
  }

  // Tracks app's specific state changes.
  private AppState appState = AppState.Idle;

עכשיו אפשר לעקוב אחרי המצב הפנימי של האפליקציה, ואז ליצור פונקציה בשם updateRecordButton() שמשנה את הטקסט של הלחצן בהתאם למצב הנוכחי של האפליקציה. צריך להוסיף את הקוד הבא במחלקה HelloArActivity ב-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;
    }
  }

בשלב הבא, יוצרים את השיטה onClickRecord() שבודקת את מצב האפליקציה, משנה אותה למצב הבא, ושולחת קריאה ל-updateRecordButton() כדי לשנות את ממשק המשתמש של הלחצן. צריך להוסיף את הקוד הבא במחלקה HelloArActivity ב-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();
  }

צריך להפעיל את האפליקציה כדי להתחיל להקליט

כדי להתחיל להקליט ב-ARCore, צריך לבצע שתי פעולות בלבד:

  1. מציינים את ה-URI של קובץ ההקלטה באובייקט RecordingConfig.
  2. קריאה אל session.startRecording עם האובייקט RecordingConfig

השאר הוא רק קוד סטנדרטי: הגדרה, רישום ביומן ובדיקה של נכונות הנתונים.

יוצרים פונקציה חדשה בשם startRecording() שמקליטה נתונים ושומרת אותם ב-URI MP4. צריך להוסיף את הקוד הבא במחלקה HelloArActivity ב-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;
  }

כדי להשהות ולהמשיך סשן ARCore בבטחה, צריך ליצור את pauseARCoreSession() ואת resumeARCoreSession() ב-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;
  }

כדי להפסיק את ההקלטה, צריך להפעיל את האפליקציה

יוצרים פונקציה בשם stopRecording() ב-HelloArActivity.java כדי להפסיק את ההקלטה של נתונים חדשים מהאפליקציה. הפונקציה הזו מפעילה את session.stopRecording() ושולחת שגיאה ליומן המסוף אם האפליקציה לא יכולה להפסיק את ההקלטה.

  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;
  }

עיצוב אחסון קבצים באמצעות אחסון בהיקף של Android 11

הפונקציות שקשורות לאחסון ב-Codelab הזה תוכננו בהתאם לדרישות החדשות של Android 11 לגבי אחסון בהיקף.

צריך לבצע כמה שינויים קטנים בקובץ app/build.gradle כדי לטרגט את Android 11. בחלונית של פרויקט Android Studio, הקובץ הזה נמצא בצומת Gradle Scripts, שמשויך למודול app.

app-build.gradle.png

משנים את compileSdkVersion ואת targetSdkVersion ל-30.

    compileSdkVersion 30
    defaultConfig {
      targetSdkVersion 30
    }

להקלטה, משתמשים ב-Android MediaStore API כדי ליצור את קובץ ה-MP4 בספריית הסרטים המשותפת.

יוצרים פונקציה בשם createMp4File() ב-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;
  }

טיפול בהרשאות האחסון

אם אתם משתמשים במכשיר Android 11, אתם יכולים להתחיל לבדוק את הקוד. כדי לתמוך במכשירים עם Android מגרסה 10 ומטה, צריך להעניק הרשאות לאחסון האפליקציה כדי לשמור נתונים במערכת הקבצים של מכשיר היעד.

ב-AndroidManifest.xml, צריך להצהיר שהאפליקציה צריכה הרשאות קריאה וכתיבה באחסון לפני Android 11 (רמת 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" />

צריך להוסיף פונקציית עזר בשם checkAndRequestStoragePermission() ב-HelloArActivity.java כדי לבקש את ההרשאות WRITE_EXTERNAL_STORAGE במהלך זמן ריצה.

// 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;
  }

אם אתם משתמשים ברמת API 29 או גרסאות קודמות, צריך להוסיף בדיקה של הרשאות האחסון בחלק העליון של createMp4File() ולצאת מהפונקציה בשלב מוקדם אם אין לאפליקציה את ההרשאות המתאימות. ברמת API 30 (Android 11) לא נדרשת הרשאת אחסון כדי לגשת לקבצים ב-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 ...
  }

הקלטה ממכשיר היעד

הגיע הזמן לראות מה יצרת עד עכשיו. מחברים את המכשיר הנייד למחשב הפיתוח ולוחצים על הפעלה ב-Android Studio.

אמור להופיע לחצן הקלטה אדום בפינה הימנית התחתונה של המסך. הקשה עליו אמורה לשנות את הטקסט לעצירה. מזיזים את המכשיר כדי להקליט סשן, ולוחצים על הלחצן עצירה כשרוצים להשלים את ההקלטה. הפעולה הזו אמורה לשמור קובץ חדש בשם arcore-xxxxxx_xxxxxx.mp4 באחסון החיצוני של המכשיר.

record-button.png

עכשיו אמור להיות קובץ arcore-xxxxxx_xxxxxx.mp4 חדש באחסון החיצוני של המכשיר. במכשירי Pixel 5, הנתיב הוא /storage/emulated/0/Movies/. אחרי שמתחילים הקלטה, אפשר למצוא את הנתיב בחלון Logcat.

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

צפייה בהקלטה

אפשר להשתמש באפליקציה של מערכת קבצים כמו Files by Google כדי לצפות בהקלטה, או להעתיק אותה למחשב הפיתוח. בהמשך מוצגות שתי פקודות adb להצגת קבצים ולאחזור שלהם ממכשיר Android:

  • adb shell ls '$EXTERNAL_STORAGE/Movies/*' כדי להציג את הקבצים בספריית הסרטים באחסון חיצוני במכשיר
  • adb pull /absolute_path_from_previous_adb_shell_ls/arcore-xxxxxxxx_xxxxxx.mp4 כדי להעתיק את הקובץ מהמכשיר למחשב הפיתוח

זאת דוגמה לפלט אחרי השימוש בשתי הפקודות האלה (מ-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

מה עשית בשלב הזה

  • נוסף לחצן כדי להתחיל ולהפסיק את ההקלטה
  • פונקציות יושמו להפעלה ולהפסקה של ההקלטה
  • האפליקציה נבדקה במכשיר
  • העתקת קובץ ה-MP4 המוקלט למחשב ואימות שלו

בשלב הבא, מפעילים סשן AR מקובץ MP4.

4. הפעלת סשן ARCore מקובץ MP4

עכשיו יש לך לחצן הקלטה וחלק מקובצי MP4 עם הפעלות מוקלטות. עכשיו, ניתן להשמיע אותם שוב באמצעות ARCore Playback API.

הוספת ממשק משתמש ללחצן ההפעלה

לפני הטמעת ההפעלה, יש להוסיף לחצן בממשק המשתמש כדי שהמשתמש יוכל להודיע ל-ARCore מתי הוא צריך להתחיל ולהפסיק את ההפעלה.

בחלונית Project, פותחים את הקובץ app/res/layout/activity_main.xml.

activity_main-xml-location-in-project

ב-activity_main.xml, צריך להוסיף את הקוד למטה לפני התג הסוגר כדי ליצור את לחצן ההפעלה החדש ולהגדיר את הגורם המטפל באירועים שלו לשיטה שנקראת onClickPlayback(). הלחצן הזה יהיה דומה ללחצן הקלטה ויוצג בצד שמאל של המסך.

  <!--
    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" />

לחצני עדכון במהלך ההפעלה

מצב האפליקציה החדש נקרא Playingback. מעדכנים את enum AppState ואת כל הפונקציות הקיימות שמשתמשים ב-appState כארגומנט כדי לטפל בכך.

מוסיפים את Playingback ל-enum AppState ב-HelloArActivity.java:

  public enum AppState {
    Idle,
    Recording,
    Playingback // New enum value.
  }

אם הלחצן הקלטה עדיין מוצג במסך במהלך ההפעלה, המשתמשים עלולים ללחוץ עליו בטעות. כדי למנוע זאת, צריך להסתיר את לחצן ההקלטה במהלך ההפעלה. כך אין צורך לטפל במצב Playingback בonClickRecord().

צריך לשנות את הפונקציה updateRecordButton() ב-HelloArActivity.java כדי להסתיר את הלחצן Record כשהאפליקציה במצב 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;
    }
  }

באופן דומה, יש להסתיר את לחצן ההפעלה כשהמשתמש מקליט סשן, ולהחליף אותו ל-"Stop" (עצירה). כשהמשתמש מפעיל סשן באופן פעיל. כך, הם יכולים להפסיק את ההפעלה בלי לחכות שהיא תסתיים בעצמם.

מוסיפים פונקציית updatePlaybackButton() ב-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;
    }
  }

בסוף, יש לעדכן את onClickRecord() כדי להתקשר אל updatePlaybackButton(). צריך להוסיף את השורה הבאה אל HelloArActivity.java:

  public void onClickRecord(View view) {
    // ... omitted code ...
    updatePlaybackButton(); // Add this line to the end of the function.
  }

בחירת קובץ באמצעות לחצן ההפעלה

כשמקישים עליו, הלחצן הפעלה צריך לאפשר למשתמש לבחור קובץ להפעלה. ב-Android, בחירת הקבצים מתבצעת דרך בורר קובצי המערכת בפעילות אחרת. זאת דרך Storage Access Framework (SAF). אחרי שהמשתמש בוחר קובץ, האפליקציה מקבלת קריאה חוזרת (callback) בשם 'onActivityResult()'. ההפעלה תתבצע בתוך פונקציית הקריאה החוזרת.

ב-HelloArActivity.java, צריך ליצור פונקציית onClickPlayback() כדי לבחור את הקובץ ולהפסיק את ההפעלה שלו.

  // 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();
  }

ב-HelloArActivity.java, יוצרים פונקציית selectFileToPlayback() שבוחרת קובץ מהמכשיר. כדי לבחור קובץ מ-Android File System, משתמשים ב-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 הוא קבוע לזיהוי הבקשה הזו. אפשר להגדיר אותו באמצעות כל ערך placeholder בתוך HelloArActivity ב-HelloArActivity.java:

  private int REQUEST_MP4_SELECTOR = 1;

כדי לטפל בקריאה חוזרת (callback) מבורר הקבצים, צריך לשנות את הפונקציה onActivityResult() ב-HelloArActivity.java.

  // 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);
  }

כדי להתחיל בהפעלה, צריך להפעיל את האפליקציה

בסשן ARCore נדרשות שלוש קריאות ל-API כדי להפעיל קובץ MP4:

  1. session.pause()
  2. session.setPlaybackDataset()
  3. session.resume()

ב-HelloArActivity.java, יוצרים את הפונקציה 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;
  }

כדי להפסיק את ההפעלה, צריך להפעיל את האפליקציה

יוצרים פונקציה בשם stopPlayingback() ב-HelloArActivity.java כדי לטפל בשינויים במצב האפליקציה אחרי:

  1. הפעלת ה-MP4 הופסקה על ידי המשתמש
  2. הפעלת MP4 הושלמה

אם המשתמש הפסיק את ההפעלה, האפליקציה אמורה לחזור למצב שהיה בו כשהמשתמש הפעיל אותה לראשונה.

  // 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;
  }

ההפעלה יכולה גם להפסיק באופן טבעי כשהנגן מגיע לסוף קובץ ה-MP4. במקרה כזה, stopPlayingback() צריך לשנות את מצב האפליקציה בחזרה ל-Idle. בonDrawFrame(), צריך לבדוק את PlaybackStatus. אם הוא FINISHED, צריך להפעיל את הפונקציה stopPlayingback() בשרשור של ממשק המשתמש.

  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 ...
  }

הפעלה ממכשיר היעד

הגיע הזמן לראות מה יצרת עד עכשיו. מחברים את המכשיר הנייד למחשב הפיתוח ולוחצים על הפעלה ב-Android Studio.

כשהאפליקציה מופעלת, אמור להופיע מסך עם לחצן הקלטה אדום בצד שמאל ולחצן הפעלה ירוק בצד שמאל.

playback-button.png

מקישים על הלחצן הפעלה ובוחרים באחד מקובצי ה-MP4 שהקלטתם עכשיו. אם לא מופיעים שמות קבצים שמתחילים ב-arcore-, יכול להיות שלא מוצגת במכשיר התיקייה סרטים. במקרה זה, מנווטים אל דגם טלפון > סרטים באמצעות התפריט שבפינה הימנית העליונה. יכול להיות שתצטרכו גם להפעיל את האפשרות הצגת אחסון פנימי כדי לחשוף את תיקיית דגם הטלפון.

show-internal-storage-button.png

nativate-to-movies-file-picker.jpg

מקישים על שם קובץ במסך כדי לבחור את קובץ ה-MP4. האפליקציה אמורה להפעיל את קובץ ה-MP4.

playback-stop-button.png

ההבדל בין הפעלת סשן לבין הפעלת סרטון רגיל הוא שניתן לנהל אינטראקציה עם הסשן שהוקלט. מקישים על מטוס שזוהה כדי למקם סמנים במסך.

מיקום המודעה במהלך ההפעלה

מה עשית בשלב הזה

  • נוסף לחצן להפעלה ועצירה של ההפעלה
  • הטמענו פונקציה שמאפשרת להפעיל את האפליקציה ולהפסיק את ההקלטה שלה
  • הפעלת סשן ARCore שהוקלט בעבר במכשיר

5. הקלטת נתונים נוספים ב-MP4

עם ARCore 1.24 אפשר להקליט מידע נוסף בקובץ MP4. אפשר להקליט את ה-Pose של מיקומי האובייקטים ב-AR, ואז ליצור את האובייקטים של ה-AR באותו המיקום במהלך ההפעלה.

הגדרת הטראק החדש להקלטה

הגדרת טראק חדש עם UUID ותג MIME ב-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 ...
  }

מעדכנים את הקוד היציאה כדי ליצור את האובייקט RecordingConfig באמצעות קריאה ל-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 ...
  }

שמירה של תנוחת העוגן במהלך ההקלטה

בכל פעם שמשתמש יקיש על מטוס שזוהה, סמן AR ימוקם על Anchor, התנוחה שלו תעודכן על ידי ARCore.

אם עדיין מתעדים את הסשן ב-ARCore, צריך לצלם את המיקום של Anchor בפריים שבו הוא נוצר.

שינוי הפונקציה handleTap() ב-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 ...
  }

הסיבה לכך שאנחנו שומרים על Pose היחסית של המצלמה ולא על Pose יחסי בעולם, היא שהמקור העולמי של סשן ההקלטה שונה מהמקור העולמי של סשן ההפעלה. המקור העולמי של סשן הקלטה מתחיל בפעם הראשונה שהסשן מתחדש, ואז מתבצעת קריאה ראשונה אל Session.resume(). המקור העולמי של סשן הפעלה מתחיל כשמקליטים את הפריים הראשון, כאשר מתבצעת קריאה ראשונה של Session.resume() אחרי Session.startRecording().

יצירת עוגן להפעלה

קל ליצור מחדש Anchor. מוסיפים פונקציה בשם createRecordedAnchors() ב-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);
    }
  }

קוראים לפונקציה createRecordedAnchors() בפונקציה onDrawFrame() ב-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 ...
  }

בדיקה במכשיר היעד

מחברים את המכשיר הנייד למחשב הפיתוח ולוחצים על הפעלה ב-Android Studio.

קודם מקישים על הלחצן הקלטה כדי להקליט סשן. במהלך ההקלטה, מקישים על מטוסים שזוהו כדי למקם כמה סמני AR.

כשההקלטה נפסקת, מקישים על הלחצן הפעלה ובוחרים את הקובץ שהוקלט. ההפעלה אמורה להתחיל. שימו לב איך המיקומים של סמני ה-AR הקודמים מופיעים בדיוק כפי שהקשתם על האפליקציה.

זה כל התכנות שתצטרכו לעשות ב-Codelab הזה.

6. מזל טוב

מזל טוב, הגעת לסוף Codelab זה! נחזור על מה שעשיתם ב-Codelab הזה:

  • יצירה והרצה של הדוגמה Hello AR Java של ARCore.
  • נוסף לחצן 'הקלטה' לאפליקציה כדי לשמור סשן AR בקובץ MP4
  • נוסף לחצן הפעלה לאפליקציה כדי להשמיע סשן AR מקובץ MP4
  • נוספה תכונה חדשה לשמירת עוגנים שנוצרו על ידי המשתמש ב-MP4 לצורך הפעלה חוזרת

נהניתם לעשות את זה?

כן לא

למדתם משהו שימושי במהלך ה-Codelab הזה?

כן לא

האם השלמת את יצירת האפליקציה ב-Codelab הזה?

כן לא