1. مقدمة
يمكن أن تكون إمكانية حفظ تجربة الواقع المعزّز في ملف MP4 وتشغيلها من ملف MP4 مفيدة لكل من مطوّري التطبيقات والمستخدمين النهائيين.
تصحيح الأخطاء واختبار الميزات الجديدة من مكتبك
إنّ الاستخدام الأسهل لواجهة برمجة التطبيقات "تسجيل وتشغيل جلسة ARCore" هو للمطوّرين. لقد ولّت الأيام التي كان عليك فيها إنشاء التطبيق وتشغيله على جهاز اختبار، ثم فصل كابل USB، والتجوّل في المكان لاختبار تغيير بسيط في الرمز. ما عليك سوى تسجيل ملف MP4 في بيئة الاختبار مع حركة الهاتف المتوقّعة، ثم إجراء الاختبار مباشرةً من مكتبك.
التسجيل وإعادة التشغيل من أجهزة مختلفة
باستخدام واجهات برمجة التطبيقات "التسجيل والتشغيل"، يمكن لمستخدم تسجيل جلسة باستخدام جهاز، ويمكن لمستخدم آخر تشغيل الجلسة نفسها على جهاز مختلف. من الممكن مشاركة تجربة واقع معزّز مع مستخدم آخر. هناك الكثير من الاحتمالات.
هل هذه هي المرة الأولى التي تصنع فيها تطبيق ARCore؟
كيف ستستخدم هذا الدرس التطبيقي حول الترميز؟
ما ستنشئه
في هذا الدرس التطبيقي حول الترميز، ستستخدم واجهة برمجة التطبيقات Recording & Playback API لإنشاء تطبيق يسجّل تجربة الواقع المعزّز في ملف MP4 ويشغّلها من الملف نفسه. ستتعرّف على:
- كيفية استخدام Recording API لحفظ جلسة واقع معزّز في ملف MP4
- كيفية استخدام Playback API لإعادة تشغيل جلسة واقع معزّز من ملف MP4
- كيفية تسجيل جلسة واقع معزّز على جهاز واحد وإعادة تشغيلها على جهاز آخر
المتطلبات
في هذا الدرس التطبيقي حول الترميز، ستعدّل تطبيق Hello AR Java الذي تم إنشاؤه باستخدام ARCore Android SDK. ستحتاج إلى أجهزة وبرامج معيّنة لتتمكّن من اتّباع الخطوات.
متطلبات الأجهزة
- جهاز متوافق مع ARCore مع تفعيل خيارات المطوّرين وتفعيل تصحيح أخطاء الجهاز عبر USB، ومتصل بجهاز التطوير باستخدام كابل USB
- جهاز تطوير يتم تشغيل "استوديو Android" عليه
- الوصول إلى الإنترنت لتنزيل المكتبات أثناء التطوير
متطلبات البرامج
- الإصدار 1.24 أو الإصدارات الأحدث من خدمات Google Play للواقع المعزّز (ARCore) على جهاز ARCore المخصّص للتطوير يتم عادةً تثبيت هذه الخدمة تلقائيًا على الجهاز من خلال متجر Play. يمكنك أيضًا تثبيته يدويًا على جهاز متوافق مع ARCore.
- استوديو Android (الإصدار 3.1 أو إصدار أحدث) على جهاز التطوير
يجب أيضًا أن يكون لديك فهم أساسي لـ ARCore للحصول على أفضل النتائج.
2. إعداد بيئة التطوير
ابدأ بإعداد بيئة التطوير.
تنزيل حزمة تطوير البرامج (SDK) لنظام التشغيل Android في ARCore
انقر على لتنزيل حزمة تطوير البرامج (SDK).
فكّ ضغط حزمة تطوير البرامج (SDK) لنظام التشغيل Android في ARCore
بعد تنزيل حزمة تطوير البرامج (SDK) لنظام التشغيل Android على جهازك، فُك ضغط الملف وانتقِل إلى الدليل arcore-android-sdk-1.24/samples/hello_ar_java. هذا هو الدليل الجذر للتطبيق الذي ستعمل عليه.

