Giới thiệu về API Ghi và phát lại ARCore

1. Giới thiệu

Khả năng lưu trải nghiệm thực tế tăng cường vào tệp MP4 và phát lại từ tệp MP4 có thể hữu ích cho cả nhà phát triển ứng dụng và người dùng cuối.

Gỡ lỗi và kiểm thử các tính năng mới ngay tại bàn làm việc

Cách sử dụng đơn giản nhất của ARCore Record & Playback API là dành cho nhà phát triển. Đã qua rồi cái thời bạn phải tạo và chạy ứng dụng trên một thiết bị thử nghiệm, ngắt cáp USB và đi bộ xung quanh chỉ để kiểm thử một thay đổi nhỏ về mã. Giờ đây, bạn chỉ cần ghi lại một tệp MP4 trong môi trường thử nghiệm với chuyển động dự kiến của điện thoại và kiểm thử ngay tại bàn làm việc.

Ghi và phát lại trên nhiều thiết bị

Với Recording and Playback API, một người dùng có thể ghi lại một phiên bằng một thiết bị và người dùng khác có thể phát lại phiên đó trên một thiết bị khác. Bạn có thể chia sẻ trải nghiệm thực tế tăng cường với người dùng khác. Có rất nhiều khả năng!

Đây có phải là lần đầu tiên bạn tạo ứng dụng ARCore không?

Không. Có.

Bạn sẽ sử dụng lớp học lập trình này như thế nào?

Chỉ đọc qua Đọc và hoàn thành bài tập

Sản phẩm bạn sẽ tạo ra

Trong lớp học lập trình này, bạn sẽ sử dụng Recording & Playback API (API Ghi hình và phát lại) để tạo một ứng dụng vừa ghi lại trải nghiệm AR vào một tệp MP4 vừa phát lại trải nghiệm đó từ cùng một tệp. Bạn sẽ tìm hiểu:

  • Cách sử dụng Recording API để lưu một phiên thực tế tăng cường vào tệp MP4.
  • Cách sử dụng Playback API để phát lại một phiên thực tế tăng cường từ tệp MP4.
  • Cách ghi lại một phiên thực tế tăng cường trên một thiết bị và phát lại trên một thiết bị khác.

Bạn cần có

Trong lớp học lập trình này, bạn sẽ sửa đổi ứng dụng Hello AR Java. Ứng dụng này được tạo bằng ARCore SDK Android. Bạn cần có phần cứng và phần mềm cụ thể để thực hiện theo.

Yêu cầu về phần cứng

Yêu cầu về phần mềm

Bạn cũng nên hiểu biết cơ bản về ARCore để đạt được kết quả tốt nhất.

2. Thiết lập môi trường phát triển

Bắt đầu bằng cách thiết lập môi trường phát triển.

Tải SDK Android ARCore xuống

Nhấp vào để tải SDK xuống.

Giải nén ARCore SDK Android

Sau khi tải SDK Android xuống máy, hãy giải nén tệp và chuyển đến thư mục arcore-android-sdk-1.24/samples/hello_ar_java. Đây là thư mục gốc của ứng dụng mà bạn sẽ làm việc.

hello-ar-java-extracted

Tải Hello AR Java vào Android Studio

Khởi chạy Android Studio rồi nhấp vào Open an existing Android Studio project (Mở một dự án hiện có trong Android Studio).

android-studio-open-projects

Trong cửa sổ hộp thoại xuất hiện, hãy chọn arcore-android-sdk-1.24/samples/hello_ar_java rồi nhấp vào Mở.

Chờ Android Studio hoàn tất quá trình đồng bộ hoá dự án. Nếu thiếu thành phần, quá trình nhập dự án có thể không thành công kèm theo thông báo lỗi. Hãy khắc phục những vấn đề này trước khi tiếp tục.

