مقدمه ای بر ARCore Recording and Playback API

۱. مقدمه

امکان ذخیره یک تجربه واقعیت افزوده در یک فایل MP4 و پخش مجدد آن از فایل MP4 می‌تواند هم برای توسعه‌دهندگان برنامه و هم برای کاربران نهایی مفید باشد.

اشکال‌زدایی و آزمایش ویژگی‌های جدید از طریق میزکار شما

ساده‌ترین کاربرد API ضبط و پخش ARCore برای توسعه‌دهندگان است. روزهایی که مجبور بودید برنامه را روی یک دستگاه آزمایشی بسازید و اجرا کنید، کابل USB را جدا کنید و فقط برای آزمایش یک تغییر کوچک در کد، راه بروید، گذشته است. اکنون فقط کافی است یک فایل MP4 را در محیط آزمایشی با حرکت مورد انتظار تلفن ضبط کنید و مستقیماً از روی میز خود آن را آزمایش کنید.

ضبط و پخش از دستگاه‌های مختلف

با استفاده از APIهای ضبط و پخش، یک کاربر می‌تواند یک جلسه را با استفاده از یک دستگاه ضبط کند و کاربر دیگر می‌تواند همان جلسه را در دستگاه دیگری پخش کند. می‌توان یک تجربه واقعیت افزوده را با کاربر دیگری به اشتراک گذاشت. امکانات زیادی وجود دارد!

آیا این اولین باری است که یک برنامه ARCore می‌سازید؟

خیر. بله.

چگونه از این آزمایشگاه کد استفاده خواهید کرد؟

فقط آن را بخوانید آن را بخوانید و تمرین‌ها را انجام دهید

آنچه خواهید ساخت

در این آزمایشگاه کد، شما از API ضبط و پخش برای ایجاد برنامه‌ای استفاده خواهید کرد که هم یک تجربه واقعیت افزوده را در یک فایل MP4 ضبط می‌کند و هم آن تجربه را از همان فایل پخش می‌کند. شما یاد خواهید گرفت:

  • نحوه استفاده از Recording API برای ذخیره یک جلسه AR در یک فایل MP4.
  • نحوه استفاده از API پخش برای پخش مجدد یک جلسه AR از یک فایل MP4.
  • نحوه ضبط یک جلسه AR در یک دستگاه و پخش مجدد آن در دستگاه دیگر.

آنچه نیاز دارید

در این آزمایشگاه کد، شما برنامه Hello AR Java را که با ARCore Android SDK ساخته شده است، اصلاح خواهید کرد. برای ادامه به سخت‌افزار و نرم‌افزار خاصی نیاز خواهید داشت.

الزامات سخت‌افزاری

نیازمندی‌های نرم‌افزاری

همچنین برای بهترین نتیجه باید درک اولیه‌ای از ARCore داشته باشید.

۲. محیط توسعه خود را تنظیم کنید

با تنظیم محیط توسعه خود شروع کنید.

دانلود کیت توسعه نرم‌افزار اندروید ARCore

برای دانلود SDK کلیک کنید.

کیت توسعه نرم‌افزار اندروید ARCore را از حالت فشرده خارج کنید

پس از دانلود SDK اندروید روی دستگاه خود، فایل را از حالت فشرده خارج کرده و به دایرکتوری arcore-android-sdk-1.24/samples/hello_ar_java بروید. این دایرکتوری ریشه برنامه‌ای است که با آن کار خواهید کرد.

استخراج شده از فایل hello-ar-java

بارگذاری Hello AR Java در اندروید استودیو

اندروید استودیو را اجرا کنید و روی «باز کردن یک پروژه اندروید استودیو موجود» کلیک کنید.

پروژه‌های باز اندروید استودیو

در پنجره‌ی باز شده، arcore-android-sdk-1.24/samples/hello_ar_java را انتخاب کرده و روی Open کلیک کنید.