تحميل Hello AR Java في "استوديو Android"
شغِّل "استوديو Android" وانقر على فتح مشروع حالي في "استوديو Android".

في نافذة مربّع الحوار الناتجة، انقر على arcore-android-sdk-1.24/samples/hello_ar_java ثم على فتح.
انتظِر إلى أن ينتهي "استوديو Android" من مزامنة المشروع. في حال عدم توفّر أي مكوّن، قد يتعذّر استيراد المشروع مع ظهور رسائل خطأ. يُرجى حلّ هذه المشاكل قبل المتابعة.
تشغيل نموذج التطبيق
- وصِّل جهازًا متوافقًا مع ARCore بجهاز التطوير.
- إذا تم التعرّف على الجهاز بشكل صحيح، من المفترض أن يظهر اسم الجهاز في "استوديو Android".

- انقر على الزر "تشغيل" أو اختَر تشغيل > تشغيل "التطبيق" ليثبِّت استوديو Android التطبيق ويشغّله على الجهاز.

- ستظهر لك رسالة تطلب منك منح الإذن بالتقاط الصور وتسجيل الفيديوهات. اختَر أثناء استخدام هذا التطبيق لمنح التطبيق أذونات الوصول إلى الكاميرا. بعد ذلك، ستظهر البيئة المحيطة بك على أرض الواقع على شاشة الجهاز.

- حرِّك الجهاز أفقيًا للبحث عن طائرات.
- تظهر شبكة بيضاء عندما يرصد التطبيق طائرة. انقر على هذا الزر لوضع علامة على هذا المستوى.

الإجراءات التي اتّخذتها في هذه الخطوة
- إعداد مشروع Hello AR Java
- إنشاء نموذج تطبيق وتشغيله على جهاز متوافق مع ARCore
بعد ذلك، ستسجّل جلسة واقع معزّز في ملف MP4.
3- تسجيل جلسة ARCore في ملف MP4
سنضيف ميزة التسجيل في هذه الخطوة. ويتألف من:
- زر لبدء التسجيل أو إيقافه
- وظائف التخزين لحفظ ملف MP4 على الجهاز
- طلبات بدء تسجيل جلسة ARCore أو إيقافه
إضافة واجهة مستخدم لزر التسجيل
قبل تنفيذ عملية التسجيل، أضِف زرًا إلى واجهة المستخدم ليتمكّن المستخدم من إعلام ARCore بموعد بدء التسجيل أو إيقافه.
في لوحة "المشروع"، افتح الملف app/res/layout/activity_main.xml.

سيستخدم "استوديو Android" تلقائيًا طريقة عرض التصميم بعد فتح الملف app/res/layout/activity_main.xml. انقر على الزر الرمز البرمجي في أعلى يسار علامة التبويب للتبديل إلى طريقة عرض الرمز البرمجي.