Chạy ứng dụng mẫu

  1. Kết nối một thiết bị có hỗ trợ ARCore với máy phát triển của bạn.
  2. Nếu thiết bị được nhận dạng đúng cách, bạn sẽ thấy tên thiết bị xuất hiện trong Android Studio. android-studio-pixel-5.png
  3. Nhấp vào nút Chạy hoặc chọn Chạy > Chạy "ứng dụng" để Android Studio cài đặt và chạy ứng dụng trên thiết bị. android-studio-run-button.png
  4. Bạn sẽ thấy một lời nhắc yêu cầu cấp quyền chụp ảnh và quay video. Chọn Khi dùng ứng dụng này để cấp cho ứng dụng quyền truy cập Camera. Sau đó, bạn sẽ thấy môi trường thực tế trên màn hình của thiết bị. hello-ar-java-permission
  5. Di chuyển thiết bị theo chiều ngang để quét tìm mặt phẳng.
  6. Một lưới màu trắng sẽ xuất hiện khi ứng dụng phát hiện thấy một mặt phẳng. Nhấn vào mặt phẳng đó để đặt một điểm đánh dấu. Vị trí Hello AR

Những việc bạn đã làm trong bước này

  • Thiết lập dự án Hello AR Java
  • Tạo và chạy ứng dụng mẫu trên một thiết bị hỗ trợ ARCore

Tiếp theo, bạn sẽ ghi lại một phiên thực tế tăng cường vào tệp MP4.

3. Ghi lại một phiên ArCore vào tệp MP4

Chúng ta sẽ thêm tính năng ghi âm ở bước này. Mã này bao gồm:

  • Một nút để bắt đầu hoặc dừng ghi.
  • Các hàm lưu trữ để lưu tệp MP4 trên thiết bị.
  • Các lệnh gọi để bắt đầu hoặc dừng ghi phiên ArCore.

Thêm giao diện người dùng cho nút Ghi

Trước khi triển khai tính năng ghi hình, hãy thêm một nút trên giao diện người dùng để người dùng có thể thông báo cho ARCore thời điểm bắt đầu hoặc dừng ghi hình.

Trong bảng điều khiển Project (Dự án), hãy mở tệp app/res/layout/activity_main.xml.

activity_main-xml-location-in-project

Theo mặc định, Android Studio sẽ sử dụng khung hiển thị bản thiết kế sau khi bạn mở tệp app/res/layout/activity_main.xml. Nhấp vào nút ở góc trên cùng bên phải của thẻ để chuyển sang chế độ xem mã.

swith-to-the-code-view.png

Trong activity_main.xml, hãy thêm mã sau trước thẻ đóng để tạo nút Record (Ghi) mới và đặt trình xử lý sự kiện của nút này thành một phương thức có tên là 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" />

Sau khi bạn thêm mã ở trên, có thể tạm thời xuất hiện lỗi: Corresponding method handler 'public void onClickRecord(android.view.View)' not found". Lỗi này có thể xảy ra. Bạn sẽ khắc phục lỗi bằng cách tạo hàm onClickRecord() trong vài bước tiếp theo.

Thay đổi văn bản trên nút dựa trên trạng thái

Nút Ghi thực sự xử lý cả việc ghi và dừng ghi. Khi không ghi lại dữ liệu, ứng dụng sẽ hiển thị từ "Ghi". Khi ứng dụng đang ghi dữ liệu, nút này sẽ thay đổi để hiển thị từ "Dừng".

Để nút có chức năng này, ứng dụng phải biết trạng thái hiện tại của nút. Mã sau đây tạo một enum mới có tên là AppState để biểu thị trạng thái hoạt động của ứng dụng và theo dõi các thay đổi cụ thể về trạng thái thông qua một biến thành phần riêng tư có tên là appState. Thêm mã này vào HelloArActivity.java, ở đầu lớp HelloArActivity.

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

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

Giờ đây, bạn có thể theo dõi trạng thái nội bộ của ứng dụng. Hãy tạo một hàm có tên là updateRecordButton() để thay đổi văn bản của nút dựa trên trạng thái hiện tại của ứng dụng. Thêm mã sau vào bên trong lớp HelloArActivity trong 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;
    }
  }