منتظر بمانید تا اندروید استودیو همگام‌سازی پروژه را تمام کند. اگر کامپوننتی وجود نداشته باشد، ممکن است وارد کردن پروژه با پیام‌های خطا مواجه شود. قبل از ادامه، این مشکلات را برطرف کنید.

اجرای برنامه نمونه

  1. یک دستگاه پشتیبانی‌شده با ARCore را به دستگاه توسعه خود متصل کنید.
  2. اگر دستگاه به درستی شناسایی شود، باید نام دستگاه را در اندروید استودیو مشاهده کنید. اندروید-استودیو-پیکسل-۵.png
  3. روی دکمه‌ی اجرا کلیک کنید یا مسیر Run > Run 'app' را انتخاب کنید تا اندروید استودیو برنامه را روی دستگاه نصب و اجرا کند. دکمه‌ی اجرا در اندروید استودیو.png
  4. پیامی مشاهده خواهید کرد که از شما اجازه عکس گرفتن و ضبط ویدیو را می‌خواهد. برای اعطای مجوزهای دوربین به برنامه، گزینه «هنگام استفاده از این برنامه» را انتخاب کنید. سپس محیط دنیای واقعی خود را روی صفحه دستگاه مشاهده خواهید کرد. مجوز hello-ar-java
  5. برای اسکن هواپیماها، دستگاه را به صورت افقی حرکت دهید.
  6. وقتی برنامه یک صفحه را تشخیص می‌دهد، یک شبکه سفید ظاهر می‌شود. برای قرار دادن یک نشانگر روی آن صفحه، روی آن ضربه بزنید. سلام قرار دادن AR

کاری که در این مرحله انجام داده‌اید

  • پروژه جاوای Hello AR را راه‌اندازی کنید
  • ساخت و اجرای برنامه نمونه روی دستگاهی که از ARCore پشتیبانی می‌کند

در مرحله بعد، یک جلسه AR را در یک فایل MP4 ضبط خواهید کرد.

۳. ضبط یک جلسه ARCore در قالب یک فایل MP4

ما در این مرحله قابلیت ضبط را اضافه خواهیم کرد. این قابلیت از موارد زیر تشکیل شده است:

  • یک دکمه برای شروع یا توقف ضبط.
  • توابع ذخیره‌سازی برای ذخیره فایل MP4 در دستگاه.
  • فراخوانی‌هایی برای شروع یا توقف ضبط جلسه ARCore.

اضافه کردن رابط کاربری برای دکمه ضبط

قبل از پیاده‌سازی ضبط، یک دکمه در رابط کاربری اضافه کنید تا کاربر بتواند به ARCore اطلاع دهد که چه زمانی ضبط را شروع یا متوقف کند.

در پنل پروژه، فایل app/res/layout/activity_main.xml را باز کنید.

activity_main-xml-location-in-project

به طور پیش‌فرض، اندروید استودیو پس از باز کردن فایل app/res/layout/activity_main.xml از نمای طراحی (design view) استفاده می‌کند. برای رفتن به نمای کد، روی دکمه Code در گوشه سمت راست بالای تب کلیک کنید.

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() را برای تغییر رابط کاربری دکمه فراخوانی می‌کند. کد زیر را در کلاس HelloArActivity در HelloArActivity.java اضافه کنید.

  // Handle the "Record" button click event.
  public void onClickRecord(View view) {
    Log.d(TAG, "onClickRecord");

    // Check the app's internal state and switch to the new state if needed.
    switch (appState) {
        // If the app is not recording, begin recording.
      case Idle: {
        boolean hasStarted = startRecording();
        Log.d(TAG, String.format("onClickRecord start: hasStarted %b", hasStarted));

        if (hasStarted)
          appState = AppState.Recording;

        break;
      }

      // If the app is recording, stop recording.
      case Recording: {
        boolean hasStopped = stopRecording();
        Log.d(TAG, String.format("onClickRecord stop: hasStopped %b", hasStopped));

        if (hasStopped)
          appState = AppState.Idle;

        break;
      }

      default:
        // Do nothing.
        break;
    }

    updateRecordButton();
  }

