ARCore 錄製和播放 API 簡介

1. 簡介

對於應用程式開發人員和使用者來說,如果能將 AR 體驗儲存為 MP4 檔案並播放 MP4 檔案,對他們都有幫助。

在電腦上偵錯及測試新功能

ARCore 記錄的用法最簡單明瞭,Playback API 適合開發人員使用。您再也不必在測試裝置上建構及執行應用程式,拔除 USB 傳輸線,然後四處走動,測試小小的程式碼變更。現在您只需要在測試環境中錄製 MP4 模型,並應執行預期的手機動作,然後直接在桌上進行測試。

使用不同裝置錄製及播放音訊

透過 Recording and Playback API,一位使用者可以使用某部裝置錄製工作階段,再讓另一部裝置播放相同的工作階段。您可與其他使用者分享 AR 體驗。無限可能!

這是你第一次製作 ARCore 應用程式嗎?

不可以。 可以。

您會如何使用本程式碼研究室?

只能閱讀 閱讀並完成練習

建構項目

在本程式碼研究室中,您將使用錄製和Playback API 建立應用程式,在 MP4 檔案中記錄 AR 體驗並由同一個檔案播放體驗。您將學會:

  • 如何使用 Recording API 將 AR 工作階段儲存至 MP4 檔案。
  • 如何使用 Playback API 重播 MP4 檔案中的 AR 工作階段。
  • 如何在某部裝置上錄製 AR 工作階段,然後在另一部裝置上重播該內容。

軟硬體需求

在本程式碼研究室中,您將修改以 ARCore Android SDK 建構的 Hello AR Java 應用程式。您需要搭配特定的軟硬體以順利執行。

硬體需求

  • 支援 ARCore 的裝置 (且已開啟開發人員選項)。和 USB 偵錯功能,已透過 USB 傳輸線連接至開發機器。
  • 執行 Android Studio 的開發機器。
  • 連上網際網路 (可在開發期間下載程式庫)。

軟體需求

也應該對 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-open-projects

在出現的對話方塊中,選取 arcore-android-sdk-1.24/samples/hello_ar_java,然後按一下「Open」(開啟)

等待 Android Studio 完成專案同步處理作業。如果缺少元件,匯入專案可能會失敗並顯示錯誤訊息。請先修正這些問題再繼續操作。

執行範例應用程式

  1. 將支援 ARCore 的裝置連線至開發機器。
  2. 如果裝置可正常辨識,Android Studio 中應該會顯示裝置名稱。android-studio-pixel-5.png
  3. 按一下「Run」按鈕,或依序選取「Run」>「Run」執行「app」,在裝置上安裝 Android Studio 並啟動應用程式。android-studio-run-button.png
  4. 系統會顯示提示,要求你授予拍照及錄影權限。選取「使用這個應用程式時」,授予應用程式相機權限。之後,您的裝置螢幕上將會顯示您的真實環境。hello-ar-java-permission
  5. 將裝置水平移動來掃描飛機。
  6. 應用程式偵測到飛機時,會顯示白色格線。輕觸那個平面上標記。歡迎使用 AR 顯示位置

這個步驟中的步驟

  • 設定 Hello AR Java 專案
  • 在支援 ARCore 的裝置上建構並執行範例應用程式

接下來,您需要錄製 MP4 檔案中的 AR 工作階段。

3. 以 MP4 檔案格式錄製 ARCore 工作階段

我們會在這個步驟中新增錄製功能。構成要素:

  • 開始或停止錄製的按鈕。
  • 儲存功能,可將 MP4 檔案儲存在裝置中。
  • 呼叫以開始或停止 ARCore 工作階段錄製。

新增記錄按鈕的 UI

實作錄製前,請在使用者介面新增按鈕,讓使用者能夠通知 ARCore 何時開始或停止記錄。

在「Project」面板中開啟 app/res/layout/activity_main.xml 檔案。

activity_main-xml-location-in-project

根據預設,您開啟 app/res/layout/activity_main.xml 檔案後,Android Studio 會使用設計檢視畫面。按一下分頁右上角的「Code」按鈕,切換至程式碼檢視模式。

swith-to-the-code-view.png

activity_main.xml 中,在結束標記前加入下列程式碼,建立新的「Record」按鈕,並將事件處理常式設為名為 onClickRecord() 的方法:

  <!--
    Add a new "Record" button with those attributes:
        text is "Record",
        onClick event handler is "onClickRecord",
        text color is "red".
  -->
  <Button
      android:id="@+id/record_button"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignLeft="@id/surfaceview"
      android:layout_alignBottom="@id/surfaceview"
      android:layout_marginBottom="100dp"
      android:onClick="onClickRecord"
      android:text="Record"
      android:textColor="@android:color/holo_red_light" />