Tiếp theo, hãy tạo phương thức onClickRecord() để kiểm tra trạng thái của ứng dụng, thay đổi trạng thái đó thành trạng thái tiếp theo và gọi updateRecordButton() để thay đổi giao diện người dùng của nút. Thêm mã sau vào bên trong lớp HelloArActivity trong 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();
  }

Cho phép ứng dụng bắt đầu ghi

Bạn chỉ cần làm hai việc để bắt đầu ghi trong ARCore:

  1. Chỉ định URI của tệp ghi trong một đối tượng RecordingConfig.
  2. Gọi session.startRecording bằng đối tượng RecordingConfig

Phần còn lại chỉ là mã nguyên mẫu: cấu hình, ghi nhật ký và kiểm tra tính chính xác.

Tạo một hàm mới có tên là startRecording() để ghi lại dữ liệu và lưu vào một URI MP4. Thêm mã sau vào bên trong lớp HelloArActivity trong 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;
  }

Để tạm dừng và tiếp tục phiên ARCore một cách an toàn, hãy tạo pauseARCoreSession()resumeARCoreSession() trong 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;
  }

Cho phép ứng dụng dừng ghi

Tạo một hàm có tên là stopRecording() trong HelloArActivity.java để ngăn ứng dụng ghi dữ liệu mới. Hàm này gọi session.stopRecording() và gửi lỗi đến nhật ký bảng điều khiển nếu ứng dụng không thể dừng ghi.

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

Thiết kế bộ nhớ tệp bằng bộ nhớ có giới hạn trên Android 11

Các hàm liên quan đến bộ nhớ trong lớp học lập trình này được thiết kế theo các yêu cầu mới về bộ nhớ có giới hạn trên Android 11.

Thực hiện một số thay đổi nhỏ trong tệp app/build.gradle để nhắm đến Android 11. Trong bảng điều khiển Project (Dự án) của Android Studio, tệp này nằm trong nút Gradle Scripts (Tập lệnh Gradle), được liên kết với mô-đun app.

app-build.gradle.png

Thay đổi compileSdkVersiontargetSdkVersion thành 30.

    compileSdkVersion 30
    defaultConfig {
      targetSdkVersion 30
    }

Đối với hoạt động Ghi hình, hãy dùng API Android MediaStore để tạo tệp MP4 trong thư mục Phim dùng chung.

Tạo một hàm có tên là createMp4File() trong 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;
  }

Xử lý quyền truy cập vào bộ nhớ

Nếu đang dùng thiết bị Android 11, bạn có thể bắt đầu kiểm thử mã. Để hỗ trợ các thiết bị chạy Android 10 trở xuống, bạn cần cấp cho ứng dụng quyền truy cập vào bộ nhớ để lưu dữ liệu vào hệ thống tệp của thiết bị mục tiêu.

Trong AndroidManifest.xml, hãy khai báo rằng ứng dụng cần có quyền đọc và ghi bộ nhớ trước Android 11 (cấp độ 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" />

Thêm một hàm trợ giúp có tên là checkAndRequestStoragePermission() vào HelloArActivity.java để yêu cầu các quyền WRITE_EXTERNAL_STORAGE trong thời gian chạy.

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

Nếu bạn đang ở cấp độ API 29 trở xuống, hãy thêm một quy trình kiểm tra quyền truy cập vào bộ nhớ ở đầu createMp4File() và thoát sớm khỏi hàm nếu ứng dụng không có các quyền phù hợp. API cấp độ 30 (Android 11) không yêu cầu quyền truy cập vào bộ nhớ để truy cập vào các tệp trong 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 ...
  }

Ghi hình từ thiết bị mục tiêu

Đã đến lúc xem xét những gì bạn đã xây dựng cho đến nay. Kết nối thiết bị di động với máy phát triển rồi nhấp vào Run (Chạy) trong Android Studio.