برنامه را برای شروع ضبط فعال کنید

برای شروع ضبط در ARCore فقط باید دو کار انجام دهید:

  1. آدرس فایل ضبط را در یک شیء RecordingConfig مشخص کنید.
  2. فراخوانی session.startRecording با شیء RecordingConfig

بقیه‌اش فقط کد تکراری است: پیکربندی، گزارش‌گیری و بررسی صحت.

یک تابع جدید به نام startRecording() ایجاد کنید که داده‌ها را ثبت کرده و در یک URL با فرمت 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;
  }

طراحی فضای ذخیره‌سازی فایل با استفاده از فضای ذخیره‌سازی محدود اندروید ۱۱

توابع مربوط به ذخیره‌سازی در این آزمایشگاه کد، مطابق با الزامات جدید ذخیره‌سازی محدود اندروید ۱۱ طراحی شده‌اند.

برای هدف قرار دادن اندروید ۱۱، تغییرات کوچکی در فایل app/build.gradle ایجاد کنید. در پنل پروژه اندروید استودیو، این فایل زیر گره Gradle Scripts قرار دارد که با ماژول app مرتبط است.

ساخت برنامه.gradle.png

مقدار compileSdkVersion و targetSdkVersion را به 30 تغییر دهید.

    compileSdkVersion 30
    defaultConfig {
      targetSdkVersion 30
    }

برای ضبط، از API Android MediaStore برای ایجاد فایل MP4 در دایرکتوری مشترک Movie استفاده کنید.

یک تابع به نام 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;
  }

مدیریت مجوزهای ذخیره‌سازی

اگر از دستگاه اندروید ۱۱ استفاده می‌کنید، می‌توانید تست کد را شروع کنید. برای پشتیبانی از دستگاه‌های اندروید ۱۰ یا پایین‌تر، باید مجوزهای ذخیره‌سازی برنامه را برای ذخیره داده‌ها در سیستم فایل دستگاه هدف اعطا کنید.

در AndroidManifest.xml ، اعلام کنید که برنامه قبل از اندروید ۱۱ (سطح 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 سطح ۲۹ یا قبل از آن استفاده می‌کنید، در بالای createMp4File() بررسی مجوزهای ذخیره‌سازی را اضافه کنید و اگر برنامه مجوزهای صحیح را ندارد، زودتر از تابع خارج شوید. API سطح ۳۰ (اندروید ۱۱) برای دسترسی به فایل‌ها در MediaStore نیازی به مجوز ذخیره‌سازی ندارد.

  private Uri createMp4File() {
    // Since we use legacy external storage for Android 10,
    // we still need to request for storage permission on Android 10.
    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
      if (!checkAndRequestStoragePermission()) {
        Log.i(TAG, String.format(
            "Didn't createMp4File. No storage permission, API Level = %d",
            Build.VERSION.SDK_INT));
        return null;
      }
    }
    // ... omitted code ...
  }

ضبط از دستگاه هدف

وقت آن است که ببینید تاکنون چه چیزی ساخته‌اید. دستگاه تلفن همراه خود را به دستگاه توسعه خود وصل کنید و روی Run in Android Studio کلیک کنید.

باید یک دکمه قرمز رنگ ضبط در پایین سمت چپ صفحه نمایش ببینید. با ضربه زدن روی آن، متن به «توقف» تغییر می‌کند. برای ضبط یک جلسه، دستگاه خود را حرکت دهید و وقتی می‌خواهید ضبط را کامل کنید، روی دکمه « توقف» کلیک کنید. با این کار، یک فایل جدید به نام arcore-xxxxxx_xxxxxx.mp4 در حافظه خارجی دستگاه شما ذخیره می‌شود.