في 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() في الخطوات القليلة التالية.
تغيير النص على الزر استنادًا إلى الحالة
يتولّى زر التسجيل مهمة بدء التسجيل وإيقافه. عندما لا يسجّل التطبيق بيانات، يجب أن يعرض الكلمة "تسجيل". عندما يسجّل التطبيق البيانات، يجب أن يتغيّر الزر لعرض الكلمة "إيقاف".
ولكي يتمكّن الزر من تنفيذ هذه الوظيفة، يجب أن يعرف التطبيق حالته الحالية. ينشئ الرمز التالي تعدادًا جديدًا باسم 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:
- حدِّد معرّف الموارد المنتظم (URI) لملف التسجيل في عنصر
RecordingConfig. - الاتصال بـ "
session.startRecording" باستخدام العنصر "RecordingConfig"
أما بقية الرمز، فهو مجرد رمز النص النموذجي: الإعداد وتسجيل والتحقّق من الصحة.
أنشئ دالة جديدة باسم startRecording() تسجّل البيانات وتحفظها في معرّف موارد منتظم (URI) بتنسيق MP4. أضِف الرمز التالي داخل الفئة HelloArActivity في HelloArActivity.java.
// Add imports to the beginning of the file.
import android.net.Uri;
import com.google.ar.core.RecordingConfig;
import com.google.ar.core.RecordingStatus;
import com.google.ar.core.exceptions.RecordingFailedException;
private boolean startRecording() {
Uri mp4FileUri = createMp4File();
if (mp4FileUri == null)
return false;
Log.d(TAG, "startRecording at: " + mp4FileUri);
pauseARCoreSession();
// Configure the ARCore session to start recording.
RecordingConfig recordingConfig = new RecordingConfig(session)
.setMp4DatasetUri(mp4FileUri)
.setAutoStopOnPause(true);
try {
// Prepare the session for recording, but do not start recording yet.
session.startRecording(recordingConfig);
} catch (RecordingFailedException e) {
Log.e(TAG, "startRecording - Failed to prepare to start recording", e);
return false;
}
boolean canResume = resumeARCoreSession();
if (!canResume)
return false;
// Correctness checking: check the ARCore session's RecordingState.
RecordingStatus recordingStatus = session.getRecordingStatus();
Log.d(TAG, String.format("startRecording - recordingStatus %s", recordingStatus));
return recordingStatus == RecordingStatus.OK;
}
لإيقاف جلسة ARCore مؤقتًا واستئنافها بأمان، أنشِئ pauseARCoreSession() وresumeARCoreSession() في HelloArActivity.java.
private void pauseARCoreSession() {
// Pause the GLSurfaceView so that it doesn't update the ARCore session.
// Pause the ARCore session so that we can update its configuration.
// If the GLSurfaceView is not paused,
// onDrawFrame() will try to update the ARCore session
// while it's paused, resulting in a crash.
surfaceView.onPause();
session.pause();
}
private boolean resumeARCoreSession() {
// We must resume the ARCore session before the GLSurfaceView.
// Otherwise, the GLSurfaceView will try to update the ARCore session.
try {
session.resume();
} catch (CameraNotAvailableException e) {
Log.e(TAG, "CameraNotAvailableException in resumeARCoreSession", e);
return false;
}
surfaceView.onResume();
return true;
}
السماح للتطبيق بإيقاف التسجيل
أنشئ دالة باسم stopRecording() في HelloArActivity.java لمنع تطبيقك من تسجيل بيانات جديدة. تستدعي هذه الدالة session.stopRecording() وترسل خطأً إلى سجلّ وحدة التحكّم إذا تعذّر على التطبيق إيقاف التسجيل.
private boolean stopRecording() {
try {
session.stopRecording();
} catch (RecordingFailedException e) {
Log.e(TAG, "stopRecording - Failed to stop recording", e);
return false;
}
// Correctness checking: check if the session stopped recording.
return session.getRecordingStatus() == RecordingStatus.NONE;
}
تخزين ملفات التصميم باستخدام ميزة "التخزين الفرعي" في نظام التشغيل Android 11
تم تصميم الدوال ذات الصلة بمساحة التخزين في هذا الدرس التطبيقي العملي وفقًا لمتطلبات مساحة التخزين الجديدة المحصورة النطاق في Android 11.
أجرِ بعض التغييرات الصغيرة في ملف app/build.gradle لاستهداف الإصدار Android 11. في لوحة "المشروع" في "استوديو Android"، يظهر هذا الملف ضمن عقدة برامج نصوص Gradle المرتبطة بوحدة التطبيق.