Bạn sẽ thấy nút Ghi màu đỏ ở phía dưới cùng bên trái màn hình. Khi bạn nhấn vào nút này, văn bản sẽ thay đổi thành Dừng. Di chuyển thiết bị để ghi lại một phiên và nhấp vào nút Dừng khi bạn muốn hoàn tất quá trình ghi. Thao tác này sẽ lưu một tệp mới có tên là arcore-xxxxxx_xxxxxx.mp4 vào bộ nhớ ngoài của thiết bị.

record-button.png

Giờ đây, bạn sẽ có một tệp arcore-xxxxxx_xxxxxx.mp4 mới trong bộ nhớ ngoài của thiết bị. Trên thiết bị Pixel 5, đường dẫn là /storage/emulated/0/Movies/. Bạn có thể tìm thấy đường dẫn này trong cửa sổ Logcat sau khi bắt đầu ghi.

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

Xem bản ghi

Bạn có thể sử dụng một ứng dụng hệ thống tệp như Files by Google để xem bản ghi hoặc sao chép bản ghi đó vào máy phát triển. Dưới đây là 2 lệnh adb để liệt kê và tìm nạp các tệp từ thiết bị Android:

  • adb shell ls '$EXTERNAL_STORAGE/Movies/*' để hiện các tệp trong thư mục Movies (Phim) trong bộ nhớ ngoài trên thiết bị
  • adb pull /absolute_path_from_previous_adb_shell_ls/arcore-xxxxxxxx_xxxxxx.mp4 để sao chép tệp từ thiết bị vào máy phát triển

Đây là kết quả đầu ra mẫu sau khi sử dụng hai lệnh này (trên 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

Những việc bạn đã làm trong bước này

  • Thêm nút để bắt đầu và dừng ghi
  • Triển khai các hàm để bắt đầu và dừng ghi
  • Đã kiểm thử ứng dụng trên thiết bị
  • Sao chép tệp MP4 đã ghi vào máy của bạn và xác minh tệp đó

Tiếp theo, bạn sẽ phát lại một phiên thực tế tăng cường từ tệp MP4.

4. Phát một phiên ArCore từ tệp MP4

Giờ đây, bạn sẽ thấy nút Ghi và một số tệp MP4 chứa các phiên được ghi lại. Giờ đây, bạn sẽ phát lại các tệp này bằng ARCore Playback API.

Thêm giao diện người dùng cho nút Phát

Trước khi triển khai tính năng phát lại, hãy thêm một nút trên giao diện người dùng để người dùng có thể thông báo cho ARCore thời điểm bắt đầu và dừng phát lại phiên.

Trong bảng Project (Dự án), hãy mở tệp app/res/layout/activity_main.xml.

activity_main-xml-location-in-project

Trong activity_main.xml, hãy thêm mã bên dưới trước thẻ đóng để tạo nút Playback (Phát) mới và đặt trình xử lý sự kiện của nút này thành một phương thức có tên là onClickPlayback(). Nút này sẽ tương tự như nút Ghi và sẽ xuất hiện ở bên phải màn hình.

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

Nút cập nhật trong khi phát

Ứng dụng hiện có một trạng thái mới tên là Playingback. Cập nhật enum AppState và tất cả các hàm hiện có lấy appState làm đối số để xử lý việc này.

Thêm Playingback vào enum AppState trong HelloArActivity.java:

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

Nếu nút Ghi hình vẫn xuất hiện trên màn hình trong khi phát, người dùng có thể vô tình nhấp vào nút này. Để tránh tình trạng này, hãy ẩn nút Ghi trong quá trình Phát. Bằng cách này, bạn không cần xử lý trạng thái cho Playingback trong onClickRecord().

Sửa đổi hàm updateRecordButton() trong HelloArActivity.java để ẩn nút Record (Ghi lại) khi ứng dụng ở trạng thái 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;
    }
  }

Tương tự, hãy ẩn nút Phát khi người dùng đang ghi lại một phiên và thay đổi nút này thành "Dừng" khi người dùng đang phát một phiên. Như vậy, họ có thể dừng quá trình phát mà không cần phải đợi quá trình này tự hoàn tất.

Thêm hàm updatePlaybackButton() trong 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;
    }
  }