دکمه ضبط.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 برای فهرست کردن و دریافت فایل‌ها از دستگاه اندروید آمده است:

  • adb shell ls '$EXTERNAL_STORAGE/Movies/*' فایل‌های موجود در پوشه Movies در حافظه خارجی دستگاه را نمایش می‌دهد.
  • adb pull /absolute_path_from_previous_adb_shell_ls/arcore-xxxxxxxx_xxxxxx.mp4 برای کپی کردن فایل از دستگاه به دستگاه توسعه

این یک نمونه خروجی پس از استفاده از این دو دستور (از macOS) است:

$ adb shell ls '$EXTERNAL_STORAGE/Movies/*'
/sdcard/Movies/arcore-xxxxxxxx_xxxxxx.mp4


$ adb pull /sdcard/Movies/arcore-xxxxxxxx_xxxxxx.mp4
/sdcard/Movies/arcore-xxxxxxxx_xxxxxx.mp4: ... pulled

کاری که در این مرحله انجام داده‌اید

  • اضافه شدن دکمه شروع و توقف ضبط
  • توابع پیاده‌سازی شده برای شروع و توقف ضبط
  • برنامه روی دستگاه تست شد
  • فایل MP4 ضبط شده را در دستگاه خود کپی کرده و آن را تأیید کردم

در مرحله بعد، یک جلسه AR را از یک فایل MP4 پخش خواهید کرد.

۴. پخش یک جلسه ARCore از یک فایل MP4

اکنون یک دکمه ضبط و چند فایل MP4 حاوی جلسات ضبط شده دارید. اکنون، آنها را با استفاده از API پخش ARCore پخش خواهید کرد.

اضافه کردن رابط کاربری برای دکمه پخش

قبل از پیاده‌سازی پخش، یک دکمه در رابط کاربری اضافه کنید تا کاربر بتواند به 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 به AppState enum در 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.
  }

انتخاب فایل با دکمه پخش

وقتی دکمه پخش (Playback) زده شود، کاربر باید بتواند فایلی را برای پخش انتخاب کند. در اندروید، انتخاب فایل در انتخابگر فایل سیستمی در یک Activity دیگر انجام می‌شود. این کار از طریق Storage Access Framework ( 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() ایجاد کنید که یک فایل را از دستگاه انتخاب کند. برای انتخاب فایل از سیستم فایل اندروید، از یک ACTION_OPEN_DOCUMENT Intent استفاده کنید.

// Add imports to the beginning of the file.
import android.content.Intent;
import android.provider.DocumentsContract;

  private boolean selectFileToPlayback() {
    // Start file selection from Movies directory.
    // Android 10 and above requires VOLUME_EXTERNAL_PRIMARY to write to MediaStore.
    Uri videoCollection;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
      videoCollection = MediaStore.Video.Media.getContentUri(
          MediaStore.VOLUME_EXTERNAL_PRIMARY);
    } else {
      videoCollection = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
    }

    // Create an Intent to select a file.
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);

    // Add file filters such as the MIME type, the default directory and the file category.
    intent.setType(MP4_VIDEO_MIME_TYPE); // Only select *.mp4 files
    intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, videoCollection); // Set default directory
    intent.addCategory(Intent.CATEGORY_OPENABLE); // Must be files that can be opened

    this.startActivityForResult(intent, REQUEST_MP4_SELECTOR);

    return true;
  }

REQUEST_MP4_SELECTOR یک ثابت برای شناسایی این درخواست است. می‌توانید آن را با استفاده از هر مقدار placeholder درون HelloArActivity در HelloArActivity.java تعریف کنید:

  private int REQUEST_MP4_SELECTOR = 1;

تابع onActivityResult() را در HelloArActivity.java برای مدیریت فراخوانی از انتخابگر فایل، بازنویسی کنید.

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

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

    // Begin playback.
    startPlayingback(mp4FileUri);
  }

برنامه را برای شروع پخش فعال کنید

یک جلسه ARCore برای پخش یک فایل MP4 به سه فراخوانی API نیاز دارد:

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

در HelloArActivity.java ، تابع startPlayingback() را ایجاد کنید.

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

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

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

    pauseARCoreSession();

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

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

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

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


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

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

    return true;
  }