غيِّر compileSdkVersion وtargetSdkVersion إلى 30.
compileSdkVersion 30
defaultConfig {
targetSdkVersion 30
}
للتسجيل، استخدِم Android MediaStore API لإنشاء ملف MP4 في دليل "الأفلام" المشترَك.
أنشئ دالة باسم createMp4File() في HelloArActivity.java:
// Add imports to the beginning of the file.
import java.text.SimpleDateFormat;
import android.content.ContentResolver;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import android.content.ContentValues;
import java.io.File;
import android.content.CursorLoader;
import android.database.Cursor;
import java.util.Date;
private final String MP4_VIDEO_MIME_TYPE = "video/mp4";
private Uri createMp4File() {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
String mp4FileName = "arcore-" + dateFormat.format(new Date()) + ".mp4";
ContentResolver resolver = this.getContentResolver();
Uri videoCollection = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
videoCollection = MediaStore.Video.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL_PRIMARY);
} else {
videoCollection = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
}
// Create a new Media file record.
ContentValues newMp4FileDetails = new ContentValues();
newMp4FileDetails.put(MediaStore.Video.Media.DISPLAY_NAME, mp4FileName);
newMp4FileDetails.put(MediaStore.Video.Media.MIME_TYPE, MP4_VIDEO_MIME_TYPE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// The Relative_Path column is only available since API Level 29.
newMp4FileDetails.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES);
} else {
// Use the Data column to set path for API Level <= 28.
File mp4FileDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
String absoluteMp4FilePath = new File(mp4FileDir, mp4FileName).getAbsolutePath();
newMp4FileDetails.put(MediaStore.Video.Media.DATA, absoluteMp4FilePath);
}
Uri newMp4FileUri = resolver.insert(videoCollection, newMp4FileDetails);
// Ensure that this file exists and can be written.
if (newMp4FileUri == null) {
Log.e(TAG, String.format("Failed to insert Video entity in MediaStore. API Level = %d", Build.VERSION.SDK_INT));
return null;
}
// This call ensures the file exist before we pass it to the ARCore API.
if (!testFileWriteAccess(newMp4FileUri)) {
return null;
}
Log.d(TAG, String.format("createMp4File = %s, API Level = %d", newMp4FileUri, Build.VERSION.SDK_INT));
return newMp4FileUri;
}
// Test if the file represented by the content Uri can be open with write access.
private boolean testFileWriteAccess(Uri contentUri) {
try (java.io.OutputStream mp4File = this.getContentResolver().openOutputStream(contentUri)) {
Log.d(TAG, String.format("Success in testFileWriteAccess %s", contentUri.toString()));
return true;
} catch (java.io.FileNotFoundException e) {
Log.e(TAG, String.format("FileNotFoundException in testFileWriteAccess %s", contentUri.toString()), e);
} catch (java.io.IOException e) {
Log.e(TAG, String.format("IOException in testFileWriteAccess %s", contentUri.toString()), e);
}
return false;
}
التعامل مع أذونات مساحة التخزين
إذا كنت تستخدم جهاز Android 11، يمكنك بدء اختبار الرمز. لإتاحة استخدام التطبيق على الأجهزة التي تعمل بالإصدار 10 من نظام التشغيل Android أو الإصدارات الأقدم، عليك منح التطبيق أذونات الوصول إلى مساحة التخزين لحفظ البيانات في نظام ملفات الجهاز المستهدف.
في AndroidManifest.xml، يجب الإفصاح عن أنّ التطبيق يحتاج إلى أذونات القراءة والكتابة في وحدة التخزين قبل الإصدار Android 11 (المستوى 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;
}
إذا كنت تستخدم المستوى 29 من واجهة برمجة التطبيقات أو مستوى أقدم، أضِف عملية تحقّق من أذونات التخزين في أعلى createMp4File() واخرج من الدالة مبكرًا إذا لم يكن لدى التطبيق الأذونات الصحيحة. لا يتطلّب المستوى 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".
من المفترض أن يظهر زر تسجيل أحمر اللون في أسفل يمين الشاشة. يجب أن يؤدي النقر عليه إلى تغيير النص إلى إيقاف. حرِّك جهازك لتسجيل جلسة، وانقر على الزر إيقاف عندما تريد إكمال التسجيل. من المفترض أن يؤدي ذلك إلى حفظ ملف جديد باسم arcore-xxxxxx_xxxxxx.mp4 في وحدة التخزين الخارجية لجهازك.