新增上述程式碼後,可能會暫時顯示以下錯誤訊息:Corresponding method handler 'public void onClickRecord(android.view.View)' not found"。這個狀況有可能發生:您將在後續步驟中建立 onClickRecord() 函式來解決錯誤。

根據狀態變更按鈕文字

「Record」按鈕實際上會處理錄製和停止作業。如果應用程式沒有記錄資料,應顯示「Record」。當應用程式正在記錄資料時,按鈕應變更為顯示「Stop」字樣。

為了提供按鈕這項功能,應用程式必須知道目前狀態。以下程式碼會建立名為 AppState 的新列舉,代表應用程式的工作狀態,並透過名為 appState 的私人成員變數追蹤特定狀態變更。將其新增至 HelloArActivity 類別開頭的 HelloArActivity.java

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

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

您現在可以追蹤應用程式的內部狀態,建立名為 updateRecordButton() 的函式,根據應用程式目前的狀態變更按鈕的文字。在 HelloArActivity.javaHelloArActivity 類別中加入下列程式碼。

// 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.javaHelloArActivity 類別中加入下列程式碼。

  // 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. RecordingConfig 物件中指定記錄檔案 URI。
  2. 使用 RecordingConfig 物件呼叫 session.startRecording

接下來只是樣板程式碼:設定、記錄和檢查正確性。

建立名為 startRecording() 的新函式,用於記錄資料並儲存至 MP4 URI。在 HelloArActivity.javaHelloArActivity 類別中加入下列程式碼。

// 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 工作階段,請在 HelloArActivity.java 中建立 pauseARCoreSession()resumeARCoreSession()

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

啟用應用程式即可停止錄製

HelloArActivity.java 中建立名為 stopRecording() 的函式,讓應用程式停止記錄新資料。如果應用程式無法停止錄製,這個函式會呼叫 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 限定範圍儲存空間設計檔案儲存空間

本程式碼研究室中的儲存空間相關函式的設計,是依照 Android 11 新的限定範圍儲存空間規定設計。

app/build.gradle 檔案進行一些微幅變更,以便指定 Android 11。在「Android Studio 專案」面板中,這個檔案位於「Gradle Scripts」節點下方,與「app」模組相關聯。

app-build.gradle.png

compileSdkVersiontargetSdkVersion 變更為 30

    compileSdkVersion 30
    defaultConfig {
      targetSdkVersion 30
    }

如要錄製,請使用 Android MediaStore API 在分享 Movie 目錄中建立 MP4 檔案。

HelloArActivity.java 中建立名為 createMp4File() 的函式:

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

HelloArActivity.java 中新增名為 checkAndRequestStoragePermission() 的輔助函式,以在執行階段要求 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 中按一下「Run」

畫面左下方應會顯示紅色的「Record」按鈕。輕觸按鈕後,文字會變成「停止」。移動裝置來錄製工作階段,然後在想完成錄影時按一下「Stop」按鈕。這個檔案應該會在裝置的外部儲存空間中儲存名為 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 複製到您的電腦並完成驗證

接著,你將返回播放 MP4 檔案的 AR 工作階段。

4. 播放 MP4 檔案中的 ARCore 工作階段

現在,您已新增「Record」按鈕和一些包含工作階段的 MP4 檔案。現在請使用 ARCore Playback API 播放這些內容。

新增播放按鈕的使用者介面

實作播放前,請在使用者介面上新增按鈕,讓使用者可在 ARCore 開始和停止播放工作階段時通知 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 的新狀態。請更新 AppState 列舉,以及所有採用 appState 做為引數的現有函式,以便處理這個問題。

Playingback 新增至 HelloArActivity.java 中的 AppState 列舉:

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

如果播放期間仍顯示「錄製」按鈕,使用者可能會不小心按一下。如要避免這種情況,請在播放時隱藏「錄製」按鈕。如此一來,您不需要處理 onClickRecord() 中的 Playingback 狀態。

修改 HelloArActivity.java 中的 updateRecordButton() 函式,在應用程式處於 Playingback 狀態時隱藏「Record」按鈕。

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

同樣地,在使用者錄製工作階段時隱藏「播放」按鈕,將按鈕變更為「停止」。使用者主動重播工作階段時。如此一來,他們就能直接停止播放,不必等待播放完成。

HelloArActivity.java 中新增 updatePlaybackButton() 函式:

  // 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 上,檔案選取程序會透過其他 Activity 的系統檔案選擇器處理。這項工具使用儲存空間存取架構 (SAF)。使用者選取檔案後,應用程式會收到名為 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 是用來識別這項要求的常數。您可以使用 HelloArActivity.javaHelloArActivity 內的任何預留位置值來定義:

  private int REQUEST_MP4_SELECTOR = 1;