برنامه را برای توقف پخش فعال کنید

یک تابع به نام stopPlayingback() در HelloArActivity.java ایجاد کنید تا تغییرات وضعیت برنامه را پس از موارد زیر مدیریت کند:

  1. پخش MP4 توسط کاربر متوقف شد
  2. پخش MP4 خود به خود انجام شد

اگر کاربر پخش را متوقف کند، برنامه باید به همان وضعیتی که هنگام اولین اجرای آن توسط کاربر داشت، بازگردد.

  // Stop the current playback, and restore app status to Idle.
  private boolean stopPlayingback() {
    // Correctness check, only stop playing back when the app is playing back.
    if (appState != AppState.Playingback)
      return false;

    pauseARCoreSession();

    // Close the current session and create a new session.
    session.close();
    try {
      session = new Session(this);
    } catch (UnavailableArcoreNotInstalledException
        |UnavailableApkTooOldException
        |UnavailableSdkTooOldException
        |UnavailableDeviceNotCompatibleException e) {
      Log.e(TAG, "Error in return to Idle state. Cannot create new ARCore session", e);
      return false;
    }
    configureSession();

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

    // A new session will not have a camera texture name.
    // Manually set hasSetTextureNames to false to trigger a reset.
    hasSetTextureNames = false;

    // Reset appState to Idle, and update the "Record" and "Playback" buttons.
    appState = AppState.Idle;
    updateRecordButton();
    updatePlaybackButton();

    return true;
  }

پخش همچنین می‌تواند به طور طبیعی پس از رسیدن پخش‌کننده به انتهای فایل MP4 متوقف شود. وقتی این اتفاق می‌افتد، stopPlayingback() باید وضعیت برنامه را به Idle برگرداند. در onDrawFrame() ، PlaybackStatus بررسی کنید. اگر FINISHED بود، تابع stopPlayingback() را در نخ رابط کاربری فراخوانی کنید.

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

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

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

      // ... omitted code ...
  }

پخش از دستگاه هدف

وقت آن است که ببینید تاکنون چه چیزی ساخته‌اید. دستگاه تلفن همراه خود را به دستگاه توسعه خود وصل کنید و روی Run in Android Studio کلیک کنید.

وقتی برنامه اجرا می‌شود، باید صفحه‌ای با دکمه قرمز ضبط در سمت چپ و دکمه سبز پخش در سمت راست ببینید.

دکمه پخش.png

روی دکمه پخش (Playback) ضربه بزنید و یکی از فایل‌های MP4 که تازه ضبط کرده‌اید را انتخاب کنید. اگر هیچ نام فایلی که با arcore- شروع می‌شود را نمی‌بینید، شاید دستگاه شما پوشه Movies را نشان نمی‌دهد. در این حالت، با استفاده از منوی گوشه بالا سمت چپ به Phone model > Movies folder بروید. همچنین ممکن است لازم باشد گزینه Show internal storage را فعال کنید تا پوشه مدل گوشی نمایش داده شود.

دکمه-نمایش-حافظه-داخلی.png

انتخابگر فایل nativate-to-movies.jpg

برای انتخاب فایل MP4، روی نام فایل روی صفحه ضربه بزنید. برنامه باید فایل MP4 را پخش کند.

دکمه توقف پخش.png

تفاوت بین پخش یک جلسه و پخش یک ویدیوی معمولی این است که می‌توانید با جلسه ضبط شده تعامل داشته باشید. برای قرار دادن نشانگرها روی صفحه، روی صفحه شناسایی شده ضربه بزنید.

پخش-قرارگیری

کاری که در این مرحله انجام داده‌اید

  • دکمه‌ای برای شروع و توقف پخش اضافه شد
  • تابعی پیاده‌سازی شده است که باعث می‌شود برنامه ضبط را شروع و متوقف کند.
  • یک جلسه ARCore که قبلاً ضبط شده بود را روی دستگاه پخش کرد

