diff --git a/plugin.xml b/plugin.xml index 297a332..2a13972 100644 --- a/plugin.xml +++ b/plugin.xml @@ -19,6 +19,7 @@ + diff --git a/src/android/CameraPreviewFragment.java b/src/android/CameraPreviewFragment.java index c0a299f..bd34780 100644 --- a/src/android/CameraPreviewFragment.java +++ b/src/android/CameraPreviewFragment.java @@ -1,13 +1,17 @@ package com.spoon.simplecamerapreview; +import android.Manifest; import android.annotation.SuppressLint; +import android.content.ContentValues; import android.content.Context; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Point; import android.hardware.camera2.CameraCharacteristics; import android.location.Location; import android.net.Uri; import android.os.Bundle; +import android.provider.MediaStore; import android.os.Handler; import android.os.Looper; import android.util.Log; @@ -29,8 +33,17 @@ import androidx.camera.core.ImageCaptureException; import androidx.camera.core.Preview; import androidx.camera.lifecycle.ProcessCameraProvider; +import androidx.camera.video.FileOutputOptions; +import androidx.camera.video.Quality; +import androidx.camera.video.QualitySelector; +import androidx.camera.video.Recorder; +import androidx.camera.video.Recording; +import androidx.camera.video.VideoCapture; +import androidx.camera.video.VideoRecordEvent; import androidx.camera.view.PreviewView; +import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; + import androidx.exifinterface.media.ExifInterface; import androidx.fragment.app.Fragment; @@ -51,6 +64,11 @@ interface CameraCallback { void onCompleted(Exception err, String nativePath); } +interface VideoCallback { + void onStart(Boolean recording, String nativePath); + void onStop(Boolean recording, String nativePath); +} + interface CameraStartedCallback { void onCameraStarted(Exception err); } @@ -76,6 +94,9 @@ public class CameraPreviewFragment extends Fragment { private PreviewView viewFinder; private Preview preview; private ImageCapture imageCapture; + private VideoCapture videoCapture; + Recording recording = null; + ProcessCameraProvider cameraProvider = null; private Camera camera; private CameraStartedCallback startCameraCallback; private Location location; @@ -120,13 +141,11 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c viewFinder.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)); containerView.addView(viewFinder); startCamera(); - return containerView; } public void startCamera() { ListenableFuture cameraProviderFuture = ProcessCameraProvider.getInstance(getActivity()); - ProcessCameraProvider cameraProvider = null; try { cameraProvider = cameraProviderFuture.get(); @@ -136,8 +155,8 @@ public void startCamera() { startCameraCallback.onCameraStarted(new Exception("Unable to start camera")); return; } - setUpCamera(captureDevice,cameraProvider); + setUpCamera(captureDevice,cameraProvider); preview.setSurfaceProvider(viewFinder.getSurfaceProvider()); if (startCameraCallback != null) { @@ -258,6 +277,60 @@ public void hasFlash(HasFlashCallback hasFlashCallback) { hasFlashCallback.onResult(camera.getCameraInfo().hasFlashUnit()); } + public void stopCaptureVideo() { + if (recording != null) { + recording.stop(); + recording = null; + } + } + + public void captureVideo(VideoCallback videoCallback) { + if (recording != null) { + recording.stop(); + recording = null; + return; + } + UUID uuid = UUID.randomUUID(); + + String filename = uuid.toString() + ".mp4"; + if (ActivityCompat.checkSelfPermission(this.getContext(), Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this.getActivity(), new String[]{Manifest.permission.RECORD_AUDIO}, 200); + } + File videoFile = new File( + getContext().getFilesDir(), + filename + ); + + FileOutputOptions outputOptions = new FileOutputOptions.Builder(videoFile).build(); + + recording = videoCapture.getOutput() + .prepareRecording(this.getContext().getApplicationContext(), outputOptions) + .withAudioEnabled() + .start(ContextCompat.getMainExecutor(this.getContext()), videoRecordEvent -> { + if (videoRecordEvent instanceof VideoRecordEvent.Start) { + videoCallback.onStart(true, null); + } else if (videoRecordEvent instanceof VideoRecordEvent.Finalize) { + VideoRecordEvent.Finalize finalizeEvent = (VideoRecordEvent.Finalize) videoRecordEvent; + if (finalizeEvent.hasError()) { + // Handle the error + int errorCode = finalizeEvent.getError(); + Throwable errorCause = finalizeEvent.getCause(); + Log.e(TAG, "Video recording error: " + errorCode, errorCause); + } else { + // Handle video saved + videoCallback.onStop(false, Uri.fromFile(videoFile).toString()); + Uri savedUri = finalizeEvent.getOutputResults().getOutputUri(); + Log.i(TAG, "Video saved to: " + savedUri); + } + recording = null; + } + // Other event types can be handled if needed + }); + + } + + + public void takePicture(boolean useFlash, CameraCallback takePictureCallback) { if (torchActivated) { useFlash = true; @@ -398,18 +471,24 @@ public void setUpCamera(String captureDevice, ProcessCameraProvider cameraProvid targetResolution = CameraPreviewFragment.calculateResolution(getContext(), targetSize); } + Recorder recorder = new Recorder.Builder() + .setQualitySelector(QualitySelector.from(Quality.LOWEST)) + .build(); + videoCapture = VideoCapture.withOutput(recorder); + + preview = new Preview.Builder().build(); imageCapture = new ImageCapture.Builder() .setTargetResolution(targetResolution) .build(); - cameraProvider.unbindAll(); try { camera = cameraProvider.bindToLifecycle( getActivity(), cameraSelector, preview, - imageCapture + imageCapture, + videoCapture ); } catch (IllegalArgumentException e) { // Error with result in capturing image with default resolution @@ -420,9 +499,9 @@ public void setUpCamera(String captureDevice, ProcessCameraProvider cameraProvid getActivity(), cameraSelector, preview, - imageCapture + imageCapture, + videoCapture ); } - } } diff --git a/src/android/SimpleCameraPreview.java b/src/android/SimpleCameraPreview.java index 4bc7b40..8d3c87c 100644 --- a/src/android/SimpleCameraPreview.java +++ b/src/android/SimpleCameraPreview.java @@ -43,12 +43,15 @@ public class SimpleCameraPreview extends CordovaPlugin { private LocationListener mLocationCallback; private ViewParent webViewParent; + private CallbackContext videoCallback; + private static final int containerViewId = 20; private static final int DIRECTION_FRONT = 0; private static final int DIRECTION_BACK = 1; private static final int REQUEST_CODE_PERMISSIONS = 4582679; private static final String[] REQUIRED_PERMISSIONS = {Manifest.permission.CAMERA, Manifest.permission.ACCESS_FINE_LOCATION}; + public SimpleCameraPreview() { super(); } @@ -72,6 +75,13 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo case "torchSwitch": return torchSwitch(args.getBoolean(0), callbackContext); + case "captureVideo": + return captureVideo(callbackContext); + + case "stopCaptureVideo": + this.videoStopCallback = callbackContext; + return stopCaptureVideo(callbackContext); + case "deviceHasFlash": return deviceHasFlash(callbackContext); @@ -267,6 +277,64 @@ public void fetchLocation() { } } + CallbackContext videoStopCallback; + + private boolean stopCaptureVideo(CallbackContext callbackContext) { + if (fragment == null) { + callbackContext.error("Camera is closed"); + return true; + } + fragment.stopCaptureVideo(); + return true; + } + private boolean captureVideo(CallbackContext callbackContext) { + if (fragment == null) { + callbackContext.error("Camera is closed"); + return true; + } + + fragment.captureVideo(new VideoCallback() { + public void onStart(Boolean recording, String nativePath) { + JSONObject data = new JSONObject(); + if (recording) { + try { + data.put("recording", true); + data.put("nativePath", null); + } catch (JSONException e) { + e.printStackTrace(); + callbackContext.error("Cannot send recording data"); + return; + } + + PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, data); +// pluginResult.setKeepCallback(true); + callbackContext.sendPluginResult(pluginResult); + } + } + + public void onStop(Boolean recording, String nativePath) { + JSONObject data = new JSONObject(); + try { + data.put("recording", false); + data.put("nativePath", nativePath); + } catch (JSONException e) { + e.printStackTrace(); + callbackContext.error("Cannot send recording data"); + return; + } + PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, data); +// pluginResult.setKeepCallback(true); + if (videoStopCallback != null) { + videoStopCallback.sendPluginResult(pluginResult); + videoStopCallback = null; + return; + } + callbackContext.sendPluginResult(pluginResult); + } + }); + return true; + } + private boolean capture(boolean useFlash, CallbackContext callbackContext) { if (fragment == null) { callbackContext.error("Camera is closed"); diff --git a/www/SimpleCameraPreview.js b/www/SimpleCameraPreview.js index 7b1b9e8..9d4333f 100755 --- a/www/SimpleCameraPreview.js +++ b/www/SimpleCameraPreview.js @@ -20,6 +20,14 @@ SimpleCameraPreview.capture = function (options, onSuccess, onError) { exec(onSuccess, onError, PLUGIN_NAME, "capture", [options.flash]); }; +SimpleCameraPreview.captureVideo = function (onSuccess, onError) { + exec(onSuccess, onError, PLUGIN_NAME, "captureVideo"); +}; + +SimpleCameraPreview.stopCaptureVideo = function (onSuccess, onError) { + exec(onSuccess, onError, PLUGIN_NAME, "stopCaptureVideo"); +}; + SimpleCameraPreview.setSize = function (options, onSuccess, onError) { exec(onSuccess, onError, PLUGIN_NAME, "setSize", [options]); };