覆寫 HelloArActivity.java 中的 onActivityResult() 函式,處理檔案選擇器中的回呼。

  // Begin playback once the user has selected the file.
  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    // Check request status. Log an error if the selection fails.
    if (resultCode != android.app.Activity.RESULT_OK || requestCode != REQUEST_MP4_SELECTOR) {
      Log.e(TAG, "onActivityResult select file failed");
      return;
    }

    Uri mp4FileUri = data.getData();
    Log.d(TAG, String.format("onActivityResult result is %s", mp4FileUri));

    // Begin playback.
    startPlayingback(mp4FileUri);
  }

啟用應用程式即可開始播放

ARCore 工作階段需要三次 API 呼叫才能播放 MP4 檔案:

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

HelloArActivity.java 中建立 startPlayingback() 函式。

// Add imports to the beginning of the file.
import com.google.ar.core.PlaybackStatus;
import com.google.ar.core.exceptions.PlaybackFailedException;

  private boolean startPlayingback(Uri mp4FileUri) {
    if (mp4FileUri == null)
      return false;

    Log.d(TAG, "startPlayingback at:" + mp4FileUri);

    pauseARCoreSession();

    try {
      session.setPlaybackDatasetUri(mp4FileUri);
    } catch (PlaybackFailedException e) {
      Log.e(TAG, "startPlayingback - setPlaybackDataset failed", e);
    }

    // The session's camera texture name becomes invalid when the
    // ARCore session is set to play back.
    // Workaround: Reset the Texture to start Playback
    // so it doesn't crashes with AR_ERROR_TEXTURE_NOT_SET.
    hasSetTextureNames = false;

    boolean canResume = resumeARCoreSession();
    if (!canResume)
      return false;

    PlaybackStatus playbackStatus = session.getPlaybackStatus();
    Log.d(TAG, String.format("startPlayingback - playbackStatus %s", playbackStatus));


    if (playbackStatus != PlaybackStatus.OK) { // Correctness check
      return false;
    }

    appState = AppState.Playingback;
    updateRecordButton();
    updatePlaybackButton();

    return true;
  }

啟用應用程式即可停止播放

HelloArActivity.java 中建立名為 stopPlayingback() 的函式,以便在發生應用程式狀態變更之後處理應用程式狀態變更:

  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,請在 UI 執行緒上呼叫 stopPlayingback() 函式。

  public void onDrawFrame(SampleRender render) {
      // ... omitted code ...

      // Insert before this line:
      // frame = session.update();

      // Check the playback status and return early if playback reaches the end.
      if (appState == AppState.Playingback
          && session.getPlaybackStatus() == PlaybackStatus.FINISHED) {
        this.runOnUiThread(this::stopPlayingback);
        return;
      }

      // ... omitted code ...
  }

從目標裝置播放

是時候展現您到目前為止打造的成果了。將行動裝置連結至開發機器,然後在 Android Studio 中按一下「Run」

應用程式啟動後,畫面左側會顯示紅色的「Record」按鈕,右側則是一個綠色的「Playback」按鈕。

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 檔案中記錄額外資訊。您可以錄製 AR 物件刊登位置的 Pose,然後在播放期間,在同一個位置建立 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 標記,而 ARCore 也會更新其姿勢。

如果仍在記錄 ARCore 工作階段,請在建立影格時記錄 Anchor 的姿勢。

修改 HelloArActivity.java 中的 handleTap() 函式。

// 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.startRecording()「之後」 首次呼叫 Session.resume() 時,啟動播放工作階段的全球來源。

建立播放錨定廣告

重新建立 Anchor 非常簡單。在 HelloArActivity.java 中新增名為 createRecordedAnchors() 的函式。

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

HelloArActivity.javaonDrawFrame() 函式中呼叫 createRecordedAnchors()

  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 中按一下「Run」

首先,輕觸「錄製」按鈕錄製練習活動。錄製過程中,只要輕觸偵測到的平面,即可放置多個 AR 標記。

錄音停止後,請輕觸「播放」按鈕並選取你剛才錄製的檔案。此時影片會開始播放。請注意,就像輕觸應用程式一樣,系統會如何顯示先前的 AR 標記。

以上就是這個程式碼研究室需要執行的所有程式碼。

6. 恭喜

恭喜,您已經順利完成本程式碼研究室課程!我們來看看您在本程式碼研究室中完成的內容:

  • 建立並執行 ARCore Hello AR Java 範例。
  • 在應用程式中新增「Record」按鈕,以便將 AR 工作階段儲存至 MP4 檔案
  • 在應用程式中新增「播放」按鈕,用於播放來自 MP4 檔案的 AR 工作階段
  • 新增功能,可將使用者建立的錨定標記儲存在 MP4 中以供播放返回

您是否覺得參與本程式碼研究室有趣?

您在進行本程式碼研究室時學到一些有用的資訊嗎?

您是否已完成本程式碼研究室中的應用程式製作程序?