من المفترض أن يتوفّر لديك الآن ملف 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 من Google، لعرض التسجيل أو نسخه إلى جهاز التطوير. في ما يلي أمرَا adb لعرض الملفات واسترجاعها من جهاز Android:
adb shell ls '$EXTERNAL_STORAGE/Movies/*'لعرض الملفات في دليل "الأفلام" في وحدة التخزين الخارجية على الجهازadb pull /absolute_path_from_previous_adb_shell_ls/arcore-xxxxxxxx_xxxxxx.mp4لنسخ الملف من الجهاز إلى جهاز التطوير
في ما يلي مثال على الناتج بعد استخدام هذين الأمرَين (من نظام التشغيل macOS):
$ adb shell ls '$EXTERNAL_STORAGE/Movies/*' /sdcard/Movies/arcore-xxxxxxxx_xxxxxx.mp4 $ adb pull /sdcard/Movies/arcore-xxxxxxxx_xxxxxx.mp4 /sdcard/Movies/arcore-xxxxxxxx_xxxxxx.mp4: ... pulled
الإجراءات التي اتّخذتها في هذه الخطوة
- تمت إضافة زر لبدء التسجيل وإيقافه
- تم تنفيذ وظائف لبدء التسجيل وإيقافه
- اختبار التطبيق على الجهاز
- نسخ ملف MP4 المسجّل إلى جهازك والتحقّق منه
بعد ذلك، ستعيد تشغيل جلسة AR من ملف MP4.
4. تشغيل جلسة ARCore من ملف MP4
يظهر الآن زر تسجيل وبعض ملفات MP4 التي تحتوي على الجلسات المسجّلة. يمكنك الآن تشغيلها باستخدام واجهة ARCore Playback API.
إضافة واجهة مستخدم لزر التشغيل
قبل تنفيذ ميزة التشغيل، أضِف زرًا على واجهة المستخدم ليتمكّن المستخدم من إعلام ARCore بموعد بدء تشغيل الجلسة وإيقافها.
في لوحة المشروع، افتح ملف app/res/layout/activity_main.xml.

