מבוא ל-ARCore Recording and Playback API

1. מבוא

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

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

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

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

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

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

לא. כן.

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

רק לקרוא לקרוא ולהשלים את התרגילים

מה תפַתחו

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

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

הדרישות

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

דרישות חומרה

דרישות תוכנה

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

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

קודם כול, צריך להגדיר את סביבת הפיתוח.

הורדת ARCore Android SDK

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

ביטול הדחיסה של ARCore Android SDK

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

hello-ar-java-extracted

טעינת Hello AR Java ב-Android Studio

מפעילים את Android Studio ולוחצים על Open an existing Android Studio project (פתיחת פרויקט קיים של Android Studio).

android-studio-open-projects

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

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

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

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

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

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

בשלב הבא, תקליטו סשן AR לקובץ MP4.

3. הקלטה של פעילות ArCore לקובץ MP4

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

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

הוספת ממשק משתמש לכפתור ההקלטה

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

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

activity_main-xml-location-in-project

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

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

השאר הוא רק קוד שחוזר על עצמו (boilerplate): הגדרה, רישום ביומן ובדיקה של הנכונות.

יוצרים פונקציה חדשה בשם 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. בחלונית Project ב-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 ...
  }

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

הגיע הזמן לראות מה בנינו עד עכשיו. מחברים את המכשיר הנייד למחשב הפיתוח ולוחצים על Run (הפעלה) ב-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;
    }
  }

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

הוספת פונקציה 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, משתמשים ב-ACTION_OPEN_DOCUMENT Intent.

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

מחליפים את הפונקציה 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);
  }

הפעלת האפליקציה כדי להתחיל בהשמעה

כדי להפעיל קובץ MP4, צריך לבצע שלוש קריאות ל-API של ARCore:

  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() בשרשור UI.

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

הפעלה חוזרת ממכשיר היעד

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

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

playback-button.png

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

show-internal-storage-button.png

nativate-to-movies-file-picker.jpg

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

playback-stop-button.png

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

playback-placement

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

  • הוספנו כפתור להפעלה ולהפסקה של ההשמעה
  • הטמענו פונקציה שמאפשרת להתחיל ולהפסיק את ההקלטה באפליקציה
  • הפעלה חוזרת של פעילות 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 ...
  }

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

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

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

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

זה כל הקוד שתצטרכו לכתוב בסדנת הקוד הזו.

6. מזל טוב

כל הכבוד, הגעתם לסוף ה-Codelab הזה! בואו נסתכל על מה שעשיתם ב-Codelab הזה:

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

נהניתם מה-Codelab הזה?

כן לא

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

כן לא

האם סיימת ליצור את האפליקציה בסדנת הקוד הזו?

כן לא