diff --git a/android/app/src/main/java/org/openbot/common/CameraFragment.java b/android/app/src/main/java/org/openbot/common/CameraFragment.java index 0789cee74..4cb728b57 100644 --- a/android/app/src/main/java/org/openbot/common/CameraFragment.java +++ b/android/app/src/main/java/org/openbot/common/CameraFragment.java @@ -1,13 +1,18 @@ package org.openbot.common; import android.annotation.SuppressLint; +import android.app.Dialog; +import android.content.Context; import android.content.res.Configuration; import android.graphics.Bitmap; +import android.net.Uri; import android.os.Bundle; +import android.os.Environment; import android.util.Size; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.Window; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; @@ -17,14 +22,19 @@ import androidx.camera.core.ImageAnalysis; import androidx.camera.core.ImageProxy; import androidx.camera.core.Preview; +import androidx.camera.core.VideoCapture; import androidx.camera.lifecycle.ProcessCameraProvider; import androidx.camera.view.PreviewView; import androidx.core.content.ContextCompat; import androidx.viewbinding.ViewBinding; import com.google.common.util.concurrent.ListenableFuture; +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Locale; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import org.jetbrains.annotations.NotNull; import org.openbot.R; import org.openbot.env.ImageUtils; import org.openbot.utils.Constants; @@ -44,6 +54,8 @@ public abstract class CameraFragment extends ControlsFragment { private YuvToRgbConverter converter; private Bitmap bitmapBuffer; private int rotationDegrees; + private VideoCapture videoCapture; + private Dialog loadingDialog; protected View inflateFragment(int resId, LayoutInflater inflater, ViewGroup container) { return addCamera(inflater.inflate(resId, container, false), inflater, container); @@ -60,6 +72,7 @@ private View addCamera(View view, LayoutInflater inflater, ViewGroup container) previewView = cameraView.findViewById(R.id.viewFinder); rootView.addView(view); + videoCapture = new VideoCapture.Builder().build(); if (!PermissionUtils.hasCameraPermission(requireActivity())) { requestPermissionLauncherCamera.launch(Constants.PERMISSION_CAMERA); @@ -95,9 +108,10 @@ private void setupCamera() { } @SuppressLint({"UnsafeExperimentalUsageError", "UnsafeOptInUsageError"}) - private void bindCameraUseCases() { + protected void bindCameraUseCases() { converter = new YuvToRgbConverter(requireContext()); bitmapBuffer = null; + videoCapture = new VideoCapture.Builder().build(); preview = new Preview.Builder().setTargetAspectRatio(AspectRatio.RATIO_16_9).build(); final boolean rotated = ImageUtils.getScreenOrientation(requireActivity()) % 180 == 90; final PreviewView.ScaleType scaleType = @@ -113,7 +127,7 @@ private void bindCameraUseCases() { new ImageAnalysis.Builder().setTargetAspectRatio(AspectRatio.RATIO_16_9).build(); else imageAnalysis = new ImageAnalysis.Builder().setTargetResolution(analyserResolution).build(); - // insert your code here. + imageAnalysis.setAnalyzer( cameraExecutor, image -> { @@ -130,7 +144,7 @@ private void bindCameraUseCases() { try { if (cameraProvider != null) { cameraProvider.unbindAll(); - cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalysis); + cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalysis, videoCapture); } } catch (Exception e) { Timber.e("Use case binding failed: %s", e.toString()); @@ -188,5 +202,67 @@ public void setAnalyserResolution(Size resolutionSize) { bindCameraUseCases(); } + @SuppressLint("RestrictedApi") + protected void startVideoRecording() { + String outputDirectory = + Environment.getExternalStorageDirectory().getAbsolutePath() + + File.separator + + getString(R.string.app_name) + + File.separator + + "videos"; + final File myDir = new File(outputDirectory); + + if (!myDir.exists()) { + if (!myDir.mkdirs()) { + Timber.i("Make dir failed"); + } + } + + File videoFile = + new File( + outputDirectory, + new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()) + .format(System.currentTimeMillis()) + + ".mp4"); + VideoCapture.OutputFileOptions outputOptions = + new VideoCapture.OutputFileOptions.Builder(videoFile).build(); + + videoCapture.startRecording( + outputOptions, + ContextCompat.getMainExecutor(requireContext()), + new VideoCapture.OnVideoSavedCallback() { + @Override + public void onVideoSaved( + @NonNull @NotNull VideoCapture.OutputFileResults outputFileResults) { + Uri savedUri = Uri.fromFile(videoFile); + if (loadingDialog != null) loadingDialog.cancel(); + Timber.d("Video capture succeeded:" + savedUri); + } + + @Override + public void onError( + int videoCaptureError, + @NonNull @NotNull String message, + @Nullable @org.jetbrains.annotations.Nullable Throwable cause) { + Timber.e("Video capture failed: " + message); + } + }); + } + + @SuppressLint("RestrictedApi") + public void stopVideoRecording() { + videoCapture.stopRecording(); + showLoading(requireContext()); + } + + public void showLoading(final Context context) { + loadingDialog = new Dialog(context); + loadingDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + loadingDialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent); + loadingDialog.getWindow().setDimAmount(0.1f); + loadingDialog.setContentView(R.layout.dialog_loader); + loadingDialog.show(); + } + protected abstract void processFrame(Bitmap image, ImageProxy imageProxy); } diff --git a/android/app/src/main/java/org/openbot/logging/LoggerFragment.java b/android/app/src/main/java/org/openbot/logging/LoggerFragment.java index f788a0a36..b289bd8e8 100644 --- a/android/app/src/main/java/org/openbot/logging/LoggerFragment.java +++ b/android/app/src/main/java/org/openbot/logging/LoggerFragment.java @@ -308,12 +308,15 @@ private void startLogging() { Timber.e(e, "Got interrupted."); } }); + if (binding.videoCaptureCheckBox.isChecked()) startVideoRecording(); } private void stopLogging() { if (sensorConnection != null) requireActivity().unbindService(sensorConnection); requireActivity().stopService(intentSensorService); + if (binding.videoCaptureCheckBox.isChecked()) stopVideoRecording(); + // Pack and upload the collected data runInBackground( () -> { @@ -357,8 +360,10 @@ protected void setIsLoggingActive(boolean loggingActive) { new ActivityResultContracts.RequestMultiplePermissions(), result -> { result.forEach((permission, granted) -> allGranted = allGranted && granted); - if (allGranted) setIsLoggingActive(true); - else { + if (allGranted) { + bindCameraUseCases(); + setIsLoggingActive(true); + } else { PermissionUtils.showLoggingPermissionsToast(requireActivity()); } }); diff --git a/android/app/src/main/java/org/openbot/utils/Constants.java b/android/app/src/main/java/org/openbot/utils/Constants.java index 88bbfdae2..f3ac8a523 100644 --- a/android/app/src/main/java/org/openbot/utils/Constants.java +++ b/android/app/src/main/java/org/openbot/utils/Constants.java @@ -20,7 +20,7 @@ public class Constants { public static final String PERMISSION_AUDIO = Manifest.permission.RECORD_AUDIO; public static final String[] PERMISSIONS_LOGGING = - new String[] {PERMISSION_CAMERA, PERMISSION_STORAGE, PERMISSION_LOCATION}; + new String[] {PERMISSION_CAMERA, PERMISSION_STORAGE, PERMISSION_AUDIO, PERMISSION_LOCATION}; public static final String[] PERMISSIONS_CONTROLLER = new String[] {PERMISSION_CAMERA, PERMISSION_AUDIO, PERMISSION_LOCATION}; diff --git a/android/app/src/main/java/org/openbot/utils/PermissionUtils.java b/android/app/src/main/java/org/openbot/utils/PermissionUtils.java index 2ee563343..06c595031 100644 --- a/android/app/src/main/java/org/openbot/utils/PermissionUtils.java +++ b/android/app/src/main/java/org/openbot/utils/PermissionUtils.java @@ -56,7 +56,10 @@ public static boolean hasAudioPermission(Activity activity) { public static boolean hasLoggingPermissions(Activity activity) { return hasPermissions( - activity, new String[] {PERMISSION_CAMERA, PERMISSION_STORAGE, PERMISSION_LOCATION}); + activity, + new String[] { + PERMISSION_CAMERA, PERMISSION_AUDIO, PERMISSION_STORAGE, PERMISSION_LOCATION + }); } public static boolean hasControllerPermissions(Activity activity) { @@ -89,7 +92,7 @@ public static void requestAudioPermission(Activity activity) { public static void requestLoggingPermissions(Activity activity) { requestPermissions( activity, - new String[] {PERMISSION_CAMERA, PERMISSION_STORAGE, PERMISSION_LOCATION}, + new String[] {PERMISSION_CAMERA, PERMISSION_AUDIO, PERMISSION_STORAGE, PERMISSION_LOCATION}, REQUEST_LOGGING_PERMISSIONS); } @@ -108,10 +111,11 @@ public static boolean checkControllerPermissions(int[] grantResults) { } public static boolean checkLoggingPermissions(int[] grantResults) { - return grantResults.length > 2 + return grantResults.length > 3 && grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED - && grantResults[2] == PackageManager.PERMISSION_GRANTED; + && grantResults[2] == PackageManager.PERMISSION_GRANTED + && grantResults[3] == PackageManager.PERMISSION_GRANTED; } public static void showControllerPermissionsToast(Activity activity) { @@ -147,6 +151,16 @@ public static void showAudioPermissionControllerToast(Activity activity) { .show(); } + public static void showAudioPermissionLoggingToast(Activity activity) { + Toast.makeText( + activity.getApplicationContext(), + activity.getResources().getString(R.string.record_audio_permission_denied) + + " " + + activity.getResources().getString(R.string.permission_reason_save_audio), + Toast.LENGTH_LONG) + .show(); + } + public static void showCameraPermissionControllerToast(Activity activity) { Toast.makeText( activity.getApplicationContext(), @@ -162,7 +176,7 @@ public static void showCameraPermissionsPreviewToast(Activity activity) { activity.getApplicationContext(), activity.getResources().getString(R.string.camera_permission_denied) + " " - + activity.getResources().getString(R.string.permission_reason_preview), + + activity.getResources().getString(R.string.permission_reason_preview_video), Toast.LENGTH_LONG) .show(); } @@ -179,6 +193,10 @@ public static void showLoggingPermissionsToast(Activity activity) { if (shouldShowRational(activity, Constants.PERMISSION_STORAGE)) { showStoragePermissionLoggingToast(activity); } + + if (shouldShowRational(activity, PERMISSION_AUDIO)) { + showAudioPermissionLoggingToast(activity); + } } public static boolean shouldShowRational(Activity activity, String permission) { diff --git a/android/app/src/main/res/layout-land/fragment_logger.xml b/android/app/src/main/res/layout-land/fragment_logger.xml index 74a206171..c93d87f37 100644 --- a/android/app/src/main/res/layout-land/fragment_logger.xml +++ b/android/app/src/main/res/layout-land/fragment_logger.xml @@ -156,15 +156,25 @@ app:layout_constraintStart_toEndOf="@+id/ipAddress" app:layout_constraintTop_toTopOf="parent" /> + + + + app:layout_constraintStart_toStartOf="@id/videoCaptureCheckBox" + app:layout_constraintTop_toBottomOf="@+id/videoCaptureCheckBox" /> + + diff --git a/android/app/src/main/res/layout/fragment_logger.xml b/android/app/src/main/res/layout/fragment_logger.xml index eea80a14c..5cbbf0df4 100644 --- a/android/app/src/main/res/layout/fragment_logger.xml +++ b/android/app/src/main/res/layout/fragment_logger.xml @@ -172,14 +172,24 @@ app:layout_constraintStart_toEndOf="@+id/ipAddress" app:layout_constraintTop_toTopOf="parent" /> + + + app:layout_constraintStart_toStartOf="@id/videoCaptureCheckBox" + app:layout_constraintTop_toBottomOf="@+id/videoCaptureCheckBox" /> + to log datasets. to run AI models. to stream video to the controller. - to preview video. + to preview video. + to save video files. to stream audio to the controller. to find the controller. to select a model from phone storage. diff --git a/android/controller/build.gradle b/android/controller/build.gradle index 35ba6e3ed..7e3af7c7c 100644 --- a/android/controller/build.gradle +++ b/android/controller/build.gradle @@ -78,7 +78,7 @@ dependencies { implementation 'org.videolan.android:libvlc-all:3.3.14' // WebRTC - implementation 'org.webrtc:google-webrtc:1.0.+' + implementation 'org.webrtc:google-webrtc:1.0.32006' // For a library module, uncomment the following line and comment the one after // apply plugin: 'com.android.library'