في 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 إلى تعداد AppState في HelloArActivity.java:
public enum AppState {
Idle,
Recording,
Playingback // New enum value.
}
إذا كان زر التسجيل لا يزال يظهر على الشاشة أثناء التشغيل، قد ينقر عليه المستخدم عن طريق الخطأ. لتجنُّب ذلك، أخفِ زر تسجيل أثناء التشغيل. بهذه الطريقة، لن تحتاج إلى التعامل مع حالة Playingback في onClickRecord().
عدِّل الدالة updateRecordButton() في HelloArActivity.java لإخفاء الزر تسجيل عندما يكون التطبيق في الحالة Playingback.
// Update the "Record" button based on app's internal state.
private void updateRecordButton() {
View buttonView = findViewById(R.id.record_button);
Button button = (Button)buttonView;
switch (appState) {
// The app is neither recording nor playing back. The "Record" button is visible.
case Idle:
button.setText("Record");
button.setVisibility(View.VISIBLE);
break;
// While recording, the "Record" button is visible and says "Stop".
case Recording:
button.setText("Stop");
button.setVisibility(View.VISIBLE);
break;
// During playback, the "Record" button is not visible.
case Playingback:
button.setVisibility(View.INVISIBLE);
break;
}
}
وبالمثل، يمكنك إخفاء زر التشغيل عندما يسجّل المستخدم جلسة، وتغييره إلى "إيقاف" عندما يشغّل المستخدم جلسة بشكل نشط. بهذه الطريقة، يمكنهم إيقاف التشغيل بدون الحاجة إلى الانتظار حتى يكتمل تلقائيًا.
أضِف دالة updatePlaybackButton() في HelloArActivity.java:
// Update the "Playback" button based on app's internal state.
private void updatePlaybackButton() {
View buttonView = findViewById(R.id.playback_button);
Button button = (Button)buttonView;
switch (appState) {
// The app is neither recording nor playing back. The "Playback" button is visible.
case Idle:
button.setText("Playback");
button.setVisibility(View.VISIBLE);
break;
// While playing back, the "Playback" button is visible and says "Stop".
case Playingback:
button.setText("Stop");
button.setVisibility(View.VISIBLE);
break;
// During recording, the "Playback" button is not visible.
case Recording:
button.setVisibility(View.INVISIBLE);
break;
}
}
أخيرًا، عدِّل onClickRecord() إلى updatePlaybackButton() للاتصال. أضِف السطر التالي إلى HelloArActivity.java:
public void onClickRecord(View view) {
// ... omitted code ...
updatePlaybackButton(); // Add this line to the end of the function.
}
اختيار ملف باستخدام زر التشغيل
عند النقر على زر التشغيل، يجب أن يسمح للمستخدم باختيار ملف لتشغيله. على أجهزة Android، يتم التعامل مع عملية اختيار الملفات في أداة اختيار ملفات النظام في نشاط آخر. ويتم ذلك من خلال إطار عمل الوصول إلى مساحة التخزين (SAF). بعد أن يختار المستخدم ملفًا، يتلقّى التطبيق رد اتصال يُسمى onActivityResult(). ستبدأ عملية التشغيل الفعلية داخل دالّة رد الاتصال هذه.
في HelloArActivity.java، أنشئ دالة onClickPlayback() لاختيار الملف وإيقاف التشغيل.
// Handle the click event of the "Playback" button.
public void onClickPlayback(View view) {
Log.d(TAG, "onClickPlayback");
switch (appState) {
// If the app is not playing back, open the file picker.
case Idle: {
boolean hasStarted = selectFileToPlayback();
Log.d(TAG, String.format("onClickPlayback start: selectFileToPlayback %b", hasStarted));
break;
}
// If the app is playing back, stop playing back.
case Playingback: {
boolean hasStopped = stopPlayingback();
Log.d(TAG, String.format("onClickPlayback stop: hasStopped %b", hasStopped));
break;
}
default:
// Recording - do nothing.
break;
}
// Update the UI for the "Record" and "Playback" buttons.
updateRecordButton();
updatePlaybackButton();
}
في HelloArActivity.java، أنشئ دالة selectFileToPlayback() تختار ملفًا من الجهاز. لاختيار ملف من نظام ملفات Android، استخدِم ACTION_OPEN_DOCUMENT Intent.
// Add imports to the beginning of the file.
import android.content.Intent;
import android.provider.DocumentsContract;
private boolean selectFileToPlayback() {
// Start file selection from Movies directory.
// Android 10 and above requires VOLUME_EXTERNAL_PRIMARY to write to MediaStore.
Uri videoCollection;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
videoCollection = MediaStore.Video.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL_PRIMARY);
} else {
videoCollection = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
}
// Create an Intent to select a file.
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
// Add file filters such as the MIME type, the default directory and the file category.
intent.setType(MP4_VIDEO_MIME_TYPE); // Only select *.mp4 files
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, videoCollection); // Set default directory
intent.addCategory(Intent.CATEGORY_OPENABLE); // Must be files that can be opened
this.startActivityForResult(intent, REQUEST_MP4_SELECTOR);
return true;
}
REQUEST_MP4_SELECTOR هو ثابت لتحديد هذا الطلب. يمكنك تحديدها باستخدام أي قيمة عنصر نائب داخل HelloArActivity في HelloArActivity.java:
private int REQUEST_MP4_SELECTOR = 1;
تجاهل الدالة onActivityResult() في HelloArActivity.java للتعامل مع رد الاتصال من أداة اختيار الملفات.
// 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:
session.pause()session.setPlaybackDataset()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 للتعامل مع تغييرات حالة التطبيق بعد:
- أوقف المستخدم تشغيل ملف MP4
- اكتمل تشغيل ملف 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 ...
}
تشغيل المحتوى من جهاز الاختبار
حان الوقت للاطّلاع على ما أنجزته حتى الآن. وصِّل جهازك الجوّال بجهاز التطوير وانقر على تشغيل في "استوديو Android".
عند تشغيل التطبيق، من المفترض أن تظهر لك شاشة تتضمّن زر تسجيل أحمر اللون على اليسار وزر تشغيل أخضر اللون على اليمين.