Cuối cùng, hãy cập nhật onClickRecord() để gọi updatePlaybackButton(). Thêm dòng sau vào HelloArActivity.java:

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

Chọn một tệp bằng nút Phát

Khi được nhấn, nút Phát sẽ cho phép người dùng chọn một tệp để phát. Trên Android, việc chọn tệp được xử lý trong bộ chọn tệp hệ thống ở một Hoạt động khác. Việc này được thực hiện thông qua Khung truy cập bộ nhớ (SAF). Sau khi người dùng chọn một tệp, ứng dụng sẽ nhận được một lệnh gọi lại có tên là onActivityResult(). Bạn sẽ bắt đầu phát thực tế bên trong hàm callback này.

Trong HelloArActivity.java, hãy tạo một hàm onClickPlayback() để chọn tệp và dừng phát.

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

Trong HelloArActivity.java, hãy tạo một hàm selectFileToPlayback() để chọn một tệp trên thiết bị. Để chọn một tệp trong Hệ thống tệp Android, hãy sử dụng ACTION_OPEN_DOCUMENT Ý định.

// 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 là một hằng số để xác định yêu cầu này. Bạn có thể xác định giá trị này bằng cách sử dụng bất kỳ giá trị phần giữ chỗ nào bên trong HelloArActivity trong HelloArActivity.java:

  private int REQUEST_MP4_SELECTOR = 1;

Ghi đè hàm onActivityResult() trong HelloArActivity.java để xử lý lệnh gọi lại từ bộ chọn tệp.

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

Cho phép ứng dụng bắt đầu phát

Một phiên ARCore cần 3 lệnh gọi API để phát tệp MP4:

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

Trong HelloArActivity.java, hãy tạo hàm 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;
  }

Cho phép ứng dụng dừng phát

Tạo một hàm có tên là stopPlayingback() trong HelloArActivity.java để xử lý các thay đổi về trạng thái ứng dụng sau:

  1. Người dùng đã dừng phát tệp MP4
  2. Quá trình phát tệp MP4 đã tự động hoàn tất

Nếu người dùng dừng phát, ứng dụng sẽ trở về trạng thái ban đầu khi người dùng khởi chạy ứng dụng.

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

Quá trình phát cũng có thể dừng tự nhiên sau khi trình phát đã phát đến cuối tệp MP4. Khi điều này xảy ra, stopPlayingback() sẽ chuyển trạng thái của ứng dụng trở lại Idle. Trong onDrawFrame(), hãy kiểm tra PlaybackStatus. Nếu là FINISHED, hãy gọi hàm stopPlayingback() trên luồng giao diện người dùng.

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

Phát lại từ thiết bị mục tiêu

Đã đến lúc xem xét những gì bạn đã xây dựng cho đến nay. Kết nối thiết bị di động với máy phát triển rồi nhấp vào Run (Chạy) trong Android Studio.

Khi ứng dụng khởi chạy, bạn sẽ thấy một màn hình có nút Ghi màu đỏ ở bên trái và nút Phát màu xanh lục ở bên phải.

playback-button.png

Nhấn vào nút Phát rồi chọn một trong các tệp MP4 mà bạn vừa quay. Nếu bạn không thấy tên tệp nào bắt đầu bằng arcore-, có thể thiết bị của bạn không hiển thị thư mục Phim. Trong trường hợp này, hãy chuyển đến thư mục Phone model > Movies (Mẫu điện thoại > Phim) bằng trình đơn ở góc trên cùng bên trái. Bạn cũng có thể cần bật tuỳ chọn Hiện bộ nhớ trong để hiển thị thư mục mô hình điện thoại.

show-internal-storage-button.png

nativate-to-movies-file-picker.jpg

Nhấn vào tên tệp trên màn hình để chọn tệp MP4. Ứng dụng sẽ phát tệp MP4.

playback-stop-button.png

Điểm khác biệt giữa việc phát lại một phiên và phát lại một video thông thường là bạn có thể tương tác với phiên đã ghi. Nhấn vào một mặt phẳng được phát hiện để đặt điểm đánh dấu trên màn hình.