۵. داده‌های اضافی را در MP4 ضبط کنید

با ARCore 1.24، امکان ثبت اطلاعات اضافی در فایل MP4 وجود دارد. می‌توانید Pose قرارگیری اشیاء واقعیت افزوده را ضبط کنید، سپس در حین پخش، اشیاء واقعیت افزوده را در همان مکان ایجاد کنید.

آهنگ جدید را برای ضبط پیکربندی کنید

یک مسیر جدید با UUID و برچسب MIME در HelloArActivity.java تعریف کنید.

// Add imports to the beginning of the file.
import java.util.UUID;
import com.google.ar.core.Track;

  // Inside the HelloArActiity class.
  private static final UUID ANCHOR_TRACK_ID = UUID.fromString("53069eb5-21ef-4946-b71c-6ac4979216a6");;
  private static final String ANCHOR_TRACK_MIME_TYPE = "application/recording-playback-anchor";

  private boolean startRecording() {
    // ... omitted code ...

    // Insert after line:
    //   pauseARCoreSession();

    // Create a new Track, with an ID and MIME tag.
    Track anchorTrack = new Track(session)
        .setId(ANCHOR_TRACK_ID).
        .setMimeType(ANCHOR_TRACK_MIME_TYPE);
    // ... omitted code ...
  }

کد فعلی را به‌روزرسانی کنید تا شیء RecordingConfig با فراخوانی addTrack() ایجاد شود.

  private boolean startRecording() {
    // ... omitted code ...

    // Update the lines below with a call to the addTrack() function:
    //   RecordingConfig recordingConfig = new RecordingConfig(session)
    //    .setMp4DatasetUri(mp4FileUri)
    //    .setAutoStopOnPause(true);

    RecordingConfig recordingConfig = new RecordingConfig(session)
        .setMp4DatasetUri(mp4FileUri)
        .setAutoStopOnPause(true)
        .addTrack(anchorTrack); // add the new track onto the recordingConfig

    // ... omitted code ...
  }

ذخیره حالت لنگر در حین ضبط

هر بار که کاربر روی یک صفحه شناسایی‌شده ضربه می‌زند، یک نشانگر AR روی یک Anchor قرار می‌گیرد که موقعیت آن توسط ARCore به‌روزرسانی می‌شود.

اگر هنوز در حال ضبط جلسه ARCore هستید، ژست یک Anchor را در فریمی که ایجاد می‌شود ضبط کنید.

تابع handleTap() را در HelloArActivity.java تغییر دهید.

// Add imports to the beginning of the file.
import com.google.ar.core.Pose;
import java.nio.FloatBuffer;

  private void handleTap(Frame frame, Camera camera) {
          // ... omitted code ...

          // Insert after line:
          // anchors.add(hit.createAnchor());

          // If the app is recording a session,
          // save the new Anchor pose (relative to the camera)
          // into the ANCHOR_TRACK_ID track.
          if (appState == AppState.Recording) {
            // Get the pose relative to the camera pose.
            Pose cameraRelativePose = camera.getPose().inverse().compose(hit.getHitPose());
            float[] translation = cameraRelativePose.getTranslation();
            float[] quaternion = cameraRelativePose.getRotationQuaternion();
            ByteBuffer payload = ByteBuffer.allocate(4 * (translation.length + quaternion.length));
            FloatBuffer floatBuffer = payload.asFloatBuffer();
            floatBuffer.put(translation);
            floatBuffer.put(quaternion);

            try {
              frame.recordTrackData(ANCHOR_TRACK_ID, payload);
            } catch (IllegalStateException e) {
              Log.e(TAG, "Error in recording anchor into external data track.", e);
            }
          }
          // ... omitted code ...
  }

