ข้อมูลเบื้องต้นเกี่ยวกับ ARCore Recording and Playback API

1. บทนำ

การบันทึกประสบการณ์ AR เป็นไฟล์ MP4 และเล่นจากไฟล์ MP4 มีประโยชน์กับทั้งนักพัฒนาแอปและผู้ใช้ปลายทาง

แก้ไขข้อบกพร่องและทดสอบฟีเจอร์ใหม่ได้จากโต๊ะทำงาน

การใช้งานระเบียน ARCore ที่ง่ายที่สุด API การเล่นมีไว้สำหรับนักพัฒนา ไม่ต้องเสียเวลาไปกับการสร้างและเรียกใช้แอปบนอุปกรณ์ทดสอบ ถอดสาย USB และเดินไปรอบๆ เพียงเพื่อทดสอบการเปลี่ยนแปลงโค้ดเล็กๆ น้อยๆ ตอนนี้คุณเพียงแค่บันทึก MP4 ในสภาพแวดล้อมการทดสอบที่มีการเคลื่อนไหวของโทรศัพท์ที่คาดไว้ และทดสอบจากโต๊ะทำงานได้โดยตรง

บันทึกและเล่นจากอุปกรณ์เครื่องอื่น

เมื่อใช้ API การบันทึกและการเล่น ผู้ใช้คนหนึ่งจะสามารถบันทึกเซสชันโดยใช้อุปกรณ์หนึ่ง ส่วนอีกเครื่องหนึ่งสามารถเล่นเซสชันเดียวกันนั้นในอีกอุปกรณ์หนึ่งได้ คุณอาจแชร์ประสบการณ์ AR กับผู้ใช้รายอื่นได้ มีหลากหลายความเป็นไปได้!

นี่เป็นการสร้างแอป ARCore เป็นครั้งแรกใช่ไหม

ไม่ ได้

คุณจะใช้ Codelab นี้อย่างไร

โปรดอ่านเท่านั้น อ่านและทำแบบฝึกหัด

สิ่งที่คุณจะสร้าง

ใน Codelab นี้ คุณจะใช้การบันทึกและ API การเล่น เพื่อสร้างแอปที่บันทึกประสบการณ์ AR ลงในไฟล์ MP4 และเล่นไฟล์เดียวกันนั้น คุณจะได้เรียนรู้:

  • วิธีใช้ API การบันทึกเพื่อบันทึกเซสชัน AR เป็นไฟล์ MP4
  • วิธีใช้ 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 แล้วคลิกเปิดโปรเจ็กต์ 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 > (เรียกใช้) เรียกใช้ "แอป" เพื่อให้ Android Studio ติดตั้งและเปิดแอปในอุปกรณ์ android-studio-run-button.png
  4. คุณจะเห็นข้อความที่ขออนุญาตถ่ายภาพและบันทึกวิดีโอ เลือกขณะใช้แอปนี้เพื่อให้สิทธิ์กล้องถ่ายรูปแก่แอป จากนั้นคุณจะเห็นสภาพแวดล้อมในโลกจริงบนหน้าจออุปกรณ์ hello-ar-java-permission
  5. เลื่อนอุปกรณ์ในแนวนอนเพื่อสแกนหาเครื่องบิน
  6. ตารางกริดสีขาวจะปรากฏขึ้นเมื่อแอปตรวจพบเครื่องบิน แตะเพื่อวางเครื่องหมายบนระนาบนั้น ตำแหน่ง AR ของ Hello

สิ่งที่คุณดำเนินการในขั้นตอนนี้

  • ตั้งค่าโปรเจ็กต์ Hello AR Java
  • สร้างและเรียกใช้แอปตัวอย่างในอุปกรณ์ที่รองรับ ARCore

ขั้นตอนต่อไปคือการบันทึกเซสชัน AR เป็นไฟล์ MP4

3. บันทึกเซสชัน ARCore เป็นไฟล์ MP4

เราจะเพิ่มฟีเจอร์การบันทึกในขั้นตอนนี้ ซึ่งประกอบด้วย

  • ปุ่มสำหรับเริ่มหรือหยุดการบันทึก
  • ฟังก์ชันพื้นที่เก็บข้อมูลเพื่อบันทึกไฟล์ MP4 ในอุปกรณ์
  • เรียกใช้หรือหยุดการบันทึกเซสชัน ARCore

เพิ่ม UI สำหรับปุ่มบันทึก

ก่อนใช้การบันทึก ให้เพิ่มปุ่มใน UI เพื่อให้ผู้ใช้แจ้ง ARCore ว่าจะเริ่มหรือหยุดบันทึกได้เมื่อใด

เปิดไฟล์ 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 ให้เพิ่มโค้ดต่อไปนี้ก่อนแท็กปิดเพื่อสร้างปุ่มบันทึกใหม่ และตั้งค่าเครื่องจัดการเหตุการณ์เป็นเมธอดที่ชื่อ 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() เพื่อเปลี่ยน UI ของปุ่ม เพิ่มโค้ดต่อไปนี้ในชั้นเรียน 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();
  }