playback-placement

Những việc bạn đã làm trong bước này

  • Thêm nút bắt đầu và dừng phát
  • Triển khai một hàm để giúp ứng dụng bắt đầu và dừng ghi
  • Phát lại một phiên ARCore đã ghi trước đó trên thiết bị

5. Ghi thêm dữ liệu vào tệp MP4

Với ARCore 1.24, bạn có thể ghi thêm thông tin vào tệp MP4. Bạn có thể ghi lại Pose của vị trí đặt đối tượng thực tế tăng cường, sau đó trong quá trình phát, hãy tạo các đối tượng thực tế tăng cường ở cùng một vị trí.

Định cấu hình bản phụ đề mới để ghi lại

Xác định một bản nhạc mới bằng UUID và thẻ MIME trong 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 ...
  }

Cập nhật mã thoát để tạo đối tượng RecordingConfig bằng lệnh gọi đến 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 ...
  }

Lưu tư thế neo trong khi ghi hình

Mỗi khi người dùng nhấn vào một mặt phẳng được phát hiện, một điểm đánh dấu thực tế tăng cường sẽ được đặt trên một Anchor, tư thế của điểm đánh dấu này sẽ được ARCore cập nhật.

Ghi lại tư thế của một Anchor tại khung hình mà tư thế đó được tạo, nếu bạn vẫn đang ghi lại phiên ArCore.

Sửa đổi hàm handleTap() trong 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 ...
  }

Lý do chúng tôi duy trì Pose tương đối của camera thay vì Pose tương đối của thế giới là vì nguồn gốc thế giới của một phiên ghi hình và nguồn gốc thế giới của một phiên phát lại không giống nhau. Nguồn gốc thế giới của một phiên ghi hình bắt đầu từ lần đầu tiên phiên được tiếp tục, khi Session.resume() được gọi lần đầu tiên. Nguồn gốc thế giới của một phiên phát bắt đầu khi khung hình đầu tiên được ghi lại, khi Session.resume() được gọi lần đầu tiên sau Session.startRecording().

Tạo điểm neo phát

Bạn có thể dễ dàng tạo lại Anchor. Thêm một hàm có tên là createRecordedAnchors() trong 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);
    }
  }

Gọi createRecordedAnchors() trong hàm onDrawFrame() trong 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 ...
  }

Kiểm thử trên thiết bị mục tiêu

Kết nối thiết bị di động với máy phát triển rồi nhấp vào Run (Chạy) trong Android Studio.

Trước tiên, hãy nhấn vào nút Ghi để ghi lại một phiên. Trong quá trình ghi hình, hãy nhấn vào các mặt phẳng được phát hiện để đặt một vài điểm đánh dấu thực tế tăng cường.

Sau khi quá trình ghi dừng lại, hãy nhấn vào nút Phát rồi chọn tệp bạn vừa ghi. Quá trình phát sẽ bắt đầu. Lưu ý cách vị trí đặt điểm đánh dấu thực tế tăng cường trước đó xuất hiện ngay khi bạn nhấn vào ứng dụng.

Đó là tất cả những gì bạn phải làm trong lớp học lập trình này.

6. Xin chúc mừng

Xin chúc mừng, bạn đã hoàn thành lớp học lập trình này! Hãy nhìn lại những gì bạn đã làm trong lớp học lập trình này:

  • Tạo và chạy mẫu ARCore Hello AR Java.
  • Thêm nút Ghi vào ứng dụng để lưu một phiên thực tế tăng cường vào tệp MP4
  • Thêm nút Phát vào ứng dụng để phát lại một phiên thực tế tăng cường từ tệp MP4
  • Thêm một tính năng mới để lưu các điểm đánh dấu do người dùng tạo trong tệp MP4 để phát

Bạn có thấy vui khi thực hiện lớp học lập trình này không?

Không

Bạn có học được điều gì hữu ích khi thực hiện lớp học lập trình này không?

Không

Bạn đã hoàn tất việc tạo ứng dụng trong lớp học lập trình này chưa?

Không