دلیل اینکه ما به جای Pose نسبی دوربین، Pose نسبی جهان را حفظ می‌کنیم، این است که مبدأ جهانی یک جلسه ضبط و مبدأ جهانی یک جلسه پخش یکسان نیستند. مبدأ جهانی یک جلسه ضبط از اولین باری که جلسه از سر گرفته می‌شود، زمانی که Session.resume() برای اولین بار فراخوانی می‌شود، شروع می‌شود. مبدأ جهانی یک جلسه پخش از زمانی شروع می‌شود که اولین فریم ضبط می‌شود، زمانی که Session.resume() برای اولین بار پس از Session.startRecording() فراخوانی می‌شود.

ایجاد لنگر پخش

ایجاد مجدد یک Anchor ساده است. تابعی به نام createRecordedAnchors() را در HelloArActivity.java اضافه کنید.

// Add imports to the beginning of the file.
import com.google.ar.core.TrackData;

  // Extract poses from the ANCHOR_TRACK_ID track, and create new anchors.
  private void createRecordedAnchors(Frame frame, Camera camera) {
    // Get all `ANCHOR_TRACK_ID` TrackData from the frame.
    for (TrackData trackData : frame.getUpdatedTrackData(ANCHOR_TRACK_ID)) {
      ByteBuffer payload = trackData.getData();
      FloatBuffer floatBuffer = payload.asFloatBuffer();

      // Extract translation and quaternion from TrackData payload.
      float[] translation = new float[3];
      float[] quaternion = new float[4];

      floatBuffer.get(translation);
      floatBuffer.get(quaternion);

      // Transform the recorded anchor pose
      // from the camera coordinate
      // into world coordinates.
      Pose worldPose = camera.getPose().compose(new Pose(translation, quaternion));

      // Re-create an anchor at the recorded pose.
      Anchor recordedAnchor = session.createAnchor(worldPose);

      // Add the new anchor into the list of anchors so that
      // the AR marker can be displayed on top.
      anchors.add(recordedAnchor);
    }
  }

createRecordedAnchors() onDrawFrame() در فایل HelloArActivity.java را فراخوانی کنید.

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

    // Insert after this line:
    // handleTap(frame, camera);

    // If the app is currently playing back a session, create recorded anchors.
    if (appState == AppState.Playingback) {
      createRecordedAnchors(frame, camera);
    }
    // ... omitted code ...
  }

روی دستگاه هدف تست کنید

دستگاه تلفن همراه خود را به دستگاه توسعه خود وصل کنید و روی Run در Android Studio کلیک کنید.

ابتدا، برای ضبط یک جلسه، روی دکمه ضبط ضربه بزنید. در حین ضبط، روی هواپیماهای شناسایی شده ضربه بزنید تا چند نشانگر AR قرار دهید.

پس از توقف ضبط، روی دکمه پخش (Playback) ضربه بزنید و فایلی را که تازه ضبط کرده‌اید انتخاب کنید. پخش باید شروع شود. توجه کنید که چگونه مکان‌های نشانگر AR قبلی شما درست همانطور که روی برنامه ضربه می‌زنید، ظاهر می‌شوند.

این تمام کدنویسی است که باید برای این آزمایشگاه کد انجام دهید.

۶. تبریک

تبریک می‌گویم، به پایان این آزمایشگاه کد رسیدید! بیایید نگاهی به کارهایی که در این آزمایشگاه کد انجام داده‌اید بیندازیم:

  • نمونه ARCore Hello AR Java را ساخته و اجرا کنید.
  • یک دکمه ضبط به برنامه اضافه شد تا یک جلسه AR را در یک فایل MP4 ذخیره کنید
  • یک دکمه پخش به برنامه اضافه شد تا یک جلسه AR را از یک فایل MP4 پخش کند.
  • یک ویژگی جدید برای ذخیره لنگرهای ایجاد شده توسط کاربر در MP4 برای پخش اضافه شد

از انجام این کدنویسی لذت بردی؟

بله خیر

آیا در انجام این کدنویسی چیز مفیدی یاد گرفتید؟

بله خیر

آیا ساخت برنامه را در این آزمایشگاه کد کامل کردید؟

بله خیر