انقر على زر التشغيل واختَر أحد ملفات MP4 التي سجّلتها للتو. إذا لم تظهر لك أي أسماء ملفات تبدأ بـ arcore-، قد لا يعرض جهازك مجلد الأفلام. في هذه الحالة، انتقِل إلى المجلد طراز الهاتف > الأفلام باستخدام القائمة في أعلى يمين الشاشة. قد تحتاج أيضًا إلى تفعيل الخيار عرض وحدة التخزين الداخلية للكشف عن مجلد طراز الهاتف.


انقر على اسم ملف على الشاشة لاختيار ملف MP4. من المفترض أن يشغّل التطبيق ملف MP4.

يختلف تشغيل جلسة مسجّلة عن تشغيل فيديو عادي في أنّه يمكنك التفاعل مع الجلسة المسجّلة. انقر على مستوى تم رصده لوضع علامات على الشاشة.

الإجراءات التي اتّخذتها في هذه الخطوة
- تمت إضافة زر لبدء التشغيل وإيقافه
- تم تنفيذ وظيفة لبدء التسجيل وإيقافه في التطبيق
- تمت إعادة تشغيل جلسة ARCore مسجّلة سابقًا على الجهاز
5- تسجيل بيانات إضافية في ملف MP4
باستخدام ARCore 1.24، يمكن تسجيل معلومات إضافية في ملف MP4. يمكنك تسجيل Pose لعمليات وضع عناصر الواقع المعزّز، ثم إنشاء عناصر الواقع المعزّز في الموقع الجغرافي نفسه أثناء التشغيل.
ضبط المقطع الصوتي الجديد لتسجيل الصوت
حدِّد مسارًا جديدًا باستخدام معرّف فريد عالميًا وعلامة 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، وسيتم تعديل وضعها بواسطة ARCore.
سجِّل وضع Anchor في الإطار الذي تم إنشاؤه فيه، إذا كنت لا تزال تسجّل جلسة ARCore.
عدِّل الدالة 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 ...
}
الاختبار على جهاز الاختبار
وصِّل جهازك الجوّال بجهاز التطوير وانقر على تشغيل في "استوديو Android".
أولاً، انقر على الزر تسجيل لتسجيل جلسة. أثناء التسجيل، انقر على الطائرات التي تم رصدها لوضع بعض علامات الواقع المعزّز.
بعد إيقاف التسجيل، انقر على زر التشغيل واختَر الملف الذي سجّلته للتو. من المفترض أن يبدأ التشغيل. لاحظ كيف تظهر مواضع علامات الواقع المعزّز السابقة تمامًا كما كنت تنقر على التطبيق.
هذا كل ما عليك فعله من ترميز في هذا الدرس التطبيقي حول الترميز.
6. تهانينا
تهانينا، لقد وصلت إلى نهاية هذا الدرس التطبيقي حول الترميز. لنلقِ نظرة على ما أنجزته في هذا الدرس العملي:
- أنشئ نموذج Hello AR Java الخاص بـ ARCore وشغِّله.
- تمت إضافة زر "تسجيل" إلى التطبيق لحفظ جلسة الواقع المعزّز في ملف MP4
- تمت إضافة زر "تشغيل" إلى التطبيق لتشغيل جلسة واقع معزّز من ملف MP4
- تمت إضافة ميزة جديدة لحفظ نقاط الارتكاز التي أنشأها المستخدم في ملف MP4 لتشغيلها لاحقًا