เปิดใช้แอปเพื่อเริ่มบันทึก

เพียงทำ 2 อย่างเพื่อเริ่มบันทึกใน 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-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 2 รายการสำหรับแสดงรายการและดึงไฟล์จากอุปกรณ์ Android มีดังนี้

  • adb shell ls '$EXTERNAL_STORAGE/Movies/*' เพื่อแสดงไฟล์ในไดเรกทอรีภาพยนตร์ในพื้นที่เก็บข้อมูลภายนอกของอุปกรณ์
  • adb pull /absolute_path_from_previous_adb_shell_ls/arcore-xxxxxxxx_xxxxxx.mp4 เพื่อคัดลอกไฟล์จากอุปกรณ์ไปยังเครื่องพัฒนา

ต่อไปนี้คือตัวอย่างเอาต์พุตหลังจากใช้คำสั่ง 2 คำสั่งนี้ (จาก 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

เพิ่ม UI สำหรับปุ่มการเล่น

ก่อนที่จะใช้การเล่น ให้เพิ่มปุ่มใน UI เพื่อให้ผู้ใช้แจ้ง ARCore ว่าเมื่อใดควรเริ่มเล่นและหยุดเล่นเซสชัน

เปิดไฟล์ 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 เพื่อซ่อนปุ่มบันทึกเมื่อแอปอยู่ในสถานะ 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 การเลือกไฟล์จะจัดการในเครื่องมือเลือกไฟล์ของระบบในกิจกรรมอื่น ซึ่งดำเนินการผ่านเฟรมเวิร์กการเข้าถึงพื้นที่เก็บข้อมูล (SAF) เมื่อผู้ใช้เลือกไฟล์ แอปจะได้รับการติดต่อกลับที่ชื่อว่า onActivityResult() คุณจะเริ่มเล่นจริงภายในฟังก์ชัน Callback นี้

ใน 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 เป็นค่าคงที่ที่จะระบุคำขอนี้ คุณจะกำหนดได้โดยใช้ค่าตัวยึดตำแหน่งภายใน HelloArActivity ใน HelloArActivity.java ดังนี้

  private int REQUEST_MP4_SELECTOR = 1;

ลบล้างฟังก์ชัน onActivityResult() ใน HelloArActivity.java เพื่อจัดการ Callback จากเครื่องมือเลือกไฟล์

  // 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 3 ครั้งเพื่อเล่นไฟล์ 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() ในชุดข้อความ 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 ...
  }

เล่นจากอุปกรณ์เป้าหมาย

ถึงเวลาดูว่าคุณสร้างอะไรมาบ้างแล้ว เชื่อมต่ออุปกรณ์เคลื่อนที่กับเครื่องพัฒนาซอฟต์แวร์ แล้วคลิกเรียกใช้ใน 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 ...
  }

บันทึกท่าทาง Anchor ระหว่างบันทึก

ทุกครั้งที่ผู้ใช้แตะบนเครื่องบินที่ตรวจพบ เครื่องหมาย 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 สำหรับการเล่น

การสร้าง 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 2-3 เครื่องหมาย

หลังจากบันทึกแล้ว ให้แตะปุ่มเล่น แล้วเลือกไฟล์ที่คุณเพิ่งบันทึก วิดีโอควรเริ่มเล่น คุณจะสังเกตได้ว่าตำแหน่งเครื่องหมาย AR ก่อนหน้าจะปรากฏขึ้นเหมือนกับที่คุณแตะในแอป

นี่คือการเขียนโค้ดสำหรับ Codelab นี้ทั้งหมด

6. ขอแสดงความยินดี

ขอแสดงความยินดี คุณมาถึงส่วนสุดท้ายของ Codelab แล้ว มาย้อนดูสิ่งที่คุณได้ทำใน Codelab กัน

  • สร้างและเรียกใช้ตัวอย่าง Hello AR Java ของ ARCore
  • เพิ่มปุ่มบันทึกลงในแอปเพื่อบันทึกเซสชัน AR เป็นไฟล์ MP4
  • เพิ่มปุ่ม "เล่น" ลงในแอปเพื่อเล่นเซสชัน AR จากไฟล์ MP4
  • เพิ่มฟีเจอร์ใหม่เพื่อบันทึก Anchor ที่ผู้ใช้สร้างไว้ใน MP4 เพื่อเล่น

สนุกกับการทำ Codelab นี้ไหม

ใช่ ไม่

ได้เรียนรู้สิ่งที่เป็นประโยชน์ในการทำ Codelab นี้ไหม

ใช่ ไม่

คุณสร้างแอปใน Codelab นี้เสร็จแล้วหรือยัง

ใช่ ไม่